apcore-js 0.1.1 → 0.2.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.
@@ -5,7 +5,8 @@
5
5
  "Bash(npm test:*)",
6
6
  "Bash(npx tsc:*)",
7
7
  "Bash(wc:*)",
8
- "Bash(ls:*)"
8
+ "Bash(ls:*)",
9
+ "Skill(code-forge:review)"
9
10
  ]
10
11
  }
11
12
  }
@@ -5,13 +5,13 @@ repos:
5
5
  hooks:
6
6
  - id: check-chars
7
7
  name: apdev-js check-chars
8
- entry: apdev-js check-chars
8
+ entry: npx apdev-js check-chars
9
9
  language: system
10
10
  types_or: [text, ts, javascript]
11
11
 
12
12
  - id: check-imports
13
13
  name: apdev-js check-imports
14
- entry: apdev-js check-imports
14
+ entry: npx apdev-js check-imports --package apcore-js
15
15
  language: system
16
16
  pass_filenames: false
17
17
  always_run: true
package/CHANGELOG.md CHANGED
@@ -5,6 +5,69 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.0] - 2026-02-20
9
+
10
+ ### Added
11
+
12
+ - **Error classes and constants**
13
+ - `ModuleExecuteError` — New error class for module execution failures
14
+ - `InternalError` — New error class for general internal errors
15
+ - `ErrorCodes` — Frozen object with all 26 error code strings for consistent error code usage
16
+ - `ErrorCode` — Type definition for all error codes
17
+ - **Registry constants**
18
+ - `REGISTRY_EVENTS` — Frozen object with standard event names (`register`, `unregister`)
19
+ - `MODULE_ID_PATTERN` — Regex pattern enforcing lowercase/digits/underscores/dots for module IDs (no hyphens allowed to ensure bijective MCP tool name normalization)
20
+ - **Executor methods**
21
+ - `Executor.callAsync()` — Alias for `call()` for compatibility with MCP bridge packages
22
+
23
+ ### Changed
24
+
25
+ - **Module ID validation** — Registry now validates module IDs against `MODULE_ID_PATTERN` on registration, rejecting IDs with hyphens or invalid characters
26
+ - **Event handling** — Registry event validation now uses `REGISTRY_EVENTS` constants instead of hardcoded strings
27
+ - **Test updates** — Updated tests to use underscore-separated module IDs instead of hyphens (e.g., `math.add_ten` instead of `math.addTen`, `ctx_test` instead of `ctx-test`)
28
+
29
+ ### Fixed
30
+
31
+ - **String literals in Registry** — Replaced hardcoded `'register'` and `'unregister'` strings with `REGISTRY_EVENTS.REGISTER` and `REGISTRY_EVENTS.UNREGISTER` constants in event triggers for consistency
32
+
33
+ ## [0.1.2] - 2026-02-18
34
+
35
+ ### Fixed
36
+
37
+ - **Timer leak in executor** — `_executeWithTimeout` now calls `clearTimeout` in `.finally()` to prevent timer leak on normal completion
38
+ - **Path traversal protection** — `resolveTarget` in binding loader rejects module paths containing `..` segments before dynamic `import()`
39
+ - **Bare catch blocks** — 6 silent `catch {}` blocks in registry and middleware manager now log warnings with `[apcore:<subsystem>]` prefix
40
+ - **Python-style error messages** — Fixed `FuncMissingTypeHintError` and `FuncMissingReturnTypeError` to use TypeScript syntax (`: string`, `: Record<string, unknown>`)
41
+ - **Console.log in production** — Replaced `console.log` with `console.info` in logging middleware and `process.stdout.write` in tracing exporter
42
+
43
+ ### Changed
44
+
45
+ - **Long method decomposition** — Broke up 4 oversized methods to meet ≤50 line guideline:
46
+ - `Executor.call()` (108 → 6 private helpers)
47
+ - `Registry.discover()` (110 → 7 private helpers)
48
+ - `ACL.load()` (71 → extracted `parseAclRule`)
49
+ - `jsonSchemaToTypeBox()` (80 → 5 converter helpers)
50
+ - **Deeply readonly callChain** — `Context.callChain` type narrowed from `readonly string[]` to `readonly (readonly string[])` preventing mutation via push/splice
51
+ - **Consolidated `deepCopy`** — Removed 4 duplicate `deepCopy` implementations; single shared version now lives in `src/utils/index.ts`
52
+
53
+ ### Added
54
+
55
+ - **42 new tests** for previously uncovered modules:
56
+ - `tests/schema/test-annotations.test.ts` — 16 tests for `mergeAnnotations`, `mergeExamples`, `mergeMetadata`
57
+ - `tests/schema/test-exporter.test.ts` — 14 tests for `SchemaExporter` across all 4 export profiles
58
+ - `tests/test-logging-middleware.test.ts` — 12 tests for `LoggingMiddleware` before/after/onError
59
+
60
+ ## [0.1.1] - 2026-02-17
61
+
62
+ ### Fixed
63
+
64
+ - Updated logo URL in README
65
+
66
+ ### Changed
67
+
68
+ - Renamed package from `apcore` to `apcore-js`
69
+ - Updated installation instructions
70
+
8
71
  ## [0.1.0] - 2026-02-16
