apcore-js 0.1.0 → 0.1.2

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,44 @@ 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.1.2] - 2026-02-18
9
+
10
+ ### Fixed
11
+
12
+ - **Timer leak in executor** — `_executeWithTimeout` now calls `clearTimeout` in `.finally()` to prevent timer leak on normal completion
13
+ - **Path traversal protection** — `resolveTarget` in binding loader rejects module paths containing `..` segments before dynamic `import()`
14
+ - **Bare catch blocks** — 6 silent `catch {}` blocks in registry and middleware manager now log warnings with `[apcore:<subsystem>]` prefix
15
+ - **Python-style error messages** — Fixed `FuncMissingTypeHintError` and `FuncMissingReturnTypeError` to use TypeScript syntax (`: string`, `: Record<string, unknown>`)
16
+ - **Console.log in production** — Replaced `console.log` with `console.info` in logging middleware and `process.stdout.write` in tracing exporter
17
+
18
+ ### Changed
19
+
20
+ - **Long method decomposition** — Broke up 4 oversized methods to meet ≤50 line guideline:
21
+ - `Executor.call()` (108 → 6 private helpers)
22
+ - `Registry.discover()` (110 → 7 private helpers)
23
+ - `ACL.load()` (71 → extracted `parseAclRule`)
24
+ - `jsonSchemaToTypeBox()` (80 → 5 converter helpers)
25
+ - **Deeply readonly callChain** — `Context.callChain` type narrowed from `readonly string[]` to `readonly (readonly string[])` preventing mutation via push/splice
26
+ - **Consolidated `deepCopy`** — Removed 4 duplicate `deepCopy` implementations; single shared version now lives in `src/utils/index.ts`
27
+
28
+ ### Added
29
+
30
+ - **42 new tests** for previously uncovered modules:
31
+ - `tests/schema/test-annotations.test.ts` — 16 tests for `mergeAnnotations`, `mergeExamples`, `mergeMetadata`
32
+ - `tests/schema/test-exporter.test.ts` — 14 tests for `SchemaExporter` across all 4 export profiles
33
+ - `tests/test-logging-middleware.test.ts` — 12 tests for `LoggingMiddleware` before/after/onError
34
+
35
+ ## [0.1.1] - 2026-02-17
36
+
37
+ ### Fixed
38
+
39
+ - Updated logo URL in README
40
+
41
+ ### Changed
42
+
43
+ - Renamed package from `apcore` to `apcore-js`
44
+ - Updated installation instructions
45
+
8
46
  ## [0.1.0] - 2026-02-16
9
47
 
10
48
  ### Added
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <div align="center">
2
- <img src="./apcore-logo.svg" alt="apcore logo" width="200"/>
2
+ <img src="https://raw.githubusercontent.com/aipartnerup/apcore-typescript/main/apcore-logo.svg" alt="apcore logo" width="200"/>
3
3
  </div>
4
4
 
5
5
  # apcore
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apcore-js",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "AI-Perceivable Core — schema-driven module development framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,6 +21,14 @@
21
21
  "engines": {
22
22
  "node": ">=18.0.0"
23
23
  },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/aipartnerup/apcore-typescript.git"
27
+ },
28
+ "homepage": "https://github.com/aipartnerup",
29
+ "bugs": {
30
+ "url": "https://github.com/aipartnerup/apcore-typescript/issues"
31
+ },
24
32
  "license": "MIT",
25
33
  "dependencies": {
26
34
  "@sinclair/typebox": "^0.34.0",
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,
package/src/executor.ts CHANGED
@@ -157,60 +157,85 @@ 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
+ }
160
170
 
161
- // Step 1 -- Context
162
- let ctx: Context;
171
+ private _createContext(moduleId: string, context?: Context | null): Context {
163
172
  if (context == null) {
164
- ctx = Context.create(this);
165
- ctx = ctx.child(moduleId);
166
- } else {
167
- ctx = context.child(moduleId);
173
+ return Context.create(this).child(moduleId);
168
174
  }
175
+ return context.child(moduleId);
176
+ }
169
177
 
170
- // Step 2 -- Safety Checks
171
- this._checkSafety(moduleId, ctx);
172
-
173
- // Step 3 -- Lookup
178
+ private _lookupModule(moduleId: string): Record<string, unknown> {
174
179
  const module = this._registry.get(moduleId);
175
180
  if (module === null) {
176
181
  throw new ModuleNotFoundError(moduleId);
177
182
  }
183
+ return module as Record<string, unknown>;
184
+ }
178
185
 
