@buenojs/bueno 0.8.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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Layer
|
|
3
|
+
*
|
|
4
|
+
* Unified interface over Bun.s3 for S3-compatible storage with local filesystem fallback.
|
|
5
|
+
* Uses Bun 1.3+ native S3 client for cloud storage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============= Types =============
|
|
9
|
+
|
|
10
|
+
export interface StorageConfig {
|
|
11
|
+
driver?: "s3" | "local";
|
|
12
|
+
bucket?: string;
|
|
13
|
+
region?: string;
|
|
14
|
+
endpoint?: string;
|
|
15
|
+
localPath?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UploadOptions {
|
|
19
|
+
key: string;
|
|
20
|
+
body: string | Buffer | ArrayBuffer | Blob | File;
|
|
21
|
+
contentType?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DownloadOptions {
|
|
25
|
+
key: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PresignedURLOptions {
|
|
29
|
+
key: string;
|
|
30
|
+
expiresIn?: number;
|
|
31
|
+
method?: "GET" | "PUT";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FileInfo {
|
|
35
|
+
key: string;
|
|
36
|
+
size: number;
|
|
37
|
+
lastModified?: Date;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ListOptions {
|
|
41
|
+
prefix?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ListResult {
|
|
45
|
+
files: FileInfo[];
|
|
46
|
+
directories: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============= Local Storage (Fallback) =============
|
|
50
|
+
|
|
51
|
+
class LocalStorage {
|
|
52
|
+
private basePath: string;
|
|
53
|
+
|
|
54
|
+
constructor(basePath: string) {
|
|
55
|
+
this.basePath = basePath;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private getFilePath(key: string): string {
|
|
59
|
+
const sanitized = key.replace(/\.\./g, "").replace(/^\/+/, "");
|
|
60
|
+
return `${this.basePath}/${sanitized}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async upload(options: UploadOptions): Promise<{ key: string }> {
|
|
64
|
+
const filePath = this.getFilePath(options.key);
|
|
65
|
+
|
|
66
|
+
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
67
|
+
await Bun.$`mkdir -p ${dir}`.quiet().catch(() => {});
|
|
68
|
+
|
|
69
|
+
// Convert body to appropriate type for Bun.write
|
|
70
|
+
if (typeof options.body === "string") {
|
|
71
|
+
await Bun.write(filePath, options.body);
|
|
72
|
+
} else if (options.body instanceof File) {
|
|
73
|
+
await Bun.write(filePath, await options.body.arrayBuffer());
|
|
74
|
+
} else if (options.body instanceof Blob) {
|
|
75
|
+
await Bun.write(filePath, await options.body.arrayBuffer());
|
|
76
|
+
} else {
|
|
77
|
+
await Bun.write(filePath, options.body);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { key: options.key };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async download(options: DownloadOptions): Promise<Blob> {
|
|
84
|
+
const filePath = this.getFilePath(options.key);
|
|
85
|
+
const file = Bun.file(filePath);
|
|
86
|
+
|
|
87
|
+
if (!(await file.exists())) {
|
|
88
|
+
throw new Error(`File not found: ${options.key}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return file;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async delete(key: string): Promise<void> {
|
|
95
|
+
const filePath = this.getFilePath(key);
|
|
96
|
+
await Bun.$`rm -f ${filePath}`.quiet().catch(() => {});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async exists(key: string): Promise<boolean> {
|
|
100
|
+
const filePath = this.getFilePath(key);
|
|
101
|
+
return Bun.file(filePath).exists();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async info(key: string): Promise<FileInfo | null> {
|
|
105
|
+
const filePath = this.getFilePath(key);
|
|
106
|
+
const file = Bun.file(filePath);
|
|
107
|
+
|
|
108
|
+
if (!(await file.exists())) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const stat = await file.stat();
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
key,
|
|
116
|
+
size: stat?.size ?? 0,
|
|
117
|
+
lastModified: stat?.mtime ? new Date(stat.mtime) : undefined,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async list(options: ListOptions): Promise<ListResult> {
|
|
122
|
+
const prefix = options.prefix ?? "";
|
|
123
|
+
const dirPath = this.getFilePath(prefix);
|
|
124
|
+
|
|
125
|
+
const files: FileInfo[] = [];
|
|
126
|
+
const directories: string[] = [];
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const glob = new Bun.Glob("**/*");
|
|
130
|
+
const baseDir = dirPath || this.basePath;
|
|
131
|
+
|
|
132
|
+
for await (const file of glob.scan(baseDir)) {
|
|
133
|
+
const fullPath = `${baseDir}/${file}`;
|
|
134
|
+
const stat = await Bun.file(fullPath).stat();
|
|
135
|
+
|
|
136
|
+
if (stat && !stat.isDirectory()) {
|
|
137
|
+
files.push({
|
|
138
|
+
key: `${prefix}${file}`,
|
|
139
|
+
size: stat.size,
|
|
140
|
+
lastModified: stat.mtime ? new Date(stat.mtime) : undefined,
|
|
141
|
+
});
|
|
142
|
+
} else if (stat?.isDirectory()) {
|
|
143
|
+
directories.push(`${prefix}${file}/`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// Directory doesn't exist
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { files, directories };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
getPublicUrl(key: string): string {
|
|
154
|
+
return `/storage/${key}`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============= S3 Storage (Bun.s3 Native) =============
|
|
159
|
+
|
|
160
|
+
class S3Storage {
|
|
161
|
+
private config: StorageConfig;
|
|
162
|
+
|
|
163
|
+
constructor(config: StorageConfig) {
|
|
164
|
+
this.config = config;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get S3 file reference using Bun.s3
|
|
169
|
+
*/
|
|
170
|
+
private getFile(key: string): Bun.S3File {
|
|
171
|
+
return Bun.s3.file(key, {
|
|
172
|
+
bucket: this.config.bucket,
|
|
173
|
+
region: this.config.region,
|
|
174
|
+
endpoint: this.config.endpoint,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async upload(options: UploadOptions): Promise<{ key: string }> {
|
|
179
|
+
const file = this.getFile(options.key);
|
|
180
|
+
|
|
181
|
+
// Bun.s3 supports write via Bun.write on S3File
|
|
182
|
+
if (typeof options.body === "string") {
|
|
183
|
+
await file.write(options.body);
|
|
184
|
+
} else if (options.body instanceof File) {
|
|
185
|
+
await file.write(await options.body.arrayBuffer());
|
|
186
|
+
} else if (options.body instanceof Blob) {
|
|
187
|
+
await file.write(await options.body.arrayBuffer());
|
|
188
|
+
} else {
|
|
189
|
+
await file.write(options.body);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { key: options.key };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async download(options: DownloadOptions): Promise<Blob> {
|
|
196
|
+
const file = this.getFile(options.key);
|
|
197
|
+
|
|
198
|
+
if (!(await file.exists())) {
|
|
199
|
+
throw new Error(`File not found: ${options.key}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return file;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async delete(key: string): Promise<void> {
|
|
206
|
+
const file = this.getFile(key);
|
|
207
|
+
await file.delete();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async exists(key: string): Promise<boolean> {
|
|
211
|
+
const file = this.getFile(key);
|
|
212
|
+
return file.exists();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async info(key: string): Promise<FileInfo | null> {
|
|
216
|
+
const file = this.getFile(key);
|
|
217
|
+
|
|
218
|
+
if (!(await file.exists())) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
key,
|
|
224
|
+
size: 0,
|
|
225
|
+
lastModified: undefined,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async list(options: ListOptions): Promise<ListResult> {
|
|
230
|
+
// S3 list not directly supported by Bun.s3 yet
|
|
231
|
+
console.warn("S3 list operation not yet supported");
|
|
232
|
+
return { files: [], directories: [] };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
getPresignedUrl(options: PresignedURLOptions): string {
|
|
236
|
+
const file = this.getFile(options.key);
|
|
237
|
+
return file.presign({
|
|
238
|
+
expiresIn: options.expiresIn ?? 3600,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
getPublicUrl(key: string): string {
|
|
243
|
+
const region = this.config.region ?? "us-east-1";
|
|
244
|
+
const endpoint =
|
|
245
|
+
this.config.endpoint ?? `https://s3.${region}.amazonaws.com`;
|
|
246
|
+
const bucket = this.config.bucket!;
|
|
247
|
+
return `${endpoint}/${bucket}/${key}`;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ============= Storage Class =============
|
|
252
|
+
|
|
253
|
+
export class Storage {
|
|
254
|
+
private driver: "s3" | "local";
|
|
255
|
+
private s3Storage: S3Storage | null = null;
|
|
256
|
+
private localStorage: LocalStorage | null = null;
|
|
257
|
+
private _isConnected = false;
|
|
258
|
+
|
|
259
|
+
constructor(config: StorageConfig = {}) {
|
|
260
|
+
this.driver = config.driver ?? "local";
|
|
261
|
+
|
|
262
|
+
if (this.driver === "s3") {
|
|
263
|
+
this.s3Storage = new S3Storage(config);
|
|
264
|
+
} else {
|
|
265
|
+
this.localStorage = new LocalStorage(config.localPath ?? "./storage");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async connect(): Promise<void> {
|
|
270
|
+
this._isConnected = true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
get isConnected(): boolean {
|
|
274
|
+
return this._isConnected;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
getDriver(): "s3" | "local" {
|
|
278
|
+
return this.driver;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async upload(options: UploadOptions): Promise<{ key: string }> {
|
|
282
|
+
if (this.driver === "s3" && this.s3Storage) {
|
|
283
|
+
return this.s3Storage.upload(options);
|
|
284
|
+
}
|
|
285
|
+
if (!this.localStorage) {
|
|
286
|
+
throw new Error("Local storage not initialized");
|
|
287
|
+
}
|
|
288
|
+
return this.localStorage.upload(options);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async download(options: DownloadOptions): Promise<Blob> {
|
|
292
|
+
if (this.driver === "s3" && this.s3Storage) {
|
|
293
|
+
return this.s3Storage.download(options);
|
|
294
|
+
}
|
|
295
|
+
if (!this.localStorage) {
|
|
296
|
+
throw new Error("Local storage not initialized");
|
|
297
|
+
}
|
|
298
|
+
return this.localStorage.download(options);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async downloadText(key: string): Promise<string> {
|
|
302
|
+
const blob = await this.download({ key });
|
|
303
|
+
return blob.text();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async downloadBuffer(key: string): Promise<ArrayBuffer> {
|
|
307
|
+
const blob = await this.download({ key });
|
|
308
|
+
return blob.arrayBuffer();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async stream(key: string): Promise<ReadableStream<Uint8Array>> {
|
|
312
|
+
const blob = await this.download({ key });
|
|
313
|
+
return blob.stream();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async delete(key: string): Promise<void> {
|
|
317
|
+
if (this.driver === "s3" && this.s3Storage) {
|
|
318
|
+
return this.s3Storage.delete(key);
|
|
319
|
+
}
|
|
320
|
+
return this.localStorage?.delete(key);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async deleteMany(keys: string[]): Promise<void> {
|
|
324
|
+
await Promise.all(keys.map((key) => this.delete(key)));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async exists(key: string): Promise<boolean> {
|
|
328
|
+
if (this.driver === "s3" && this.s3Storage) {
|
|
329
|
+
return this.s3Storage.exists(key);
|
|
330
|
+
}
|
|
331
|
+
if (!this.localStorage) {
|
|
332
|
+
throw new Error("Local storage not initialized");
|
|
333
|
+
}
|
|
334
|
+
return this.localStorage.exists(key);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async info(key: string): Promise<FileInfo | null> {
|
|
338
|
+
if (this.driver === "s3" && this.s3Storage) {
|
|
339
|
+
return this.s3Storage.info(key);
|
|
340
|
+
}
|
|
341
|
+
if (!this.localStorage) {
|
|
342
|
+
throw new Error("Local storage not initialized");
|
|
343
|
+
}
|
|
344
|
+
return this.localStorage.info(key);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async copy(sourceKey: string, destKey: string): Promise<void> {
|
|
348
|
+
const blob = await this.download({ key: sourceKey });
|
|
349
|
+
await this.upload({ key: destKey, body: blob });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async move(sourceKey: string, destKey: string): Promise<void> {
|
|
353
|
+
await this.copy(sourceKey, destKey);
|
|
354
|
+
await this.delete(sourceKey);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async list(options?: ListOptions): Promise<ListResult> {
|
|
358
|
+
if (this.driver === "s3" && this.s3Storage) {
|
|
359
|
+
return this.s3Storage.list(options ?? {});
|
|
360
|
+
}
|
|
361
|
+
if (!this.localStorage) {
|
|
362
|
+
throw new Error("Local storage not initialized");
|
|
363
|
+
}
|
|
364
|
+
return this.localStorage.list(options ?? {});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
getPresignedUrl(options: PresignedURLOptions): string {
|
|
368
|
+
if (this.driver === "s3" && this.s3Storage) {
|
|
369
|
+
return this.s3Storage.getPresignedUrl(options);
|
|
370
|
+
}
|
|
371
|
+
throw new Error("Presigned URLs are only available for S3 storage");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
getPublicUrl(key: string): string {
|
|
375
|
+
if (this.driver === "s3" && this.s3Storage) {
|
|
376
|
+
return this.s3Storage.getPublicUrl(key);
|
|
377
|
+
}
|
|
378
|
+
if (!this.localStorage) {
|
|
379
|
+
throw new Error("Local storage not initialized");
|
|
380
|
+
}
|
|
381
|
+
return this.localStorage.getPublicUrl(key);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async uploadFile(localPath: string, key: string): Promise<{ key: string }> {
|
|
385
|
+
const file = Bun.file(localPath);
|
|
386
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
387
|
+
return this.upload({
|
|
388
|
+
key,
|
|
389
|
+
body: arrayBuffer,
|
|
390
|
+
contentType: file.type,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ============= Secrets Wrapper (Bun.secrets) =============
|
|
396
|
+
|
|
397
|
+
export interface SecretOptions {
|
|
398
|
+
service: string;
|
|
399
|
+
name: string;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export interface SetSecretOptions extends SecretOptions {
|
|
403
|
+
value: string;
|
|
404
|
+
allowUnrestrictedAccess?: boolean;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export const Secrets = {
|
|
408
|
+
/**
|
|
409
|
+
* Get a secret from OS credential storage
|
|
410
|
+
*/
|
|
411
|
+
async get(options: SecretOptions): Promise<string | null> {
|
|
412
|
+
return Bun.secrets.get(options);
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Store a secret in OS credential storage
|
|
417
|
+
*/
|
|
418
|
+
async set(options: SetSecretOptions): Promise<void> {
|
|
419
|
+
await Bun.secrets.set(options);
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Delete a secret from OS credential storage
|
|
424
|
+
*/
|
|
425
|
+
async delete(options: SecretOptions): Promise<void> {
|
|
426
|
+
await Bun.secrets.delete(options);
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Get a secret or throw if not found
|
|
431
|
+
*/
|
|
432
|
+
async getOrThrow(options: SecretOptions): Promise<string> {
|
|
433
|
+
const value = await this.get(options);
|
|
434
|
+
if (!value) {
|
|
435
|
+
throw new Error(`Secret not found: ${options.service}/${options.name}`);
|
|
436
|
+
}
|
|
437
|
+
return value;
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Get a secret or use a default value
|
|
442
|
+
*/
|
|
443
|
+
async getOrDefault(
|
|
444
|
+
options: SecretOptions,
|
|
445
|
+
defaultValue: string,
|
|
446
|
+
): Promise<string> {
|
|
447
|
+
const value = await this.get(options);
|
|
448
|
+
return value ?? defaultValue;
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// ============= Factory Functions =============
|
|
453
|
+
|
|
454
|
+
export function createStorage(config?: StorageConfig): Storage {
|
|
455
|
+
return new Storage(config);
|
|
456
|
+
}
|