@ultimat3/action 23.0.0 → 25.0.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.
- package/CLAUDE.md +91 -42
- package/README.md +167 -47
- package/package.json +10 -9
- package/src/action.ts +25 -69
- package/src/api-declaration.ts +1 -1
- package/src/audit-gate.ts +3 -4
- package/src/audit-memory.ts +5 -3
- package/src/audit-postgres.ts +16 -8
- package/src/audit.ts +7 -106
- package/src/cache-gate.ts +25 -6
- package/src/client.ts +18 -6
- package/src/contract-test.ts +50 -14
- package/src/error-titles.ts +51 -0
- package/src/errors-http.ts +3 -1
- package/src/errors-idempotency.ts +57 -5
- package/src/errors.ts +44 -61
- package/src/facade.ts +6 -7
- package/src/http-path.ts +1 -1
- package/src/http.ts +23 -20
- package/src/idempotency-memory.ts +92 -8
- package/src/idempotency-postgres.ts +91 -39
- package/src/idempotency-redact.ts +140 -0
- package/src/idempotency.ts +58 -7
- package/src/index.ts +37 -88
- package/src/invoke.ts +57 -21
- package/src/job-handle.ts +5 -3
- package/src/json-schema.ts +6 -10
- package/src/mutator.ts +14 -4
- package/src/naming.ts +2 -3
- package/src/openapi-complete.ts +16 -1
- package/src/openapi.ts +25 -2
- package/src/policy-gate.ts +8 -51
- package/src/rate-limit-gate.ts +114 -0
- package/src/record-wire.ts +2 -2
- package/src/registry.ts +5 -4
- package/src/request-deadline.ts +25 -0
- package/src/stable.ts +0 -7
- package/src/transition.ts +35 -9
- package/src/tx-scope.ts +57 -0
- package/src/type-pins.ts +98 -3
- package/src/deprecation.ts +0 -77
- package/src/mcp-tool.ts +0 -87
- package/src/wire-headers.ts +0 -20
package/src/deprecation.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A declared retirement, rendered as the two headers the standards already define — RFC 9745
|
|
3
|
-
* `Deprecation` and RFC 8594 `Sunset` — plus the successor link. Pure string and date maths, and
|
|
4
|
-
* deliberately throw-free: each package raises its own `X_*` for a date it cannot render.
|
|
5
|
-
*
|
|
6
|
-
* `@ultimat3/query` carries a twin of this file. Both are tier 3, so neither may import the
|
|
7
|
-
* other, and the shared home is `@ultimat3/http` (tier 2) once that package grows one — the same
|
|
8
|
-
* compromise `naming.ts` is ported under.
|
|
9
|
-
*/
|
|
10
|
-
import { counter } from '@ultimat3/core';
|
|
11
|
-
|
|
12
|
-
export interface Deprecation {
|
|
13
|
-
/** When it was deprecated. ISO-8601, e.g. `'2026-08-01T00:00:00Z'`. */
|
|
14
|
-
readonly since: string;
|
|
15
|
-
/** When it stops answering. ISO-8601 — the date `Sunset` publishes and clients plan against. */
|
|
16
|
-
readonly sunset: string;
|
|
17
|
-
/** The export name of the replacement, projected to a `rel="successor-version"` link. */
|
|
18
|
-
readonly replacedBy?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type DeprecationField = 'since' | 'sunset';
|
|
22
|
-
|
|
23
|
-
export type DeprecationRender =
|
|
24
|
-
| {
|
|
25
|
-
readonly ok: true;
|
|
26
|
-
readonly headers: Readonly<Record<string, string>>;
|
|
27
|
-
/** The same facts as data, for `x-ultimate` in the OpenAPI operation and the manifest. */
|
|
28
|
-
readonly meta: Readonly<Record<string, string>>;
|
|
29
|
-
}
|
|
30
|
-
| { readonly ok: false; readonly field: DeprecationField; readonly value: string };
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* How many calls a deprecated declaration is still taking — the number "can we remove it yet?"
|
|
34
|
-
* needs and the one nothing in the framework could answer. Attributes are the primitive and the
|
|
35
|
-
* declared NAME, both bounded by the size of the codebase; a caller id here would be an unbounded
|
|
36
|
-
* series, which is the cardinality mistake core's own overflow bucket exists to catch.
|
|
37
|
-
*/
|
|
38
|
-
const deprecatedCalls = counter('deprecated_calls_total', {
|
|
39
|
-
unit: '{call}',
|
|
40
|
-
description: 'Calls served by a declaration that has been deprecated, by primitive and name',
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
export function recordDeprecatedCall(primitive: 'action' | 'query', name: string): void {
|
|
44
|
-
deprecatedCalls.add(1, { primitive, name });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* `Deprecation` is a structured-field Date (`@` + unix seconds, RFC 9745); `Sunset` is an
|
|
49
|
-
* HTTP-date (IMF-fixdate, RFC 8594). Two spellings of one instant because two RFCs chose
|
|
50
|
-
* differently — never render one in the other's format, and never emit `Invalid Date`.
|
|
51
|
-
*/
|
|
52
|
-
export function renderDeprecation(
|
|
53
|
-
deprecation: Deprecation,
|
|
54
|
-
successorPath: string | undefined,
|
|
55
|
-
): DeprecationRender {
|
|
56
|
-
const since = Date.parse(deprecation.since);
|
|
57
|
-
if (Number.isNaN(since)) return { ok: false, field: 'since', value: deprecation.since };
|
|
58
|
-
const sunset = Date.parse(deprecation.sunset);
|
|
59
|
-
if (Number.isNaN(sunset)) return { ok: false, field: 'sunset', value: deprecation.sunset };
|
|
60
|
-
|
|
61
|
-
const headers: Record<string, string> = {
|
|
62
|
-
deprecation: `@${Math.floor(since / 1000)}`,
|
|
63
|
-
sunset: new Date(sunset).toUTCString(),
|
|
64
|
-
};
|
|
65
|
-
// The successor's URL, derived by the caller from the same `naming.ts` the client uses — a
|
|
66
|
-
// link this file built from the export name would be the second URL derivation in the package.
|
|
67
|
-
if (successorPath !== undefined) {
|
|
68
|
-
headers['link'] = `<${successorPath}>; rel="successor-version"`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const meta: Record<string, string> = {
|
|
72
|
-
since: new Date(since).toISOString(),
|
|
73
|
-
sunset: new Date(sunset).toISOString(),
|
|
74
|
-
...(deprecation.replacedBy === undefined ? {} : { replacedBy: deprecation.replacedBy }),
|
|
75
|
-
};
|
|
76
|
-
return { ok: true, headers, meta };
|
|
77
|
-
}
|
package/src/mcp-tool.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Projection 4: an action as an MCP tool. Its `invoke` is the package's `invoke`
|
|
3
|
-
* — the same core the HTTP route calls — so the tool cannot drift from the
|
|
4
|
-
* endpoint and cannot acquire a second authz path. One authz system, never two.
|
|
5
|
-
*/
|
|
6
|
-
import type { Ctx } from '@ultimat3/core';
|
|
7
|
-
import { isMcpExposed } from '@ultimat3/core';
|
|
8
|
-
import type { AnyAction } from './action';
|
|
9
|
-
import { actionName, defOf, invoke } from './invoke';
|
|
10
|
-
import { type JsonSchemaObject, mcpSchemaOf, sortSchema } from './json-schema';
|
|
11
|
-
import type { ActionPolicy } from './policy-gate';
|
|
12
|
-
import { listActions } from './registry';
|
|
13
|
-
|
|
14
|
-
export interface McpToolDescriptor {
|
|
15
|
-
/**
|
|
16
|
-
* The export name VERBATIM — `@ultimat3/mcp` serves that name and nothing else, so any
|
|
17
|
-
* transformation here would be a label no `tools/call` could spell. See `toMcpTool`.
|
|
18
|
-
*/
|
|
19
|
-
readonly name: string;
|
|
20
|
-
/** The action's `mcp.description`, or its name when the author gave none. */
|
|
21
|
-
readonly description: string;
|
|
22
|
-
/**
|
|
23
|
-
* The action this tool projects. Equal to `name` by construction since 2026-08 — kept because
|
|
24
|
-
* a reader asking "which action is behind this tool?" should not have to know that.
|
|
25
|
-
*/
|
|
26
|
-
readonly action: string;
|
|
27
|
-
/**
|
|
28
|
-
* The action's own policy object, not a copy — `tool().policy === action.policy`
|
|
29
|
-
* is what makes "an MCP call cannot reach a different authz path" checkable.
|
|
30
|
-
*/
|
|
31
|
-
readonly policy: ActionPolicy;
|
|
32
|
-
readonly inputSchema: JsonSchemaObject;
|
|
33
|
-
readonly outputSchema: JsonSchemaObject;
|
|
34
|
-
invoke(input: unknown, options?: McpInvokeOptions): Promise<unknown>;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface McpInvokeOptions {
|
|
38
|
-
readonly ctx?: Ctx;
|
|
39
|
-
readonly idempotencyKey?: string | null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* The tool name is the export name VERBATIM — it was `snake_case`d until 2026-08, and
|
|
44
|
-
* `@ultimat3/mcp` has only ever served the verbatim one, so `.tool().name` was a label no
|
|
45
|
-
* `tools/call` accepted. One name per action, on every surface.
|
|
46
|
-
*/
|
|
47
|
-
export function toMcpTool(target: AnyAction): McpToolDescriptor {
|
|
48
|
-
const name = actionName(target);
|
|
49
|
-
const def = defOf(target);
|
|
50
|
-
return {
|
|
51
|
-
name,
|
|
52
|
-
description: def.mcp?.description ?? name,
|
|
53
|
-
action: name,
|
|
54
|
-
policy: def.policy,
|
|
55
|
-
inputSchema: sortSchema(mcpSchemaOf(def.input)),
|
|
56
|
-
outputSchema: sortSchema(mcpSchemaOf(def.output)),
|
|
57
|
-
invoke: (input, options = {}) =>
|
|
58
|
-
invoke(target, input, {
|
|
59
|
-
surface: 'mcp',
|
|
60
|
-
...(options.ctx === undefined ? {} : { ctx: options.ctx }),
|
|
61
|
-
idempotencyKey: options.idempotencyKey ?? null,
|
|
62
|
-
}),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Opt-in, exactly like a query's: only a literal `mcp: { expose: true }` exposes an action.
|
|
68
|
-
*
|
|
69
|
-
* It read `!== false` until 2026-08, which made writing an action silently hand every agent a
|
|
70
|
-
* new write capability — and disagreed with `@ultimat3/mcp`'s `exposedPrimitives`, the projection
|
|
71
|
-
* that actually builds a catalog. Two functions answering "is this a tool?" differently is the
|
|
72
|
-
* ambiguity axiom 1 rejects, so the fail-closed one wins — and `isMcpExposed` from
|
|
73
|
-
* `@ultimat3/core` is now the single answer every reader in the framework asks.
|
|
74
|
-
*/
|
|
75
|
-
export function isExposed(target: AnyAction): boolean {
|
|
76
|
-
return isMcpExposed(target.mcp);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Deterministic order — the tool list is part of the agent-visible contract. */
|
|
80
|
-
export function toMcpTools(
|
|
81
|
-
actions: readonly AnyAction[] = listActions(),
|
|
82
|
-
): readonly McpToolDescriptor[] {
|
|
83
|
-
return actions
|
|
84
|
-
.filter(isExposed)
|
|
85
|
-
.map(toMcpTool)
|
|
86
|
-
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
87
|
-
}
|
package/src/wire-headers.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The two request headers the typed client and the HTTP projection both name. Their own module and
|
|
3
|
-
* not `http.ts`'s, because `client.ts` needs exactly these two strings: importing them from there
|
|
4
|
-
* dragged `@ultimat3/http`, `@ultimat3/cache`, `@ultimat3/policy` and the whole `invoke` runtime
|
|
5
|
-
* into every browser bundle that called `rpc()` — 42,204 B against the read client's 11,977 B,
|
|
6
|
-
* measured, which is why two islands in this repo write a bare `fetch` instead of importing one.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Matches `HttpConfig.buildIdHeader`; the pipeline reads it into `ctx.clientBuildId` — the
|
|
11
|
-
* CLIENT's claim, never `ctx.buildId`, which is the build this process serves. A mismatch the
|
|
12
|
-
* client sees on the way back is `X_CONTRACT_DRIFT`.
|
|
13
|
-
*/
|
|
14
|
-
export const BUILD_ID_HEADER = 'x-ultimate-build';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* RFC 9110's spelling, lower-cased. Core's, re-exported by name: `clientTransport` sets it and
|
|
18
|
-
* this package's route reads it, so a second declaration here would be two answers to one header.
|
|
19
|
-
*/
|
|
20
|
-
export { IDEMPOTENCY_HEADER } from '@ultimat3/core';
|