@jini-ai/core 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # @jini-ai/core
2
2
 
3
+ ## 0.4.1 — 2026-10-05
4
+
5
+ - New `readToolLimit({ input, max, fallback }, { key })`: an absent limit is `fallback`, one above `max` is capped at `max`, and a non-integer or one below 1 throws `ToolInputError` ("'limit' must be an integer between 1 and <max>").
6
+ - New `optionalOneOf({ input, key, values })`: a value outside `values` throws `ToolInputError` ("'<key>' must be one of: ..."). Both are additive.
7
+
3
8
  ## 0.4.0 — 2026-10-02
4
9
 
5
10
  ### BREAKING
@@ -167,6 +167,40 @@ export declare function optionalBoolean(requiredArgs: {
167
167
  input: Record<string, unknown>;
168
168
  key: string;
169
169
  }, optionalArgs?: Record<string, never>): boolean | undefined;
170
+ /**
171
+ * Reads a list tool's page-size field. A list schema's `minimum: 1, maximum: <max>` is advice to the
172
+ * model, not enforcement: without this a `limit` of 0, -5 or 2.5 reached the query and came back as an
173
+ * empty or nonsensical page the model could not tell apart from "no rows".
174
+ *
175
+ * @param required.max The schema's documented cap; a larger request is capped to it, not refused,
176
+ * because the schemas promise "server-capped at <max>".
177
+ * @param required.fallback Returned when the key is absent.
178
+ * @param _optional.key The field name, `limit` by default; it is also the name the error message uses.
179
+ * @returns An integer in `[1, max]`.
180
+ * @throws {ToolInputError} when the value is present but not an integer >= 1.
181
+ * @complexity O(1).
182
+ */
183
+ export declare function readToolLimit(required: {
184
+ input: Record<string, unknown>;
185
+ max: number;
186
+ fallback: number;
187
+ }, _optional?: {
188
+ key?: string;
189
+ }): number;
190
+ /**
191
+ * Reads an optional enum field (a list filter's `status`, say). An off-enum value is refused rather
192
+ * than passed through, because as a filter it silently matches nothing and reads as "no rows".
193
+ *
194
+ * @param required.values The schema's `enum`, in the order the error message lists them.
195
+ * @returns The value, or `undefined` when the key is absent.
196
+ * @throws {ToolInputError} naming every allowed value when the value is present but not listed.
197
+ * @complexity O(values).
198
+ */
199
+ export declare function optionalOneOf<T extends string>(required: {
200
+ input: Record<string, unknown>;
201
+ key: string;
202
+ values: readonly T[];
203
+ }, _optional?: Record<string, never>): T | undefined;
170
204
  /**
171
205
  * The reader for a parameterless tool: `ctx.input` may be omitted entirely or passed as `{}`,
172
206
  * nothing else. Refusing a populated object is deliberate — silently ignoring keys would teach a
@@ -163,6 +163,49 @@ export function optionalBoolean(requiredArgs, optionalArgs = {}) {
163
163
  throw new ToolInputError({ message: `'${key}' must be a boolean` });
164
164
  return value;
165
165
  }
166
+ /**
167
+ * Reads a list tool's page-size field. A list schema's `minimum: 1, maximum: <max>` is advice to the
168
+ * model, not enforcement: without this a `limit` of 0, -5 or 2.5 reached the query and came back as an
169
+ * empty or nonsensical page the model could not tell apart from "no rows".
170
+ *
171
+ * @param required.max The schema's documented cap; a larger request is capped to it, not refused,
172
+ * because the schemas promise "server-capped at <max>".
173
+ * @param required.fallback Returned when the key is absent.
174
+ * @param _optional.key The field name, `limit` by default; it is also the name the error message uses.
175
+ * @returns An integer in `[1, max]`.
176
+ * @throws {ToolInputError} when the value is present but not an integer >= 1.
177
+ * @complexity O(1).
178
+ */
179
+ export function readToolLimit(required, _optional = {}) {
180
+ const { input, max, fallback } = required;
181
+ const key = _optional.key ?? "limit";
182
+ const value = input[key];
183
+ if (value === undefined)
184
+ return fallback;
185
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
186
+ throw new ToolInputError({ message: `'${key}' must be an integer between 1 and ${max}` });
187
+ }
188
+ return Math.min(value, max);
189
+ }
190
+ /**
191
+ * Reads an optional enum field (a list filter's `status`, say). An off-enum value is refused rather
192
+ * than passed through, because as a filter it silently matches nothing and reads as "no rows".
193
+ *
194
+ * @param required.values The schema's `enum`, in the order the error message lists them.
195
+ * @returns The value, or `undefined` when the key is absent.
196
+ * @throws {ToolInputError} naming every allowed value when the value is present but not listed.
197
+ * @complexity O(values).
198
+ */
199
+ export function optionalOneOf(required, _optional = {}) {
200
+ const { input, key, values } = required;
201
+ const value = input[key];
202
+ if (value === undefined)
203
+ return undefined;
204
+ if (typeof value !== "string" || !values.includes(value)) {
205
+ throw new ToolInputError({ message: `'${key}' must be one of: ${values.join(", ")}` });
206
+ }
207
+ return value;
208
+ }
166
209
  /**
167
210
  * The reader for a parameterless tool: `ctx.input` may be omitted entirely or passed as `{}`,
168
211
  * nothing else. Refusing a populated object is deliberate — silently ignoring keys would teach a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/core",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "The Jini kernel: typed DI tokens, pack composition and lifecycle, the tool registry, redaction, and origin/API-token auth primitives. Zero dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -79,7 +79,8 @@
79
79
  "!dist/**/*.test.js.map",
