@serovaai/ficta-contract 0.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/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@serovaai/ficta-contract",
3
+ "version": "0.0.0",
4
+ "private": false,
5
+ "description": "Typed oRPC and OpenAPI contract for the Ficta control plane",
6
+ "homepage": "https://ficta.sh",
7
+ "bugs": {
8
+ "url": "https://github.com/SerovaAI/ficta/issues"
9
+ },
10
+ "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/SerovaAI/ficta.git",
14
+ "directory": "packages/contract"
15
+ },
16
+ "files": [
17
+ "dist/",
18
+ "openapi/",
19
+ "src/",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "type": "module",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./src/index.ts",
27
+ "development": "./src/index.ts",
28
+ "import": "./dist/index.js",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "./openapi.json": "./openapi/ficta-control-plane.openapi.json"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "dependencies": {
37
+ "@orpc/client": "1.15.0",
38
+ "@orpc/contract": "1.15.0",
39
+ "@orpc/openapi-client": "1.15.0",
40
+ "@serovaai/ficta-protocol": "^0.2.3",
41
+ "zod": "^4.5.4"
42
+ },
43
+ "devDependencies": {
44
+ "@orpc/openapi": "1.15.0",
45
+ "@orpc/zod": "1.15.0",
46
+ "@types/node": "^26.4.1",
47
+ "oxfmt": "^0.66.0",
48
+ "typescript": "^7.0.2",
49
+ "vitest": "^4.1.11"
50
+ },
51
+ "engines": {
52
+ "node": ">=20"
53
+ },
54
+ "scripts": {
55
+ "build": "pnpm build:dist && node scripts/generate-openapi.mjs",
56
+ "build:dist": "tsc",
57
+ "check": "pnpm check:static && pnpm typecheck && pnpm test && pnpm check:openapi",
58
+ "check:openapi": "pnpm build:dist && node scripts/generate-openapi.mjs --check",
59
+ "check:static": "oxlint . && oxfmt --check .",
60
+ "test": "vitest run",
61
+ "typecheck": "tsc --noEmit"
62
+ }
63
+ }
package/src/client.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { createORPCClient } from "@orpc/client";
2
+ import type { ContractRouterClient } from "@orpc/contract";
3
+ import type { JsonifiedClient } from "@orpc/openapi-client";
4
+ import { OpenAPILink } from "@orpc/openapi-client/fetch";
5
+ import { fictaControlContract } from "./contract.js";
6
+ import { decodeFictaControlError } from "./errors.js";
7
+
8
+ export type FictaControlClient = JsonifiedClient<ContractRouterClient<typeof fictaControlContract>>;
9
+
10
+ export interface CreateFictaControlClientOptions {
11
+ baseUrl: string | URL;
12
+ headers?: Headers | Record<string, string>;
13
+ fetch?: typeof globalThis.fetch;
14
+ }
15
+
16
+ /** Create a fetch-based client for any conforming Ficta control-plane implementation. */
17
+ export function createFictaControlClient(options: CreateFictaControlClientOptions): FictaControlClient {
18
+ const link = new OpenAPILink(fictaControlContract, {
19
+ url: options.baseUrl,
20
+ headers: options.headers,
21
+ customErrorResponseBodyDecoder: decodeFictaControlError,
22
+ ...(options.fetch
23
+ ? {
24
+ fetch: (request, init) => options.fetch?.(request, init) ?? globalThis.fetch(request, init),
25
+ }
26
+ : {}),
27
+ });
28
+ return createORPCClient<FictaControlClient>(link);
29
+ }
@@ -0,0 +1,60 @@
1
+ import { FICTA_HEALTH_PATH, FICTA_PROTECTION_PREVIEW_PATH, FICTA_STATUS_PATH } from "@serovaai/ficta-protocol";
2
+ import { oc } from "@orpc/contract";
3
+ import {
4
+ capabilitiesSchema,
5
+ FICTA_CAPABILITIES_PATH,
6
+ healthSchema,
7
+ protectionPreviewDetectorUnavailableErrorSchema,
8
+ protectionPreviewForbiddenErrorSchema,
9
+ protectionPreviewInputSchema,
10
+ protectionPreviewInvalidRequestErrorSchema,
11
+ protectionPreviewInvariantErrorSchema,
12
+ protectionPreviewSchema,
13
+ protectionStatusSchema,
14
+ } from "./schemas.js";
15
+
16
+ const controlProcedure = oc;
17
+
18
+ export const fictaControlContract = oc.tag("Ficta control plane").router({
19
+ capabilities: controlProcedure
20
+ .route({
21
+ method: "GET",
22
+ path: FICTA_CAPABILITIES_PATH,
23
+ operationId: "getFictaCapabilities",
24
+ summary: "Discover the Ficta control-plane version and supported procedures",
25
+ })
26
+ .output(capabilitiesSchema),
27
+ health: controlProcedure
28
+ .route({
29
+ method: "GET",
30
+ path: FICTA_HEALTH_PATH,
31
+ operationId: "getFictaHealth",
32
+ summary: "Check whether the Ficta proxy process is serving requests",
33
+ })
34
+ .output(healthSchema),
35
+ status: controlProcedure
36
+ .route({
37
+ method: "GET",
38
+ path: FICTA_STATUS_PATH,
39
+ operationId: "getFictaProtectionStatus",
40
+ summary: "Read values-free protection readiness and activity metadata",
41
+ })
42
+ .output(protectionStatusSchema),
43
+ protectionPreview: controlProcedure
44
+ .route({
45
+ method: "POST",
46
+ path: FICTA_PROTECTION_PREVIEW_PATH,
47
+ operationId: "createFictaProtectionPreview",
48
+ summary: "Preview protection and issue a short-lived send ticket",
49
+ })
50
+ .input(protectionPreviewInputSchema)
51
+ .output(protectionPreviewSchema)
52
+ .errors({
53
+ FORBIDDEN: { status: 403, data: protectionPreviewForbiddenErrorSchema },
54
+ INVALID_REQUEST: { status: 400, data: protectionPreviewInvalidRequestErrorSchema },
55
+ DETECTOR_UNAVAILABLE: { status: 503, data: protectionPreviewDetectorUnavailableErrorSchema },
56
+ INVARIANT: { status: 422, data: protectionPreviewInvariantErrorSchema },
57
+ }),
58
+ });
59
+
60
+ export type FictaControlContract = typeof fictaControlContract;
package/src/errors.ts ADDED
@@ -0,0 +1,76 @@
1
+ import { ORPCError } from "@orpc/client";
2
+ import { protectionPreviewErrorSchema, type FictaProtectionPreviewError } from "./schemas.js";
3
+
4
+ const previewErrorCodes = {
5
+ forbidden: "FORBIDDEN",
6
+ invalid_request: "INVALID_REQUEST",
7
+ detector_unavailable: "DETECTOR_UNAVAILABLE",
8
+ invariant: "INVARIANT",
9
+ } as const;
10
+
11
+ /** Encode defined preview errors with the stable pre-oRPC HTTP response body. */
12
+ export function encodeFictaControlError(error: ORPCError<string, unknown>): unknown {
13
+ const parsed = protectionPreviewErrorSchema.safeParse(error.data);
14
+ if (parsed.success) return parsed.data;
15
+ if (error.code === "BAD_REQUEST") {
16
+ return {
17
+ ok: false,
18
+ service: "ficta",
19
+ status: "invalid_request",
20
+ message: protectionPreviewValidationMessage(error.data),
21
+ } satisfies FictaProtectionPreviewError;
22
+ }
23
+ return undefined;
24
+ }
25
+
26
+ function protectionPreviewValidationMessage(data: unknown): string {
27
+ if (!data || typeof data !== "object" || !("issues" in data) || !Array.isArray(data.issues)) {
28
+ return "Invalid protection preview request.";
29
+ }
30
+ const issue = data.issues[0];
31
+ if (!issue || typeof issue !== "object") return "Invalid protection preview request.";
32
+ const record = issue as { code?: unknown; message?: unknown; path?: unknown };
33
+ const path = Array.isArray(record.path) ? record.path : [];
34
+ if (path.length === 0) return "Preview body must be an object.";
35
+ if (path[0] === "text") {
36
+ return record.code === "too_big" || record.message === "Preview text is too large."
37
+ ? "Preview text is too large."
38
+ : "Preview text is required.";
39
+ }
40
+ if (path[0] === "protectedValues") {
41
+ if (path.length === 1) {
42
+ if (record.code === "too_big") return "Too many protected values for one chat.";
43
+ if (record.message === "Protected values are too large for one chat.") return record.message;
44
+ return "Protected values must be a list.";
45
+ }
46
+ return record.code === "invalid_type"
47
+ ? "Every protected value must be text."
48
+ : "A protected value is empty or too long.";
49
+ }
50
+ return "Invalid protection preview request.";
51
+ }
52
+
53
+ /** Decode the stable HTTP error body back into the contract's typed oRPC error. */
54
+ export function decodeFictaControlError(
55
+ body: unknown,
56
+ response: { status: number },
57
+ ): ORPCError<string, unknown> | undefined {
58
+ const parsed = protectionPreviewErrorSchema.safeParse(body);
59
+ if (!parsed.success) return undefined;
60
+ return new ORPCError(previewErrorCodes[parsed.data.status], {
61
+ defined: true,
62
+ status: response.status,
63
+ message: parsed.data.message,
64
+ data: parsed.data,
65
+ });
66
+ }
67
+
68
+ export function fictaControlErrorStatus(error: unknown): number | undefined {
69
+ return error instanceof ORPCError ? error.status : undefined;
70
+ }
71
+
72
+ export function fictaControlErrorData(error: unknown): FictaProtectionPreviewError | undefined {
73
+ if (!(error instanceof ORPCError)) return undefined;
74
+ const parsed = protectionPreviewErrorSchema.safeParse(error.data);
75
+ return parsed.success ? parsed.data : undefined;
76
+ }
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ export {
2
+ FICTA_HEALTH_PATH,
3
+ FICTA_PROTECTION_PREVIEW_PATH,
4
+ FICTA_PROTECTION_TICKET_HEADER,
5
+ FICTA_SCOPE_HEADER,
6
+ FICTA_STATUS_PATH,
7
+ } from "@serovaai/ficta-protocol";
8
+ export { createFictaControlClient, type CreateFictaControlClientOptions, type FictaControlClient } from "./client.js";
9
+ export { fictaControlContract, type FictaControlContract } from "./contract.js";
10
+ export {
11
+ decodeFictaControlError,
12
+ encodeFictaControlError,
13
+ fictaControlErrorData,
14
+ fictaControlErrorStatus,
15
+ } from "./errors.js";
16
+ export {
17
+ capabilitiesSchema,
18
+ FICTA_CAPABILITIES_PATH,
19
+ FICTA_CONTROL_CAPABILITIES,
20
+ FICTA_CONTROL_PROTOCOL_VERSION,
21
+ FICTA_SCOPE_MAX_LENGTH,
22
+ healthSchema,
23
+ PROTECTION_PREVIEW_TEXT_MAX_BYTES,
24
+ PROTECTION_PREVIEW_VALUE_MAX,
25
+ PROTECTION_PREVIEW_VALUES_MAX,
26
+ PROTECTION_PREVIEW_VALUES_MAX_BYTES,
27
+ protectionPreviewDetectorUnavailableErrorSchema,
28
+ protectionPreviewErrorSchema,
29
+ protectionPreviewFindingSchema,
30
+ protectionPreviewForbiddenErrorSchema,
31
+ protectionPreviewInputSchema,
32
+ protectionPreviewInvalidRequestErrorSchema,
33
+ protectionPreviewInvariantErrorSchema,
34
+ protectionPreviewSchema,
35
+ protectionStatusSchema,
36
+ registryProtectionStatusSchema,
37
+ type FictaCapabilities,
38
+ type FictaHealth,
39
+ type FictaProtectionPreview,
40
+ type FictaProtectionPreviewError,
41
+ type FictaProtectionPreviewInput,
42
+ type FictaProtectionStatus,
43
+ } from "./schemas.js";
package/src/schemas.ts ADDED
@@ -0,0 +1,202 @@
1
+ import { z } from "zod";
2
+
3
+ export const FICTA_CAPABILITIES_PATH = "/__ficta/capabilities" as const;
4
+ export const FICTA_CONTROL_PROTOCOL_VERSION = 1 as const;
5
+ export const FICTA_CONTROL_CAPABILITIES = ["health", "status", "protection-preview"] as const;
6
+ export const FICTA_SCOPE_MAX_LENGTH = 256;
7
+
8
+ export const PROTECTION_PREVIEW_TEXT_MAX_BYTES = 2 * 1024 * 1024;
9
+ export const PROTECTION_PREVIEW_VALUES_MAX = 200;
10
+ export const PROTECTION_PREVIEW_VALUE_MAX = 2_000;
11
+ export const PROTECTION_PREVIEW_VALUES_MAX_BYTES = 64 * 1024;
12
+
13
+ const utf8Length = (value: string): number => new TextEncoder().encode(value).byteLength;
14
+
15
+ export const healthSchema = z
16
+ .object({
17
+ ok: z.literal(true),
18
+ service: z.literal("ficta"),
19
+ })
20
+ .strict();
21
+
22
+ export const capabilitiesSchema = z
23
+ .object({
24
+ ok: z.literal(true),
25
+ service: z.literal("ficta"),
26
+ protocolVersion: z
27
+ .literal(FICTA_CONTROL_PROTOCOL_VERSION)
28
+ .describe("Breaking wire-contract version implemented by this control plane."),
29
+ capabilities: z
30
+ .array(z.string().min(1))
31
+ .describe("Supported optional procedures. Clients must ignore capability names they do not recognize."),
32
+ })
33
+ .strict();
34
+
35
+ export const registryProtectionStatusSchema = z
36
+ .object({
37
+ required: z.boolean().describe("Whether provider requests are blocked until the registry is ready."),
38
+ status: z.enum(["ready", "empty", "error"]).describe("Current exact-match registry readiness."),
39
+ message: z.string().describe("Values-free operator guidance for the current registry state."),
40
+ })
41
+ .strict();
42
+
43
+ export const protectionStatusSchema = z
44
+ .object({
45
+ ok: z.literal(true),
46
+ service: z.literal("ficta"),
47
+ protection: z
48
+ .object({
49
+ enabled: z.boolean().describe("Whether the engine has registered values or detector plugins available."),
50
+ protecting: z.boolean().describe("Whether registered values or an active detector are currently configured."),
51
+ registeredValues: z.number().int().nonnegative().describe("Count of loaded exact-match protected values."),
52
+ policyExcluded: z
53
+ .number()
54
+ .int()
55
+ .nonnegative()
56
+ .describe("Count of discovered registry values excluded by configured policy."),
57
+ })
58
+ .strict(),
59
+ registry: registryProtectionStatusSchema.optional(),
60
+ secretShapes: z
61
+ .object({
62
+ enabled: z.boolean().describe("Whether request-time secret-shape detection is enabled."),
63
+ status: z.enum(["off", "ok"]).describe("Secret-shape detector posture."),
64
+ message: z.string().describe("Values-free explanation of the secret-shape posture."),
65
+ })
66
+ .strict(),
67
+ pii: z
68
+ .object({
69
+ enabled: z.boolean().describe("Whether request-time PII detection is enabled."),
70
+ configuredBackend: z.string().describe("Compatibility string naming the configured PII backend set."),
71
+ configuredBackends: z.array(z.string()).optional().describe("Configured PII backend names."),
72
+ backend: z.string().describe("Active PII backend names as a compatibility string."),
73
+ status: z.enum(["off", "ok", "degraded", "blocking"]).describe("Current PII detector posture."),
74
+ failureMode: z
75
+ .enum(["fail-open", "fail-closed"])
76
+ .describe("Whether a required PII backend outage skips that backend or blocks provider traffic."),
77
+ url: z.string().optional().describe("Values-free health URL for a single configured network backend."),
78
+ detail: z.string().optional().describe("Values-free backend health diagnostic."),
79
+ message: z.string().describe("Values-free explanation of the current PII posture."),
80
+ })
81
+ .strict(),
82
+ activity: z
83
+ .object({
84
+ restoredValues: z
85
+ .number()
86
+ .int()
87
+ .nonnegative()
88
+ .describe("Cumulative protected values restored during this proxy run."),
89
+ withheldFromTools: z
90
+ .number()
91
+ .int()
92
+ .nonnegative()
93
+ .describe("Cumulative protected values withheld from tool-call arguments during this proxy run."),
94
+ })
95
+ .strict()
96
+ .optional(),
97
+ })
98
+ .strict();
99
+
100
+ export const protectionHitSchema = z
101
+ .object({
102
+ name: z.string().describe("Values-free detector or registry label for the finding."),
103
+ source: z.string().describe("Values-free source category for the finding."),
104
+ plugin: z.string().optional().describe("Plugin that produced the finding, when available."),
105
+ kind: z.enum(["secret", "pii", "custom"]).optional().describe("Coarse protected-value category."),
106
+ confidence: z
107
+ .enum(["exact", "high", "probabilistic"])
108
+ .optional()
109
+ .describe("Confidence class assigned by the protection source."),
110
+ })
111
+ .strict();
112
+
113
+ export const protectionPreviewFindingSchema = protectionHitSchema.extend({
114
+ start: z.number().int().nonnegative().describe("Inclusive UTF-16 offset into the exact preview text."),
115
+ end: z.number().int().nonnegative().describe("Exclusive UTF-16 offset into the exact preview text."),
116
+ surrogate: z.string().describe("Opaque replacement rendered in redactedText."),
117
+ origin: z.enum(["registry", "detected", "user"]).describe("How this protected value entered the preview."),
118
+ });
119
+
120
+ const protectedValueSchema = z
121
+ .string()
122
+ .min(1)
123
+ .max(PROTECTION_PREVIEW_VALUE_MAX)
124
+ .transform((value) => value.trim())
125
+ .refine((value) => value.length > 0 && value.length <= PROTECTION_PREVIEW_VALUE_MAX, {
126
+ message: "A protected value is empty or too long.",
127
+ });
128
+
129
+ export const protectionPreviewTextSchema = z
130
+ .string()
131
+ .max(PROTECTION_PREVIEW_TEXT_MAX_BYTES)
132
+ .refine((value) => utf8Length(value) <= PROTECTION_PREVIEW_TEXT_MAX_BYTES, {
133
+ message: "Preview text is too large.",
134
+ });
135
+
136
+ export const protectionPreviewProtectedValuesSchema = z
137
+ .array(protectedValueSchema)
138
+ .max(PROTECTION_PREVIEW_VALUES_MAX)
139
+ .optional();
140
+
141
+ export const protectionPreviewInputSchema = z
142
+ .object({
143
+ text: protectionPreviewTextSchema,
144
+ protectedValues: protectionPreviewProtectedValuesSchema,
145
+ })
146
+ .transform(({ text, protectedValues = [] }, context) => {
147
+ const uniqueValues = [...new Set(protectedValues)];
148
+ const valuesBytes = uniqueValues.reduce((total, value) => total + utf8Length(value), 0);
149
+ if (valuesBytes > PROTECTION_PREVIEW_VALUES_MAX_BYTES) {
150
+ context.addIssue({ code: "custom", message: "Protected values are too large for one chat." });
151
+ return z.NEVER;
152
+ }
153
+ return { text, protectedValues: uniqueValues };
154
+ });
155
+
156
+ export const protectionPreviewSchema = z
157
+ .object({
158
+ ok: z.literal(true),
159
+ service: z.literal("ficta"),
160
+ ticket: z.string().describe("Opaque, short-lived, single-use authorization for the reviewed provider send."),
161
+ textSha256: z
162
+ .string()
163
+ .regex(/^[0-9a-f]{64}$/u)
164
+ .describe("Lowercase SHA-256 of the exact preview text bound to the ticket."),
165
+ redactedText: z.string().describe("Preview text with all planned protections applied."),
166
+ findings: z.array(protectionPreviewFindingSchema).describe("Ordered protected occurrences in the preview text."),
167
+ })
168
+ .strict();
169
+
170
+ const protectionPreviewErrorBaseSchema = z
171
+ .object({
172
+ ok: z.literal(false),
173
+ service: z.literal("ficta"),
174
+ message: z.string(),
175
+ })
176
+ .strict();
177
+
178
+ export const protectionPreviewForbiddenErrorSchema = protectionPreviewErrorBaseSchema.extend({
179
+ status: z.literal("forbidden"),
180
+ });
181
+ export const protectionPreviewInvalidRequestErrorSchema = protectionPreviewErrorBaseSchema.extend({
182
+ status: z.literal("invalid_request"),
183
+ });
184
+ export const protectionPreviewDetectorUnavailableErrorSchema = protectionPreviewErrorBaseSchema.extend({
185
+ status: z.literal("detector_unavailable"),
186
+ });
187
+ export const protectionPreviewInvariantErrorSchema = protectionPreviewErrorBaseSchema.extend({
188
+ status: z.literal("invariant"),
189
+ });
190
+ export const protectionPreviewErrorSchema = z.discriminatedUnion("status", [
191
+ protectionPreviewForbiddenErrorSchema,
192
+ protectionPreviewInvalidRequestErrorSchema,
193
+ protectionPreviewDetectorUnavailableErrorSchema,
194
+ protectionPreviewInvariantErrorSchema,
195
+ ]);
196
+
197
+ export type FictaCapabilities = z.output<typeof capabilitiesSchema>;
198
+ export type FictaHealth = z.output<typeof healthSchema>;
199
+ export type FictaProtectionStatus = z.output<typeof protectionStatusSchema>;
200
+ export type FictaProtectionPreviewInput = z.input<typeof protectionPreviewInputSchema>;
201
+ export type FictaProtectionPreview = z.output<typeof protectionPreviewSchema>;
202
+ export type FictaProtectionPreviewError = z.output<typeof protectionPreviewErrorSchema>;