179
- const mod = module as Record<string, unknown>;
180
-
181
- // Step 4 -- ACL
186
+ private _checkAcl(moduleId: string, ctx: Context): void {
182
187
  if (this._acl !== null) {
183
188
  const allowed = this._acl.check(ctx.callerId, moduleId, ctx);
184
189
  if (!allowed) {
185
190
  throw new ACLDeniedError(ctx.callerId, moduleId);
186
191
  }
187
192
  }
193
+ }
188
194
 
189
- // Step 5 -- Input Validation and Redaction
195
+ private _validateInputs(
196
+ mod: Record<string, unknown>,
197
+ inputs: Record<string, unknown>,
198
+ ctx: Context,
199
+ ): Record<string, unknown> {
190
200
  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
- }
201
+ if (inputSchema == null) return inputs;
202
+
203
+ this._validateSchema(inputSchema, inputs, 'Input');
204
+ ctx.redactedInputs = redactSensitive(
205
+ inputs,
206
+ inputSchema as unknown as Record<string, unknown>,
207
+ );
208
+ return inputs;
209
+ }
210
+
211
+ private _validateSchema(
212
+ schema: TSchema,
213
+ data: Record<string, unknown>,
214
+ direction: string,
215
+ ): void {
216
+ if (Value.Check(schema, data)) return;
203
217
 
204
- ctx.redactedInputs = redactSensitive(
205
- effectiveInputs,
206
- inputSchema as unknown as Record<string, unknown>,
207
- );
218
+ const errors: Array<Record<string, unknown>> = [];
219
+ for (const error of Value.Errors(schema, data)) {
220
+ errors.push({
221
+ field: error.path || '/',
222
+ code: String(error.type),
223
+ message: error.message,
224
+ });
208
225
  }
226
+ throw new SchemaValidationError(`${direction} validation failed`, errors);
227
+ }
209
228
 
229
+ private async _executeWithMiddleware(
230
+ mod: Record<string, unknown>,
231
+ moduleId: string,
232
+ inputs: Record<string, unknown>,
233
+ ctx: Context,
234
+ ): Promise<Record<string, unknown>> {
235
+ let effectiveInputs = inputs;
210
236
  let executedMiddlewares: Middleware[] = [];
211
237
 
212
238
  try {
213
- // Step 6 -- Middleware Before
214
239
  try {
215
240
  [effectiveInputs, executedMiddlewares] = this._middlewareManager.executeBefore(moduleId, effectiveInputs, ctx);
216
241
  } catch (e) {
@@ -226,29 +251,14 @@ export class Executor {
226
251
  throw e;
227
252
  }
228
253
 
229
- // Step 7 -- Execute with timeout
230
254
  let output = await this._executeWithTimeout(mod, moduleId, effectiveInputs, ctx);
231
255
 
232
- // Step 8 -- Output Validation
233
256
  const outputSchema = mod['outputSchema'] as TSchema | undefined;
234
257
  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
- }
258
+ this._validateSchema(outputSchema, output, 'Output');
246
259
  }
247
260
 
248
- // Step 9 -- Middleware After
249
261
  output = this._middlewareManager.executeAfter(moduleId, effectiveInputs, output, ctx);
250
-
251
- // Step 10 -- Return
252
262
  return output;
253
263
  } catch (exc) {
254
264
  if (executedMiddlewares.length > 0) {
@@ -337,12 +347,15 @@ export class Executor {
337
347
  return executionPromise;
338
348
  }
339
349
 
350
+ let timer: ReturnType<typeof setTimeout>;
340
351
  const timeoutPromise = new Promise<never>((_, reject) => {
341
- setTimeout(() => {
352
+ timer = setTimeout(() => {
342
353
  reject(new ModuleTimeoutError(moduleId, timeoutMs));
343
354
  }, timeoutMs);
344
355
  });
345
356
 
346
- return Promise.race([executionPromise, timeoutPromise]);
357
+ return Promise.race([executionPromise, timeoutPromise]).finally(() => {
358
+ clearTimeout(timer);
359
+ });
347
360
  }
348
361
  }
package/src/index.ts CHANGED
@@ -78,4 +78,4 @@ export type { Span, SpanExporter } from './observability/tracing.js';
78
78
  export { MetricsCollector, MetricsMiddleware } from './observability/metrics.js';
79
79
  export { ContextLogger, ObsLoggingMiddleware } from './observability/context-logger.js';
80
80
 
81
- export const VERSION = '0.1.0';
81
+ 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