@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/src/index.ts ADDED
@@ -0,0 +1,1565 @@
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
+ // =============================================================================
12
+ // Version
13
+ // =============================================================================
14
+
15
+ /**
16
+ * The current version of the ZLayer SDK.
17
+ */
18
+ export const VERSION = '0.1.0';
19
+
20
+ // =============================================================================
21
+ // Error Classes
22
+ // =============================================================================
23
+
24
+ /**
25
+ * Error thrown when configuration operations fail.
26
+ */
27
+ export class ConfigError extends Error {
28
+ /** The configuration key that caused the error */
29
+ public readonly key: string;
30
+
31
+ constructor(key: string, message: string) {
32
+ super(message);
33
+ this.name = 'ConfigError';
34
+ this.key = key;
35
+ Object.setPrototypeOf(this, ConfigError.prototype);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Error thrown when key-value storage operations fail.
41
+ */
42
+ export class KVError extends Error {
43
+ /** The key that caused the error */
44
+ public readonly key: string;
45
+ /** The bucket where the error occurred */
46
+ public readonly bucket: string;
47
+ /** The error code from the host */
48
+ public readonly code: KVErrorCode;
49
+
50
+ constructor(bucket: string, key: string, code: KVErrorCode, message: string) {
51
+ super(message);
52
+ this.name = 'KVError';
53
+ this.bucket = bucket;
54
+ this.key = key;
55
+ this.code = code;
56
+ Object.setPrototypeOf(this, KVError.prototype);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Error codes for key-value operations.
62
+ */
63
+ export enum KVErrorCode {
64
+ /** Key not found */
65
+ NotFound = 'not_found',
66
+ /** Value too large */
67
+ ValueTooLarge = 'value_too_large',
68
+ /** Storage quota exceeded */
69
+ QuotaExceeded = 'quota_exceeded',
70
+ /** Key format invalid */
71
+ InvalidKey = 'invalid_key',
72
+ /** Generic storage error */
73
+ Storage = 'storage',
74
+ }
75
+
76
+ /**
77
+ * Error thrown when secret operations fail.
78
+ */
79
+ export class SecretError extends Error {
80
+ /** The secret name that caused the error */
81
+ public readonly secretName: string;
82
+
83
+ constructor(secretName: string, message: string) {
84
+ super(message);
85
+ this.name = 'SecretError';
86
+ this.secretName = secretName;
87
+ Object.setPrototypeOf(this, SecretError.prototype);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Error thrown when a required value is missing.
93
+ */
94
+ export class NotFoundError extends Error {
95
+ /** The identifier that was not found */
96
+ public readonly identifier: string;
97
+
98
+ constructor(identifier: string, message: string) {
99
+ super(message);
100
+ this.name = 'NotFoundError';
101
+ this.identifier = identifier;
102
+ Object.setPrototypeOf(this, NotFoundError.prototype);
103
+ }
104
+ }
105
+
106
+ // =============================================================================
107
+ // Log Level Enum
108
+ // =============================================================================
109
+
110
+ /**
111
+ * Log severity levels matching the WIT interface.
112
+ */
113
+ export enum LogLevel {
114
+ /** Finest-grained debugging information */
115
+ Trace = 0,
116
+ /** Debugging information */
117
+ Debug = 1,
118
+ /** Informational messages */
119
+ Info = 2,
120
+ /** Warning messages */
121
+ Warn = 3,
122
+ /** Error messages */
123
+ Error = 4,
124
+ }
125
+
126
+ // =============================================================================
127
+ // Type Definitions
128
+ // =============================================================================
129
+
130
+ /**
131
+ * A key-value pair used throughout the SDK.
132
+ */
133
+ export interface KeyValue {
134
+ key: string;
135
+ value: string;
136
+ }
137
+
138
+ /**
139
+ * HTTP methods supported by the plugin system.
140
+ */
141
+ export enum HttpMethod {
142
+ Get = 'GET',
143
+ Post = 'POST',
144
+ Put = 'PUT',
145
+ Delete = 'DELETE',
146
+ Patch = 'PATCH',
147
+ Head = 'HEAD',
148
+ Options = 'OPTIONS',
149
+ Connect = 'CONNECT',
150
+ Trace = 'TRACE',
151
+ }
152
+
153
+ /**
154
+ * Semantic version representation.
155
+ */
156
+ export interface Version {
157
+ major: number;
158
+ minor: number;
159
+ patch: number;
160
+ preRelease?: string;
161
+ }
162
+
163
+ /**
164
+ * Plugin information returned by the info() method.
165
+ */
166
+ export interface PluginInfo {
167
+ /** Unique plugin identifier (e.g., "zlayer:auth-jwt") */
168
+ id: string;
169
+ /** Human-readable name */
170
+ name: string;
171
+ /** Plugin version */
172
+ version: Version;
173
+ /** Brief description of plugin functionality */
174
+ description: string;
175
+ /** Plugin author or organization */
176
+ author: string;
177
+ /** License identifier (e.g., "MIT", "Apache-2.0") */
178
+ license?: string;
179
+ /** Homepage or repository URL */
180
+ homepage?: string;
181
+ /** Additional metadata as key-value pairs */
182
+ metadata?: KeyValue[];
183
+ }
184
+
185
+ /**
186
+ * Incoming request to be processed by a plugin.
187
+ */
188
+ export interface PluginRequest {
189
+ /** Unique request identifier for tracing */
190
+ requestId: string;
191
+ /** Request path (e.g., "/api/users/123") */
192
+ path: string;
193
+ /** HTTP method */
194
+ method: HttpMethod;
195
+ /** Query string (without leading ?) */
196
+ query?: string;
197
+ /** Request headers */
198
+ headers: KeyValue[];
199
+ /** Request body as bytes */
200
+ body: Uint8Array;
201
+ /** Request timestamp in nanoseconds since Unix epoch */
202
+ timestamp: bigint;
203
+ /** Additional context from the host */
204
+ context: KeyValue[];
205
+ }
206
+
207
+ /**
208
+ * Plugin response returned to the host.
209
+ */
210
+ export interface PluginResponse {
211
+ /** HTTP status code (200, 404, 500, etc.) */
212
+ status: number;
213
+ /** Response headers */
214
+ headers: KeyValue[];
215
+ /** Response body */
216
+ body: Uint8Array;
217
+ }
218
+
219
+ /**
220
+ * Result of handling a request.
221
+ */
222
+ export type HandleResult =
223
+ | { type: 'response'; response: PluginResponse }
224
+ | { type: 'passThrough' }
225
+ | { type: 'error'; message: string };
226
+
227
+ /**
228
+ * Plugin capabilities that can be requested.
229
+ */
230
+ export interface Capabilities {
231
+ config?: boolean;
232
+ keyvalue?: boolean;
233
+ logging?: boolean;
234
+ secrets?: boolean;
235
+ metrics?: boolean;
236
+ httpClient?: boolean;
237
+ }
238
+
239
+ /**
240
+ * Plugin initialization error types.
241
+ */
242
+ export type InitError =
243
+ | { type: 'configMissing'; key: string }
244
+ | { type: 'configInvalid'; key: string }
245
+ | { type: 'capabilityUnavailable'; capability: string }
246
+ | { type: 'failed'; message: string };
247
+
248
+ /**
249
+ * Authentication result from an authenticator plugin.
250
+ */
251
+ export type AuthResult =
252
+ | { type: 'authenticated'; claims: KeyValue[] }
253
+ | { type: 'denied'; reason: string }
254
+ | { type: 'challenge'; challenge: string }
255
+ | { type: 'skip' };
256
+
257
+ /**
258
+ * Rate limit information.
259
+ */
260
+ export interface RateLimitInfo {
261
+ /** Requests remaining in current window */
262
+ remaining: bigint;
263
+ /** Total limit for the window */
264
+ limit: bigint;
265
+ /** Time until limit resets (nanoseconds) */
266
+ resetAfter: bigint;
267
+ /** Retry after this duration if denied (nanoseconds) */
268
+ retryAfter?: bigint;
269
+ }
270
+
271
+ /**
272
+ * Rate limit decision.
273
+ */
274
+ export type RateLimitResult =
275
+ | { type: 'allowed'; info: RateLimitInfo }
276
+ | { type: 'denied'; info: RateLimitInfo };
277
+
278
+ // =============================================================================
279
+ // Plugin Interfaces
280
+ // =============================================================================
281
+
282
+ /**
283
+ * The main plugin handler interface that plugins must export.
284
+ */
285
+ export interface ZLayerPlugin {
286
+ /**
287
+ * Initialize the plugin.
288
+ * Called once when the plugin is loaded.
289
+ * @returns Capabilities the plugin requires, or an error
290
+ */
291
+ init?(): Capabilities | InitError;
292
+
293
+ /**
294
+ * Return plugin metadata.
295
+ * Called after successful initialization.
296
+ */
297
+ info(): PluginInfo;
298
+
299
+ /**
300
+ * Handle an incoming request.
301
+ * @param request The incoming request to process
302
+ * @returns The result of handling the request
303
+ */
304
+ handle(request: PluginRequest): HandleResult;
305
+
306
+ /**
307
+ * Graceful shutdown hook.
308
+ * Called when the plugin is being unloaded.
309
+ */
310
+ shutdown?(): void;
311
+ }
312
+
313
+ /**
314
+ * A simpler plugin interface for stateless transformations.
315
+ */
316
+ export interface TransformerPlugin {
317
+ /**
318
+ * Transform request headers before forwarding.
319
+ */
320
+ transformRequestHeaders?(headers: KeyValue[]): KeyValue[];
321
+
322
+ /**
323
+ * Transform response headers before returning.
324
+ */
325
+ transformResponseHeaders?(headers: KeyValue[]): KeyValue[];
326
+
327
+ /**
328
+ * Transform request body before forwarding.
329
+ */
330
+ transformRequestBody?(body: Uint8Array): Uint8Array;
331
+
332
+ /**
333
+ * Transform response body before returning.
334
+ */
335
+ transformResponseBody?(body: Uint8Array): Uint8Array;
336
+ }
337
+
338
+ /**
339
+ * Authentication plugin interface.
340
+ */
341
+ export interface AuthenticatorPlugin {
342
+ /**
343
+ * Authenticate an incoming request.
344
+ * @param request The request to authenticate
345
+ * @returns Authentication result with identity claims on success
346
+ */
347
+ authenticate(request: PluginRequest): AuthResult;
348
+
349
+ /**
350
+ * Validate a token (e.g., JWT, API key).
351
+ * @param token The token to validate
352
+ * @returns Claims if valid
353
+ * @throws Error if invalid
354
+ */
355
+ validateToken?(token: string): KeyValue[];
356
+ }
357
+
358
+ /**
359
+ * Rate limiting plugin interface.
360
+ */
361
+ export interface RateLimiterPlugin {
362
+ /**
363
+ * Check if request should be rate limited.
364
+ * @param request The request to check
365
+ * @returns Rate limit decision
366
+ */
367
+ check(request: PluginRequest): RateLimitResult;
368
+
369
+ /**
370
+ * Get current rate limit status for a key.
371
+ * @param key The rate limit key
372
+ * @returns Current status or undefined if not found
373
+ */
374
+ status?(key: string): RateLimitInfo | undefined;
375
+ }
376
+
377
+ // =============================================================================
378
+ // Host Bindings Stub
379
+ // =============================================================================
380
+
381
+ /**
382
+ * Stub for host bindings. In actual WASM execution, these are replaced
383
+ * by the generated WIT bindings that call into the host.
384
+ */
385
+ const hostBindings = {
386
+ config: {
387
+ get: (_key: string): string | undefined => {
388
+ throw new Error('Host bindings not available: config.get not implemented');
389
+ },
390
+ getRequired: (_key: string): string => {
391
+ throw new Error('Host bindings not available: config.getRequired not implemented');
392
+ },
393
+ getMany: (_keys: string[]): Array<[string, string]> => {
394
+ throw new Error('Host bindings not available: config.getMany not implemented');
395
+ },
396
+ getPrefix: (_prefix: string): Array<[string, string]> => {
397
+ throw new Error('Host bindings not available: config.getPrefix not implemented');
398
+ },
399
+ exists: (_key: string): boolean => {
400
+ throw new Error('Host bindings not available: config.exists not implemented');
401
+ },
402
+ getBool: (_key: string): boolean | undefined => {
403
+ throw new Error('Host bindings not available: config.getBool not implemented');
404
+ },
405
+ getInt: (_key: string): bigint | undefined => {
406
+ throw new Error('Host bindings not available: config.getInt not implemented');
407
+ },
408
+ getFloat: (_key: string): number | undefined => {
409
+ throw new Error('Host bindings not available: config.getFloat not implemented');
410
+ },
411
+ },
412
+ keyvalue: {
413
+ get: (_key: string): Uint8Array | undefined => {
414
+ throw new Error('Host bindings not available: keyvalue.get not implemented');
415
+ },
416
+ getString: (_key: string): string | undefined => {
417
+ throw new Error('Host bindings not available: keyvalue.getString not implemented');
418
+ },
419
+ set: (_key: string, _value: Uint8Array): void => {
420
+ throw new Error('Host bindings not available: keyvalue.set not implemented');
421
+ },
422
+ setString: (_key: string, _value: string): void => {
423
+ throw new Error('Host bindings not available: keyvalue.setString not implemented');
424
+ },
425
+ setWithTtl: (_key: string, _value: Uint8Array, _ttl: bigint): void => {
426
+ throw new Error('Host bindings not available: keyvalue.setWithTtl not implemented');
427
+ },
428
+ delete: (_key: string): boolean => {
429
+ throw new Error('Host bindings not available: keyvalue.delete not implemented');
430
+ },
431
+ exists: (_key: string): boolean => {
432
+ throw new Error('Host bindings not available: keyvalue.exists not implemented');
433
+ },
434
+ listKeys: (_prefix: string): string[] => {
435
+ throw new Error('Host bindings not available: keyvalue.listKeys not implemented');
436
+ },
437
+ increment: (_key: string, _delta: bigint): bigint => {
438
+ throw new Error('Host bindings not available: keyvalue.increment not implemented');
439
+ },
440
+ compareAndSwap: (_key: string, _expected: Uint8Array | undefined, _newValue: Uint8Array): boolean => {
441
+ throw new Error('Host bindings not available: keyvalue.compareAndSwap not implemented');
442
+ },
443
+ },
444
+ logging: {
445
+ log: (_level: LogLevel, _message: string): void => {
446
+ throw new Error('Host bindings not available: logging.log not implemented');
447
+ },
448
+ logStructured: (_level: LogLevel, _message: string, _fields: KeyValue[]): void => {
449
+ throw new Error('Host bindings not available: logging.logStructured not implemented');
450
+ },
451
+ trace: (_message: string): void => {
452
+ throw new Error('Host bindings not available: logging.trace not implemented');
453
+ },
454
+ debug: (_message: string): void => {
455
+ throw new Error('Host bindings not available: logging.debug not implemented');
456
+ },
457
+ info: (_message: string): void => {
458
+ throw new Error('Host bindings not available: logging.info not implemented');
459
+ },
460
+ warn: (_message: string): void => {
461
+ throw new Error('Host bindings not available: logging.warn not implemented');
462
+ },
463
+ error: (_message: string): void => {
464
+ throw new Error('Host bindings not available: logging.error not implemented');
465
+ },
466
+ isEnabled: (_level: LogLevel): boolean => {
467
+ throw new Error('Host bindings not available: logging.isEnabled not implemented');
468
+ },
469
+ },
470
+ secrets: {
471
+ get: (_name: string): string | undefined => {
472
+ throw new Error('Host bindings not available: secrets.get not implemented');
473
+ },
474
+ getRequired: (_name: string): string => {
475
+ throw new Error('Host bindings not available: secrets.getRequired not implemented');
476
+ },
477
+ exists: (_name: string): boolean => {
478
+ throw new Error('Host bindings not available: secrets.exists not implemented');
479
+ },
480
+ listNames: (): string[] => {
481
+ throw new Error('Host bindings not available: secrets.listNames not implemented');
482
+ },
483
+ },
484
+ metrics: {
485
+ counterInc: (_name: string, _value: bigint): void => {
486
+ throw new Error('Host bindings not available: metrics.counterInc not implemented');
487
+ },
488
+ counterIncLabeled: (_name: string, _value: bigint, _labels: KeyValue[]): void => {
489
+ throw new Error('Host bindings not available: metrics.counterIncLabeled not implemented');
490
+ },
491
+ gaugeSet: (_name: string, _value: number): void => {
492
+ throw new Error('Host bindings not available: metrics.gaugeSet not implemented');
493
+ },
494
+ gaugeSetLabeled: (_name: string, _value: number, _labels: KeyValue[]): void => {
495
+ throw new Error('Host bindings not available: metrics.gaugeSetLabeled not implemented');
496
+ },
497
+ gaugeAdd: (_name: string, _delta: number): void => {
498
+ throw new Error('Host bindings not available: metrics.gaugeAdd not implemented');
499
+ },
500
+ histogramObserve: (_name: string, _value: number): void => {
501
+ throw new Error('Host bindings not available: metrics.histogramObserve not implemented');
502
+ },
503
+ histogramObserveLabeled: (_name: string, _value: number, _labels: KeyValue[]): void => {
504
+ throw new Error('Host bindings not available: metrics.histogramObserveLabeled not implemented');
505
+ },
506
+ recordDuration: (_name: string, _durationNs: bigint): void => {
507
+ throw new Error('Host bindings not available: metrics.recordDuration not implemented');
508
+ },
509
+ recordDurationLabeled: (_name: string, _durationNs: bigint, _labels: KeyValue[]): void => {
510
+ throw new Error('Host bindings not available: metrics.recordDurationLabeled not implemented');
511
+ },
512
+ },
513
+ };
514
+
515
+ // =============================================================================
516
+ // Configuration Helpers
517
+ // =============================================================================
518
+
519
+ /**
520
+ * Get a configuration value by key.
521
+ *
522
+ * @param key - The configuration key to retrieve
523
+ * @returns The configuration value, or undefined if not found
524
+ *
525
+ * @example
526
+ * ```typescript
527
+ * const dbHost = getConfig('database.host');
528
+ * if (dbHost) {
529
+ * console.log(`Database host: ${dbHost}`);
530
+ * }
531
+ * ```
532
+ */
533
+ export function getConfig(key: string): string | undefined {
534
+ return hostBindings.config.get(key);
535
+ }
536
+
537
+ /**
538
+ * Get a required configuration value by key.
539
+ * Throws ConfigError if the key does not exist.
540
+ *
541
+ * @param key - The configuration key to retrieve
542
+ * @returns The configuration value
543
+ * @throws {ConfigError} If the configuration key does not exist
544
+ *
545
+ * @example
546
+ * ```typescript
547
+ * try {
548
+ * const apiKey = getConfigRequired('api.key');
549
+ * } catch (e) {
550
+ * if (e instanceof ConfigError) {
551
+ * log.error(`Missing config: ${e.key}`);
552
+ * }
553
+ * }
554
+ * ```
555
+ */
556
+ export function getConfigRequired(key: string): string {
557
+ const value = hostBindings.config.get(key);
558
+ if (value === undefined) {
559
+ throw new ConfigError(key, `Required configuration key '${key}' not found`);
560
+ }
561
+ return value;
562
+ }
563
+
564
+ /**
565
+ * Get a configuration value as a boolean.
566
+ * Recognizes: "true", "false", "1", "0", "yes", "no" (case-insensitive).
567
+ *
568
+ * @param key - The configuration key to retrieve
569
+ * @returns The boolean value, or undefined if not found or not parseable
570
+ *
571
+ * @example
572
+ * ```typescript
573
+ * const debugMode = getConfigBool('debug.enabled') ?? false;
574
+ * ```
575
+ */
576
+ export function getConfigBool(key: string): boolean | undefined {
577
+ return hostBindings.config.getBool(key);
578
+ }
579
+
580
+ /**
581
+ * Get a configuration value as an integer.
582
+ *
583
+ * @param key - The configuration key to retrieve
584
+ * @returns The integer value, or undefined if not found or not parseable
585
+ *
586
+ * @example
587
+ * ```typescript
588
+ * const maxRetries = getConfigInt('http.max_retries') ?? 3;
589
+ * ```
590
+ */
591
+ export function getConfigInt(key: string): number | undefined {
592
+ const value = hostBindings.config.getInt(key);
593
+ if (value === undefined) {
594
+ return undefined;
595
+ }
596
+ return Number(value);
597
+ }
598
+
599
+ /**
600
+ * Get a configuration value as a float.
601
+ *
602
+ * @param key - The configuration key to retrieve
603
+ * @returns The float value, or undefined if not found or not parseable
604
+ *
605
+ * @example
606
+ * ```typescript
607
+ * const timeout = getConfigFloat('http.timeout_seconds') ?? 30.0;
608
+ * ```
609
+ */
610
+ export function getConfigFloat(key: string): number | undefined {
611
+ return hostBindings.config.getFloat(key);
612
+ }
613
+
614
+ /**
615
+ * Get multiple configuration values at once.
616
+ *
617
+ * @param keys - The configuration keys to retrieve
618
+ * @returns A Map of key to value for keys that exist
619
+ *
620
+ * @example
621
+ * ```typescript
622
+ * const config = getConfigMany(['db.host', 'db.port', 'db.name']);
623
+ * const host = config.get('db.host') ?? 'localhost';
624
+ * ```
625
+ */
626
+ export function getConfigMany(keys: string[]): Map<string, string> {
627
+ const pairs = hostBindings.config.getMany(keys);
628
+ return new Map(pairs);
629
+ }
630
+
631
+ /**
632
+ * Get all configuration keys with a given prefix.
633
+ *
634
+ * @param prefix - The prefix to match (e.g., "database.")
635
+ * @returns A Map of key to value for matching keys
636
+ *
637
+ * @example
638
+ * ```typescript
639
+ * const dbConfig = getConfigPrefix('database.');
640
+ * // Returns: Map { 'database.host' => 'localhost', 'database.port' => '5432' }
641
+ * ```
642
+ */
643
+ export function getConfigPrefix(prefix: string): Map<string, string> {
644
+ const pairs = hostBindings.config.getPrefix(prefix);
645
+ return new Map(pairs);
646
+ }
647
+
648
+ /**
649
+ * Check if a configuration key exists.
650
+ *
651
+ * @param key - The configuration key to check
652
+ * @returns true if the key exists, false otherwise
653
+ */
654
+ export function configExists(key: string): boolean {
655
+ return hostBindings.config.exists(key);
656
+ }
657
+
658
+ /**
659
+ * Get all configuration as a JSON string.
660
+ * Useful for debugging or serialization.
661
+ *
662
+ * @returns JSON string of all configuration
663
+ *
664
+ * @example
665
+ * ```typescript
666
+ * log.debug(`All config: ${getAllConfig()}`);
667
+ * ```
668
+ */
669
+ export function getAllConfig(): string {
670
+ const pairs = hostBindings.config.getPrefix('');
671
+ const obj: Record<string, string> = {};
672
+ for (const [key, value] of pairs) {
673
+ obj[key] = value;
674
+ }
675
+ return JSON.stringify(obj);
676
+ }
677
+
678
+ // =============================================================================
679
+ // Key-Value Storage Helpers
680
+ // =============================================================================
681
+
682
+ /**
683
+ * Construct a namespaced key from bucket and key.
684
+ *
685
+ * @param bucket - The bucket/namespace
686
+ * @param key - The key within the bucket
687
+ * @returns The full namespaced key
688
+ */
689
+ function makeKey(bucket: string, key: string): string {
690
+ return `${bucket}:${key}`;
691
+ }
692
+
693
+ /**
694
+ * Get a value from key-value storage as bytes.
695
+ *
696
+ * @param bucket - The bucket/namespace
697
+ * @param key - The key to retrieve
698
+ * @returns The value as bytes, or undefined if not found
699
+ *
700
+ * @example
701
+ * ```typescript
702
+ * const data = kvGet('sessions', sessionId);
703
+ * if (data) {
704
+ * const session = JSON.parse(new TextDecoder().decode(data));
705
+ * }
706
+ * ```
707
+ */
708
+ export function kvGet(bucket: string, key: string): Uint8Array | undefined {
709
+ return hostBindings.keyvalue.get(makeKey(bucket, key));
710
+ }
711
+
712
+ /**
713
+ * Get a value from key-value storage as a string.
714
+ *
715
+ * @param bucket - The bucket/namespace
716
+ * @param key - The key to retrieve
717
+ * @returns The value as a string, or undefined if not found
718
+ *
719
+ * @example
720
+ * ```typescript
721
+ * const username = kvGetString('users', `user:${userId}:name`);
722
+ * ```
723
+ */
724
+ export function kvGetString(bucket: string, key: string): string | undefined {
725
+ return hostBindings.keyvalue.getString(makeKey(bucket, key));
726
+ }
727
+
728
+ /**
729
+ * Get a value from key-value storage as JSON.
730
+ *
731
+ * @param bucket - The bucket/namespace
732
+ * @param key - The key to retrieve
733
+ * @returns The parsed JSON value, or undefined if not found
734
+ *
735
+ * @example
736
+ * ```typescript
737
+ * interface User { name: string; email: string; }
738
+ * const user = kvGetJson<User>('users', userId);
739
+ * ```
740
+ */
741
+ export function kvGetJson<T>(bucket: string, key: string): T | undefined {
742
+ const value = kvGetString(bucket, key);
743
+ if (value === undefined) {
744
+ return undefined;
745
+ }
746
+ return JSON.parse(value) as T;
747
+ }
748
+
749
+ /**
750
+ * Set a value in key-value storage as bytes.
751
+ *
752
+ * @param bucket - The bucket/namespace
753
+ * @param key - The key to set
754
+ * @param value - The value as bytes
755
+ *
756
+ * @example
757
+ * ```typescript
758
+ * const encoder = new TextEncoder();
759
+ * kvSet('sessions', sessionId, encoder.encode(JSON.stringify(session)));
760
+ * ```
761
+ */
762
+ export function kvSet(bucket: string, key: string, value: Uint8Array): void {
763
+ hostBindings.keyvalue.set(makeKey(bucket, key), value);
764
+ }
765
+
766
+ /**
767
+ * Set a value in key-value storage as a string.
768
+ *
769
+ * @param bucket - The bucket/namespace
770
+ * @param key - The key to set
771
+ * @param value - The value as a string
772
+ *
773
+ * @example
774
+ * ```typescript
775
+ * kvSetString('users', `user:${userId}:name`, 'John Doe');
776
+ * ```
777
+ */
778
+ export function kvSetString(bucket: string, key: string, value: string): void {
779
+ hostBindings.keyvalue.setString(makeKey(bucket, key), value);
780
+ }
781
+
782
+ /**
783
+ * Set a value in key-value storage as JSON.
784
+ *
785
+ * @param bucket - The bucket/namespace
786
+ * @param key - The key to set
787
+ * @param value - The value to serialize as JSON
788
+ *
789
+ * @example
790
+ * ```typescript
791
+ * kvSetJson('users', userId, { name: 'John', email: 'john@example.com' });
792
+ * ```
793
+ */
794
+ export function kvSetJson<T>(bucket: string, key: string, value: T): void {
795
+ kvSetString(bucket, key, JSON.stringify(value));
796
+ }
797
+
798
+ /**
799
+ * Set a value in key-value storage with a TTL (time-to-live).
800
+ *
801
+ * @param bucket - The bucket/namespace
802
+ * @param key - The key to set
803
+ * @param value - The value as bytes
804
+ * @param ttlMs - Time-to-live in milliseconds
805
+ *
806
+ * @example
807
+ * ```typescript
808
+ * // Cache for 5 minutes
809
+ * kvSetWithTtl('cache', cacheKey, data, 5 * 60 * 1000);
810
+ * ```
811
+ */
812
+ export function kvSetWithTtl(bucket: string, key: string, value: Uint8Array, ttlMs: number): void {
813
+ const ttlNs = BigInt(ttlMs) * BigInt(1_000_000);
814
+ hostBindings.keyvalue.setWithTtl(makeKey(bucket, key), value, ttlNs);
815
+ }
816
+
817
+ /**
818
+ * Set a string value in key-value storage with a TTL.
819
+ *
820
+ * @param bucket - The bucket/namespace
821
+ * @param key - The key to set
822
+ * @param value - The value as a string
823
+ * @param ttlMs - Time-to-live in milliseconds
824
+ */
825
+ export function kvSetStringWithTtl(bucket: string, key: string, value: string, ttlMs: number): void {
826
+ const encoder = new TextEncoder();
827
+ kvSetWithTtl(bucket, key, encoder.encode(value), ttlMs);
828
+ }
829
+
830
+ /**
831
+ * Delete a key from key-value storage.
832
+ *
833
+ * @param bucket - The bucket/namespace
834
+ * @param key - The key to delete
835
+ * @returns true if the key was deleted, false if it didn't exist
836
+ *
837
+ * @example
838
+ * ```typescript
839
+ * kvDelete('sessions', sessionId);
840
+ * ```
841
+ */
842
+ export function kvDelete(bucket: string, key: string): boolean {
843
+ return hostBindings.keyvalue.delete(makeKey(bucket, key));
844
+ }
845
+
846
+ /**
847
+ * List all keys in a bucket with an optional prefix.
848
+ *
849
+ * @param bucket - The bucket/namespace
850
+ * @param prefix - Optional prefix to filter keys
851
+ * @returns Array of keys matching the prefix
852
+ *
853
+ * @example
854
+ * ```typescript
855
+ * const userKeys = kvKeys('users', 'user:');
856
+ * // Returns: ['user:1', 'user:2', 'user:3']
857
+ * ```
858
+ */
859
+ export function kvKeys(bucket: string, prefix: string = ''): string[] {
860
+ const fullPrefix = makeKey(bucket, prefix);
861
+ const keys = hostBindings.keyvalue.listKeys(fullPrefix);
862
+ // Strip the bucket prefix from returned keys
863
+ const bucketPrefix = `${bucket}:`;
864
+ return keys.map((k) => k.startsWith(bucketPrefix) ? k.slice(bucketPrefix.length) : k);
865
+ }
866
+
867
+ /**
868
+ * Check if a key exists in key-value storage.
869
+ *
870
+ * @param bucket - The bucket/namespace
871
+ * @param key - The key to check
872
+ * @returns true if the key exists, false otherwise
873
+ */
874
+ export function kvExists(bucket: string, key: string): boolean {
875
+ return hostBindings.keyvalue.exists(makeKey(bucket, key));
876
+ }
877
+
878
+ /**
879
+ * Atomically increment a numeric value in key-value storage.
880
+ *
881
+ * @param bucket - The bucket/namespace
882
+ * @param key - The key to increment
883
+ * @param delta - The amount to increment by (default: 1)
884
+ * @returns The new value after incrementing
885
+ *
886
+ * @example
887
+ * ```typescript
888
+ * const newCount = kvIncrement('counters', 'page_views', 1);
889
+ * ```
890
+ */
891
+ export function kvIncrement(bucket: string, key: string, delta: number = 1): bigint {
892
+ return hostBindings.keyvalue.increment(makeKey(bucket, key), BigInt(delta));
893
+ }
894
+
895
+ /**
896
+ * Atomically compare and swap a value in key-value storage.
897
+ *
898
+ * @param bucket - The bucket/namespace
899
+ * @param key - The key to update
900
+ * @param expected - The expected current value (undefined if key shouldn't exist)
901
+ * @param newValue - The new value to set if expected matches
902
+ * @returns true if the swap succeeded, false if current value didn't match
903
+ *
904
+ * @example
905
+ * ```typescript
906
+ * // Optimistic locking
907
+ * const current = kvGet('locks', 'resource');
908
+ * const swapped = kvCompareAndSwap('locks', 'resource', current, newLockValue);
909
+ * if (!swapped) {
910
+ * throw new Error('Resource was modified by another process');
911
+ * }
912
+ * ```
913
+ */
914
+ export function kvCompareAndSwap(
915
+ bucket: string,
916
+ key: string,
917
+ expected: Uint8Array | undefined,
918
+ newValue: Uint8Array
919
+ ): boolean {
920
+ return hostBindings.keyvalue.compareAndSwap(makeKey(bucket, key), expected, newValue);
921
+ }
922
+
923
+ // =============================================================================
924
+ // Logging Helpers
925
+ // =============================================================================
926
+
927
+ /**
928
+ * Logging utilities for emitting structured logs to the host.
929
+ */
930
+ export const log = {
931
+ /**
932
+ * Emit a trace-level log message.
933
+ * Use for finest-grained debugging information.
934
+ *
935
+ * @param msg - The log message
936
+ */
937
+ trace: (msg: string): void => {
938
+ hostBindings.logging.trace(msg);
939
+ },
940
+
941
+ /**
942
+ * Emit a debug-level log message.
943
+ * Use for debugging information.
944
+ *
945
+ * @param msg - The log message
946
+ */
947
+ debug: (msg: string): void => {
948
+ hostBindings.logging.debug(msg);
949
+ },
950
+
951
+ /**
952
+ * Emit an info-level log message.
953
+ * Use for informational messages about normal operation.
954
+ *
955
+ * @param msg - The log message
956
+ */
957
+ info: (msg: string): void => {
958
+ hostBindings.logging.info(msg);
959
+ },
960
+
961
+ /**
962
+ * Emit a warn-level log message.
963
+ * Use for warning messages about potential issues.
964
+ *
965
+ * @param msg - The log message
966
+ */
967
+ warn: (msg: string): void => {
968
+ hostBindings.logging.warn(msg);
969
+ },
970
+
971
+ /**
972
+ * Emit an error-level log message.
973
+ * Use for error messages.
974
+ *
975
+ * @param msg - The log message
976
+ */
977
+ error: (msg: string): void => {
978
+ hostBindings.logging.error(msg);
979
+ },
980
+
981
+ /**
982
+ * Emit a log message at the specified level.
983
+ *
984
+ * @param level - The log level
985
+ * @param msg - The log message
986
+ */
987
+ log: (level: LogLevel, msg: string): void => {
988
+ hostBindings.logging.log(level, msg);
989
+ },
990
+
991
+ /**
992
+ * Emit a structured log message with key-value fields.
993
+ *
994
+ * @param level - The log level
995
+ * @param msg - The log message
996
+ * @param fields - Additional structured fields
997
+ *
998
+ * @example
999
+ * ```typescript
1000
+ * log.structured(LogLevel.Info, 'Request processed', {
1001
+ * requestId: '123',
1002
+ * duration: '45ms',
1003
+ * status: '200',
1004
+ * });
1005
+ * ```
1006
+ */
1007
+ structured: (level: LogLevel, msg: string, fields: Record<string, string>): void => {
1008
+ const kvFields: KeyValue[] = Object.entries(fields).map(([key, value]) => ({
1009
+ key,
1010
+ value,
1011
+ }));
1012
+ hostBindings.logging.logStructured(level, msg, kvFields);
1013
+ },
1014
+
1015
+ /**
1016
+ * Check if a log level is enabled.
1017
+ * Useful for avoiding expensive log construction when the level is disabled.
1018
+ *
1019
+ * @param level - The log level to check
1020
+ * @returns true if the level is enabled
1021
+ *
1022
+ * @example
1023
+ * ```typescript
1024
+ * if (log.isEnabled(LogLevel.Debug)) {
1025
+ * log.debug(`Complex data: ${JSON.stringify(largeObject)}`);
1026
+ * }
1027
+ * ```
1028
+ */
1029
+ isEnabled: (level: LogLevel): boolean => {
1030
+ return hostBindings.logging.isEnabled(level);
1031
+ },
1032
+ };
1033
+
1034
+ // =============================================================================
1035
+ // Secrets Helpers
1036
+ // =============================================================================
1037
+
1038
+ /**
1039
+ * Get a secret by name.
1040
+ *
1041
+ * @param name - The secret name
1042
+ * @returns The secret value, or undefined if not found
1043
+ *
1044
+ * @example
1045
+ * ```typescript
1046
+ * const apiKey = getSecret('external_api_key');
1047
+ * ```
1048
+ */
1049
+ export function getSecret(name: string): string | undefined {
1050
+ return hostBindings.secrets.get(name);
1051
+ }
1052
+
1053
+ /**
1054
+ * Get a required secret by name.
1055
+ * Throws SecretError if the secret does not exist.
1056
+ *
1057
+ * @param name - The secret name
1058
+ * @returns The secret value
1059
+ * @throws {SecretError} If the secret does not exist
1060
+ *
1061
+ * @example
1062
+ * ```typescript
1063
+ * const dbPassword = getSecretRequired('database_password');
1064
+ * ```
1065
+ */
1066
+ export function getSecretRequired(name: string): string {
1067
+ const value = hostBindings.secrets.get(name);
1068
+ if (value === undefined) {
1069
+ throw new SecretError(name, `Required secret '${name}' not found`);
1070
+ }
1071
+ return value;
1072
+ }
1073
+
1074
+ /**
1075
+ * Check if a secret exists.
1076
+ *
1077
+ * @param name - The secret name
1078
+ * @returns true if the secret exists, false otherwise
1079
+ */
1080
+ export function secretExists(name: string): boolean {
1081
+ return hostBindings.secrets.exists(name);
1082
+ }
1083
+
1084
+ /**
1085
+ * List all available secret names (not values).
1086
+ * Useful for diagnostics without exposing sensitive data.
1087
+ *
1088
+ * @returns Array of secret names
1089
+ */
1090
+ export function listSecretNames(): string[] {
1091
+ return hostBindings.secrets.listNames();
1092
+ }
1093
+
1094
+ // =============================================================================
1095
+ // Metrics Helpers
1096
+ // =============================================================================
1097
+
1098
+ /**
1099
+ * Metrics utilities for emitting observability data to the host.
1100
+ */
1101
+ export const metrics = {
1102
+ /**
1103
+ * Increment a counter metric.
1104
+ *
1105
+ * @param name - The metric name
1106
+ * @param value - The amount to increment by (default: 1)
1107
+ *
1108
+ * @example
1109
+ * ```typescript
1110
+ * metrics.counterInc('requests_total');
1111
+ * metrics.counterInc('bytes_processed', byteCount);
1112
+ * ```
1113
+ */
1114
+ counterInc: (name: string, value: number = 1): void => {
1115
+ hostBindings.metrics.counterInc(name, BigInt(value));
1116
+ },
1117
+
1118
+ /**
1119
+ * Increment a counter metric with labels.
1120
+ *
1121
+ * @param name - The metric name
1122
+ * @param value - The amount to increment by
1123
+ * @param labels - The metric labels
1124
+ *
1125
+ * @example
1126
+ * ```typescript
1127
+ * metrics.counterIncLabeled('http_requests_total', 1, {
1128
+ * method: 'GET',
1129
+ * path: '/api/users',
1130
+ * status: '200',
1131
+ * });
1132
+ * ```
1133
+ */
1134
+ counterIncLabeled: (name: string, value: number, labels: Record<string, string>): void => {
1135
+ const kvLabels: KeyValue[] = Object.entries(labels).map(([key, val]) => ({
1136
+ key,
1137
+ value: val,
1138
+ }));
1139
+ hostBindings.metrics.counterIncLabeled(name, BigInt(value), kvLabels);
1140
+ },
1141
+
1142
+ /**
1143
+ * Set a gauge metric to a value.
1144
+ *
1145
+ * @param name - The metric name
1146
+ * @param value - The value to set
1147
+ *
1148
+ * @example
1149
+ * ```typescript
1150
+ * metrics.gaugeSet('active_connections', connectionCount);
1151
+ * ```
1152
+ */
1153
+ gaugeSet: (name: string, value: number): void => {
1154
+ hostBindings.metrics.gaugeSet(name, value);
1155
+ },
1156
+
1157
+ /**
1158
+ * Set a gauge metric with labels.
1159
+ *
1160
+ * @param name - The metric name
1161
+ * @param value - The value to set
1162
+ * @param labels - The metric labels
1163
+ */
1164
+ gaugeSetLabeled: (name: string, value: number, labels: Record<string, string>): void => {
1165
+ const kvLabels: KeyValue[] = Object.entries(labels).map(([key, val]) => ({
1166
+ key,
1167
+ value: val,
1168
+ }));
1169
+ hostBindings.metrics.gaugeSetLabeled(name, value, kvLabels);
1170
+ },
1171
+
1172
+ /**
1173
+ * Add to a gauge value (can be negative).
1174
+ *
1175
+ * @param name - The metric name
1176
+ * @param delta - The amount to add (can be negative)
1177
+ */
1178
+ gaugeAdd: (name: string, delta: number): void => {
1179
+ hostBindings.metrics.gaugeAdd(name, delta);
1180
+ },
1181
+
1182
+ /**
1183
+ * Record a histogram observation.
1184
+ *
1185
+ * @param name - The metric name
1186
+ * @param value - The observed value
1187
+ *
1188
+ * @example
1189
+ * ```typescript
1190
+ * metrics.histogramObserve('request_duration_seconds', 0.045);
1191
+ * ```
1192
+ */
1193
+ histogramObserve: (name: string, value: number): void => {
1194
+ hostBindings.metrics.histogramObserve(name, value);
1195
+ },
1196
+
1197
+ /**
1198
+ * Record a histogram observation with labels.
1199
+ *
1200
+ * @param name - The metric name
1201
+ * @param value - The observed value
1202
+ * @param labels - The metric labels
1203
+ */
1204
+ histogramObserveLabeled: (name: string, value: number, labels: Record<string, string>): void => {
1205
+ const kvLabels: KeyValue[] = Object.entries(labels).map(([key, val]) => ({
1206
+ key,
1207
+ value: val,
1208
+ }));
1209
+ hostBindings.metrics.histogramObserveLabeled(name, value, kvLabels);
1210
+ },
1211
+
1212
+ /**
1213
+ * Record request duration in milliseconds.
1214
+ * Convenience method that converts to nanoseconds.
1215
+ *
1216
+ * @param name - The metric name
1217
+ * @param durationMs - Duration in milliseconds
1218
+ */
1219
+ recordDuration: (name: string, durationMs: number): void => {
1220
+ const durationNs = BigInt(Math.floor(durationMs * 1_000_000));
1221
+ hostBindings.metrics.recordDuration(name, durationNs);
1222
+ },
1223
+
1224
+ /**
1225
+ * Record request duration with labels.
1226
+ *
1227
+ * @param name - The metric name
1228
+ * @param durationMs - Duration in milliseconds
1229
+ * @param labels - The metric labels
1230
+ */
1231
+ recordDurationLabeled: (name: string, durationMs: number, labels: Record<string, string>): void => {
1232
+ const durationNs = BigInt(Math.floor(durationMs * 1_000_000));
1233
+ const kvLabels: KeyValue[] = Object.entries(labels).map(([key, val]) => ({
1234
+ key,
1235
+ value: val,
1236
+ }));
1237
+ hostBindings.metrics.recordDurationLabeled(name, durationNs, kvLabels);
1238
+ },
1239
+ };
1240
+
1241
+ // =============================================================================
1242
+ // Response Helpers
1243
+ // =============================================================================
1244
+
1245
+ /**
1246
+ * Create a successful response with JSON body.
1247
+ *
1248
+ * @param data - The data to serialize as JSON
1249
+ * @param status - HTTP status code (default: 200)
1250
+ * @param headers - Additional headers
1251
+ * @returns A PluginResponse
1252
+ *
1253
+ * @example
1254
+ * ```typescript
1255
+ * return jsonResponse({ users: ['alice', 'bob'] });
1256
+ * return jsonResponse({ error: 'Not found' }, 404);
1257
+ * ```
1258
+ */
1259
+ export function jsonResponse<T>(
1260
+ data: T,
1261
+ status: number = 200,
1262
+ headers: Record<string, string> = {}
1263
+ ): PluginResponse {
1264
+ const body = JSON.stringify(data);
1265
+ const encoder = new TextEncoder();
1266
+ const responseHeaders: KeyValue[] = [
1267
+ { key: 'Content-Type', value: 'application/json' },
1268
+ ...Object.entries(headers).map(([key, value]) => ({ key, value })),
1269
+ ];
1270
+
1271
+ return {
1272
+ status,
1273
+ headers: responseHeaders,
1274
+ body: encoder.encode(body),
1275
+ };
1276
+ }
1277
+
1278
+ /**
1279
+ * Create a text response.
1280
+ *
1281
+ * @param text - The response text
1282
+ * @param status - HTTP status code (default: 200)
1283
+ * @param contentType - Content type (default: text/plain)
1284
+ * @returns A PluginResponse
1285
+ */
1286
+ export function textResponse(
1287
+ text: string,
1288
+ status: number = 200,
1289
+ contentType: string = 'text/plain'
1290
+ ): PluginResponse {
1291
+ const encoder = new TextEncoder();
1292
+ return {
1293
+ status,
1294
+ headers: [{ key: 'Content-Type', value: contentType }],
1295
+ body: encoder.encode(text),
1296
+ };
1297
+ }
1298
+
1299
+ /**
1300
+ * Create an HTML response.
1301
+ *
1302
+ * @param html - The HTML content
1303
+ * @param status - HTTP status code (default: 200)
1304
+ * @returns A PluginResponse
1305
+ */
1306
+ export function htmlResponse(html: string, status: number = 200): PluginResponse {
1307
+ return textResponse(html, status, 'text/html; charset=utf-8');
1308
+ }
1309
+
1310
+ /**
1311
+ * Create a binary response.
1312
+ *
1313
+ * @param data - The binary data
1314
+ * @param contentType - Content type
1315
+ * @param status - HTTP status code (default: 200)
1316
+ * @returns A PluginResponse
1317
+ */
1318
+ export function binaryResponse(
1319
+ data: Uint8Array,
1320
+ contentType: string,
1321
+ status: number = 200
1322
+ ): PluginResponse {
1323
+ return {
1324
+ status,
1325
+ headers: [{ key: 'Content-Type', value: contentType }],
1326
+ body: data,
1327
+ };
1328
+ }
1329
+
1330
+ /**
1331
+ * Create an error response.
1332
+ *
1333
+ * @param message - The error message
1334
+ * @param status - HTTP status code (default: 500)
1335
+ * @returns A HandleResult with error response
1336
+ */
1337
+ export function errorResponse(message: string, status: number = 500): HandleResult {
1338
+ return {
1339
+ type: 'response',
1340
+ response: jsonResponse({ error: message }, status),
1341
+ };
1342
+ }
1343
+
1344
+ /**
1345
+ * Create a redirect response.
1346
+ *
1347
+ * @param location - The URL to redirect to
1348
+ * @param status - HTTP status code (default: 302)
1349
+ * @returns A PluginResponse
1350
+ */
1351
+ export function redirectResponse(location: string, status: number = 302): PluginResponse {
1352
+ return {
1353
+ status,
1354
+ headers: [{ key: 'Location', value: location }],
1355
+ body: new Uint8Array(0),
1356
+ };
1357
+ }
1358
+
1359
+ /**
1360
+ * Create a pass-through result.
1361
+ * Use this when the plugin doesn't want to handle the request.
1362
+ *
1363
+ * @returns A HandleResult indicating pass-through
1364
+ */
1365
+ export function passThrough(): HandleResult {
1366
+ return { type: 'passThrough' };
1367
+ }
1368
+
1369
+ // =============================================================================
1370
+ // Request Helpers
1371
+ // =============================================================================
1372
+
1373
+ /**
1374
+ * Get a header value from a request (case-insensitive).
1375
+ *
1376
+ * @param request - The plugin request
1377
+ * @param name - The header name
1378
+ * @returns The header value, or undefined if not found
1379
+ */
1380
+ export function getHeader(request: PluginRequest, name: string): string | undefined {
1381
+ const lowerName = name.toLowerCase();
1382
+ const header = request.headers.find((h) => h.key.toLowerCase() === lowerName);
1383
+ return header?.value;
1384
+ }
1385
+
1386
+ /**
1387
+ * Get all values for a header (case-insensitive).
1388
+ *
1389
+ * @param request - The plugin request
1390
+ * @param name - The header name
1391
+ * @returns Array of header values
1392
+ */
1393
+ export function getHeaders(request: PluginRequest, name: string): string[] {
1394
+ const lowerName = name.toLowerCase();
1395
+ return request.headers
1396
+ .filter((h) => h.key.toLowerCase() === lowerName)
1397
+ .map((h) => h.value);
1398
+ }
1399
+
1400
+ /**
1401
+ * Parse the request body as JSON.
1402
+ *
1403
+ * @param request - The plugin request
1404
+ * @returns The parsed JSON body
1405
+ */
1406
+ export function parseJsonBody<T>(request: PluginRequest): T {
1407
+ const decoder = new TextDecoder();
1408
+ const text = decoder.decode(request.body);
1409
+ return JSON.parse(text) as T;
1410
+ }
1411
+
1412
+ /**
1413
+ * Get the request body as a string.
1414
+ *
1415
+ * @param request - The plugin request
1416
+ * @returns The body as a string
1417
+ */
1418
+ export function getBodyString(request: PluginRequest): string {
1419
+ const decoder = new TextDecoder();
1420
+ return decoder.decode(request.body);
1421
+ }
1422
+
1423
+ /**
1424
+ * Parse query string into a Map.
1425
+ *
1426
+ * @param request - The plugin request
1427
+ * @returns Map of query parameters
1428
+ */
1429
+ export function parseQuery(request: PluginRequest): Map<string, string> {
1430
+ const params = new Map<string, string>();
1431
+ if (!request.query) {
1432
+ return params;
1433
+ }
1434
+ const pairs = request.query.split('&');
1435
+ for (const pair of pairs) {
1436
+ const [key, value] = pair.split('=');
1437
+ if (key) {
1438
+ params.set(decodeURIComponent(key), decodeURIComponent(value ?? ''));
1439
+ }
1440
+ }
1441
+ return params;
1442
+ }
1443
+
1444
+ /**
1445
+ * Get a context value from a request.
1446
+ *
1447
+ * @param request - The plugin request
1448
+ * @param key - The context key
1449
+ * @returns The context value, or undefined if not found
1450
+ */
1451
+ export function getContext(request: PluginRequest, key: string): string | undefined {
1452
+ const ctx = request.context.find((c) => c.key === key);
1453
+ return ctx?.value;
1454
+ }
1455
+
1456
+ // =============================================================================
1457
+ // Timing Helpers
1458
+ // =============================================================================
1459
+
1460
+ /**
1461
+ * Measure the duration of an async operation and record it as a metric.
1462
+ *
1463
+ * @param name - The metric name
1464
+ * @param fn - The async function to measure
1465
+ * @returns The result of the function
1466
+ *
1467
+ * @example
1468
+ * ```typescript
1469
+ * const result = await timed('database_query', async () => {
1470
+ * return await db.query('SELECT * FROM users');
1471
+ * });
1472
+ * ```
1473
+ */
1474
+ export async function timed<T>(name: string, fn: () => Promise<T>): Promise<T> {
1475
+ const start = Date.now();
1476
+ try {
1477
+ return await fn();
1478
+ } finally {
1479
+ const duration = Date.now() - start;
1480
+ metrics.recordDuration(name, duration);
1481
+ }
1482
+ }
1483
+
1484
+ /**
1485
+ * Measure the duration of a sync operation and record it as a metric.
1486
+ *
1487
+ * @param name - The metric name
1488
+ * @param fn - The function to measure
1489
+ * @returns The result of the function
1490
+ */
1491
+ export function timedSync<T>(name: string, fn: () => T): T {
1492
+ const start = Date.now();
1493
+ try {
1494
+ return fn();
1495
+ } finally {
1496
+ const duration = Date.now() - start;
1497
+ metrics.recordDuration(name, duration);
1498
+ }
1499
+ }
1500
+
1501
+ // =============================================================================
1502
+ // Version Helpers
1503
+ // =============================================================================
1504
+
1505
+ /**
1506
+ * Create a Version object from a semver string.
1507
+ *
1508
+ * @param semver - The semver string (e.g., "1.2.3" or "1.2.3-beta.1")
1509
+ * @returns A Version object
1510
+ */
1511
+ export function parseVersion(semver: string): Version {
1512
+ const [versionPart, preRelease] = semver.split('-', 2);
1513
+ const [major, minor, patch] = versionPart.split('.').map(Number);
1514
+ return {
1515
+ major: major ?? 0,
1516
+ minor: minor ?? 0,
1517
+ patch: patch ?? 0,
1518
+ preRelease,
1519
+ };
1520
+ }
1521
+
1522
+ /**
1523
+ * Format a Version object as a semver string.
1524
+ *
1525
+ * @param version - The Version object
1526
+ * @returns The semver string
1527
+ */
1528
+ export function formatVersion(version: Version): string {
1529
+ const base = `${version.major}.${version.minor}.${version.patch}`;
1530
+ return version.preRelease ? `${base}-${version.preRelease}` : base;
1531
+ }
1532
+
1533
+ // =============================================================================
1534
+ // Plugin Registration Helper
1535
+ // =============================================================================
1536
+
1537
+ /**
1538
+ * Register a plugin implementation.
1539
+ * This is a convenience function for setting up plugin exports.
1540
+ *
1541
+ * @param plugin - The plugin implementation
1542
+ * @returns The plugin for export
1543
+ *
1544
+ * @example
1545
+ * ```typescript
1546
+ * export default registerPlugin({
1547
+ * info() {
1548
+ * return {
1549
+ * id: 'my-org:my-plugin',
1550
+ * name: 'My Plugin',
1551
+ * version: { major: 1, minor: 0, patch: 0 },
1552
+ * description: 'A sample plugin',
1553
+ * author: 'My Org',
1554
+ * };
1555
+ * },
1556
+ * handle(request) {
1557
+ * return { type: 'response', response: jsonResponse({ hello: 'world' }) };
1558
+ * },
1559
+ * });
1560
+ * ```
1561
+ */
1562
+ export function registerPlugin(plugin: ZLayerPlugin): ZLayerPlugin {
1563
+ return plugin;
1564
+ }
1565
+