@glubean/sdk 0.1.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.
@@ -0,0 +1,141 @@
1
+ /**
2
+ * File-level configuration for Glubean tests.
3
+ *
4
+ * `configure()` lets you declare shared dependencies (vars, secrets, HTTP config)
5
+ * once at the top of a test file (or in a shared `configure.ts`), eliminating
6
+ * repetitive `ctx.vars.require()` / `ctx.secrets.require()` calls in every test.
7
+ *
8
+ * All returned values are **lazy** — they are not resolved until a test function
9
+ * actually accesses them at runtime. This means:
10
+ * - Safe to call at module top-level (scanner won't trigger resolution)
11
+ * - Safe to share across files via re-exports
12
+ * - Each test execution gets the correct runtime values
13
+ *
14
+ * @example Single file usage
15
+ * ```ts
16
+ * import { test, configure } from "@glubean/sdk";
17
+ *
18
+ * const { vars, secrets, http } = configure({
19
+ * vars: { baseUrl: "BASE_URL" },
20
+ * secrets: { apiKey: "API_KEY" },
21
+ * http: {
22
+ * prefixUrl: "BASE_URL",
23
+ * headers: { Authorization: "Bearer {{API_KEY}}" },
24
+ * },
25
+ * });
26
+ *
27
+ * export const listUsers = test("list-users", async (ctx) => {
28
+ * const res = await http.get("users").json();
29
+ * ctx.assert(res.length > 0, "has users");
30
+ * });
31
+ * ```
32
+ *
33
+ * @example Shared across files (tests/configure.ts)
34
+ * ```ts
35
+ * // tests/configure.ts
36
+ * import { configure } from "@glubean/sdk";
37
+ * export const { vars, secrets, http } = configure({
38
+ * vars: { baseUrl: "BASE_URL" },
39
+ * http: { prefixUrl: "BASE_URL" },
40
+ * });
41
+ *
42
+ * // tests/users.test.ts
43
+ * import { test } from "@glubean/sdk";
44
+ * import { http } from "./configure.js";
45
+ *
46
+ * export const listUsers = test("list-users", async (ctx) => {
47
+ * const res = await http.get("users").json();
48
+ * });
49
+ * ```
50
+ *
51
+ * @module configure
52
+ */
53
+ import type { ConfigureOptions, ConfigureResult, GlubeanAction, GlubeanEvent, GlubeanRuntime, HttpClient, PluginEntry, PluginFactory, ReservedConfigureKeys, ResolvePlugins } from "./types.js";
54
+ /**
55
+ * Shape of the runtime context injected by the harness before test execution.
56
+ * This is the internal shape — the public `GlubeanRuntime` in types.ts adds
57
+ * helper methods (requireVar, requireSecret, resolveTemplate) for plugins.
58
+ *
59
+ * @internal
60
+ */
61
+ export interface InternalRuntime {
62
+ vars: Record<string, string>;
63
+ secrets: Record<string, string>;
64
+ http: HttpClient;
65
+ test?: GlubeanRuntime["test"];
66
+ action?(a: GlubeanAction): void;
67
+ event?(ev: GlubeanEvent): void;
68
+ log?(message: string, data?: unknown): void;
69
+ }
70
+ /**
71
+ * Resolve `{{key}}` template placeholders in a string using runtime vars and secrets.
72
+ * Secrets take precedence over vars if both have the same key.
73
+ *
74
+ * This is used internally by `buildLazyHttp()` and exposed to plugin authors
75
+ * via `GlubeanRuntime.resolveTemplate()`.
76
+ */
77
+ export declare function resolveTemplate(template: string, vars: Record<string, string>, secrets: Record<string, string>): string;
78
+ /**
79
+ * Declare file-level dependencies on vars, secrets, and HTTP configuration.
80
+ *
81
+ * Returns lazy accessors that resolve at test runtime, not at import time.
82
+ * All declared vars and secrets are **required** — missing values cause the test
83
+ * to fail immediately with a clear error message.
84
+ *
85
+ * The returned objects can be shared across files via re-exports.
86
+ *
87
+ * @param options Configuration declaring vars, secrets, and HTTP defaults
88
+ * @returns Lazy accessors for vars, secrets, and a pre-configured HTTP client
89
+ *
90
+ * @example Basic usage
91
+ * ```ts
92
+ * import { test, configure } from "@glubean/sdk";
93
+ *
94
+ * const { vars, http } = configure({
95
+ * vars: { baseUrl: "base_url" },
96
+ * http: { prefixUrl: "base_url" },
97
+ * });
98
+ *
99
+ * export const listUsers = test("list-users", async (ctx) => {
100
+ * const res = await http.get("users").json();
101
+ * ctx.log(`Base URL: ${vars.baseUrl}`);
102
+ * });
103
+ * ```
104
+ *
105
+ * @example Full configuration with secrets
106
+ * ```ts
107
+ * const { vars, secrets, http } = configure({
108
+ * vars: { baseUrl: "base_url", orgId: "org_id" },
109
+ * secrets: { apiKey: "api_key" },
110
+ * http: {
111
+ * prefixUrl: "base_url",
112
+ * headers: { Authorization: "Bearer {{api_key}}" },
113
+ * },
114
+ * });
115
+ * ```
116
+ *
117
+ * @example Shared across test files
118
+ * ```ts
119
+ * // tests/configure.ts
120
+ * export const { vars, secrets, http } = configure({ ... });
121
+ *
122
+ * // tests/users.test.ts
123
+ * import { http, vars } from "./configure.js";
124
+ * ```
125
+ */
126
+ export declare function configure<V extends Record<string, string> = Record<string, string>, S extends Record<string, string> = Record<string, string>, P extends Record<string, PluginFactory<any> | PluginEntry<any>> = Record<string, never>>(options: ConfigureOptions & {
127
+ vars?: {
128
+ [K in keyof V]: string;
129
+ };
130
+ secrets?: {
131
+ [K in keyof S]: string;
132
+ };
133
+ plugins?: P & {
134
+ [K in ReservedConfigureKeys]?: never;
135
+ };
136
+ }): ConfigureResult<{
137
+ [K in keyof V]: string;
138
+ }, {
139
+ [K in keyof S]: string;
140
+ }> & ResolvePlugins<P>;
141
+ //# sourceMappingURL=configure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configure.d.ts","sourceRoot":"","sources":["../src/configure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,KAAK,EAEV,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,YAAY,EACZ,cAAc,EACd,UAAU,EAGV,WAAW,EACX,aAAa,EAEb,qBAAqB,EACrB,cAAc,EACf,MAAM,YAAY,CAAC;AAMpB;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,CAAC,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IAC9B,MAAM,CAAC,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IAChC,KAAK,CAAC,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC7C;AA0GD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,MAAM,CAYR;AA6YD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,SAAS,CACvB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACzD,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAEzD,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CACtE,MAAM,EACN,KAAK,CACN,EAED,OAAO,EAAE,gBAAgB,GAAG;IAC1B,IAAI,CAAC,EAAE;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM;KAAE,CAAC;IAClC,OAAO,CAAC,EAAE;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM;KAAE,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,GAAG;SAAG,CAAC,IAAI,qBAAqB,CAAC,CAAC,EAAE,KAAK;KAAE,CAAC;CACxD,GAEC,eAAe,CACf;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM;CAAE,EAC1B;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM;CAAE,CAC3B,GACC,cAAc,CAAC,CAAC,CAAC,CAuBpB"}
@@ -0,0 +1,535 @@
1
+ /**
2
+ * File-level configuration for Glubean tests.
3
+ *
4
+ * `configure()` lets you declare shared dependencies (vars, secrets, HTTP config)
5
+ * once at the top of a test file (or in a shared `configure.ts`), eliminating
6
+ * repetitive `ctx.vars.require()` / `ctx.secrets.require()` calls in every test.
7
+ *
8
+ * All returned values are **lazy** — they are not resolved until a test function
9
+ * actually accesses them at runtime. This means:
10
+ * - Safe to call at module top-level (scanner won't trigger resolution)
11
+ * - Safe to share across files via re-exports
12
+ * - Each test execution gets the correct runtime values
13
+ *
14
+ * @example Single file usage
15
+ * ```ts
16
+ * import { test, configure } from "@glubean/sdk";
17
+ *
18
+ * const { vars, secrets, http } = configure({
19
+ * vars: { baseUrl: "BASE_URL" },
20
+ * secrets: { apiKey: "API_KEY" },
21
+ * http: {
22
+ * prefixUrl: "BASE_URL",
23
+ * headers: { Authorization: "Bearer {{API_KEY}}" },
24
+ * },
25
+ * });
26
+ *
27
+ * export const listUsers = test("list-users", async (ctx) => {
28
+ * const res = await http.get("users").json();
29
+ * ctx.assert(res.length > 0, "has users");
30
+ * });
31
+ * ```
32
+ *
33
+ * @example Shared across files (tests/configure.ts)
34
+ * ```ts
35
+ * // tests/configure.ts
36
+ * import { configure } from "@glubean/sdk";
37
+ * export const { vars, secrets, http } = configure({
38
+ * vars: { baseUrl: "BASE_URL" },
39
+ * http: { prefixUrl: "BASE_URL" },
40
+ * });
41
+ *
42
+ * // tests/users.test.ts
43
+ * import { test } from "@glubean/sdk";
44
+ * import { http } from "./configure.js";
45
+ *
46
+ * export const listUsers = test("list-users", async (ctx) => {
47
+ * const res = await http.get("users").json();
48
+ * });
49
+ * ```
50
+ *
51
+ * @module configure
52
+ */
53
+ /**
54
+ * Get the current runtime context from the global slot.
55
+ * Throws a clear error if accessed outside of test execution (e.g., at scan time).
56
+ *
57
+ * @internal
58
+ */
59
+ function getRuntime() {
60
+ const runtime = globalThis.__glubeanRuntime;
61
+ if (!runtime) {
62
+ throw new Error("configure() values can only be accessed during test execution. " +
63
+ "Did you try to read a var or secret at module load time? " +
64
+ "Move the access inside a test function.");
65
+ }
66
+ return runtime;
67
+ }
68
+ /**
69
+ * Require a var from the runtime context.
70
+ * Throws if the var is missing or empty.
71
+ *
72
+ * @internal
73
+ */
74
+ function requireVar(key) {
75
+ const runtime = getRuntime();
76
+ const value = runtime.vars[key];
77
+ if (value === undefined || value === null || value === "") {
78
+ throw new Error(`Missing required var: ${key}`);
79
+ }
80
+ return value;
81
+ }
82
+ /**
83
+ * Require a secret from the runtime context.
84
+ * Throws if the secret is missing or empty.
85
+ *
86
+ * @internal
87
+ */
88
+ function requireSecret(key) {
89
+ const runtime = getRuntime();
90
+ const value = runtime.secrets[key];
91
+ if (value === undefined || value === null || value === "") {
92
+ throw new Error(`Missing required secret: ${key}`);
93
+ }
94
+ return value;
95
+ }
96
+ // =============================================================================
97
+ // Lazy proxy builders
98
+ // =============================================================================
99
+ /**
100
+ * Regex for `{{key}}` template placeholders in header values.
101
+ */
102
+ const TEMPLATE_RE = /\{\{([\w-]+)\}\}/g;
103
+ /**
104
+ * Build a lazy vars accessor object.
105
+ * Each property is a getter that calls `requireVar()` on access.
106
+ *
107
+ * @internal
108
+ */
109
+ function buildLazyVars(mapping) {
110
+ const obj = {};
111
+ for (const [prop, varKey] of Object.entries(mapping)) {
112
+ Object.defineProperty(obj, prop, {
113
+ get() {
114
+ return requireVar(varKey);
115
+ },
116
+ enumerable: true,
117
+ configurable: false,
118
+ });
119
+ }
120
+ return obj;
121
+ }
122
+ /**
123
+ * Build a lazy secrets accessor object.
124
+ * Each property is a getter that calls `requireSecret()` on access.
125
+ *
126
+ * @internal
127
+ */
128
+ function buildLazySecrets(mapping) {
129
+ const obj = {};
130
+ for (const [prop, secretKey] of Object.entries(mapping)) {
131
+ Object.defineProperty(obj, prop, {
132
+ get() {
133
+ return requireSecret(secretKey);
134
+ },
135
+ enumerable: true,
136
+ configurable: false,
137
+ });
138
+ }
139
+ return obj;
140
+ }
141
+ /**
142
+ * Resolve `{{key}}` template placeholders in a string using runtime vars and secrets.
143
+ * Secrets take precedence over vars if both have the same key.
144
+ *
145
+ * This is used internally by `buildLazyHttp()` and exposed to plugin authors
146
+ * via `GlubeanRuntime.resolveTemplate()`.
147
+ */
148
+ export function resolveTemplate(template, vars, secrets) {
149
+ return template.replace(TEMPLATE_RE, (_match, key) => {
150
+ // Try secrets first (more likely for auth headers), then vars
151
+ const value = secrets[key] ?? vars[key];
152
+ if (value === undefined || value === null || value === "") {
153
+ throw new Error(`Missing value for template placeholder "{{${key}}}" in configure() http headers. ` +
154
+ `Ensure "${key}" is available as a var or secret.`);
155
+ }
156
+ return value;
157
+ });
158
+ }
159
+ /**
160
+ * Build a lazy HTTP client proxy.
161
+ * On first method call, resolves the config and creates an extended client.
162
+ *
163
+ * @internal
164
+ */
165
+ function buildLazyHttp(httpOptions) {
166
+ // Cache the resolved client per runtime identity to avoid re-extending on every call.
167
+ // Since each test runs in its own subprocess, a WeakMap keyed on runtime object
168
+ // ensures we get one extended client per test execution.
169
+ const cache = new WeakMap();
170
+ function getClient() {
171
+ const runtime = getRuntime();
172
+ let client = cache.get(runtime);
173
+ if (client)
174
+ return client;
175
+ // Build ky-compatible options from the configure http config
176
+ const extendOptions = {};
177
+ if (httpOptions.prefixUrl) {
178
+ extendOptions.prefixUrl = requireVar(httpOptions.prefixUrl);
179
+ }
180
+ if (httpOptions.headers) {
181
+ const resolvedHeaders = {};
182
+ for (const [name, template] of Object.entries(httpOptions.headers)) {
183
+ resolvedHeaders[name] = resolveTemplate(template, runtime.vars, runtime.secrets);
184
+ }
185
+ extendOptions.headers = resolvedHeaders;
186
+ }
187
+ if (httpOptions.timeout !== undefined) {
188
+ extendOptions.timeout = httpOptions.timeout;
189
+ }
190
+ if (httpOptions.retry !== undefined) {
191
+ extendOptions.retry = httpOptions.retry;
192
+ }
193
+ if (httpOptions.throwHttpErrors !== undefined) {
194
+ extendOptions.throwHttpErrors = httpOptions.throwHttpErrors;
195
+ }
196
+ if (httpOptions.hooks) {
197
+ extendOptions.hooks = httpOptions.hooks;
198
+ }
199
+ client = runtime.http.extend(extendOptions);
200
+ cache.set(runtime, client);
201
+ return client;
202
+ }
203
+ // Create a callable proxy that delegates all method calls to the lazily-resolved client.
204
+ const HTTP_METHODS = [
205
+ "get",
206
+ "post",
207
+ "put",
208
+ "patch",
209
+ "delete",
210
+ "head",
211
+ ];
212
+ // The callable function (for `http(url, options)` shorthand)
213
+ const proxy = function (url, options) {
214
+ return getClient()(url, options);
215
+ };
216
+ // Method shortcuts
217
+ for (const method of HTTP_METHODS) {
218
+ proxy[method] = (url, options) => getClient()[method](url, options);
219
+ }
220
+ // extend() — returns a new HttpClient that merges options with the resolved base
221
+ proxy.extend = (options) => getClient().extend(options);
222
+ return proxy;
223
+ }
224
+ // =============================================================================
225
+ // Lazy plugin builder
226
+ // =============================================================================
227
+ /**
228
+ * Build lazy property descriptors for plugin factories.
229
+ * Each plugin is instantiated on first property access with a WeakMap cache
230
+ * keyed by the internal runtime identity.
231
+ *
232
+ * Returns PropertyDescriptorMap (not a ready object) so the caller can use
233
+ * Object.defineProperties() without triggering getters via spread.
234
+ *
235
+ * @internal
236
+ */
237
+ /** Reserved keys that plugins cannot shadow. */
238
+ const RESERVED_KEYS = new Set(["vars", "secrets", "http"]);
239
+ function normalizePluginEntry(entry) {
240
+ if ("factory" in entry)
241
+ return entry;
242
+ return { factory: entry };
243
+ }
244
+ function toMethodList(value) {
245
+ if (!value)
246
+ return [];
247
+ const list = Array.isArray(value) ? value : [value];
248
+ return list.map((method) => method.toUpperCase());
249
+ }
250
+ function toUrlString(input) {
251
+ if (input instanceof Request)
252
+ return input.url;
253
+ if (input instanceof URL)
254
+ return input.toString();
255
+ return input;
256
+ }
257
+ function toPathname(input) {
258
+ try {
259
+ if (input instanceof Request)
260
+ return new URL(input.url).pathname;
261
+ if (input instanceof URL)
262
+ return input.pathname;
263
+ const isAbsolute = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(input);
264
+ const parsed = isAbsolute ? new URL(input) : new URL(input, "http://glubean.local");
265
+ return parsed.pathname;
266
+ }
267
+ catch {
268
+ return "";
269
+ }
270
+ }
271
+ function matchesPattern(value, pattern) {
272
+ if (pattern instanceof RegExp)
273
+ return pattern.test(value);
274
+ return value.startsWith(pattern);
275
+ }
276
+ function matchesRequestMatcher(matcher, method, url) {
277
+ const methods = toMethodList(matcher.method);
278
+ if (methods.length > 0 && !methods.includes(method)) {
279
+ return false;
280
+ }
281
+ if (matcher.url) {
282
+ const rawUrl = toUrlString(url);
283
+ if (!matchesPattern(rawUrl, matcher.url)) {
284
+ return false;
285
+ }
286
+ }
287
+ if (matcher.path) {
288
+ const pathname = toPathname(url);
289
+ if (!matchesPattern(pathname, matcher.path)) {
290
+ return false;
291
+ }
292
+ }
293
+ return true;
294
+ }
295
+ function evaluateRequestActivation(activation, method, url) {
296
+ const rules = activation?.requests;
297
+ if (!rules)
298
+ return { active: true };
299
+ const exclude = rules.exclude ?? [];
300
+ if (exclude.some((matcher) => matchesRequestMatcher(matcher, method, url))) {
301
+ return {
302
+ active: false,
303
+ reason: "request matches activation.requests.exclude",
304
+ };
305
+ }
306
+ const include = rules.include ?? [];
307
+ if (include.length > 0 && !include.some((matcher) => matchesRequestMatcher(matcher, method, url))) {
308
+ return {
309
+ active: false,
310
+ reason: "request does not match activation.requests.include",
311
+ };
312
+ }
313
+ return { active: true };
314
+ }
315
+ function evaluateTagActivation(activation, runtime) {
316
+ const rules = activation?.tags;
317
+ if (!rules)
318
+ return { active: true };
319
+ const runtimeTags = new Set(runtime.test?.tags ?? []);
320
+ const disable = rules.disable ?? [];
321
+ for (const tag of disable) {
322
+ if (runtimeTags.has(tag)) {
323
+ return {
324
+ active: false,
325
+ reason: `test tag "${tag}" matches activation.tags.disable`,
326
+ };
327
+ }
328
+ }
329
+ const enable = rules.enable ?? [];
330
+ if (enable.length > 0) {
331
+ const matched = enable.some((tag) => runtimeTags.has(tag));
332
+ if (!matched) {
333
+ return {
334
+ active: false,
335
+ reason: `current test tags do not match activation.tags.enable (${enable.join(", ")})`,
336
+ };
337
+ }
338
+ }
339
+ return { active: true };
340
+ }
341
+ function buildActivationAwareHttpClient(pluginName, activation, http) {
342
+ if (!activation?.requests)
343
+ return http;
344
+ function assertRequestActive(method, url) {
345
+ const decision = evaluateRequestActivation(activation, method, url);
346
+ if (decision.active)
347
+ return;
348
+ throw new Error(`Plugin "${pluginName}" is inactive for request ${method} ${toUrlString(url)}: ${decision.reason}.`);
349
+ }
350
+ const METHODS = ["get", "post", "put", "patch", "delete", "head"];
351
+ const wrapped = function (url, options) {
352
+ const method = (options?.method ?? (url instanceof Request ? url.method : "GET")).toUpperCase();
353
+ assertRequestActive(method, url);
354
+ return http(url, options);
355
+ };
356
+ for (const methodName of METHODS) {
357
+ wrapped[methodName] = (url, options) => {
358
+ assertRequestActive(methodName.toUpperCase(), url);
359
+ return http[methodName](url, options);
360
+ };
361
+ }
362
+ wrapped.extend = (options) => buildActivationAwareHttpClient(pluginName, activation, http.extend(options));
363
+ return wrapped;
364
+ }
365
+ /**
366
+ * Resolve (or retrieve cached) the real plugin instance for the current runtime.
367
+ *
368
+ * @internal
369
+ */
370
+ function resolvePlugin(name, entry, cache) {
371
+ const runtime = getRuntime();
372
+ const tagDecision = evaluateTagActivation(entry.activation, runtime);
373
+ if (!tagDecision.active) {
374
+ const testId = runtime.test?.id;
375
+ throw new Error(`Plugin "${name}" is inactive${testId ? ` for test "${testId}"` : ""}: ${tagDecision.reason}.`);
376
+ }
377
+ if (cache.has(runtime))
378
+ return cache.get(runtime);
379
+ // Build the augmented runtime that plugins see
380
+ const noop = () => { };
381
+ const augmented = {
382
+ vars: runtime.vars,
383
+ secrets: runtime.secrets,
384
+ http: buildActivationAwareHttpClient(name, entry.activation, runtime.http),
385
+ test: runtime.test,
386
+ requireVar,
387
+ requireSecret,
388
+ resolveTemplate: (template) => resolveTemplate(template, runtime.vars, runtime.secrets),
389
+ action: runtime.action?.bind(runtime) ?? noop,
390
+ event: runtime.event?.bind(runtime) ?? noop,
391
+ log: runtime.log?.bind(runtime) ?? noop,
392
+ };
393
+ const instance = entry.factory.create(augmented);
394
+ cache.set(runtime, instance);
395
+ return instance;
396
+ }
397
+ /**
398
+ * Build a Proxy that defers plugin creation until the plugin is actually used.
399
+ *
400
+ * This allows `const { chrome } = configure(...)` to work at module top-level —
401
+ * the destructured value is a transparent Proxy, not the real plugin instance.
402
+ * The real instance is created lazily on first property access / method call
403
+ * during test execution.
404
+ *
405
+ * @internal
406
+ */
407
+ function buildLazyPlugin(name, entry) {
408
+ const cache = new WeakMap();
409
+ return new Proxy(Object.create(null), {
410
+ get(_target, prop, receiver) {
411
+ const instance = resolvePlugin(name, entry, cache);
412
+ const value = Reflect.get(instance, prop, receiver);
413
+ return typeof value === "function"
414
+ ? value.bind(instance)
415
+ : value;
416
+ },
417
+ set(_target, prop, value) {
418
+ const instance = resolvePlugin(name, entry, cache);
419
+ return Reflect.set(instance, prop, value);
420
+ },
421
+ has(_target, prop) {
422
+ const instance = resolvePlugin(name, entry, cache);
423
+ return Reflect.has(instance, prop);
424
+ },
425
+ ownKeys() {
426
+ const instance = resolvePlugin(name, entry, cache);
427
+ return Reflect.ownKeys(instance);
428
+ },
429
+ getOwnPropertyDescriptor(_target, prop) {
430
+ const instance = resolvePlugin(name, entry, cache);
431
+ return Object.getOwnPropertyDescriptor(instance, prop);
432
+ },
433
+ });
434
+ }
435
+ function buildLazyPlugins(plugins) {
436
+ const result = {};
437
+ for (const [name, rawEntry] of Object.entries(plugins)) {
438
+ if (RESERVED_KEYS.has(name)) {
439
+ throw new Error(`Plugin name "${name}" conflicts with a reserved configure() field. ` +
440
+ `Choose a different key (reserved: ${[...RESERVED_KEYS].join(", ")}).`);
441
+ }
442
+ result[name] = buildLazyPlugin(name, normalizePluginEntry(rawEntry));
443
+ }
444
+ return result;
445
+ }
446
+ // =============================================================================
447
+ // Public API
448
+ // =============================================================================
449
+ /**
450
+ * Declare file-level dependencies on vars, secrets, and HTTP configuration.
451
+ *
452
+ * Returns lazy accessors that resolve at test runtime, not at import time.
453
+ * All declared vars and secrets are **required** — missing values cause the test
454
+ * to fail immediately with a clear error message.
455
+ *
456
+ * The returned objects can be shared across files via re-exports.
457
+ *
458
+ * @param options Configuration declaring vars, secrets, and HTTP defaults
459
+ * @returns Lazy accessors for vars, secrets, and a pre-configured HTTP client
460
+ *
461
+ * @example Basic usage
462
+ * ```ts
463
+ * import { test, configure } from "@glubean/sdk";
464
+ *
465
+ * const { vars, http } = configure({
466
+ * vars: { baseUrl: "base_url" },
467
+ * http: { prefixUrl: "base_url" },
468
+ * });
469
+ *
470
+ * export const listUsers = test("list-users", async (ctx) => {
471
+ * const res = await http.get("users").json();
472
+ * ctx.log(`Base URL: ${vars.baseUrl}`);
473
+ * });
474
+ * ```
475
+ *
476
+ * @example Full configuration with secrets
477
+ * ```ts
478
+ * const { vars, secrets, http } = configure({
479
+ * vars: { baseUrl: "base_url", orgId: "org_id" },
480
+ * secrets: { apiKey: "api_key" },
481
+ * http: {
482
+ * prefixUrl: "base_url",
483
+ * headers: { Authorization: "Bearer {{api_key}}" },
484
+ * },
485
+ * });
486
+ * ```
487
+ *
488
+ * @example Shared across test files
489
+ * ```ts
490
+ * // tests/configure.ts
491
+ * export const { vars, secrets, http } = configure({ ... });
492
+ *
493
+ * // tests/users.test.ts
494
+ * import { http, vars } from "./configure.js";
495
+ * ```
496
+ */
497
+ export function configure(options) {
498
+ const vars = options.vars
499
+ ? buildLazyVars(options.vars)
500
+ : {};
501
+ const secrets = options.secrets
502
+ ? buildLazySecrets(options.secrets)
503
+ : {};
504
+ const http = options.http ? buildLazyHttp(options.http) : buildPassthroughHttp();
505
+ const base = { vars, secrets, http };
506
+ if (options.plugins) {
507
+ Object.assign(base, buildLazyPlugins(options.plugins));
508
+ }
509
+ return base;
510
+ }
511
+ /**
512
+ * Build a passthrough HTTP client that simply delegates to ctx.http.
513
+ * Used when `configure()` is called without `http` options.
514
+ *
515
+ * @internal
516
+ */
517
+ function buildPassthroughHttp() {
518
+ const HTTP_METHODS = [
519
+ "get",
520
+ "post",
521
+ "put",
522
+ "patch",
523
+ "delete",
524
+ "head",
525
+ ];
526
+ const proxy = function (url, options) {
527
+ return getRuntime().http(url, options);
528
+ };
529
+ for (const method of HTTP_METHODS) {
530
+ proxy[method] = (url, options) => getRuntime().http[method](url, options);
531
+ }
532
+ proxy.extend = (options) => getRuntime().http.extend(options);
533
+ return proxy;
534
+ }
535
+ //# sourceMappingURL=configure.js.map