@bunbase-ae/js 1.0.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/package.json +19 -0
- package/src/admin.ts +912 -0
- package/src/auth.ts +338 -0
- package/src/client.ts +61 -0
- package/src/collection.ts +185 -0
- package/src/http.ts +217 -0
- package/src/index.ts +76 -0
- package/src/realtime.ts +374 -0
- package/src/storage.ts +206 -0
- package/src/types.ts +266 -0
package/src/storage.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Storage client — upload, download URL, signed URL, list, delete.
|
|
2
|
+
|
|
3
|
+
import type { HttpClient } from "./http";
|
|
4
|
+
import type { FileRecord } from "./types";
|
|
5
|
+
import { BunBaseError } from "./types";
|
|
6
|
+
|
|
7
|
+
export interface UploadOptions {
|
|
8
|
+
// Target storage bucket. Defaults to "default" if omitted.
|
|
9
|
+
bucket?: string;
|
|
10
|
+
// Associate the file with a collection record.
|
|
11
|
+
collection?: string;
|
|
12
|
+
recordId?: string;
|
|
13
|
+
// Make the file publicly accessible without auth.
|
|
14
|
+
isPublic?: boolean;
|
|
15
|
+
// Upload progress callback (0–1). Uses XMLHttpRequest internally.
|
|
16
|
+
// Only available in browser and React Native environments.
|
|
17
|
+
onProgress?: (progress: number) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SignedUploadResult {
|
|
21
|
+
// Pre-signed PUT URL to upload directly to the storage backend.
|
|
22
|
+
// For local provider: points at BunBase — PUT registers metadata automatically.
|
|
23
|
+
// For S3 provider: points at S3 — call confirmUpload() after PUT to register metadata.
|
|
24
|
+
url: string;
|
|
25
|
+
key: string;
|
|
26
|
+
expires_in: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class StorageClient {
|
|
30
|
+
constructor(private readonly http: HttpClient) {}
|
|
31
|
+
|
|
32
|
+
// Upload a file through BunBase (works for local storage; also works for S3
|
|
33
|
+
// but large files are better served with signedUpload()).
|
|
34
|
+
// Pass onProgress to track upload progress (browser/React Native only).
|
|
35
|
+
async upload(file: File | Blob, options: UploadOptions = {}): Promise<FileRecord> {
|
|
36
|
+
const form = new FormData();
|
|
37
|
+
form.append("file", file);
|
|
38
|
+
if (options.bucket) form.append("bucket", options.bucket);
|
|
39
|
+
if (options.collection) form.append("collection", options.collection);
|
|
40
|
+
if (options.recordId) form.append("record_id", options.recordId);
|
|
41
|
+
if (options.isPublic) form.append("is_public", "true");
|
|
42
|
+
|
|
43
|
+
if (options.onProgress) {
|
|
44
|
+
return this.uploadWithProgress(form, options.onProgress);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return this.http.request<FileRecord>("POST", "/api/v1/storage/upload", {
|
|
48
|
+
formData: form,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private uploadWithProgress(
|
|
53
|
+
form: FormData,
|
|
54
|
+
onProgress: (progress: number) => void,
|
|
55
|
+
): Promise<FileRecord> {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const xhr = new XMLHttpRequest();
|
|
58
|
+
xhr.open("POST", `${this.http.baseUrl}/api/v1/storage/upload`);
|
|
59
|
+
|
|
60
|
+
for (const [key, value] of Object.entries(this.http.getAuthHeaders())) {
|
|
61
|
+
xhr.setRequestHeader(key, value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
xhr.upload.onprogress = (e) => {
|
|
65
|
+
if (e.lengthComputable) onProgress(e.loaded / e.total);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
xhr.onload = () => {
|
|
69
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
70
|
+
try {
|
|
71
|
+
resolve(JSON.parse(xhr.responseText) as FileRecord);
|
|
72
|
+
} catch {
|
|
73
|
+
reject(new BunBaseError("Invalid response from server.", xhr.status, null));
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
let message = `Upload failed with status ${xhr.status}`;
|
|
77
|
+
let code: string | undefined;
|
|
78
|
+
let field: string | undefined;
|
|
79
|
+
try {
|
|
80
|
+
const body = JSON.parse(xhr.responseText) as Record<string, unknown>;
|
|
81
|
+
if (body.error) message = String(body.error);
|
|
82
|
+
if (body.code) code = String(body.code);
|
|
83
|
+
if (body.field) field = String(body.field);
|
|
84
|
+
} catch {
|
|
85
|
+
// ignore parse errors
|
|
86
|
+
}
|
|
87
|
+
reject(new BunBaseError(message, xhr.status, null, code, field));
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
xhr.onerror = () => reject(new BunBaseError("Upload failed: network error.", 0, null));
|
|
92
|
+
xhr.onabort = () => reject(new BunBaseError("Upload aborted.", 0, null));
|
|
93
|
+
xhr.send(form);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Get a pre-signed PUT URL for direct upload.
|
|
98
|
+
// Local provider: PUT the file to the URL — metadata is registered automatically.
|
|
99
|
+
// S3 provider: PUT the file to the URL, then call confirmUpload() to register metadata.
|
|
100
|
+
async signedUpload(
|
|
101
|
+
filename: string,
|
|
102
|
+
options: UploadOptions & { expiresIn?: number; contentType?: string } = {},
|
|
103
|
+
): Promise<SignedUploadResult> {
|
|
104
|
+
return this.http.request<SignedUploadResult>("POST", "/api/v1/storage/sign", {
|
|
105
|
+
body: {
|
|
106
|
+
filename,
|
|
107
|
+
bucket: options.bucket,
|
|
108
|
+
is_public: options.isPublic,
|
|
109
|
+
content_type: options.contentType,
|
|
110
|
+
expires_in: options.expiresIn,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Perform the full signed-upload flow in one call:
|
|
116
|
+
// 1. Get the pre-signed PUT URL from BunBase.
|
|
117
|
+
// 2. PUT the file directly to the storage backend.
|
|
118
|
+
// 3. For S3: call confirmUpload() to register metadata. Local handles this in step 2.
|
|
119
|
+
async signedUploadFile(
|
|
120
|
+
file: File | Blob,
|
|
121
|
+
options: UploadOptions & { expiresIn?: number } = {},
|
|
122
|
+
): Promise<FileRecord> {
|
|
123
|
+
const filename = file instanceof File ? file.name : `upload-${Date.now()}`;
|
|
124
|
+
const { url, key } = await this.signedUpload(filename, {
|
|
125
|
+
...options,
|
|
126
|
+
contentType: file.type || "application/octet-stream",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const putRes = await fetch(url, {
|
|
130
|
+
method: "PUT",
|
|
131
|
+
body: file,
|
|
132
|
+
headers: { "Content-Type": file.type || "application/octet-stream" },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Local provider PUT returns FileRecord directly (201).
|
|
136
|
+
if (putRes.status === 201) {
|
|
137
|
+
return putRes.json() as Promise<FileRecord>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!putRes.ok) {
|
|
141
|
+
throw new BunBaseError(
|
|
142
|
+
`Storage PUT failed with status ${putRes.status}.`,
|
|
143
|
+
putRes.status,
|
|
144
|
+
null,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// S3 provider: PUT succeeded (200/204) — register metadata with BunBase.
|
|
149
|
+
return this.confirmUpload({
|
|
150
|
+
key,
|
|
151
|
+
bucket: options.bucket,
|
|
152
|
+
filename,
|
|
153
|
+
collection: options.collection,
|
|
154
|
+
recordId: options.recordId,
|
|
155
|
+
isPublic: options.isPublic,
|
|
156
|
+
mimeType: file.type || "application/octet-stream",
|
|
157
|
+
size: file.size,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Confirm an S3 presigned upload by registering the file metadata in BunBase.
|
|
162
|
+
// Not needed for local provider (the PUT handler registers metadata automatically).
|
|
163
|
+
async confirmUpload(options: {
|
|
164
|
+
key: string;
|
|
165
|
+
bucket?: string;
|
|
166
|
+
filename?: string;
|
|
167
|
+
collection?: string;
|
|
168
|
+
recordId?: string;
|
|
169
|
+
isPublic?: boolean;
|
|
170
|
+
mimeType?: string;
|
|
171
|
+
size?: number;
|
|
172
|
+
}): Promise<FileRecord> {
|
|
173
|
+
return this.http.request<FileRecord>("POST", "/api/v1/storage/confirm", {
|
|
174
|
+
body: {
|
|
175
|
+
key: options.key,
|
|
176
|
+
bucket: options.bucket,
|
|
177
|
+
filename: options.filename,
|
|
178
|
+
collection: options.collection,
|
|
179
|
+
record_id: options.recordId,
|
|
180
|
+
is_public: options.isPublic,
|
|
181
|
+
mime_type: options.mimeType,
|
|
182
|
+
size: options.size,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Returns the direct download URL for a public file.
|
|
188
|
+
// Pass filename (from FileRecord.filename) to include the original file
|
|
189
|
+
// extension in the URL — helps browsers and CDNs identify the file type.
|
|
190
|
+
// Do NOT use this for private files — the browser cannot send the
|
|
191
|
+
// Authorization header via <img src>. For private files, call signedUrl()
|
|
192
|
+
// to get a time-limited signed URL instead.
|
|
193
|
+
downloadUrl(id: string, filename?: string | null): string {
|
|
194
|
+
if (id.startsWith("http")) return id;
|
|
195
|
+
const base = `${this.http.baseUrl}/api/v1/storage/${encodeURIComponent(id)}`;
|
|
196
|
+
return filename ? `${base}/${encodeURIComponent(filename)}` : base;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async list(): Promise<FileRecord[]> {
|
|
200
|
+
return this.http.request<FileRecord[]>("GET", "/api/v1/storage");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async delete(id: string): Promise<void> {
|
|
204
|
+
await this.http.request<{ deleted: boolean }>("DELETE", `/api/v1/storage/${id}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Shared types for the BunBase TypeScript SDK.
|
|
2
|
+
// These mirror the server's response shapes exactly.
|
|
3
|
+
|
|
4
|
+
export interface BunBaseRecord {
|
|
5
|
+
_id: string;
|
|
6
|
+
_created_at: number;
|
|
7
|
+
_updated_at: number;
|
|
8
|
+
_owner_id: string | null;
|
|
9
|
+
_deleted_at?: number | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolves an expand shape to the concrete types returned by the server.
|
|
14
|
+
*
|
|
15
|
+
* "one" relation: { author_id: Profile } → author_id is Profile & BunBaseRecord
|
|
16
|
+
* "many" relation: { comments: Comment[] } → comments is (Comment & BunBaseRecord)[]
|
|
17
|
+
*/
|
|
18
|
+
export type ExpandResult<TExpand extends Record<string, unknown>> = {
|
|
19
|
+
[K in keyof TExpand]: TExpand[K] extends (infer Item)[]
|
|
20
|
+
? Item extends Record<string, unknown>
|
|
21
|
+
? (Item & BunBaseRecord)[]
|
|
22
|
+
: Item[]
|
|
23
|
+
: TExpand[K] extends Record<string, unknown>
|
|
24
|
+
? TExpand[K] & BunBaseRecord
|
|
25
|
+
: TExpand[K];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Adds a typed `expand` field to a record type.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* type Post = { title: string; author_id: string };
|
|
33
|
+
* type Profile = { display_name: string; avatar_url: string };
|
|
34
|
+
*
|
|
35
|
+
* // With CollectionClient:
|
|
36
|
+
* const posts = await client.collection<Post>("posts").list<{ author_id: Profile }>({ expand: ["author_id"] });
|
|
37
|
+
* posts.items[0].expand?.author_id // Profile & BunBaseRecord ✓
|
|
38
|
+
*
|
|
39
|
+
* // Or define the type upfront:
|
|
40
|
+
* type PostWithAuthor = WithExpand<Post, { author_id: Profile }>;
|
|
41
|
+
*/
|
|
42
|
+
export type WithExpand<T, TExpand extends Record<string, unknown> = Record<string, never>> = T & {
|
|
43
|
+
expand?: Partial<ExpandResult<TExpand>>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export interface ListResult<T> {
|
|
47
|
+
items: T[];
|
|
48
|
+
/** Total count. Only present when `count: true` is passed in the list options. */
|
|
49
|
+
total?: number;
|
|
50
|
+
page: number;
|
|
51
|
+
limit: number;
|
|
52
|
+
/** Cursor for the next page. Pass as `after` in the next list call. */
|
|
53
|
+
next_cursor: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AuthUser {
|
|
57
|
+
id: string;
|
|
58
|
+
email: string;
|
|
59
|
+
created_at: number;
|
|
60
|
+
updated_at: number;
|
|
61
|
+
is_verified: boolean;
|
|
62
|
+
role: string | null;
|
|
63
|
+
/** Freeform JSON object. Store display name, bio, avatar URL, etc. */
|
|
64
|
+
metadata: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface AuthResult {
|
|
68
|
+
access_token: string;
|
|
69
|
+
refresh_token: string;
|
|
70
|
+
expires_in: number;
|
|
71
|
+
user: AuthUser;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface FileRecord {
|
|
75
|
+
id: string;
|
|
76
|
+
key: string;
|
|
77
|
+
bucket: string;
|
|
78
|
+
filename: string | null;
|
|
79
|
+
collection: string | null;
|
|
80
|
+
record_id: string | null;
|
|
81
|
+
owner_id: string | null;
|
|
82
|
+
size: number;
|
|
83
|
+
mime_type: string;
|
|
84
|
+
is_public: boolean;
|
|
85
|
+
created_at: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Query types ──────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
export type FilterOperator = "eq" | "ne" | "gt" | "lt" | "gte" | "lte" | "like" | "in";
|
|
91
|
+
|
|
92
|
+
export type FilterValue = string | number | boolean | string[] | number[];
|
|
93
|
+
|
|
94
|
+
export interface FieldFilter {
|
|
95
|
+
[op: string]: FilterValue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Simple: { status: "published" } → filter[status]=published
|
|
99
|
+
// Operator: { age: { gte: 18 } } → filter[age][gte]=18
|
|
100
|
+
export type Filter<T = Record<string, unknown>> = {
|
|
101
|
+
[K in keyof T]?: T[K] | FieldFilter;
|
|
102
|
+
} & {
|
|
103
|
+
[key: string]: FilterValue | FieldFilter | undefined;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export interface ListQuery<T = Record<string, unknown>> {
|
|
107
|
+
filter?: Filter<T>;
|
|
108
|
+
// Prefix field name with "-" for descending: "-_created_at"
|
|
109
|
+
sort?: string;
|
|
110
|
+
page?: number;
|
|
111
|
+
limit?: number;
|
|
112
|
+
// Return only these fields in each record
|
|
113
|
+
fields?: string[];
|
|
114
|
+
// Inline related records declared via the relations API.
|
|
115
|
+
// For "one" relations: pass the foreign key field name (e.g. "author_id").
|
|
116
|
+
// For "many" / "many_via" relations: pass from_field as declared.
|
|
117
|
+
expand?: string[];
|
|
118
|
+
// Cursor-based pagination: pass next_cursor from the previous list response.
|
|
119
|
+
// When set, page is ignored.
|
|
120
|
+
after?: string;
|
|
121
|
+
// Include soft-deleted records in the response. Default: false.
|
|
122
|
+
include_deleted?: boolean;
|
|
123
|
+
// Full-text search query. When set, results are filtered to matching records.
|
|
124
|
+
search?: string;
|
|
125
|
+
// Request total count. Default false — COUNT(*) is skipped for performance.
|
|
126
|
+
count?: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Batch operation types ────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
export interface BatchCreate<T = Record<string, unknown>> {
|
|
132
|
+
op: "create";
|
|
133
|
+
data: Partial<T>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface BatchUpdate<T = Record<string, unknown>> {
|
|
137
|
+
op: "update";
|
|
138
|
+
id: string;
|
|
139
|
+
data: Partial<T>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface BatchDelete {
|
|
143
|
+
op: "delete";
|
|
144
|
+
id: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type BatchOperation<T = Record<string, unknown>> =
|
|
148
|
+
| BatchCreate<T>
|
|
149
|
+
| BatchUpdate<T>
|
|
150
|
+
| BatchDelete;
|
|
151
|
+
|
|
152
|
+
export interface BatchResultCreate<T = Record<string, unknown>> {
|
|
153
|
+
op: "create";
|
|
154
|
+
record: T & BunBaseRecord;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface BatchResultUpdate<T = Record<string, unknown>> {
|
|
158
|
+
op: "update";
|
|
159
|
+
record: T & BunBaseRecord;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface BatchResultDelete {
|
|
163
|
+
op: "delete";
|
|
164
|
+
id: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export type BatchResult<T = Record<string, unknown>> =
|
|
168
|
+
| BatchResultCreate<T>
|
|
169
|
+
| BatchResultUpdate<T>
|
|
170
|
+
| BatchResultDelete;
|
|
171
|
+
|
|
172
|
+
export interface GetQuery {
|
|
173
|
+
// Same expand semantics as ListQuery.
|
|
174
|
+
expand?: string[];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Field validation ─────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
export interface FieldRule {
|
|
180
|
+
field: string;
|
|
181
|
+
required?: boolean;
|
|
182
|
+
/** Strings: min length. Numbers: min value. Arrays: min item count. */
|
|
183
|
+
min?: number;
|
|
184
|
+
/** Strings: max length. Numbers: max value. Arrays: max item count. */
|
|
185
|
+
max?: number;
|
|
186
|
+
/** Regex pattern — applied to string values only. */
|
|
187
|
+
regex?: string;
|
|
188
|
+
/** Allowed values — applied to any type. */
|
|
189
|
+
enum?: unknown[];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Aggregations ─────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
export type AggregateFunction = "sum" | "avg" | "min" | "max" | "count";
|
|
195
|
+
|
|
196
|
+
export type AggregateResult =
|
|
197
|
+
| { value: number | null }
|
|
198
|
+
| { groups: Array<{ group: unknown; value: number | null }> };
|
|
199
|
+
|
|
200
|
+
// ─── Realtime types ───────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
export type RealtimeEventType = "create" | "update" | "delete";
|
|
203
|
+
|
|
204
|
+
export interface RealtimeEvent<T = BunBaseRecord> {
|
|
205
|
+
event: RealtimeEventType;
|
|
206
|
+
collection: string;
|
|
207
|
+
record: T & BunBaseRecord;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export type RealtimeCallback<T = BunBaseRecord> = (event: RealtimeEvent<T>) => void;
|
|
211
|
+
|
|
212
|
+
export type UnsubscribeFn = () => void;
|
|
213
|
+
|
|
214
|
+
// ─── TOTP management ──────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
// Returned by setupTotp(). Display the secret or otpauth_url to the user so
|
|
217
|
+
// they can add the account to their authenticator app.
|
|
218
|
+
export interface TotpSetup {
|
|
219
|
+
secret: string;
|
|
220
|
+
otpauth_url: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ─── Client options ───────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
// Returned by login() when the account has 2FA enabled.
|
|
226
|
+
// Pass totp_token + a TOTP code to auth.verifyTotp() to complete sign-in.
|
|
227
|
+
export interface TotpChallenge {
|
|
228
|
+
totp_required: true;
|
|
229
|
+
totp_token: string;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export type LoginResult = AuthResult | TotpChallenge;
|
|
233
|
+
|
|
234
|
+
export interface ApiKey {
|
|
235
|
+
id: string;
|
|
236
|
+
name: string;
|
|
237
|
+
created_at: number;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export interface BunBaseClientOptions {
|
|
241
|
+
// Base URL of the BunBase server — no trailing slash.
|
|
242
|
+
url: string;
|
|
243
|
+
// Long-lived API key for server-to-server use. When set, the client sends
|
|
244
|
+
// X-Api-Key instead of Bearer tokens and skips the token refresh flow.
|
|
245
|
+
apiKey?: string;
|
|
246
|
+
// Admin secret — grants access to all /admin/* endpoints.
|
|
247
|
+
// When set, the client sends Authorization: Bearer <adminSecret> and skips
|
|
248
|
+
// the token refresh flow. Use for server-to-server admin calls or Studio.
|
|
249
|
+
// Alternatively, authenticate as a user with the "admin" role via login().
|
|
250
|
+
adminSecret?: string;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export class BunBaseError extends Error {
|
|
254
|
+
constructor(
|
|
255
|
+
message: string,
|
|
256
|
+
public readonly status: number,
|
|
257
|
+
public readonly body: unknown,
|
|
258
|
+
// Machine-readable error code from the server (e.g. "INVALID_CREDENTIALS").
|
|
259
|
+
public readonly code?: string,
|
|
260
|
+
// Field name that caused the error, for form validation UIs.
|
|
261
|
+
public readonly field?: string,
|
|
262
|
+
) {
|
|
263
|
+
super(message);
|
|
264
|
+
this.name = "BunBaseError";
|
|
265
|
+
}
|
|
266
|
+
}
|