@gobing-ai/ts-runtime 0.2.7 → 0.2.9

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,304 @@
1
+ import { parse as parseYaml } from 'yaml';
2
+ import { getFs } from './fs.js';
3
+ import { dirnamePath, isAbsolutePath, joinPath } from './path.js';
4
+ /** Default time budget for a single remote schema fetch. */
5
+ const REMOTE_SCHEMA_FETCH_TIMEOUT_MS = 5_000;
6
+ /** Upper bound on a remote schema body. A timeout alone lets a slow multi-GB drip exhaust memory. */
7
+ const REMOTE_SCHEMA_MAX_BYTES = 5 * 1024 * 1024;
8
+ export class StructuredConfigSchemaError extends Error {
9
+ violations;
10
+ constructor(message, violations = []) {
11
+ super(message);
12
+ this.violations = violations;
13
+ this.name = 'StructuredConfigSchemaError';
14
+ }
15
+ }
16
+ export async function loadStructuredConfig(path, options = {}) {
17
+ const content = await getFs().readFile(path);
18
+ return await parseStructuredConfig(content, path, options);
19
+ }
20
+ export async function parseStructuredConfig(content, source, options = {}) {
21
+ const parsed = source.endsWith('.json') ? JSON.parse(content) : parseYaml(content);
22
+ if (options.validateSchema !== false) {
23
+ await validateDeclaredJsonSchema(parsed, source, options);
24
+ }
25
+ return parsed;
26
+ }
27
+ export async function validateDeclaredJsonSchema(value, source, options = {}) {
28
+ if (!isObject(value))
29
+ return;
30
+ const schemaRef = value.$schema;
31
+ if (typeof schemaRef !== 'string' || schemaRef.length === 0)
32
+ return;
33
+ const schemaLocation = resolveSchemaRef(schemaRef, source, options.resolve);
34
+ const schemaText = await readSchema(schemaLocation, options);
35
+ let schema;
36
+ try {
37
+ schema = JSON.parse(schemaText);
38
+ }
39
+ catch (error) {
40
+ throw new StructuredConfigSchemaError(`Invalid JSON schema "${schemaLocation}" referenced by "${source}": ${errorMessage(error)}`);
41
+ }
42
+ if (!isObject(schema)) {
43
+ throw new StructuredConfigSchemaError(`JSON schema "${schemaLocation}" referenced by "${source}" must be an object`);
44
+ }
45
+ const violations = validateJsonSchema(value, schema, '', (schema.$defs ?? {}));
46
+ if (violations.length > 0) {
47
+ throw new StructuredConfigSchemaError(`Configuration "${source}" failed JSON schema validation against "${schemaLocation}": ${violations
48
+ .map((violation) => `${violation.path}: ${violation.message}`)
49
+ .join('; ')}`, violations);
50
+ }
51
+ }
52
+ export function validateJsonSchema(value, schema, path = '', defs = {}, seenRefs = new Set()) {
53
+ const violations = [];
54
+ // Applicator keywords compose with their siblings (logical AND), per JSON Schema 2020-12 —
55
+ // a node may carry `$ref`/`oneOf`/`anyOf` *and* `type`/`properties`/... and all apply.
56
+ if (schema.$ref !== undefined) {
57
+ // Cyclic `$ref` (A→B→A) would recurse until stack overflow — a DoS surface for untrusted
58
+ // schemas. Following a ref already on the current path is a no-op: that branch is being
59
+ // validated higher up the stack, so re-entering adds nothing but unbounded depth.
60
+ if (!seenRefs.has(schema.$ref)) {
61
+ const resolved = resolveRef(schema.$ref, defs, schema.$defs);
62
+ if (resolved !== undefined) {
63
+ const nextSeen = new Set(seenRefs).add(schema.$ref);
64
+ violations.push(...validateJsonSchema(value, resolved, path, defs, nextSeen));
65
+ }
66
+ }
67
+ }
68
+ if (schema.oneOf !== undefined) {
69
+ violations.push(...validateCombinator(value, schema.oneOf, path, defs, 'oneOf', seenRefs));
70
+ }
71
+ if (schema.anyOf !== undefined) {
72
+ violations.push(...validateCombinator(value, schema.anyOf, path, defs, 'anyOf', seenRefs));
73
+ }
74
+ if (schema.const !== undefined && !jsonEqual(value, schema.const)) {
75
+ violations.push({ path: path || '(root)', message: `expected constant ${JSON.stringify(schema.const)}` });
76
+ }
77
+ if (schema.enum !== undefined && !schema.enum.some((entry) => jsonEqual(value, entry))) {
78
+ violations.push({ path: path || '(root)', message: `expected one of ${schema.enum.map(String).join(', ')}` });
79
+ }
80
+ if (schema.type !== undefined) {
81
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
82
+ if (!types.some((type) => matchesType(value, type))) {
83
+ violations.push({
84
+ path: path || '(root)',
85
+ message: `expected ${types.join(' or ')}, got ${typeName(value)}`,
86
+ });
87
+ return violations;
88
+ }
89
+ }
90
+ const hasObjectKeywords = schema.properties !== undefined || schema.required !== undefined || schema.additionalProperties !== undefined;
91
+ if (schema.type === 'object' || (hasObjectKeywords && isObject(value))) {
92
+ violations.push(...validateObject(value, schema, path, defs, seenRefs));
93
+ }
94
+ if (schema.type === 'array' || (schema.items !== undefined && Array.isArray(value))) {
95
+ violations.push(...validateArray(value, schema, path, defs, seenRefs));
96
+ }
97
+ return violations;
98
+ }
99
+ function validateObject(value, schema, path, defs, seenRefs) {
100
+ if (!isObject(value))
101
+ return [{ path: path || '(root)', message: `expected object, got ${typeName(value)}` }];
102
+ const violations = [];
103
+ for (const key of schema.required ?? []) {
104
+ if (!(key in value)) {
105
+ violations.push({ path: path ? `${path}.${key}` : key, message: `missing required field "${key}"` });
106
+ }
107
+ }
108
+ const properties = schema.properties ?? {};
109
+ for (const [key, childSchema] of Object.entries(properties)) {
110
+ if (key in value) {
111
+ violations.push(...validateJsonSchema(value[key], childSchema, path ? `${path}.${key}` : key, defs, seenRefs));
112
+ }
113
+ }
114
+ if (schema.additionalProperties === false) {
115
+ const allowed = new Set(Object.keys(properties));
116
+ for (const key of Object.keys(value)) {
117
+ if (!allowed.has(key)) {
118
+ violations.push({ path: path ? `${path}.${key}` : key, message: `unknown field "${key}"` });
119
+ }
120
+ }
121
+ }
122
+ else if (isObject(schema.additionalProperties)) {
123
+ for (const [key, child] of Object.entries(value)) {
124
+ if (!(key in properties)) {
125
+ violations.push(...validateJsonSchema(child, schema.additionalProperties, path ? `${path}.${key}` : key, defs, seenRefs));
126
+ }
127
+ }
128
+ }
129
+ return violations;
130
+ }
131
+ function validateArray(value, schema, path, defs, seenRefs) {
132
+ if (!Array.isArray(value))
133
+ return [{ path: path || '(root)', message: `expected array, got ${typeName(value)}` }];
134
+ if (schema.items === undefined)
135
+ return [];
136
+ return value.flatMap((entry, index) => validateJsonSchema(entry, schema.items, `${path}[${index}]`, defs, seenRefs));
137
+ }
138
+ function validateCombinator(value, schemas, path, defs, mode, seenRefs) {
139
+ const branchViolations = schemas.map((schema) => validateJsonSchema(value, schema, path, defs, seenRefs));
140
+ const passing = branchViolations.filter((violations) => violations.length === 0).length;
141
+ if (mode === 'anyOf' && passing >= 1)
142
+ return [];
143
+ if (mode === 'oneOf' && passing === 1)
144
+ return [];
145
+ const at = path || '(root)';
146
+ // oneOf matching more than one branch is a failure, not a pass — report it explicitly.
147
+ if (mode === 'oneOf' && passing > 1) {
148
+ return [{ path: at, message: `expected to match exactly one oneOf branch, matched ${passing}` }];
149
+ }
150
+ // No branch matched: surface every branch's reason so the author sees all options, not just branch 0.
151
+ const detail = branchViolations
152
+ .map((branch, index) => `[${index}] ${branch.map((v) => `${v.path}: ${v.message}`).join(', ')}`)
153
+ .join(' | ');
154
+ return [
155
+ {
156
+ path: at,
157
+ message: `expected to match ${mode === 'oneOf' ? 'exactly one' : 'at least one'} branch — ${detail}`,
158
+ },
159
+ ];
160
+ }
161
+ function resolveRef(ref, defs, localDefs) {
162
+ if (!ref.startsWith('#/$defs/'))
163
+ return undefined;
164
+ const name = ref.slice('#/$defs/'.length);
165
+ return defs[name] ?? localDefs?.[name];
166
+ }
167
+ function resolveSchemaRef(schemaRef, source, resolve) {
168
+ if (isRemoteRef(schemaRef) || isAbsolutePath(schemaRef))
169
+ return schemaRef;
170
+ // Relative ref — resolve against the config file's directory.
171
+ if (schemaRef.startsWith('./') || schemaRef.startsWith('../')) {
172
+ return joinPath(isRemoteRef(source) ? '.' : dirnamePath(source), schemaRef);
173
+ }
174
+ // Bare package specifier (e.g. "@scope/pkg/schemas/x.json") — resolve through node_modules.
175
+ return resolvePackageSchema(schemaRef, source, resolve);
176
+ }
177
+ function resolvePackageSchema(specifier, source, resolve) {
178
+ const resolveFn = resolve ?? defaultResolve;
179
+ if (resolveFn === undefined) {
180
+ throw new StructuredConfigSchemaError(`Cannot resolve package schema "${specifier}" referenced by "${source}": no module resolver available`);
181
+ }
182
+ const { pkg, subpath } = splitPackageSpecifier(specifier);
183
+ if (subpath.length === 0) {
184
+ throw new StructuredConfigSchemaError(`Package schema ref "${specifier}" referenced by "${source}" must include a path within the package`);
185
+ }
186
+ const from = isRemoteRef(source) ? process.cwd() : dirnamePath(source);
187
+ try {
188
+ // Resolve the package root via its always-present package.json, then join the subpath.
189
+ // This sidesteps `exports` gating on arbitrary JSON subpaths.
190
+ const manifest = resolveFn(`${pkg}/package.json`, from);
191
+ return joinPath(dirnamePath(manifest), subpath);
192
+ }
193
+ catch (error) {
194
+ throw new StructuredConfigSchemaError(`Cannot resolve package schema "${specifier}" referenced by "${source}": ${errorMessage(error)}`);
195
+ }
196
+ }
197
+ function splitPackageSpecifier(specifier) {
198
+ const parts = specifier.split('/');
199
+ const segments = specifier.startsWith('@') ? 2 : 1;
200
+ return { pkg: parts.slice(0, segments).join('/'), subpath: parts.slice(segments).join('/') };
201
+ }
202
+ async function readSchema(schemaLocation, options) {
203
+ if (isRemoteRef(schemaLocation)) {
204
+ const fetchFn = options.fetch ?? (options.allowRemote ? boundedFetch : undefined);
205
+ if (fetchFn === undefined) {
206
+ throw new StructuredConfigSchemaError(`Refusing to fetch remote JSON schema "${schemaLocation}": pass { allowRemote: true } or a fetch implementation to opt in`);
207
+ }
208
+ const response = await fetchFn(schemaLocation);
209
+ if (!response.ok) {
210
+ throw new StructuredConfigSchemaError(`Failed to fetch JSON schema "${schemaLocation}": HTTP ${response.status}`);
211
+ }
212
+ return await readBoundedBody(response, schemaLocation);
213
+ }
214
+ return await getFs().readFile(schemaLocation);
215
+ }
216
+ /**
217
+ * Read a response body under a hard byte cap. `Content-Length` is a fast-path reject, but servers
218
+ * lie or omit it, so the stream is also tallied chunk-by-chunk — a multi-GB drip is aborted before
219
+ * it can exhaust memory. Falls back to `.text()` only when the body is not a readable stream.
220
+ */
221
+ async function readBoundedBody(response, schemaLocation) {
222
+ const declared = Number(response.headers.get('content-length'));
223
+ if (Number.isFinite(declared) && declared > REMOTE_SCHEMA_MAX_BYTES) {
224
+ throw new StructuredConfigSchemaError(`Remote JSON schema "${schemaLocation}" exceeds the ${REMOTE_SCHEMA_MAX_BYTES}-byte limit (Content-Length ${declared})`);
225
+ }
226
+ const body = response.body;
227
+ if (body === null)
228
+ return await response.text();
229
+ const reader = body.getReader();
230
+ const chunks = [];
231
+ let total = 0;
232
+ try {
233
+ while (true) {
234
+ const { done, value } = await reader.read();
235
+ if (done)
236
+ break;
237
+ total += value.byteLength;
238
+ if (total > REMOTE_SCHEMA_MAX_BYTES) {
239
+ await reader.cancel();
240
+ throw new StructuredConfigSchemaError(`Remote JSON schema "${schemaLocation}" exceeds the ${REMOTE_SCHEMA_MAX_BYTES}-byte limit`);
241
+ }
242
+ chunks.push(value);
243
+ }
244
+ }
245
+ finally {
246
+ reader.releaseLock();
247
+ }
248
+ const merged = new Uint8Array(total);
249
+ let offset = 0;
250
+ for (const chunk of chunks) {
251
+ merged.set(chunk, offset);
252
+ offset += chunk.byteLength;
253
+ }
254
+ return new TextDecoder().decode(merged);
255
+ }
256
+ const defaultResolve = typeof Bun !== 'undefined' ? (specifier, from) => Bun.resolveSync(specifier, from) : undefined;
257
+ /** Default remote fetch, time-bounded so a slow/hung schema host cannot stall config loading. */
258
+ function boundedFetch(input) {
259
+ return globalThis.fetch(input, { signal: AbortSignal.timeout(REMOTE_SCHEMA_FETCH_TIMEOUT_MS) });
260
+ }
261
+ function isObject(value) {
262
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
263
+ }
264
+ function matchesType(value, type) {
265
+ if (type === 'object')
266
+ return isObject(value);
267
+ if (type === 'array')
268
+ return Array.isArray(value);
269
+ if (type === 'integer')
270
+ return typeof value === 'number' && Number.isInteger(value);
271
+ if (type === 'null')
272
+ return value === null;
273
+ return typeof value === type;
274
+ }
275
+ function typeName(value) {
276
+ if (Array.isArray(value))
277
+ return 'array';
278
+ if (value === null)
279
+ return 'null';
280
+ return typeof value;
281
+ }
282
+ function jsonEqual(left, right) {
283
+ if (left === right)
284
+ return true;
285
+ if (Array.isArray(left) || Array.isArray(right)) {
286
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
287
+ return false;
288
+ return left.every((entry, index) => jsonEqual(entry, right[index]));
289
+ }
290
+ if (isObject(left) && isObject(right)) {
291
+ const keys = Object.keys(left);
292
+ if (keys.length !== Object.keys(right).length)
293
+ return false;
294
+ // Object member order is insignificant in JSON — compare by key, not by serialization.
295
+ return keys.every((key) => key in right && jsonEqual(left[key], right[key]));
296
+ }
297
+ return false;
298
+ }
299
+ function errorMessage(error) {
300
+ return error instanceof Error ? error.message : String(error);
301
+ }
302
+ function isRemoteRef(ref) {
303
+ return /^https?:\/\//i.test(ref);
304
+ }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,4 @@
1
1
  import type { Config } from './config';