9
72
 
10
73
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apcore-js",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "AI-Perceivable Core — schema-driven module development framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -3,7 +3,7 @@
3
3
  ## Overall Progress
4
4
 
5
5
  ```
6
- [████████████████████████████████████████] 42/42 tasks (100%)
6
+ [>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>] 42/42 tasks (100%)
7
7
  ```
8
8
 
9
9
  | Status | Count |
package/src/acl.ts CHANGED
@@ -16,6 +16,42 @@ export interface ACLRule {
16
16
  conditions?: Record<string, unknown> | null;
17
17
  }
18
18
 
19
+ function parseAclRule(rawRule: unknown, index: number): ACLRule {
20
+ if (typeof rawRule !== 'object' || rawRule === null || Array.isArray(rawRule)) {
21
+ throw new ACLRuleError(`Rule ${index} must be a mapping, got ${typeof rawRule}`);
22
+ }
23
+
24
+ const ruleObj = rawRule as Record<string, unknown>;
25
+ for (const key of ['callers', 'targets', 'effect']) {
26
+ if (!(key in ruleObj)) {
27
+ throw new ACLRuleError(`Rule ${index} missing required key '${key}'`);
28
+ }
29
+ }
30
+
31
+ const effect = ruleObj['effect'] as string;
32
+ if (effect !== 'allow' && effect !== 'deny') {
33
+ throw new ACLRuleError(`Rule ${index} has invalid effect '${effect}', must be 'allow' or 'deny'`);
34
+ }
35
+
36
+ const callers = ruleObj['callers'];
37
+ if (!Array.isArray(callers)) {
38
+ throw new ACLRuleError(`Rule ${index} 'callers' must be a list, got ${typeof callers}`);
39
+ }
40
+
41
+ const targets = ruleObj['targets'];
42
+ if (!Array.isArray(targets)) {
43
+ throw new ACLRuleError(`Rule ${index} 'targets' must be a list, got ${typeof targets}`);
44
+ }
45
+
46
+ return {
47
+ callers: callers as string[],
48
+ targets: targets as string[],
49
+ effect,
50
+ description: (ruleObj['description'] as string) ?? '',
51
+ conditions: (ruleObj['conditions'] as Record<string, unknown>) ?? null,
52
+ };
53
+ }
54
+
19
55
  export class ACL {
20
56
  private _rules: ACLRule[];
21
57
  private _defaultEffect: string;
@@ -56,44 +92,7 @@ export class ACL {
56
92
  }
57
93
 
58
94
  const defaultEffect = (dataObj['default_effect'] as string) ?? 'deny';
59
- const rules: ACLRule[] = [];
60
-
61
- for (let i = 0; i < rawRules.length; i++) {
62
- const rawRule = rawRules[i];
63
- if (typeof rawRule !== 'object' || rawRule === null || Array.isArray(rawRule)) {
64
- throw new ACLRuleError(`Rule ${i} must be a mapping, got ${typeof rawRule}`);
65
- }
66
-
67
- const ruleObj = rawRule as Record<string, unknown>;
68
- for (const key of ['callers', 'targets', 'effect']) {
69
- if (!(key in ruleObj)) {
70
- throw new ACLRuleError(`Rule ${i} missing required key '${key}'`);
71
- }
72
- }
73
-
74
- const effect = ruleObj['effect'] as string;
75
- if (effect !== 'allow' && effect !== 'deny') {
76
- throw new ACLRuleError(`Rule ${i} has invalid effect '${effect}', must be 'allow' or 'deny'`);
77
- }
78
-
79
- const callers = ruleObj['callers'];
80
- if (!Array.isArray(callers)) {
81
- throw new ACLRuleError(`Rule ${i} 'callers' must be a list, got ${typeof callers}`);
82
- }
83
-
84
- const targets = ruleObj['targets'];
85
- if (!Array.isArray(targets)) {
86
- throw new ACLRuleError(`Rule ${i} 'targets' must be a list, got ${typeof targets}`);
87
- }
88
-
89
- rules.push({
90
- callers: callers as string[],
91
- targets: targets as string[],
92
- effect,
93
- description: (ruleObj['description'] as string) ?? '',
94
- conditions: (ruleObj['conditions'] as Record<string, unknown>) ?? null,
95
- });
96
- }
95
+ const rules = rawRules.map((raw, i) => parseAclRule(raw, i));
97
96
 
98
97
  const acl = new ACL(rules, defaultEffect);
99
98
  acl._yamlPath = yamlPath;
package/src/bindings.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
6
- import { resolve, dirname, join, basename } from 'node:path';
6
+ import { resolve, dirname, join, basename, isAbsolute } from 'node:path';
7
7
  import { Type, type TSchema } from '@sinclair/typebox';
8
8
  import yaml from 'js-yaml';
9
9
  import { FunctionModule } from './decorator.js';
@@ -18,19 +18,6 @@ import {
18
18
  import type { Registry } from './registry/registry.js';
19
19
  import { jsonSchemaToTypeBox } from './schema/loader.js';
20
20
 
21
- const JSON_SCHEMA_TYPE_MAP: Record<string, () => TSchema> = {
22
- string: () => Type.String(),
23
- integer: () => Type.Integer(),
24
- number: () => Type.Number(),
25
- boolean: () => Type.Boolean(),
26
- array: () => Type.Array(Type.Unknown()),
27
- object: () => Type.Record(Type.String(), Type.Unknown()),
28
- };
29
-
30
- function buildSchemaFromJsonSchema(schema: Record<string, unknown>): TSchema {
31
- return jsonSchemaToTypeBox(schema);
32
- }
33
-
34
21
  export class BindingLoader {
35
22
  async loadBindings(filePath: string, registry: Registry): Promise<FunctionModule[]> {
36
23
  const bindingFileDir = dirname(filePath);
@@ -113,6 +100,12 @@ export class BindingLoader {
113
100
 
114
101
  const [modulePath, callableName] = targetString.split(':', 2);
115
102
 
103
+ if (modulePath.includes('..')) {
104
+ throw new BindingInvalidTargetError(
105
+ `Module path '${modulePath}' must not contain '..' segments`,
106
+ );
107
+ }
108
+
116
109
  let mod: Record<string, unknown>;
117
110
  try {
118
111
  mod = await import(modulePath);
@@ -165,8 +158,8 @@ export class BindingLoader {
165
158
  if ('input_schema' in binding || 'output_schema' in binding) {
166
159
  const inputSchemaDict = (binding['input_schema'] as Record<string, unknown>) ?? {};
167
160
  const outputSchemaDict = (binding['output_schema'] as Record<string, unknown>) ?? {};
168
- inputSchema = buildSchemaFromJsonSchema(inputSchemaDict);
169
- outputSchema = buildSchemaFromJsonSchema(outputSchemaDict);
161
+ inputSchema = jsonSchemaToTypeBox(inputSchemaDict);
162
+ outputSchema = jsonSchemaToTypeBox(outputSchemaDict);
170
163
  } else if ('schema_ref' in binding) {
171
164
  const refPath = resolve(bindingFileDir, binding['schema_ref'] as string);
172
165
  if (!existsSync(refPath)) {
@@ -178,10 +171,10 @@ export class BindingLoader {
178
171
  } catch (e) {
179
172
  throw new BindingFileInvalidError(refPath, `YAML parse error: ${e}`);
180
173
  }
181
- inputSchema = buildSchemaFromJsonSchema(
174
+ inputSchema = jsonSchemaToTypeBox(
182
175
  (refData['input_schema'] as Record<string, unknown>) ?? {},
183
176
  );
184
- outputSchema = buildSchemaFromJsonSchema(
177
+ outputSchema = jsonSchemaToTypeBox(
185
178
  (refData['output_schema'] as Record<string, unknown>) ?? {},
186
179
  );
187
180
  } else {
package/src/context.ts CHANGED
@@ -21,7 +21,7 @@ export function createIdentity(
21
21
  export class Context {
22
22
  readonly traceId: string;
23
23
  readonly callerId: string | null;
24
- readonly callChain: string[];
24
+ readonly callChain: readonly string[];
25
25
  readonly executor: unknown;
26
26
  readonly identity: Identity | null;
27
27
  redactedInputs: Record<string, unknown> | null;
package/src/errors.ts CHANGED
@@ -242,7 +242,7 @@ export class FuncMissingTypeHintError extends ModuleError {
242
242
  constructor(functionName: string, parameterName: string, options?: { cause?: Error; traceId?: string }) {
243
243
  super(
244
244
  'FUNC_MISSING_TYPE_HINT',
245
- `Parameter '${parameterName}' in function '${functionName}' has no type annotation. Add a type hint like '${parameterName}: str'.`,
245
+ `Parameter '${parameterName}' in function '${functionName}' has no type annotation. Add a type annotation like '${parameterName}: string'.`,
246
246
  { functionName, parameterName },
247
247
  options?.cause,
248
248
  options?.traceId,
@@ -255,7 +255,7 @@ export class FuncMissingReturnTypeError extends ModuleError {
255
255
  constructor(functionName: string, options?: { cause?: Error; traceId?: string }) {
256
256
  super(
257
257
  'FUNC_MISSING_RETURN_TYPE',
258
- `Function '${functionName}' has no return type annotation. Add a return type like '-> dict'.`,
258
+ `Function '${functionName}' has no return type annotation. Add a return type like ': Record<string, unknown>'.`,
259
259
  { functionName },
260
260
  options?.cause,
261
261
  options?.traceId,
@@ -367,3 +367,58 @@ export class ModuleLoadError extends ModuleError {
367
367
  this.name = 'ModuleLoadError';
368
368
  }
369
369
  }
370
+
371
+ export class ModuleExecuteError extends ModuleError {
372
+ constructor(moduleId: string, reason: string, options?: { cause?: Error; traceId?: string }) {
373
+ super(
374
+ 'MODULE_EXECUTE_ERROR',
375
+ `Failed to execute module '${moduleId}': ${reason}`,
376
+ { moduleId, reason },
377
+ options?.cause,
378
+ options?.traceId,
379
+ );
380
+ this.name = 'ModuleExecuteError';
381
+ }
382
+ }
383
+
384
+ export class InternalError extends ModuleError {
385
+ constructor(message: string = 'Internal error', options?: { cause?: Error; traceId?: string }) {
386
+ super('GENERAL_INTERNAL_ERROR', message, {}, options?.cause, options?.traceId);
387
+ this.name = 'InternalError';
388
+ }
389
+ }
390
+
391
+ /**
392
+ * All framework error codes as constants.
393
+ * Use these instead of hardcoding error code strings.
394
+ */
395
+ export const ErrorCodes = Object.freeze({
396
+ CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
397
+ CONFIG_INVALID: "CONFIG_INVALID",
398
+ ACL_RULE_ERROR: "ACL_RULE_ERROR",
399
+ ACL_DENIED: "ACL_DENIED",
400
+ MODULE_NOT_FOUND: "MODULE_NOT_FOUND",
401
+ MODULE_TIMEOUT: "MODULE_TIMEOUT",
402
+ MODULE_LOAD_ERROR: "MODULE_LOAD_ERROR",
403
+ MODULE_EXECUTE_ERROR: "MODULE_EXECUTE_ERROR",
404
+ SCHEMA_VALIDATION_ERROR: "SCHEMA_VALIDATION_ERROR",
405
+ SCHEMA_NOT_FOUND: "SCHEMA_NOT_FOUND",
406
+ SCHEMA_PARSE_ERROR: "SCHEMA_PARSE_ERROR",
407
+ SCHEMA_CIRCULAR_REF: "SCHEMA_CIRCULAR_REF",
408
+ CALL_DEPTH_EXCEEDED: "CALL_DEPTH_EXCEEDED",
409
+ CIRCULAR_CALL: "CIRCULAR_CALL",
410
+ CALL_FREQUENCY_EXCEEDED: "CALL_FREQUENCY_EXCEEDED",
411
+ GENERAL_INVALID_INPUT: "GENERAL_INVALID_INPUT",
412
+ GENERAL_INTERNAL_ERROR: "GENERAL_INTERNAL_ERROR",
413
+ FUNC_MISSING_TYPE_HINT: "FUNC_MISSING_TYPE_HINT",
414
+ FUNC_MISSING_RETURN_TYPE: "FUNC_MISSING_RETURN_TYPE",
415
+ BINDING_INVALID_TARGET: "BINDING_INVALID_TARGET",
416
+ BINDING_MODULE_NOT_FOUND: "BINDING_MODULE_NOT_FOUND",
417
+ BINDING_CALLABLE_NOT_FOUND: "BINDING_CALLABLE_NOT_FOUND",
418
+ BINDING_NOT_CALLABLE: "BINDING_NOT_CALLABLE",
419
+ BINDING_SCHEMA_MISSING: "BINDING_SCHEMA_MISSING",
420
+ BINDING_FILE_INVALID: "BINDING_FILE_INVALID",
421
+ CIRCULAR_DEPENDENCY: "CIRCULAR_DEPENDENCY",
422
+ } as const);
423
+
424
+ export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
package/src/executor.ts CHANGED
@@ -157,60 +157,97 @@ export class Executor {
157
157
  context?: Context | null,
158
158
  ): Promise<Record<string, unknown>> {
159
159
  let effectiveInputs = inputs ?? {};
160
+ const ctx = this._createContext(moduleId, context);
161
+ this._checkSafety(moduleId, ctx);
162
+
163
+ const mod = this._lookupModule(moduleId);
164
+ this._checkAcl(moduleId, ctx);
165
+
166
+ effectiveInputs = this._validateInputs(mod, effectiveInputs, ctx);
167
+
168
+ return this._executeWithMiddleware(mod, moduleId, effectiveInputs, ctx);
169
+ }
170
+
171
+ /**
172
+ * Alias for call(). Provided for compatibility with MCP bridge packages
173
+ * that may call callAsync() by convention.
174
+ */
175
+ async callAsync(
176
+ moduleId: string,
177
+ inputs?: Record<string, unknown> | null,
178
+ context?: Context | null,
179
+ ): Promise<Record<string, unknown>> {
180
+ return this.call(moduleId, inputs, context);
181
+ }
160
182
 
161
- // Step 1 -- Context
162
- let ctx: Context;
183
+ private _createContext(moduleId: string, context?: Context | null): Context {
163
184
  if (context == null) {
164
- ctx = Context.create(this);
165
- ctx = ctx.child(moduleId);
166
- } else {
167
- ctx = context.child(moduleId);
185
+ return Context.create(this).child(moduleId);
168
186
  }
187
+ return context.child(moduleId);
188
+ }
169
189
 
170
- // Step 2 -- Safety Checks
171
- this._checkSafety(moduleId, ctx);
172
-
173
- // Step 3 -- Lookup
190
+ private _lookupModule(moduleId: string): Record<string, unknown> {
174
191
  const module = this._registry.get(moduleId);
175
192
  if (module === null) {
176
193
  throw new ModuleNotFoundError(moduleId);
177
194
  }
195
+ return module as Record<string, unknown>;
196
+ }
178
197
 
179
- const mod = module as Record<string, unknown>;
180
-
181
- // Step 4 -- ACL
198
+ private _checkAcl(moduleId: string, ctx: Context): void {
182
199
  if (this._acl !== null) {
183
200
  const allowed = this._acl.check(ctx.callerId, moduleId, ctx);
184
201
  if (!allowed) {
185
202
  throw new ACLDeniedError(ctx.callerId, moduleId);
186
203
  }
187
204
  }
205
+ }
188
206
 
189
- // Step 5 -- Input Validation and Redaction
207
+ private _validateInputs(
208
+ mod: Record<string, unknown>,
209
+ inputs: Record<string, unknown>,
210
+ ctx: Context,
211
+ ): Record<string, unknown> {
190
212
  const inputSchema = mod['inputSchema'] as TSchema | undefined;
191
- if (inputSchema != null) {
192
- if (!Value.Check(inputSchema, effectiveInputs)) {
193
- const errors: Array<Record<string, unknown>> = [];
194
- for (const error of Value.Errors(inputSchema, effectiveInputs)) {
195
- errors.push({
196
- field: error.path || '/',
197
- code: String(error.type),
198
- message: error.message,
199
- });
200
- }
201
- throw new SchemaValidationError('Input validation failed', errors);
202
- }
213
+ if (inputSchema == null) return inputs;
214
+
215
+ this._validateSchema(inputSchema, inputs, 'Input');
216
+ ctx.redactedInputs = redactSensitive(
217
+ inputs,
218
+ inputSchema as unknown as Record<string, unknown>,
219
+ );
220
+ return inputs;
221
+ }
222
+
223
+ private _validateSchema(
224
+ schema: TSchema,
225
+ data: Record<string, unknown>,
226
+ direction: string,
227
+ ): void {
228
+ if (Value.Check(schema, data)) return;
203
229
 
204
- ctx.redactedInputs = redactSensitive(
205
- effectiveInputs,
206
- inputSchema as unknown as Record<string, unknown>,
207
- );
230
+ const errors: Array<Record<string, unknown>> = [];
231
+ for (const error of Value.Errors(schema, data)) {
232
+ errors.push({
233
+ field: error.path || '/',
234
+ code: String(error.type),
235
+ message: error.message,
236
+ });
208
237
  }
238
+ throw new SchemaValidationError(`${direction} validation failed`, errors);
239
+ }
209
240
 
241
+ private async _executeWithMiddleware(
242
+ mod: Record<string, unknown>,
243
+ moduleId: string,
244
+ inputs: Record<string, unknown>,
245
+ ctx: Context,
246
+ ): Promise<Record<string, unknown>> {
247
+ let effectiveInputs = inputs;
210
248
  let executedMiddlewares: Middleware[] = [];
211
249
 
212
250
  try {
213
- // Step 6 -- Middleware Before
214
251
  try {
215
252
  [effectiveInputs, executedMiddlewares] = this._middlewareManager.executeBefore(moduleId, effectiveInputs, ctx);
216
253
  } catch (e) {
@@ -226,29 +263,14 @@ export class Executor {
226
263
  throw e;
227
264
  }
228
265
 
229
- // Step 7 -- Execute with timeout
230
266
  let output = await this._executeWithTimeout(mod, moduleId, effectiveInputs, ctx);
231
267
 
232
- // Step 8 -- Output Validation
233
268
  const outputSchema = mod['outputSchema'] as TSchema | undefined;
234
269
  if (outputSchema != null) {
235
- if (!Value.Check(outputSchema, output)) {
236
- const errors: Array<Record<string, unknown>> = [];
237
- for (const error of Value.Errors(outputSchema, output)) {
238
- errors.push({
239
- field: error.path || '/',
240
- code: String(error.type),
241
- message: error.message,
242
- });
243
- }
244
- throw new SchemaValidationError('Output validation failed', errors);
245
- }
270
+ this._validateSchema(outputSchema, output, 'Output');
246
271
  }
247
272
 
248
- // Step 9 -- Middleware After
249
273
  output = this._middlewareManager.executeAfter(moduleId, effectiveInputs, output, ctx);
250
-
251
- // Step 10 -- Return
252
274
  return output;
253
275
  } catch (exc) {
254
276
  if (executedMiddlewares.length > 0) {
@@ -337,12 +359,15 @@ export class Executor {
337
359
  return executionPromise;
338
360
  }
339
361
 
362
+ let timer: ReturnType<typeof setTimeout>;
340
363
  const timeoutPromise = new Promise<never>((_, reject) => {
341
- setTimeout(() => {
364
+ timer = setTimeout(() => {
342
365
  reject(new ModuleTimeoutError(moduleId, timeoutMs));
343
366
  }, timeoutMs);
344
367
  });
345
368
 
346
- return Promise.race([executionPromise, timeoutPromise]);
369
+ return Promise.race([executionPromise, timeoutPromise]).finally(() => {
370
+ clearTimeout(timer);
371
+ });
347
372
  }
348
373
  }
package/src/index.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  // Core
6
6
  export { Context, createIdentity } from './context.js';
7
7
  export type { Identity } from './context.js';
8
- export { Registry } from './registry/registry.js';
8
+ export { Registry, REGISTRY_EVENTS, MODULE_ID_PATTERN } from './registry/registry.js';
9
9
  export { Executor, redactSensitive, REDACTED_VALUE } from './executor.js';
10
10
 
11
11
  // Module types
@@ -42,7 +42,11 @@ export {
42
42
  BindingFileInvalidError,
43
43
  CircularDependencyError,
44
44
  ModuleLoadError,
45
+ ModuleExecuteError,
46
+ InternalError,
47
+ ErrorCodes,
45
48
  } from './errors.js';
49
+ export type { ErrorCode } from './errors.js';
46
50
 
47
51
  // ACL
48
52
  export { ACL } from './acl.js';
@@ -78,4 +82,4 @@ export type { Span, SpanExporter } from './observability/tracing.js';
78
82
  export { MetricsCollector, MetricsMiddleware } from './observability/metrics.js';
79
83
  export { ContextLogger, ObsLoggingMiddleware } from './observability/context-logger.js';
80
84
 
81
- export const VERSION = '0.1.0';
85
+ export const VERSION = '0.1.2';
@@ -12,7 +12,7 @@ export interface Logger {
12
12
 
13
13
  const defaultLogger: Logger = {
14
14
  info(message: string, extra?: Record<string, unknown>) {
15
- console.log(message, extra ?? '');
15
+ console.info(message, extra ?? '');
16
16
  },
17
17
  error(message: string, extra?: Record<string, unknown>) {
18
18
  console.error(message, extra ?? '');
@@ -95,8 +95,8 @@ export class MiddlewareManager {
95
95
  if (result !== null) {
96
96
  return result;
97
97
  }
98
- } catch {
99
- // Swallow errors in onError handlers
98
+ } catch (e) {
99
+ console.warn('[apcore:middleware] Error in onError handler, continuing:', e);
100
100
  continue;
101
101
  }
102
102
  }
@@ -45,7 +45,7 @@ export interface SpanExporter {
45
45
 
46
46
  export class StdoutExporter implements SpanExporter {
47
47
  export(span: Span): void {
48
- console.log(JSON.stringify(span));
48
+ process.stdout.write(JSON.stringify(span) + '\n');
49
49
  }
50
50
  }
51
51