@zlayer/sdk 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/.eslintrc.json +35 -0
- package/.prettierrc +10 -0
- package/README.md +132 -0
- package/dist/index.d.ts +1005 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1227 -0
- package/dist/index.js.map +1 -0
- package/examples/.gitkeep +0 -0
- package/package.json +42 -0
- package/src/index.ts +1565 -0
- package/tsconfig.json +19 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZLayer SDK for building WASM plugins in TypeScript.
|
|
3
|
+
*
|
|
4
|
+
* This module provides helper functions that wrap the generated WIT bindings
|
|
5
|
+
* for easier access to ZLayer host capabilities including configuration,
|
|
6
|
+
* key-value storage, logging, secrets, and metrics.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Version
|
|
12
|
+
// =============================================================================
|
|
13
|
+
/**
|
|
14
|
+
* The current version of the ZLayer SDK.
|
|
15
|
+
*/
|
|
16
|
+
export const VERSION = '0.1.0';
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Error Classes
|
|
19
|
+
// =============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* Error thrown when configuration operations fail.
|
|
22
|
+
*/
|
|
23
|
+
export class ConfigError extends Error {
|
|
24
|
+
/** The configuration key that caused the error */
|
|
25
|
+
key;
|
|
26
|
+
constructor(key, message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'ConfigError';
|
|
29
|
+
this.key = key;
|
|
30
|
+
Object.setPrototypeOf(this, ConfigError.prototype);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Error thrown when key-value storage operations fail.
|
|
35
|
+
*/
|
|
36
|
+
export class KVError extends Error {
|
|
37
|
+
/** The key that caused the error */
|
|
38
|
+
key;
|
|
39
|
+
/** The bucket where the error occurred */
|
|
40
|
+
bucket;
|
|
41
|
+
/** The error code from the host */
|
|
42
|
+
code;
|
|
43
|
+
constructor(bucket, key, code, message) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = 'KVError';
|
|
46
|
+
this.bucket = bucket;
|
|
47
|
+
this.key = key;
|
|
48
|
+
this.code = code;
|
|
49
|
+
Object.setPrototypeOf(this, KVError.prototype);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Error codes for key-value operations.
|
|
54
|
+
*/
|
|
55
|
+
export var KVErrorCode;
|
|
56
|
+
(function (KVErrorCode) {
|
|
57
|
+
/** Key not found */
|
|
58
|
+
KVErrorCode["NotFound"] = "not_found";
|
|
59
|
+
/** Value too large */
|
|
60
|
+
KVErrorCode["ValueTooLarge"] = "value_too_large";
|
|
61
|
+
/** Storage quota exceeded */
|
|
62
|
+
KVErrorCode["QuotaExceeded"] = "quota_exceeded";
|
|
63
|
+
/** Key format invalid */
|
|
64
|
+
KVErrorCode["InvalidKey"] = "invalid_key";
|
|
65
|
+
/** Generic storage error */
|
|
66
|
+
KVErrorCode["Storage"] = "storage";
|
|
67
|
+
})(KVErrorCode || (KVErrorCode = {}));
|
|
68
|
+
/**
|
|
69
|
+
* Error thrown when secret operations fail.
|
|
70
|
+
*/
|
|
71
|
+
export class SecretError extends Error {
|
|
72
|
+
/** The secret name that caused the error */
|
|
73
|
+
secretName;
|
|
74
|
+
constructor(secretName, message) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = 'SecretError';
|
|
77
|
+
this.secretName = secretName;
|
|
78
|
+
Object.setPrototypeOf(this, SecretError.prototype);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Error thrown when a required value is missing.
|
|
83
|
+
*/
|
|
84
|
+
export class NotFoundError extends Error {
|
|
85
|
+
/** The identifier that was not found */
|
|
86
|
+
identifier;
|
|
87
|
+
constructor(identifier, message) {
|
|
88
|
+
super(message);
|
|
89
|
+
this.name = 'NotFoundError';
|
|
90
|
+
this.identifier = identifier;
|
|
91
|
+
Object.setPrototypeOf(this, NotFoundError.prototype);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// =============================================================================
|
|
95
|
+
// Log Level Enum
|
|
96
|
+
// =============================================================================
|
|
97
|
+
/**
|
|
98
|
+
* Log severity levels matching the WIT interface.
|
|
99
|
+
*/
|
|
100
|
+
export var LogLevel;
|
|
101
|
+
(function (LogLevel) {
|
|
102
|
+
/** Finest-grained debugging information */
|
|
103
|
+
LogLevel[LogLevel["Trace"] = 0] = "Trace";
|
|
104
|
+
/** Debugging information */
|
|
105
|
+
LogLevel[LogLevel["Debug"] = 1] = "Debug";
|
|
106
|
+
/** Informational messages */
|
|
107
|
+
LogLevel[LogLevel["Info"] = 2] = "Info";
|
|
108
|
+
/** Warning messages */
|
|
109
|
+
LogLevel[LogLevel["Warn"] = 3] = "Warn";
|
|
110
|
+
/** Error messages */
|
|
111
|
+
LogLevel[LogLevel["Error"] = 4] = "Error";
|
|
112
|
+
})(LogLevel || (LogLevel = {}));
|
|
113
|
+
/**
|
|
114
|
+
* HTTP methods supported by the plugin system.
|
|
115
|
+
*/
|
|
116
|
+
export var HttpMethod;
|
|
117
|
+
(function (HttpMethod) {
|
|
118
|
+
HttpMethod["Get"] = "GET";
|
|
119
|
+
HttpMethod["Post"] = "POST";
|
|
120
|
+
HttpMethod["Put"] = "PUT";
|
|
121
|
+
HttpMethod["Delete"] = "DELETE";
|
|
122
|
+
HttpMethod["Patch"] = "PATCH";
|
|
123
|
+
HttpMethod["Head"] = "HEAD";
|
|
124
|
+
HttpMethod["Options"] = "OPTIONS";
|
|
125
|
+
HttpMethod["Connect"] = "CONNECT";
|
|
126
|
+
HttpMethod["Trace"] = "TRACE";
|
|
127
|
+
})(HttpMethod || (HttpMethod = {}));
|
|
128
|
+
// =============================================================================
|
|
129
|
+
// Host Bindings Stub
|
|
130
|
+
// =============================================================================
|
|
131
|
+
/**
|
|
132
|
+
* Stub for host bindings. In actual WASM execution, these are replaced
|
|
133
|
+
* by the generated WIT bindings that call into the host.
|
|
134
|
+
*/
|
|
135
|
+
const hostBindings = {
|
|
136
|
+
config: {
|
|
137
|
+
get: (_key) => {
|
|
138
|
+
throw new Error('Host bindings not available: config.get not implemented');
|
|
139
|
+
},
|
|
140
|
+
getRequired: (_key) => {
|
|
141
|
+
throw new Error('Host bindings not available: config.getRequired not implemented');
|
|
142
|
+
},
|
|
143
|
+
getMany: (_keys) => {
|
|
144
|
+
throw new Error('Host bindings not available: config.getMany not implemented');
|
|
145
|
+
},
|
|
146
|
+
getPrefix: (_prefix) => {
|
|
147
|
+
throw new Error('Host bindings not available: config.getPrefix not implemented');
|
|
148
|
+
},
|
|
149
|
+
exists: (_key) => {
|
|
150
|
+
throw new Error('Host bindings not available: config.exists not implemented');
|
|
151
|
+
},
|
|
152
|
+
getBool: (_key) => {
|
|
153
|
+
throw new Error('Host bindings not available: config.getBool not implemented');
|
|
154
|
+
},
|
|
155
|
+
getInt: (_key) => {
|
|
156
|
+
throw new Error('Host bindings not available: config.getInt not implemented');
|
|
157
|
+
},
|
|
158
|
+
getFloat: (_key) => {
|
|
159
|
+
throw new Error('Host bindings not available: config.getFloat not implemented');
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
keyvalue: {
|
|
163
|
+
get: (_key) => {
|
|
164
|
+
throw new Error('Host bindings not available: keyvalue.get not implemented');
|
|
165
|
+
},
|
|
166
|
+
getString: (_key) => {
|
|
167
|
+
throw new Error('Host bindings not available: keyvalue.getString not implemented');
|
|
168
|
+
},
|
|
169
|
+
set: (_key, _value) => {
|
|
170
|
+
throw new Error('Host bindings not available: keyvalue.set not implemented');
|
|
171
|
+
},
|
|
172
|
+
setString: (_key, _value) => {
|
|
173
|
+
throw new Error('Host bindings not available: keyvalue.setString not implemented');
|
|
174
|
+
},
|
|
175
|
+
setWithTtl: (_key, _value, _ttl) => {
|
|
176
|
+
throw new Error('Host bindings not available: keyvalue.setWithTtl not implemented');
|
|
177
|
+
},
|
|
178
|
+
delete: (_key) => {
|
|
179
|
+
throw new Error('Host bindings not available: keyvalue.delete not implemented');
|
|
180
|
+
},
|
|
181
|
+
exists: (_key) => {
|
|
182
|
+
throw new Error('Host bindings not available: keyvalue.exists not implemented');
|
|
183
|
+
},
|
|
184
|
+
listKeys: (_prefix) => {
|
|
185
|
+
throw new Error('Host bindings not available: keyvalue.listKeys not implemented');
|
|
186
|
+
},
|
|
187
|
+
increment: (_key, _delta) => {
|
|
188
|
+
throw new Error('Host bindings not available: keyvalue.increment not implemented');
|
|
189
|
+
},
|
|
190
|
+
compareAndSwap: (_key, _expected, _newValue) => {
|
|
191
|
+
throw new Error('Host bindings not available: keyvalue.compareAndSwap not implemented');
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
logging: {
|
|
195
|
+
log: (_level, _message) => {
|
|
196
|
+
throw new Error('Host bindings not available: logging.log not implemented');
|
|
197
|
+
},
|
|
198
|
+
logStructured: (_level, _message, _fields) => {
|
|
199
|
+
throw new Error('Host bindings not available: logging.logStructured not implemented');
|
|
200
|
+
},
|
|
201
|
+
trace: (_message) => {
|
|
202
|
+
throw new Error('Host bindings not available: logging.trace not implemented');
|
|
203
|
+
},
|
|
204
|
+
debug: (_message) => {
|
|
205
|
+
throw new Error('Host bindings not available: logging.debug not implemented');
|
|
206
|
+
},
|
|
207
|
+
info: (_message) => {
|
|
208
|
+
throw new Error('Host bindings not available: logging.info not implemented');
|
|
209
|
+
},
|
|
210
|
+
warn: (_message) => {
|
|
211
|
+
throw new Error('Host bindings not available: logging.warn not implemented');
|
|
212
|
+
},
|
|
213
|
+
error: (_message) => {
|
|
214
|
+
throw new Error('Host bindings not available: logging.error not implemented');
|
|
215
|
+
},
|
|
216
|
+
isEnabled: (_level) => {
|
|
217
|
+
throw new Error('Host bindings not available: logging.isEnabled not implemented');
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
secrets: {
|
|
221
|
+
get: (_name) => {
|
|
222
|
+
throw new Error('Host bindings not available: secrets.get not implemented');
|
|
223
|
+
},
|
|
224
|
+
getRequired: (_name) => {
|
|
225
|
+
throw new Error('Host bindings not available: secrets.getRequired not implemented');
|
|
226
|
+
},
|
|
227
|
+
exists: (_name) => {
|
|
228
|
+
throw new Error('Host bindings not available: secrets.exists not implemented');
|
|
229
|
+
},
|
|
230
|
+
listNames: () => {
|
|
231
|
+
throw new Error('Host bindings not available: secrets.listNames not implemented');
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
metrics: {
|
|
235
|
+
counterInc: (_name, _value) => {
|
|
236
|
+
throw new Error('Host bindings not available: metrics.counterInc not implemented');
|
|
237
|
+
},
|
|
238
|
+
counterIncLabeled: (_name, _value, _labels) => {
|
|
239
|
+
throw new Error('Host bindings not available: metrics.counterIncLabeled not implemented');
|
|
240
|
+
},
|
|
241
|
+
gaugeSet: (_name, _value) => {
|
|
242
|
+
throw new Error('Host bindings not available: metrics.gaugeSet not implemented');
|
|
243
|
+
},
|
|
244
|
+
gaugeSetLabeled: (_name, _value, _labels) => {
|
|
245
|
+
throw new Error('Host bindings not available: metrics.gaugeSetLabeled not implemented');
|
|
246
|
+
},
|
|
247
|
+
gaugeAdd: (_name, _delta) => {
|
|
248
|
+
throw new Error('Host bindings not available: metrics.gaugeAdd not implemented');
|
|
249
|
+
},
|
|
250
|
+
histogramObserve: (_name, _value) => {
|
|
251
|
+
throw new Error('Host bindings not available: metrics.histogramObserve not implemented');
|
|
252
|
+
},
|
|
253
|
+
histogramObserveLabeled: (_name, _value, _labels) => {
|
|
254
|
+
throw new Error('Host bindings not available: metrics.histogramObserveLabeled not implemented');
|
|
255
|
+
},
|
|
256
|
+
recordDuration: (_name, _durationNs) => {
|
|
257
|
+
throw new Error('Host bindings not available: metrics.recordDuration not implemented');
|
|
258
|
+
},
|
|
259
|
+
recordDurationLabeled: (_name, _durationNs, _labels) => {
|
|
260
|
+
throw new Error('Host bindings not available: metrics.recordDurationLabeled not implemented');
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
// =============================================================================
|
|
265
|
+
// Configuration Helpers
|
|
266
|
+
// =============================================================================
|
|
267
|
+
/**
|
|
268
|
+
* Get a configuration value by key.
|
|
269
|
+
*
|
|
270
|
+
* @param key - The configuration key to retrieve
|
|
271
|
+
* @returns The configuration value, or undefined if not found
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```typescript
|
|
275
|
+
* const dbHost = getConfig('database.host');
|
|
276
|
+
* if (dbHost) {
|
|
277
|
+
* console.log(`Database host: ${dbHost}`);
|
|
278
|
+
* }
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
export function getConfig(key) {
|
|
282
|
+
return hostBindings.config.get(key);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Get a required configuration value by key.
|
|
286
|
+
* Throws ConfigError if the key does not exist.
|
|
287
|
+
*
|
|
288
|
+
* @param key - The configuration key to retrieve
|
|
289
|
+
* @returns The configuration value
|
|
290
|
+
* @throws {ConfigError} If the configuration key does not exist
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```typescript
|
|
294
|
+
* try {
|
|
295
|
+
* const apiKey = getConfigRequired('api.key');
|
|
296
|
+
* } catch (e) {
|
|
297
|
+
* if (e instanceof ConfigError) {
|
|
298
|
+
* log.error(`Missing config: ${e.key}`);
|
|
299
|
+
* }
|
|
300
|
+
* }
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
export function getConfigRequired(key) {
|
|
304
|
+
const value = hostBindings.config.get(key);
|
|
305
|
+
if (value === undefined) {
|
|
306
|
+
throw new ConfigError(key, `Required configuration key '${key}' not found`);
|
|
307
|
+
}
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Get a configuration value as a boolean.
|
|
312
|
+
* Recognizes: "true", "false", "1", "0", "yes", "no" (case-insensitive).
|
|
313
|
+
*
|
|
314
|
+
* @param key - The configuration key to retrieve
|
|
315
|
+
* @returns The boolean value, or undefined if not found or not parseable
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* ```typescript
|
|
319
|
+
* const debugMode = getConfigBool('debug.enabled') ?? false;
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
export function getConfigBool(key) {
|
|
323
|
+
return hostBindings.config.getBool(key);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Get a configuration value as an integer.
|
|
327
|
+
*
|
|
328
|
+
* @param key - The configuration key to retrieve
|
|
329
|
+
* @returns The integer value, or undefined if not found or not parseable
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```typescript
|
|
333
|
+
* const maxRetries = getConfigInt('http.max_retries') ?? 3;
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
export function getConfigInt(key) {
|
|
337
|
+
const value = hostBindings.config.getInt(key);
|
|
338
|
+
if (value === undefined) {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
return Number(value);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Get a configuration value as a float.
|
|
345
|
+
*
|
|
346
|
+
* @param key - The configuration key to retrieve
|
|
347
|
+
* @returns The float value, or undefined if not found or not parseable
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```typescript
|
|
351
|
+
* const timeout = getConfigFloat('http.timeout_seconds') ?? 30.0;
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
354
|
+
export function getConfigFloat(key) {
|
|
355
|
+
return hostBindings.config.getFloat(key);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Get multiple configuration values at once.
|
|
359
|
+
*
|
|
360
|
+
* @param keys - The configuration keys to retrieve
|
|
361
|
+
* @returns A Map of key to value for keys that exist
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* ```typescript
|
|
365
|
+
* const config = getConfigMany(['db.host', 'db.port', 'db.name']);
|
|
366
|
+
* const host = config.get('db.host') ?? 'localhost';
|
|
367
|
+
* ```
|
|
368
|
+
*/
|
|
369
|
+
export function getConfigMany(keys) {
|
|
370
|
+
const pairs = hostBindings.config.getMany(keys);
|
|
371
|
+
return new Map(pairs);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Get all configuration keys with a given prefix.
|
|
375
|
+
*
|
|
376
|
+
* @param prefix - The prefix to match (e.g., "database.")
|
|
377
|
+
* @returns A Map of key to value for matching keys
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* ```typescript
|
|
381
|
+
* const dbConfig = getConfigPrefix('database.');
|
|
382
|
+
* // Returns: Map { 'database.host' => 'localhost', 'database.port' => '5432' }
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
export function getConfigPrefix(prefix) {
|
|
386
|
+
const pairs = hostBindings.config.getPrefix(prefix);
|
|
387
|
+
return new Map(pairs);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Check if a configuration key exists.
|
|
391
|
+
*
|
|
392
|
+
* @param key - The configuration key to check
|
|
393
|
+
* @returns true if the key exists, false otherwise
|
|
394
|
+
*/
|
|
395
|
+
export function configExists(key) {
|
|
396
|
+
return hostBindings.config.exists(key);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Get all configuration as a JSON string.
|
|
400
|
+
* Useful for debugging or serialization.
|
|
401
|
+
*
|
|
402
|
+
* @returns JSON string of all configuration
|
|
403
|
+
*
|
|
404
|
+
* @example
|
|
405
|
+
* ```typescript
|
|
406
|
+
* log.debug(`All config: ${getAllConfig()}`);
|
|
407
|
+
* ```
|
|
408
|
+
*/
|
|
409
|
+
export function getAllConfig() {
|
|
410
|
+
const pairs = hostBindings.config.getPrefix('');
|
|
411
|
+
const obj = {};
|
|
412
|
+
for (const [key, value] of pairs) {
|
|
413
|
+
obj[key] = value;
|
|
414
|
+
}
|
|
415
|
+
return JSON.stringify(obj);
|
|
416
|
+
}
|
|
417
|
+
// =============================================================================
|
|
418
|
+
// Key-Value Storage Helpers
|
|
419
|
+
// =============================================================================
|
|
420
|
+
/**
|
|
421
|
+
* Construct a namespaced key from bucket and key.
|
|
422
|
+
*
|
|
423
|
+
* @param bucket - The bucket/namespace
|
|
424
|
+
* @param key - The key within the bucket
|
|
425
|
+
* @returns The full namespaced key
|
|
426
|
+
*/
|
|
427
|
+
function makeKey(bucket, key) {
|
|
428
|
+
return `${bucket}:${key}`;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Get a value from key-value storage as bytes.
|
|
432
|
+
*
|
|
433
|
+
* @param bucket - The bucket/namespace
|
|
434
|
+
* @param key - The key to retrieve
|
|
435
|
+
* @returns The value as bytes, or undefined if not found
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* ```typescript
|
|
439
|
+
* const data = kvGet('sessions', sessionId);
|
|
440
|
+
* if (data) {
|
|
441
|
+
* const session = JSON.parse(new TextDecoder().decode(data));
|
|
442
|
+
* }
|
|
443
|
+
* ```
|
|
444
|
+
*/
|
|
445
|
+
export function kvGet(bucket, key) {
|
|
446
|
+
return hostBindings.keyvalue.get(makeKey(bucket, key));
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Get a value from key-value storage as a string.
|
|
450
|
+
*
|
|
451
|
+
* @param bucket - The bucket/namespace
|
|
452
|
+
* @param key - The key to retrieve
|
|
453
|
+
* @returns The value as a string, or undefined if not found
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* ```typescript
|
|
457
|
+
* const username = kvGetString('users', `user:${userId}:name`);
|
|
458
|
+
* ```
|
|
459
|
+
*/
|
|
460
|
+
export function kvGetString(bucket, key) {
|
|
461
|
+
return hostBindings.keyvalue.getString(makeKey(bucket, key));
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Get a value from key-value storage as JSON.
|
|
465
|
+
*
|
|
466
|
+
* @param bucket - The bucket/namespace
|
|
467
|
+
* @param key - The key to retrieve
|
|
468
|
+
* @returns The parsed JSON value, or undefined if not found
|
|
469
|
+
*
|
|
470
|
+
* @example
|
|
471
|
+
* ```typescript
|
|
472
|
+
* interface User { name: string; email: string; }
|
|
473
|
+
* const user = kvGetJson<User>('users', userId);
|
|
474
|
+
* ```
|
|
475
|
+
*/
|
|
476
|
+
export function kvGetJson(bucket, key) {
|
|
477
|
+
const value = kvGetString(bucket, key);
|
|
478
|
+
if (value === undefined) {
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
return JSON.parse(value);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Set a value in key-value storage as bytes.
|
|
485
|
+
*
|
|
486
|
+
* @param bucket - The bucket/namespace
|
|
487
|
+
* @param key - The key to set
|
|
488
|
+
* @param value - The value as bytes
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* ```typescript
|
|
492
|
+
* const encoder = new TextEncoder();
|
|
493
|
+
* kvSet('sessions', sessionId, encoder.encode(JSON.stringify(session)));
|
|
494
|
+
* ```
|
|
495
|
+
*/
|
|
496
|
+
export function kvSet(bucket, key, value) {
|
|
497
|
+
hostBindings.keyvalue.set(makeKey(bucket, key), value);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Set a value in key-value storage as a string.
|
|
501
|
+
*
|
|
502
|
+
* @param bucket - The bucket/namespace
|
|
503
|
+
* @param key - The key to set
|
|
504
|
+
* @param value - The value as a string
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* ```typescript
|
|
508
|
+
* kvSetString('users', `user:${userId}:name`, 'John Doe');
|
|
509
|
+
* ```
|
|
510
|
+
*/
|
|
511
|
+
export function kvSetString(bucket, key, value) {
|
|
512
|
+
hostBindings.keyvalue.setString(makeKey(bucket, key), value);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Set a value in key-value storage as JSON.
|
|
516
|
+
*
|
|
517
|
+
* @param bucket - The bucket/namespace
|
|
518
|
+
* @param key - The key to set
|
|
519
|
+
* @param value - The value to serialize as JSON
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* ```typescript
|
|
523
|
+
* kvSetJson('users', userId, { name: 'John', email: 'john@example.com' });
|
|
524
|
+
* ```
|
|
525
|
+
*/
|
|
526
|
+
export function kvSetJson(bucket, key, value) {
|
|
527
|
+
kvSetString(bucket, key, JSON.stringify(value));
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Set a value in key-value storage with a TTL (time-to-live).
|
|
531
|
+
*
|
|
532
|
+
* @param bucket - The bucket/namespace
|
|
533
|
+
* @param key - The key to set
|
|
534
|
+
* @param value - The value as bytes
|
|
535
|
+
* @param ttlMs - Time-to-live in milliseconds
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* ```typescript
|
|
539
|
+
* // Cache for 5 minutes
|
|
540
|
+
* kvSetWithTtl('cache', cacheKey, data, 5 * 60 * 1000);
|
|
541
|
+
* ```
|
|
542
|
+
*/
|
|
543
|
+
export function kvSetWithTtl(bucket, key, value, ttlMs) {
|
|
544
|
+
const ttlNs = BigInt(ttlMs) * BigInt(1_000_000);
|
|
545
|
+
hostBindings.keyvalue.setWithTtl(makeKey(bucket, key), value, ttlNs);
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Set a string value in key-value storage with a TTL.
|
|
549
|
+
*
|
|
550
|
+
* @param bucket - The bucket/namespace
|
|
551
|
+
* @param key - The key to set
|
|
552
|
+
* @param value - The value as a string
|
|
553
|
+
* @param ttlMs - Time-to-live in milliseconds
|
|
554
|
+
*/
|
|
555
|
+
export function kvSetStringWithTtl(bucket, key, value, ttlMs) {
|
|
556
|
+
const encoder = new TextEncoder();
|
|
557
|
+
kvSetWithTtl(bucket, key, encoder.encode(value), ttlMs);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Delete a key from key-value storage.
|
|
561
|
+
*
|
|
562
|
+
* @param bucket - The bucket/namespace
|
|
563
|
+
* @param key - The key to delete
|
|
564
|
+
* @returns true if the key was deleted, false if it didn't exist
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
567
|
+
* ```typescript
|
|
568
|
+
* kvDelete('sessions', sessionId);
|
|
569
|
+
* ```
|
|
570
|
+
*/
|
|
571
|
+
export function kvDelete(bucket, key) {
|
|
572
|
+
return hostBindings.keyvalue.delete(makeKey(bucket, key));
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* List all keys in a bucket with an optional prefix.
|
|
576
|
+
*
|
|
577
|
+
* @param bucket - The bucket/namespace
|
|
578
|
+
* @param prefix - Optional prefix to filter keys
|
|
579
|
+
* @returns Array of keys matching the prefix
|
|
580
|
+
*
|
|
581
|
+
* @example
|
|
582
|
+
* ```typescript
|
|
583
|
+
* const userKeys = kvKeys('users', 'user:');
|
|
584
|
+
* // Returns: ['user:1', 'user:2', 'user:3']
|
|
585
|
+
* ```
|
|
586
|
+
*/
|
|
587
|
+
export function kvKeys(bucket, prefix = '') {
|
|
588
|
+
const fullPrefix = makeKey(bucket, prefix);
|
|
589
|
+
const keys = hostBindings.keyvalue.listKeys(fullPrefix);
|
|
590
|
+
// Strip the bucket prefix from returned keys
|
|
591
|
+
const bucketPrefix = `${bucket}:`;
|
|
592
|
+
return keys.map((k) => k.startsWith(bucketPrefix) ? k.slice(bucketPrefix.length) : k);
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Check if a key exists in key-value storage.
|
|
596
|
+
*
|
|
597
|
+
* @param bucket - The bucket/namespace
|
|
598
|
+
* @param key - The key to check
|
|
599
|
+
* @returns true if the key exists, false otherwise
|
|
600
|
+
*/
|
|
601
|
+
export function kvExists(bucket, key) {
|
|
602
|
+
return hostBindings.keyvalue.exists(makeKey(bucket, key));
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Atomically increment a numeric value in key-value storage.
|
|
606
|
+
*
|
|
607
|
+
* @param bucket - The bucket/namespace
|
|
608
|
+
* @param key - The key to increment
|
|
609
|
+
* @param delta - The amount to increment by (default: 1)
|
|
610
|
+
* @returns The new value after incrementing
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* ```typescript
|
|
614
|
+
* const newCount = kvIncrement('counters', 'page_views', 1);
|
|
615
|
+
* ```
|
|
616
|
+
*/
|
|
617
|
+
export function kvIncrement(bucket, key, delta = 1) {
|
|
618
|
+
return hostBindings.keyvalue.increment(makeKey(bucket, key), BigInt(delta));
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Atomically compare and swap a value in key-value storage.
|
|
622
|
+
*
|
|
623
|
+
* @param bucket - The bucket/namespace
|
|
624
|
+
* @param key - The key to update
|
|
625
|
+
* @param expected - The expected current value (undefined if key shouldn't exist)
|
|
626
|
+
* @param newValue - The new value to set if expected matches
|
|
627
|
+
* @returns true if the swap succeeded, false if current value didn't match
|
|
628
|
+
*
|
|
629
|
+
* @example
|
|
630
|
+
* ```typescript
|
|
631
|
+
* // Optimistic locking
|
|
632
|
+
* const current = kvGet('locks', 'resource');
|
|
633
|
+
* const swapped = kvCompareAndSwap('locks', 'resource', current, newLockValue);
|
|
634
|
+
* if (!swapped) {
|
|
635
|
+
* throw new Error('Resource was modified by another process');
|
|
636
|
+
* }
|
|
637
|
+
* ```
|
|
638
|
+
*/
|
|
639
|
+
export function kvCompareAndSwap(bucket, key, expected, newValue) {
|
|
640
|
+
return hostBindings.keyvalue.compareAndSwap(makeKey(bucket, key), expected, newValue);
|
|
641
|
+
}
|
|
642
|
+
// =============================================================================
|
|
643
|
+
// Logging Helpers
|
|
644
|
+
// =============================================================================
|
|
645
|
+
/**
|
|
646
|
+
* Logging utilities for emitting structured logs to the host.
|
|
647
|
+
*/
|
|
648
|
+
export const log = {
|
|
649
|
+
/**
|
|
650
|
+
* Emit a trace-level log message.
|
|
651
|
+
* Use for finest-grained debugging information.
|
|
652
|
+
*
|
|
653
|
+
* @param msg - The log message
|
|
654
|
+
*/
|
|
655
|
+
trace: (msg) => {
|
|
656
|
+
hostBindings.logging.trace(msg);
|
|
657
|
+
},
|
|
658
|
+
/**
|
|
659
|
+
* Emit a debug-level log message.
|
|
660
|
+
* Use for debugging information.
|
|
661
|
+
*
|
|
662
|
+
* @param msg - The log message
|
|
663
|
+
*/
|
|
664
|
+
debug: (msg) => {
|
|
665
|
+
hostBindings.logging.debug(msg);
|
|
666
|
+
},
|
|
667
|
+
/**
|
|
668
|
+
* Emit an info-level log message.
|
|
669
|
+
* Use for informational messages about normal operation.
|
|
670
|
+
*
|
|
671
|
+
* @param msg - The log message
|
|
672
|
+
*/
|
|
673
|
+
info: (msg) => {
|
|
674
|
+
hostBindings.logging.info(msg);
|
|
675
|
+
},
|
|
676
|
+
/**
|
|
677
|
+
* Emit a warn-level log message.
|
|
678
|
+
* Use for warning messages about potential issues.
|
|
679
|
+
*
|
|
680
|
+
* @param msg - The log message
|
|
681
|
+
*/
|
|
682
|
+
warn: (msg) => {
|
|
683
|
+
hostBindings.logging.warn(msg);
|
|
684
|
+
},
|
|
685
|
+
/**
|
|
686
|
+
* Emit an error-level log message.
|
|
687
|
+
* Use for error messages.
|
|
688
|
+
*
|
|
689
|
+
* @param msg - The log message
|
|
690
|
+
*/
|
|
691
|
+
error: (msg) => {
|
|
692
|
+
hostBindings.logging.error(msg);
|
|
693
|
+
},
|
|
694
|
+
/**
|
|
695
|
+
* Emit a log message at the specified level.
|
|
696
|
+
*
|
|
697
|
+
* @param level - The log level
|
|
698
|
+
* @param msg - The log message
|
|
699
|
+
*/
|
|
700
|
+
log: (level, msg) => {
|
|
701
|
+
hostBindings.logging.log(level, msg);
|
|
702
|
+
},
|
|
703
|
+
/**
|
|
704
|
+
* Emit a structured log message with key-value fields.
|
|
705
|
+
*
|
|
706
|
+
* @param level - The log level
|
|
707
|
+
* @param msg - The log message
|
|
708
|
+
* @param fields - Additional structured fields
|
|
709
|
+
*
|
|
710
|
+
* @example
|
|
711
|
+
* ```typescript
|
|
712
|
+
* log.structured(LogLevel.Info, 'Request processed', {
|
|
713
|
+
* requestId: '123',
|
|
714
|
+
* duration: '45ms',
|
|
715
|
+
* status: '200',
|
|
716
|
+
* });
|
|
717
|
+
* ```
|
|
718
|
+
*/
|
|
719
|
+
structured: (level, msg, fields) => {
|
|
720
|
+
const kvFields = Object.entries(fields).map(([key, value]) => ({
|
|
721
|
+
key,
|
|
722
|
+
value,
|
|
723
|
+
}));
|
|
724
|
+
hostBindings.logging.logStructured(level, msg, kvFields);
|
|
725
|
+
},
|
|
726
|
+
/**
|
|
727
|
+
* Check if a log level is enabled.
|
|
728
|
+
* Useful for avoiding expensive log construction when the level is disabled.
|
|
729
|
+
*
|
|
730
|
+
* @param level - The log level to check
|
|
731
|
+
* @returns true if the level is enabled
|
|
732
|
+
*
|
|
733
|
+
* @example
|
|
734
|
+
* ```typescript
|
|
735
|
+
* if (log.isEnabled(LogLevel.Debug)) {
|
|
736
|
+
* log.debug(`Complex data: ${JSON.stringify(largeObject)}`);
|
|
737
|
+
* }
|
|
738
|
+
* ```
|
|
739
|
+
*/
|
|
740
|
+
isEnabled: (level) => {
|
|
741
|
+
return hostBindings.logging.isEnabled(level);
|
|
742
|
+
},
|
|
743
|
+
};
|
|
744
|
+
// =============================================================================
|
|
745
|
+
// Secrets Helpers
|
|
746
|
+
// =============================================================================
|
|
747
|
+
/**
|
|
748
|
+
* Get a secret by name.
|
|
749
|
+
*
|
|
750
|
+
* @param name - The secret name
|
|
751
|
+
* @returns The secret value, or undefined if not found
|
|
752
|
+
*
|
|
753
|
+
* @example
|
|
754
|
+
* ```typescript
|
|
755
|
+
* const apiKey = getSecret('external_api_key');
|
|
756
|
+
* ```
|
|
757
|
+
*/
|
|
758
|
+
export function getSecret(name) {
|
|
759
|
+
return hostBindings.secrets.get(name);
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Get a required secret by name.
|
|
763
|
+
* Throws SecretError if the secret does not exist.
|
|
764
|
+
*
|
|
765
|
+
* @param name - The secret name
|
|
766
|
+
* @returns The secret value
|
|
767
|
+
* @throws {SecretError} If the secret does not exist
|
|
768
|
+
*
|
|
769
|
+
* @example
|
|
770
|
+
* ```typescript
|
|
771
|
+
* const dbPassword = getSecretRequired('database_password');
|
|
772
|
+
* ```
|
|
773
|
+
*/
|
|
774
|
+
export function getSecretRequired(name) {
|
|
775
|
+
const value = hostBindings.secrets.get(name);
|
|
776
|
+
if (value === undefined) {
|
|
777
|
+
throw new SecretError(name, `Required secret '${name}' not found`);
|
|
778
|
+
}
|
|
779
|
+
return value;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Check if a secret exists.
|
|
783
|
+
*
|
|
784
|
+
* @param name - The secret name
|
|
785
|
+
* @returns true if the secret exists, false otherwise
|
|
786
|
+
*/
|
|
787
|
+
export function secretExists(name) {
|
|
788
|
+
return hostBindings.secrets.exists(name);
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* List all available secret names (not values).
|
|
792
|
+
* Useful for diagnostics without exposing sensitive data.
|
|
793
|
+
*
|
|
794
|
+
* @returns Array of secret names
|
|
795
|
+
*/
|
|
796
|
+
export function listSecretNames() {
|
|
797
|
+
return hostBindings.secrets.listNames();
|
|
798
|
+
}
|
|
799
|
+
// =============================================================================
|
|
800
|
+
// Metrics Helpers
|
|
801
|
+
// =============================================================================
|
|
802
|
+
/**
|
|
803
|
+
* Metrics utilities for emitting observability data to the host.
|
|
804
|
+
*/
|
|
805
|
+
export const metrics = {
|
|
806
|
+
/**
|
|
807
|
+
* Increment a counter metric.
|
|
808
|
+
*
|
|
809
|
+
* @param name - The metric name
|
|
810
|
+
* @param value - The amount to increment by (default: 1)
|
|
811
|
+
*
|
|
812
|
+
* @example
|
|
813
|
+
* ```typescript
|
|
814
|
+
* metrics.counterInc('requests_total');
|
|
815
|
+
* metrics.counterInc('bytes_processed', byteCount);
|
|
816
|
+
* ```
|
|
817
|
+
*/
|
|
818
|
+
counterInc: (name, value = 1) => {
|
|
819
|
+
hostBindings.metrics.counterInc(name, BigInt(value));
|
|
820
|
+
},
|
|
821
|
+
/**
|
|
822
|
+
* Increment a counter metric with labels.
|
|
823
|
+
*
|
|
824
|
+
* @param name - The metric name
|
|
825
|
+
* @param value - The amount to increment by
|
|
826
|
+
* @param labels - The metric labels
|
|
827
|
+
*
|
|
828
|
+
* @example
|
|
829
|
+
* ```typescript
|
|
830
|
+
* metrics.counterIncLabeled('http_requests_total', 1, {
|
|
831
|
+
* method: 'GET',
|
|
832
|
+
* path: '/api/users',
|
|
833
|
+
* status: '200',
|
|
834
|
+
* });
|
|
835
|
+
* ```
|
|
836
|
+
*/
|
|
837
|
+
counterIncLabeled: (name, value, labels) => {
|
|
838
|
+
const kvLabels = Object.entries(labels).map(([key, val]) => ({
|
|
839
|
+
key,
|
|
840
|
+
value: val,
|
|
841
|
+
}));
|
|
842
|
+
hostBindings.metrics.counterIncLabeled(name, BigInt(value), kvLabels);
|
|
843
|
+
},
|
|
844
|
+
/**
|
|
845
|
+
* Set a gauge metric to a value.
|
|
846
|
+
*
|
|
847
|
+
* @param name - The metric name
|
|
848
|
+
* @param value - The value to set
|
|
849
|
+
*
|
|
850
|
+
* @example
|
|
851
|
+
* ```typescript
|
|
852
|
+
* metrics.gaugeSet('active_connections', connectionCount);
|
|
853
|
+
* ```
|
|
854
|
+
*/
|
|
855
|
+
gaugeSet: (name, value) => {
|
|
856
|
+
hostBindings.metrics.gaugeSet(name, value);
|
|
857
|
+
},
|
|
858
|
+
/**
|
|
859
|
+
* Set a gauge metric with labels.
|
|
860
|
+
*
|
|
861
|
+
* @param name - The metric name
|
|
862
|
+
* @param value - The value to set
|
|
863
|
+
* @param labels - The metric labels
|
|
864
|
+
*/
|
|
865
|
+
gaugeSetLabeled: (name, value, labels) => {
|
|
866
|
+
const kvLabels = Object.entries(labels).map(([key, val]) => ({
|
|
867
|
+
key,
|
|
868
|
+
value: val,
|
|
869
|
+
}));
|
|
870
|
+
hostBindings.metrics.gaugeSetLabeled(name, value, kvLabels);
|
|
871
|
+
},
|
|
872
|
+
/**
|
|
873
|
+
* Add to a gauge value (can be negative).
|
|
874
|
+
*
|
|
875
|
+
* @param name - The metric name
|
|
876
|
+
* @param delta - The amount to add (can be negative)
|
|
877
|
+
*/
|
|
878
|
+
gaugeAdd: (name, delta) => {
|
|
879
|
+
hostBindings.metrics.gaugeAdd(name, delta);
|
|
880
|
+
},
|
|
881
|
+
/**
|
|
882
|
+
* Record a histogram observation.
|
|
883
|
+
*
|
|
884
|
+
* @param name - The metric name
|
|
885
|
+
* @param value - The observed value
|
|
886
|
+
*
|
|
887
|
+
* @example
|
|
888
|
+
* ```typescript
|
|
889
|
+
* metrics.histogramObserve('request_duration_seconds', 0.045);
|
|
890
|
+
* ```
|
|
891
|
+
*/
|
|
892
|
+
histogramObserve: (name, value) => {
|
|
893
|
+
hostBindings.metrics.histogramObserve(name, value);
|
|
894
|
+
},
|
|
895
|
+
/**
|
|
896
|
+
* Record a histogram observation with labels.
|
|
897
|
+
*
|
|
898
|
+
* @param name - The metric name
|
|
899
|
+
* @param value - The observed value
|
|
900
|
+
* @param labels - The metric labels
|
|
901
|
+
*/
|
|
902
|
+
histogramObserveLabeled: (name, value, labels) => {
|
|
903
|
+
const kvLabels = Object.entries(labels).map(([key, val]) => ({
|
|
904
|
+
key,
|
|
905
|
+
value: val,
|
|
906
|
+
}));
|
|
907
|
+
hostBindings.metrics.histogramObserveLabeled(name, value, kvLabels);
|
|
908
|
+
},
|
|
909
|
+
/**
|
|
910
|
+
* Record request duration in milliseconds.
|
|
911
|
+
* Convenience method that converts to nanoseconds.
|
|
912
|
+
*
|
|
913
|
+
* @param name - The metric name
|
|
914
|
+
* @param durationMs - Duration in milliseconds
|
|
915
|
+
*/
|
|
916
|
+
recordDuration: (name, durationMs) => {
|
|
917
|
+
const durationNs = BigInt(Math.floor(durationMs * 1_000_000));
|
|
918
|
+
hostBindings.metrics.recordDuration(name, durationNs);
|
|
919
|
+
},
|
|
920
|
+
/**
|
|
921
|
+
* Record request duration with labels.
|
|
922
|
+
*
|
|
923
|
+
* @param name - The metric name
|
|
924
|
+
* @param durationMs - Duration in milliseconds
|
|
925
|
+
* @param labels - The metric labels
|
|
926
|
+
*/
|
|
927
|
+
recordDurationLabeled: (name, durationMs, labels) => {
|
|
928
|
+
const durationNs = BigInt(Math.floor(durationMs * 1_000_000));
|
|
929
|
+
const kvLabels = Object.entries(labels).map(([key, val]) => ({
|
|
930
|
+
key,
|
|
931
|
+
value: val,
|
|
932
|
+
}));
|
|
933
|
+
hostBindings.metrics.recordDurationLabeled(name, durationNs, kvLabels);
|
|
934
|
+
},
|
|
935
|
+
};
|
|
936
|
+
// =============================================================================
|
|
937
|
+
// Response Helpers
|
|
938
|
+
// =============================================================================
|
|
939
|
+
/**
|
|
940
|
+
* Create a successful response with JSON body.
|
|
941
|
+
*
|
|
942
|
+
* @param data - The data to serialize as JSON
|
|
943
|
+
* @param status - HTTP status code (default: 200)
|
|
944
|
+
* @param headers - Additional headers
|
|
945
|
+
* @returns A PluginResponse
|
|
946
|
+
*
|
|
947
|
+
* @example
|
|
948
|
+
* ```typescript
|
|
949
|
+
* return jsonResponse({ users: ['alice', 'bob'] });
|
|
950
|
+
* return jsonResponse({ error: 'Not found' }, 404);
|
|
951
|
+
* ```
|
|
952
|
+
*/
|
|
953
|
+
export function jsonResponse(data, status = 200, headers = {}) {
|
|
954
|
+
const body = JSON.stringify(data);
|
|
955
|
+
const encoder = new TextEncoder();
|
|
956
|
+
const responseHeaders = [
|
|
957
|
+
{ key: 'Content-Type', value: 'application/json' },
|
|
958
|
+
...Object.entries(headers).map(([key, value]) => ({ key, value })),
|
|
959
|
+
];
|
|
960
|
+
return {
|
|
961
|
+
status,
|
|
962
|
+
headers: responseHeaders,
|
|
963
|
+
body: encoder.encode(body),
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Create a text response.
|
|
968
|
+
*
|
|
969
|
+
* @param text - The response text
|
|
970
|
+
* @param status - HTTP status code (default: 200)
|
|
971
|
+
* @param contentType - Content type (default: text/plain)
|
|
972
|
+
* @returns A PluginResponse
|
|
973
|
+
*/
|
|
974
|
+
export function textResponse(text, status = 200, contentType = 'text/plain') {
|
|
975
|
+
const encoder = new TextEncoder();
|
|
976
|
+
return {
|
|
977
|
+
status,
|
|
978
|
+
headers: [{ key: 'Content-Type', value: contentType }],
|
|
979
|
+
body: encoder.encode(text),
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Create an HTML response.
|
|
984
|
+
*
|
|
985
|
+
* @param html - The HTML content
|
|
986
|
+
* @param status - HTTP status code (default: 200)
|
|
987
|
+
* @returns A PluginResponse
|
|
988
|
+
*/
|
|
989
|
+
export function htmlResponse(html, status = 200) {
|
|
990
|
+
return textResponse(html, status, 'text/html; charset=utf-8');
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Create a binary response.
|
|
994
|
+
*
|
|
995
|
+
* @param data - The binary data
|
|
996
|
+
* @param contentType - Content type
|
|
997
|
+
* @param status - HTTP status code (default: 200)
|
|
998
|
+
* @returns A PluginResponse
|
|
999
|
+
*/
|
|
1000
|
+
export function binaryResponse(data, contentType, status = 200) {
|
|
1001
|
+
return {
|
|
1002
|
+
status,
|
|
1003
|
+
headers: [{ key: 'Content-Type', value: contentType }],
|
|
1004
|
+
body: data,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Create an error response.
|
|
1009
|
+
*
|
|
1010
|
+
* @param message - The error message
|
|
1011
|
+
* @param status - HTTP status code (default: 500)
|
|
1012
|
+
* @returns A HandleResult with error response
|
|
1013
|
+
*/
|
|
1014
|
+
export function errorResponse(message, status = 500) {
|
|
1015
|
+
return {
|
|
1016
|
+
type: 'response',
|
|
1017
|
+
response: jsonResponse({ error: message }, status),
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Create a redirect response.
|
|
1022
|
+
*
|
|
1023
|
+
* @param location - The URL to redirect to
|
|
1024
|
+
* @param status - HTTP status code (default: 302)
|
|
1025
|
+
* @returns A PluginResponse
|
|
1026
|
+
*/
|
|
1027
|
+
export function redirectResponse(location, status = 302) {
|
|
1028
|
+
return {
|
|
1029
|
+
status,
|
|
1030
|
+
headers: [{ key: 'Location', value: location }],
|
|
1031
|
+
body: new Uint8Array(0),
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Create a pass-through result.
|
|
1036
|
+
* Use this when the plugin doesn't want to handle the request.
|
|
1037
|
+
*
|
|
1038
|
+
* @returns A HandleResult indicating pass-through
|
|
1039
|
+
*/
|
|
1040
|
+
export function passThrough() {
|
|
1041
|
+
return { type: 'passThrough' };
|
|
1042
|
+
}
|
|
1043
|
+
// =============================================================================
|
|
1044
|
+
// Request Helpers
|
|
1045
|
+
// =============================================================================
|
|
1046
|
+
/**
|
|
1047
|
+
* Get a header value from a request (case-insensitive).
|
|
1048
|
+
*
|
|
1049
|
+
* @param request - The plugin request
|
|
1050
|
+
* @param name - The header name
|
|
1051
|
+
* @returns The header value, or undefined if not found
|
|
1052
|
+
*/
|
|
1053
|
+
export function getHeader(request, name) {
|
|
1054
|
+
const lowerName = name.toLowerCase();
|
|
1055
|
+
const header = request.headers.find((h) => h.key.toLowerCase() === lowerName);
|
|
1056
|
+
return header?.value;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Get all values for a header (case-insensitive).
|
|
1060
|
+
*
|
|
1061
|
+
* @param request - The plugin request
|
|
1062
|
+
* @param name - The header name
|
|
1063
|
+
* @returns Array of header values
|
|
1064
|
+
*/
|
|
1065
|
+
export function getHeaders(request, name) {
|
|
1066
|
+
const lowerName = name.toLowerCase();
|
|
1067
|
+
return request.headers
|
|
1068
|
+
.filter((h) => h.key.toLowerCase() === lowerName)
|
|
1069
|
+
.map((h) => h.value);
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Parse the request body as JSON.
|
|
1073
|
+
*
|
|
1074
|
+
* @param request - The plugin request
|
|
1075
|
+
* @returns The parsed JSON body
|
|
1076
|
+
*/
|
|
1077
|
+
export function parseJsonBody(request) {
|
|
1078
|
+
const decoder = new TextDecoder();
|
|
1079
|
+
const text = decoder.decode(request.body);
|
|
1080
|
+
return JSON.parse(text);
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Get the request body as a string.
|
|
1084
|
+
*
|
|
1085
|
+
* @param request - The plugin request
|
|
1086
|
+
* @returns The body as a string
|
|
1087
|
+
*/
|
|
1088
|
+
export function getBodyString(request) {
|
|
1089
|
+
const decoder = new TextDecoder();
|
|
1090
|
+
return decoder.decode(request.body);
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Parse query string into a Map.
|
|
1094
|
+
*
|
|
1095
|
+
* @param request - The plugin request
|
|
1096
|
+
* @returns Map of query parameters
|
|
1097
|
+
*/
|
|
1098
|
+
export function parseQuery(request) {
|
|
1099
|
+
const params = new Map();
|
|
1100
|
+
if (!request.query) {
|
|
1101
|
+
return params;
|
|
1102
|
+
}
|
|
1103
|
+
const pairs = request.query.split('&');
|
|
1104
|
+
for (const pair of pairs) {
|
|
1105
|
+
const [key, value] = pair.split('=');
|
|
1106
|
+
if (key) {
|
|
1107
|
+
params.set(decodeURIComponent(key), decodeURIComponent(value ?? ''));
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return params;
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Get a context value from a request.
|
|
1114
|
+
*
|
|
1115
|
+
* @param request - The plugin request
|
|
1116
|
+
* @param key - The context key
|
|
1117
|
+
* @returns The context value, or undefined if not found
|
|
1118
|
+
*/
|
|
1119
|
+
export function getContext(request, key) {
|
|
1120
|
+
const ctx = request.context.find((c) => c.key === key);
|
|
1121
|
+
return ctx?.value;
|
|
1122
|
+
}
|
|
1123
|
+
// =============================================================================
|
|
1124
|
+
// Timing Helpers
|
|
1125
|
+
// =============================================================================
|
|
1126
|
+
/**
|
|
1127
|
+
* Measure the duration of an async operation and record it as a metric.
|
|
1128
|
+
*
|
|
1129
|
+
* @param name - The metric name
|
|
1130
|
+
* @param fn - The async function to measure
|
|
1131
|
+
* @returns The result of the function
|
|
1132
|
+
*
|
|
1133
|
+
* @example
|
|
1134
|
+
* ```typescript
|
|
1135
|
+
* const result = await timed('database_query', async () => {
|
|
1136
|
+
* return await db.query('SELECT * FROM users');
|
|
1137
|
+
* });
|
|
1138
|
+
* ```
|
|
1139
|
+
*/
|
|
1140
|
+
export async function timed(name, fn) {
|
|
1141
|
+
const start = Date.now();
|
|
1142
|
+
try {
|
|
1143
|
+
return await fn();
|
|
1144
|
+
}
|
|
1145
|
+
finally {
|
|
1146
|
+
const duration = Date.now() - start;
|
|
1147
|
+
metrics.recordDuration(name, duration);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Measure the duration of a sync operation and record it as a metric.
|
|
1152
|
+
*
|
|
1153
|
+
* @param name - The metric name
|
|
1154
|
+
* @param fn - The function to measure
|
|
1155
|
+
* @returns The result of the function
|
|
1156
|
+
*/
|
|
1157
|
+
export function timedSync(name, fn) {
|
|
1158
|
+
const start = Date.now();
|
|
1159
|
+
try {
|
|
1160
|
+
return fn();
|
|
1161
|
+
}
|
|
1162
|
+
finally {
|
|
1163
|
+
const duration = Date.now() - start;
|
|
1164
|
+
metrics.recordDuration(name, duration);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
// =============================================================================
|
|
1168
|
+
// Version Helpers
|
|
1169
|
+
// =============================================================================
|
|
1170
|
+
/**
|
|
1171
|
+
* Create a Version object from a semver string.
|
|
1172
|
+
*
|
|
1173
|
+
* @param semver - The semver string (e.g., "1.2.3" or "1.2.3-beta.1")
|
|
1174
|
+
* @returns A Version object
|
|
1175
|
+
*/
|
|
1176
|
+
export function parseVersion(semver) {
|
|
1177
|
+
const [versionPart, preRelease] = semver.split('-', 2);
|
|
1178
|
+
const [major, minor, patch] = versionPart.split('.').map(Number);
|
|
1179
|
+
return {
|
|
1180
|
+
major: major ?? 0,
|
|
1181
|
+
minor: minor ?? 0,
|
|
1182
|
+
patch: patch ?? 0,
|
|
1183
|
+
preRelease,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Format a Version object as a semver string.
|
|
1188
|
+
*
|
|
1189
|
+
* @param version - The Version object
|
|
1190
|
+
* @returns The semver string
|
|
1191
|
+
*/
|
|
1192
|
+
export function formatVersion(version) {
|
|
1193
|
+
const base = `${version.major}.${version.minor}.${version.patch}`;
|
|
1194
|
+
return version.preRelease ? `${base}-${version.preRelease}` : base;
|
|
1195
|
+
}
|
|
1196
|
+
// =============================================================================
|
|
1197
|
+
// Plugin Registration Helper
|
|
1198
|
+
// =============================================================================
|
|
1199
|
+
/**
|
|
1200
|
+
* Register a plugin implementation.
|
|
1201
|
+
* This is a convenience function for setting up plugin exports.
|
|
1202
|
+
*
|
|
1203
|
+
* @param plugin - The plugin implementation
|
|
1204
|
+
* @returns The plugin for export
|
|
1205
|
+
*
|
|
1206
|
+
* @example
|
|
1207
|
+
* ```typescript
|
|
1208
|
+
* export default registerPlugin({
|
|
1209
|
+
* info() {
|
|
1210
|
+
* return {
|
|
1211
|
+
* id: 'my-org:my-plugin',
|
|
1212
|
+
* name: 'My Plugin',
|
|
1213
|
+
* version: { major: 1, minor: 0, patch: 0 },
|
|
1214
|
+
* description: 'A sample plugin',
|
|
1215
|
+
* author: 'My Org',
|
|
1216
|
+
* };
|
|
1217
|
+
* },
|
|
1218
|
+
* handle(request) {
|
|
1219
|
+
* return { type: 'response', response: jsonResponse({ hello: 'world' }) };
|
|
1220
|
+
* },
|
|
1221
|
+
* });
|
|
1222
|
+
* ```
|
|
1223
|
+
*/
|
|
1224
|
+
export function registerPlugin(plugin) {
|
|
1225
|
+
return plugin;
|
|
1226
|
+
}
|
|
1227
|
+
//# sourceMappingURL=index.js.map
|