2
- import type { RuntimeContext } from './context';
3
- import type { FileSystem } from './fs';
4
2
  export type RuntimeName = 'node-bun' | 'cloudflare-workers' | 'test';
5
3
  export interface RuntimeCapabilities {
6
4
  readonly hasFilesystem: boolean;
@@ -11,15 +9,6 @@ export interface LoadConfigOptions {
11
9
  overrides?: Partial<Config>;
12
10
  envBindings?: Record<string, unknown>;
13
11
  }
14
- export interface RuntimeFactory {
15
- readonly runtimeName: RuntimeName;
16
- readonly capabilities: RuntimeCapabilities;
17
- createFileSystem(): FileSystem;
18
- loadConfig(options?: LoadConfigOptions): Promise<Config>;
19
- createContext?(options?: {
20
- scope?: string;
21
- }): RuntimeContext;
22
- }
23
12
  export interface SpanContext {
24
13
  traceId: string;
25
14
  spanId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAEvC,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,oBAAoB,GAAG,MAAM,CAAC;AAErE,MAAM,WAAW,mBAAmB;IAChC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;CAC1C;AAED,MAAM,WAAW,iBAAiB;IAC9B,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,cAAc;IAC3B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,gBAAgB,IAAI,UAAU,CAAC;IAC/B,UAAU,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,aAAa,CAAC,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,cAAc,CAAC;CAChE;AAED,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;CAC1D"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,oBAAoB,GAAG,MAAM,CAAC;AAErE,MAAM,WAAW,mBAAmB;IAChC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;CAC1C;AAED,MAAM,WAAW,iBAAiB;IAC9B,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;CAC1D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-runtime",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "@gobing-ai/ts-runtime — Runtime abstractions for Bun, Node, and Cloudflare Workers.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -54,7 +54,7 @@
54
54
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-runtime-v<version> && git push --tags' && exit 1"
55
55
  },
56
56
  "dependencies": {
57
- "@gobing-ai/ts-utils": "^0.2.7",
57
+ "@gobing-ai/ts-utils": "^0.2.9",
58
58
  "execa": "^9.5.0",
59
59
  "yaml": "^2.7.0",
60
60
  "zod": "^4.1.0"
package/src/config.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { parse as parseYaml } from 'yaml';
1
+ import { deepMerge } from '@gobing-ai/ts-utils';
2
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
2
3
  import { type ZodIssue, z } from 'zod';
3
4
 
4
5
  export const configSchema = z.object({
@@ -29,6 +30,40 @@ export type Config = z.output<typeof configSchema>;
29
30
 
30
31
  const ENV_INTERPOLATION_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
31
32
 
33
+ /** Raised when a YAML text cannot be parsed as a plain object. */
34
+ export class YamlParseError extends Error {
35
+ constructor(
36
+ message: string,
37
+ readonly innerError?: unknown,
38
+ ) {
39
+ super(message);
40
+ this.name = 'YamlParseError';
41
+ }
42
+ }
43
+
44
+ /** Parse YAML text into a plain object. Returns `{}` for null/undefined/empty input. */
45
+ export function parseYamlObject(text: string): Record<string, unknown> {
46
+ let parsed: unknown;
47
+ try {
48
+ parsed = parseYaml(text);
49
+ } catch (error) {
50
+ throw new YamlParseError(
51
+ `YAML parsing failed: ${(error as Error).message}`,
52
+ error instanceof Error ? error : undefined,
53
+ );
54
+ }
55
+ if (parsed === null || parsed === undefined) return {};
56
+ if (typeof parsed !== 'object' || Array.isArray(parsed)) {
57
+ throw new YamlParseError('YAML must parse to an object');
58
+ }
59
+ return parsed as Record<string, unknown>;
60
+ }
61
+
62
+ /** Serialize a plain object to a YAML string. */
63
+ export function stringifyYamlObject(value: Record<string, unknown>): string {
64
+ return stringifyYaml(value);
65
+ }
66
+
32
67
  export class ConfigLoadError extends Error {
33
68
  readonly issues: ZodIssue[];
34
69
 
@@ -39,6 +74,9 @@ export class ConfigLoadError extends Error {
39
74
  }
40
75
  }
41
76
 
77
+ // These accessors read `process.env` directly and are node-bun only (ADR-008). On
78
+ // `cloudflare-workers` there is no `process`; inject config explicitly rather than calling these.
79
+
42
80
  export function getNodeEnv(): string {
43
81
  return process.env.NODE_ENV ?? 'development';
44
82
  }
@@ -55,6 +93,7 @@ export function getDatabaseUrl(): string | undefined {
55
93
  return process.env.DATABASE_URL;
56
94
  }
57
95
 
96
+ /** Node-bun only: interpolates `${VAR}` from `process.env` (see note above). */
58
97
  export function interpolateEnv(value: string): string {
59
98
  return value.replace(ENV_INTERPOLATION_RE, (_match, name: string) => process.env[name] ?? `\${${name}}`);
60
99
  }
@@ -68,48 +107,6 @@ export function interpolateTree(value: unknown): unknown {
68
107
  return value;
69
108
  }
70
109
 
71
- export function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
72
- const result = { ...target };
73
- for (const [key, value] of Object.entries(source)) {
74
- if (isPlainObject(value) && isPlainObject(result[key])) {
75
- result[key] = deepMerge(result[key] as Record<string, unknown>, value);
76
- } else {
77
- result[key] = value;
78
- }
79
- }
80
- return result;
81
- }
82
-
83
- export function flattenKeys(obj: Record<string, unknown>, prefix = ''): Record<string, string> {
84
- const result: Record<string, string> = {};
85
- for (const [key, value] of Object.entries(obj)) {
86
- const fullKey = prefix ? `${prefix}.${key}` : key;
87
- if (isPlainObject(value)) {
88
- Object.assign(result, flattenKeys(value, fullKey));
89
- } else {
90
- result[fullKey] = JSON.stringify(value);
91
- }
92
- }
93
- return result;
94
- }
95
-
96
- export function deFlattenKeys(entries: Record<string, string>): Record<string, unknown> {
97
- const result: Record<string, unknown> = {};
98
- for (const [key, rawValue] of Object.entries(entries)) {
99
- const parts = key.split('.');
100
- let current = result;
101
- for (const part of parts.slice(0, -1)) {
102
- if (!isPlainObject(current[part])) current[part] = {};
103
- current = current[part] as Record<string, unknown>;
104
- }
105
-
106
- const last = parts.at(-1);
107
- if (last === undefined) continue;
108
- current[last] = parseConfigValue(rawValue);
109
- }
110
- return result;
111
- }
112
-
113
110
  export function buildConfigFromObject(
114
111
  raw: Record<string, unknown>,
115
112
  options: { overrides?: Partial<Config> } = {},
@@ -127,12 +124,7 @@ export function buildConfigFromObject(
127
124
 
128
125
  export function parseConfigYaml(yamlText: string): Record<string, unknown> {
129
126
  try {
130
- const parsed = parseYaml(yamlText);
131
- if (parsed === null || parsed === undefined) return {};
132
- if (!isPlainObject(parsed)) {
133
- throw new ConfigLoadError('Config YAML must parse to an object');
134
- }
135
- return parsed;
127
+ return parseYamlObject(yamlText);
136
128
  } catch (error) {
137
129
  if (error instanceof ConfigLoadError) throw error;
138
130
  throw new ConfigLoadError(`Config YAML parsing failed: ${(error as Error).message}`);
@@ -143,14 +135,6 @@ export function buildConfigFromYaml(yamlText: string, options: { overrides?: Par
143
135
  return buildConfigFromObject(parseConfigYaml(yamlText), options);
144
136
  }
145
137
 
146
- function parseConfigValue(value: string): unknown {
147
- try {
148
- return JSON.parse(value);
149
- } catch {
150
- return value;
151
- }
152
- }
153
-
154
138
  function isPlainObject(value: unknown): value is Record<string, unknown> {
155
139
  return typeof value === 'object' && value !== null && !Array.isArray(value);
156
140
  }
package/src/context.ts CHANGED
@@ -2,7 +2,7 @@ import type { Config } from './config';
2
2
  import { buildConfigFromObject } from './config';
3
3
  import type { FileSystem } from './fs';
4
4
  import { getFs } from './fs';
5
- import type { RuntimeCapabilities, RuntimeFactory, RuntimeName } from './types';
5
+ import type { RuntimeCapabilities, RuntimeName } from './types';
6
6
 
7
7
  export type RuntimeScope = 'process' | 'server-request' | 'scheduled-event' | 'test';
8
8
 
@@ -17,7 +17,6 @@ export interface RuntimeContextOptions<TServices extends RuntimeServiceMap = Run
17
17
  runtimeName?: RuntimeName;
18
18
  capabilities?: RuntimeCapabilities;
19
19
  services?: Partial<TServices>;
20
- factory?: RuntimeFactory;
21
20
  }
22
21
 
23
22
  export class RuntimeContext<TServices extends RuntimeServiceMap = RuntimeServiceMap> {
@@ -28,10 +27,9 @@ export class RuntimeContext<TServices extends RuntimeServiceMap = RuntimeService
28
27
 
29
28
  constructor(options: RuntimeContextOptions<TServices> = {}) {
30
29
  this.scope = options.scope ?? 'process';
31
- this.runtimeName = options.runtimeName ?? options.factory?.runtimeName ?? 'node-bun';
30
+ this.runtimeName = options.runtimeName ?? 'node-bun';
32
31
  this.capabilities =
33
32
  options.capabilities ??
34
- options.factory?.capabilities ??
35
33
  ({
36
34
  hasFilesystem: true,
37
35
  hasProcessExecution: true,