80
80
  "!dist/**/*.test.d.ts.map",
81
81
  "!dist/**/__tests__/**",
82
- "!dist/**/*.typecheck.*"
82
+ "!dist/**/*.typecheck.*",
83
+ "!dist/**/*.map"
83
84
  ],
84
85
  "publishConfig": {
85
86
  "access": "public",
@@ -1,41 +0,0 @@
1
- /**
2
- * @module @jini-ai/core/internal
3
- *
4
- * Package-internal entry point — NOT part of `@jini-ai/core`'s public
5
- * contract. Two unrelated escape hatches live here, each for its own
6
- * single intended consumer:
7
- *
8
- * - {@link authorizeToolInvocation} so `@jini-ai/daemon`'s `ToolExecutor` can
9
- * resolve a registered tool and run its authorization gate — the tool's
10
- * own `ToolPolicy`, then an optional transport-level veto — before ever
11
- * receiving the handler. This is the security-sensitive one: it is a
12
- * runtime *value* export, but unlike the `getToolRegistration` it
13
- * replaced (2026-07-29 hardening pass), it only ever returns a handler
14
- * closure alongside a `decision: 'allow'` — a denied or unauthorized call
15
- * gets no handler at all, so the authorization gate cannot be bypassed by
16
- * calling this export directly instead of going through `ToolExecutor`.
17
- * - `AnyPack`/`RequiredTokenIds`/`MissingTokenIds` so `@jini-ai/server`'s
18
- * `createLocalNodeDaemon` can re-derive `createDaemon`'s exact
19
- * compile-time "missing binding" gate on its own wrapper config type,
20
- * instead of either duplicating the type-level logic or losing the
21
- * compile-time check through the wrapper. These are `import type`-only —
22
- * erased at build time, carry no runtime capability, and are not a
23
- * security boundary the way {@link authorizeToolInvocation} is.
24
- *
25
- * **Correction (2026-07-19 hardening pass):** a package.json `exports` map
26
- * subpath is NOT a language-level or per-consumer access modifier — Node's
27
- * module resolution will happily resolve `@jini-ai/core/internal` for *any*
28
- * package that depends on `@jini-ai/core`, not just the intended consumer.
29
- * Earlier revisions of this doc comment claimed this boundary was
30
- * "enforced" by the exports map; that was inaccurate and left the leak
31
- * undetected. The actual enforcement is `scripts/check-engine-boundaries.ts`'s
32
- * rule forbidding any *value* import from `@jini-ai/core/internal` outside
33
- * `packages/daemon/**` (type-only imports of the DI-token-derivation types
34
- * remain unrestricted, since they carry no runtime capability) — that guard
35
- * is wired in, but is a second, independent layer on top of the 2026-07-29
36
- * structural fix above, not a substitute for it.
37
- */
38
- export { authorizeToolInvocation } from './tool-registry.js';
39
- export type { ToolAuthorizationDelegate, ToolInvocationAuthorization } from './tool-registry.js';
40
- export type { AnyPack, MissingTokenIds, RequiredTokenIds } from './daemon.js';
41
- //# sourceMappingURL=internal.d.ts.map
package/dist/internal.js DELETED
@@ -1,39 +0,0 @@
1
- /**
2
- * @module @jini-ai/core/internal
3
- *
4
- * Package-internal entry point — NOT part of `@jini-ai/core`'s public
5
- * contract. Two unrelated escape hatches live here, each for its own
6
- * single intended consumer:
7
- *
8
- * - {@link authorizeToolInvocation} so `@jini-ai/daemon`'s `ToolExecutor` can
9
- * resolve a registered tool and run its authorization gate — the tool's
10
- * own `ToolPolicy`, then an optional transport-level veto — before ever
11
- * receiving the handler. This is the security-sensitive one: it is a
12
- * runtime *value* export, but unlike the `getToolRegistration` it
13
- * replaced (2026-07-29 hardening pass), it only ever returns a handler
14
- * closure alongside a `decision: 'allow'` — a denied or unauthorized call
15
- * gets no handler at all, so the authorization gate cannot be bypassed by
16
- * calling this export directly instead of going through `ToolExecutor`.
17
- * - `AnyPack`/`RequiredTokenIds`/`MissingTokenIds` so `@jini-ai/server`'s
18
- * `createLocalNodeDaemon` can re-derive `createDaemon`'s exact
19
- * compile-time "missing binding" gate on its own wrapper config type,
20
- * instead of either duplicating the type-level logic or losing the
21
- * compile-time check through the wrapper. These are `import type`-only —
22
- * erased at build time, carry no runtime capability, and are not a
23
- * security boundary the way {@link authorizeToolInvocation} is.
24
- *
25
- * **Correction (2026-07-19 hardening pass):** a package.json `exports` map
26
- * subpath is NOT a language-level or per-consumer access modifier — Node's
27
- * module resolution will happily resolve `@jini-ai/core/internal` for *any*
28
- * package that depends on `@jini-ai/core`, not just the intended consumer.
29
- * Earlier revisions of this doc comment claimed this boundary was
30
- * "enforced" by the exports map; that was inaccurate and left the leak
31
- * undetected. The actual enforcement is `scripts/check-engine-boundaries.ts`'s
32
- * rule forbidding any *value* import from `@jini-ai/core/internal` outside
33
- * `packages/daemon/**` (type-only imports of the DI-token-derivation types
34
- * remain unrestricted, since they carry no runtime capability) — that guard
35
- * is wired in, but is a second, independent layer on top of the 2026-07-29
36
- * structural fix above, not a substitute for it.
37
- */
38
- export { authorizeToolInvocation } from './tool-registry.js';
39
- //# sourceMappingURL=internal.js.map