@tsplc/server 0.0.0-stage → 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.
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
- # Temporary Holding Version
1
+ # @tsplc/server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Part of [tsPLC](https://github.com/livingforjesus/tsplc).
4
+
5
+ The server side of PLC services: `iec` schemas that give a tRPC procedure's fields their PLC widths, the handler a Fetch host mounts so the PLC can call procedures marked `meta({ plc: true })`, the OPC UA gateway (`@tsplc/server/opcua`) and the operator HMI (`@tsplc/server/hmi`).
6
+
7
+ See [typed services](https://github.com/livingforjesus/tsplc/tree/main/docs/language/services.md). MIT licensed.
package/dist/hmi.js ADDED
@@ -0,0 +1,132 @@
1
+ // packages/server/src/hmi.ts
2
+ import { hmiSchema } from "@tsplc/config/schema";
3
+ import { z as z2 } from "zod";
4
+
5
+ // packages/server/src/contract.ts
6
+ import { z } from "zod";
7
+ var integerTypes = {
8
+ SINT: [-128, 127],
9
+ USINT: [0, 255],
10
+ BYTE: [0, 255],
11
+ INT: [-32768, 32767],
12
+ UINT: [0, 65535],
13
+ WORD: [0, 65535],
14
+ DINT: [-2147483648, 2147483647],
15
+ UDINT: [0, 4294967295],
16
+ DWORD: [0, 4294967295]
17
+ };
18
+ var integer = (type) => (bounds = {}) => z.number().int().min(bounds.min ?? integerTypes[type][0]).max(bounds.max ?? integerTypes[type][1]).meta({ iec: type });
19
+ var iec = {
20
+ SINT: integer("SINT"),
21
+ USINT: integer("USINT"),
22
+ BYTE: integer("BYTE"),
23
+ INT: integer("INT"),
24
+ UINT: integer("UINT"),
25
+ WORD: integer("WORD"),
26
+ DINT: integer("DINT"),
27
+ UDINT: integer("UDINT"),
28
+ DWORD: integer("DWORD"),
29
+ STRING: (capacity) => z.string().max(capacity),
30
+ FixedArray: (element, length) => z.array(element).length(length)
31
+ };
32
+ var jsonSchema = z.looseObject({
33
+ type: z.string().optional(),
34
+ maxLength: z.number().optional(),
35
+ minItems: z.number().optional(),
36
+ maxItems: z.number().optional(),
37
+ minimum: z.number().optional(),
38
+ maximum: z.number().optional(),
39
+ iec: z.string().optional()
40
+ });
41
+ var refSchema = z.looseObject({
42
+ $ref: z.string().optional(),
43
+ $defs: z.record(z.string(), z.unknown()).optional()
44
+ });
45
+
46
+ // packages/server/src/hmi.ts
47
+ var pressMs = 200;
48
+ var createHmi = (input) => {
49
+ const config = hmiSchema.parse(input);
50
+ const positions = config.conveyors.reduce((total, { positions }) => total + positions, 0);
51
+ const blockedSince = new Map;
52
+ let report;
53
+ const applyReport = (input) => {
54
+ const now = Date.now();
55
+ input.occupied.forEach((occupied, index) => {
56
+ if (!occupied || !input.running)
57
+ blockedSince.delete(index);
58
+ else if (!blockedSince.has(index))
59
+ blockedSince.set(index, now);
60
+ });
61
+ report = { ...input, at: now };
62
+ return { received: true };
63
+ };
64
+ return {
65
+ title: config.title,
66
+ reportInput: z2.strictObject({
67
+ running: z2.boolean(),
68
+ occupied: iec.FixedArray(z2.boolean(), positions)
69
+ }),
70
+ reportOutput: z2.strictObject({ received: z2.boolean() }),
71
+ applyReport,
72
+ state: (plc) => {
73
+ const snapshot = plc.snapshot();
74
+ const now = Date.now();
75
+ let offset = 0;
76
+ return {
77
+ title: config.title,
78
+ connected: snapshot.connected,
79
+ reportAgeMs: report ? now - report.at : null,
80
+ running: report?.running ?? false,
81
+ controls: config.controls.map((control, index) => control.kind === "button" ? { ...control, index } : { ...control, index, value: snapshot.values?.[control.target]?.value ?? null }),
82
+ conveyors: config.conveyors.map(({ diverters, label, positions }) => {
83
+ const first = offset;
84
+ offset += positions;
85
+ return {
86
+ diverters,
87
+ label,
88
+ zones: Array.from({ length: positions }, (_, position) => {
89
+ const since = blockedSince.get(first + position);
90
+ return {
91
+ occupied: report?.occupied[first + position] ?? false,
92
+ blockedMs: since === undefined ? 0 : now - since
93
+ };
94
+ })
95
+ };
96
+ })
97
+ };
98
+ },
99
+ control: async (plc, input) => {
100
+ const control = config.controls[input.index];
101
+ if (!control)
102
+ throw new RangeError("Unknown HMI control");
103
+ if (control.kind === "slider") {
104
+ const { value } = input;
105
+ if (value === undefined || !Number.isInteger(value))
106
+ throw new RangeError("A slider needs an integer value");
107
+ if (value < control.min || value > control.max)
108
+ throw new RangeError(`${control.label} must be ${control.min} to ${control.max}`);
109
+ await plc.write(new Map([[control.target, value]]));
110
+ return;
111
+ }
112
+ if (control.kind === "toggle") {
113
+ if (input.value !== 0 && input.value !== 1)
114
+ throw new RangeError("A toggle needs 0 or 1");
115
+ await plc.write(new Map([[control.target, input.value === 1]]));
116
+ return;
117
+ }
118
+ await plc.write(new Map([[control.target, true]]));
119
+ try {
120
+ await new Promise((resolve) => setTimeout(resolve, pressMs));
121
+ } finally {
122
+ await plc.write(new Map([[control.target, false]]));
123
+ }
124
+ }
125
+ };
126
+ };
127
+ export {
128
+ createHmi
129
+ };
130
+
131
+ //# debugId=80B5AB46DA3DEA5364756E2164756E21
132
+ //# sourceMappingURL=hmi.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/hmi.ts", "../src/contract.ts"],
4
+ "sourcesContent": [
5
+ "import type { ResolvedHmiConfig } from \"@tsplc/config\";\nimport { hmiSchema, type HmiConfig } from \"@tsplc/config/schema\";\nimport { z } from \"zod\";\nimport { iec } from \"./contract\";\n\nconst pressMs = 200;\n\nexport interface HmiPlc {\n snapshot: () => {\n connected: boolean;\n values?: Record<string, { value: unknown; quality: string } | undefined>;\n };\n write: (values: ReadonlyMap<string, unknown>) => Promise<unknown>;\n}\n\n/** What the PLC sends: whether each position's sensor is blocked right now. */\nexport interface HmiReport {\n running: boolean;\n occupied: readonly boolean[];\n}\n\ntype Tuple<N extends number, T extends unknown[] = []> = T[\"length\"] extends N\n ? T\n : Tuple<N, [...T, unknown]>;\n/** The positions in the type, so the PLC compiler reads the report's array length from `defineConfig`'s literals. */\ntype Positions<Conveyors, Total extends unknown[] = []> = Conveyors extends readonly [\n { positions: infer N extends number },\n ...infer Rest,\n]\n ? number extends N\n ? number\n : Positions<Rest, [...Total, ...Tuple<N>]>\n : Conveyors extends readonly []\n ? Total[\"length\"]\n : number;\n\n/** The state behind a generated HMI page: the latest report, and how long each zone has stayed blocked. */\nexport const createHmi = <const Input extends HmiConfig>(input: Input) => {\n const config: ResolvedHmiConfig = hmiSchema.parse(input);\n const positions = config.conveyors.reduce((total, { positions }) => total + positions, 0);\n /* Only counts time with the line running: a stopped line holds its zones by design. */\n const blockedSince = new Map<number, number>();\n let report: (HmiReport & { at: number }) | undefined;\n\n const applyReport = (input: HmiReport) => {\n const now = Date.now();\n input.occupied.forEach((occupied, index) => {\n if (!occupied || !input.running) blockedSince.delete(index);\n else if (!blockedSince.has(index)) blockedSince.set(index, now);\n });\n report = { ...input, at: now };\n return { received: true };\n };\n\n return {\n title: config.title,\n /** Input and output schemas for the PLC procedure that receives reports. */\n reportInput: z.strictObject({\n running: z.boolean(),\n occupied: iec.FixedArray(\n z.boolean(),\n // The runtime sum of the same literals the type adds up.\n positions as Positions<Input[\"conveyors\"]>,\n ),\n }),\n reportOutput: z.strictObject({ received: z.boolean() }),\n applyReport,\n state: (plc: HmiPlc) => {\n const snapshot = plc.snapshot();\n const now = Date.now();\n let offset = 0;\n return {\n title: config.title,\n connected: snapshot.connected,\n reportAgeMs: report ? now - report.at : null,\n running: report?.running ?? false,\n controls: config.controls.map((control, index) =>\n control.kind === \"button\"\n ? { ...control, index }\n : { ...control, index, value: snapshot.values?.[control.target]?.value ?? null },\n ),\n conveyors: config.conveyors.map(({ diverters, label, positions }) => {\n const first = offset;\n offset += positions;\n return {\n diverters,\n label,\n zones: Array.from({ length: positions }, (_, position) => {\n const since = blockedSince.get(first + position);\n return {\n occupied: report?.occupied[first + position] ?? false,\n blockedMs: since === undefined ? 0 : now - since,\n };\n }),\n };\n }),\n };\n },\n /** Runs one of the configured controls: nothing else can be written from the page. */\n control: async (plc: HmiPlc, input: { index: number; value?: number | undefined }) => {\n const control = config.controls[input.index];\n if (!control) throw new RangeError(\"Unknown HMI control\");\n if (control.kind === \"slider\") {\n const { value } = input;\n if (value === undefined || !Number.isInteger(value))\n throw new RangeError(\"A slider needs an integer value\");\n if (value < control.min || value > control.max)\n throw new RangeError(`${control.label} must be ${control.min} to ${control.max}`);\n await plc.write(new Map([[control.target, value]]));\n return;\n }\n if (control.kind === \"toggle\") {\n if (input.value !== 0 && input.value !== 1) throw new RangeError(\"A toggle needs 0 or 1\");\n await plc.write(new Map([[control.target, input.value === 1]]));\n return;\n }\n await plc.write(new Map([[control.target, true]]));\n try {\n await new Promise((resolve) => setTimeout(resolve, pressMs));\n } finally {\n await plc.write(new Map([[control.target, false]]));\n }\n },\n };\n};\n\nexport type Hmi = ReturnType<typeof createHmi>;\n",
6
+ "import { createHash } from \"node:crypto\";\nimport type { AnyRouter, AnyProcedure } from \"@trpc/server\";\nimport { z } from \"zod\";\n\n/** Marks a tRPC procedure as callable by the PLC; the PLC calls it through `createServiceClient`. */\nexport interface PlcMeta {\n plc?: boolean;\n}\n\nexport type WireScalar =\n | { kind: \"boolean\" }\n | { kind: \"integer\"; iec: IntegerType; minimum: number; maximum: number }\n | { kind: \"string\"; capacity: number }\n | { kind: \"enum\"; members: Record<string, number> };\nexport type WireField = WireScalar | { kind: \"array\"; length: number; element: WireScalar };\nexport interface WireRecord {\n fields: Record<string, WireField>;\n}\nexport interface ServiceContract {\n path: string;\n kind: \"query\" | \"mutation\";\n input: WireRecord;\n output: WireRecord;\n version: string;\n}\n\nconst integerTypes = {\n SINT: [-128, 127],\n USINT: [0, 255],\n BYTE: [0, 255],\n INT: [-32768, 32767],\n UINT: [0, 65535],\n WORD: [0, 65535],\n DINT: [-2147483648, 2147483647],\n UDINT: [0, 4294967295],\n DWORD: [0, 4294967295],\n} as const;\nexport type IntegerType = keyof typeof integerTypes;\n\n/** The type-only property that carries a wire value's PLC layout through tRPC's inference. */\nexport const wireLayout = \"~plc\";\n/** A wire value whose type names its PLC layout, so the PLC compiler reads it without running the server. */\nexport type Wire<T, Layout> = T & { readonly [wireLayout]?: Layout };\ntype Schema<T> = z.ZodType<T, T>;\n\nconst integer =\n <const Type extends IntegerType>(type: Type) =>\n <\n const Minimum extends number = (typeof integerTypes)[Type][0],\n const Maximum extends number = (typeof integerTypes)[Type][1],\n >(\n bounds: { min?: Minimum; max?: Maximum } = {},\n ): Schema<Wire<number, { iec: Type; minimum: Minimum; maximum: Maximum }>> =>\n z\n .number()\n .int()\n .min(bounds.min ?? integerTypes[type][0])\n .max(bounds.max ?? integerTypes[type][1])\n .meta({ iec: type });\n\n/**\n * Zod schemas for PLC wire values. Booleans are `z.boolean()` and enums are `z.enum()` of a\n * numeric TypeScript enum the PLC also imports; records are `z.strictObject()` of these.\n */\nexport const iec = {\n SINT: integer(\"SINT\"),\n USINT: integer(\"USINT\"),\n BYTE: integer(\"BYTE\"),\n INT: integer(\"INT\"),\n UINT: integer(\"UINT\"),\n WORD: integer(\"WORD\"),\n DINT: integer(\"DINT\"),\n UDINT: integer(\"UDINT\"),\n DWORD: integer(\"DWORD\"),\n /** A UTF-8 string of at most `capacity` bytes. */\n STRING: <const N extends number>(capacity: N): Schema<Wire<string, { capacity: N }>> =>\n z.string().max(capacity),\n FixedArray: <T, const N extends number>(\n element: Schema<T>,\n length: N,\n ): Schema<Wire<T[], { length: N }>> => z.array(element).length(length),\n};\n\nconst identifier = /^(?!__proto__$)[A-Za-z_][A-Za-z0-9_]*$/;\nexport const isIntegerType = (value: string): value is IntegerType =>\n Object.hasOwn(integerTypes, value);\n\n/**\n * Checks a scalar's layout and rebuilds it in canonical key order, with enum members zero first,\n * so the version digest does not depend on which side built it or on declaration order.\n */\nconst wireScalar = (field: WireScalar, label: string): WireScalar => {\n switch (field.kind) {\n case \"boolean\":\n return { kind: \"boolean\" };\n case \"string\":\n if (!Number.isInteger(field.capacity) || field.capacity < 1 || field.capacity > 2048)\n throw new Error(`${label}: string capacity must be 1..2048 bytes.`);\n return { kind: \"string\", capacity: field.capacity };\n case \"integer\": {\n const [minimum, maximum] = integerTypes[field.iec];\n if (!Number.isInteger(field.minimum) || !Number.isInteger(field.maximum))\n throw new Error(`${label}: integer bounds must be integers.`);\n if (field.minimum > field.maximum)\n throw new Error(`${label}: integer bounds are contradictory.`);\n if (field.minimum < minimum || field.maximum > maximum)\n throw new Error(`${label}: integer bounds do not fit ${field.iec}.`);\n return { kind: \"integer\", iec: field.iec, minimum: field.minimum, maximum: field.maximum };\n }\n case \"enum\": {\n const values = Object.entries(field.members).sort((left, right) => {\n if (left[1] === 0) return -1;\n if (right[1] === 0) return 1;\n return left[1] - right[1];\n });\n if (\n !values.length ||\n values.some(\n ([name, value]) =>\n !identifier.test(name) ||\n !Number.isInteger(value) ||\n value < -2147483648 ||\n value > 2147483647,\n )\n )\n throw new Error(`${label}: PLC enums require named DINT values.`);\n if (new Set(values.map(([, value]) => value)).size !== values.length)\n throw new Error(`${label}: PLC enum values must be distinct.`);\n return { kind: \"enum\", members: Object.fromEntries(values) };\n }\n }\n};\n\n/** Checks a record against the native JSON codec's limits, in canonical field order. */\nexport const wireRecord = (\n fields: Record<string, WireField>,\n label: string,\n output: boolean,\n): WireRecord => {\n const checked: Record<string, WireField> = {};\n for (const key of Object.keys(fields).sort()) {\n const field = fields[key]!;\n if (!identifier.test(key)) throw new Error(`${label}.${key}: use an IEC field identifier.`);\n if (field.kind !== \"array\") {\n checked[key] = wireScalar(field, `${label}.${key}`);\n continue;\n }\n if (output)\n throw new Error(\n `${label}.${key}: the native HTTP decoder currently supports scalar response fields only.`,\n );\n if (!Number.isInteger(field.length) || field.length < 1 || field.length > 256)\n throw new Error(`${label}.${key}: arrays require one fixed length from 1 to 256.`);\n checked[key] = {\n kind: \"array\",\n length: field.length,\n element: wireScalar(field.element, `${label}.${key}`),\n };\n }\n if (Object.keys(checked).length > 31 || (output && !Object.keys(checked).length))\n throw new Error(`${label}: response records require 1..31 fields; requests allow 0..31.`);\n return { fields: checked };\n};\n\n/** One owner of the path digest, so the PLC compiler and the host agree without sharing a file. */\nexport const serviceContract = (\n path: string,\n kind: ServiceContract[\"kind\"],\n input: WireRecord,\n output: WireRecord,\n): ServiceContract => {\n if (path.split(\".\").some((part) => !identifier.test(part)))\n throw new Error(`${path}: PLC procedure paths require identifier segments.`);\n const version = createHash(\"sha256\")\n .update(JSON.stringify({ abi: 2, path, kind, input, output }))\n .digest(\"hex\")\n .slice(0, 24);\n return { path, kind, input, output, version };\n};\n\nconst schemaValue = (value: unknown): value is z.ZodType =>\n typeof value === \"object\" && value !== null && \"_zod\" in value && \"safeParse\" in value;\nconst schema = (value: unknown, label: string): z.ZodType => {\n if (!schemaValue(value))\n throw new Error(\n `${label}: provide a bounded Zod input/output schema; TypeScript number/string inference has no IEC memory layout.`,\n );\n return value;\n};\nconst jsonSchema = z.looseObject({\n type: z.string().optional(),\n maxLength: z.number().optional(),\n minItems: z.number().optional(),\n maxItems: z.number().optional(),\n minimum: z.number().optional(),\n maximum: z.number().optional(),\n iec: z.string().optional(),\n});\n/** Schema metadata from the schema's own Zod instance; JSON Schema output varies by Zod version. */\nconst metadata = (value: z.ZodType) => jsonSchema.parse(value.meta() ?? {});\nconst refSchema = z.looseObject({\n $ref: z.string().optional(),\n $defs: z.record(z.string(), z.unknown()).optional(),\n});\n/** The JSON Schema of `value` itself, following the top-level `$ref` newer Zod emits for schemas with an id. */\nconst jsonFor = (value: z.ZodType, options: Parameters<typeof z.toJSONSchema>[1]) => {\n const json = refSchema.parse(z.toJSONSchema(value, options));\n const definition = json.$ref?.startsWith(\"#/$defs/\")\n ? json.$defs?.[json.$ref.slice(\"#/$defs/\".length)]\n : json;\n return jsonSchema.parse(definition);\n};\nconst scalar = (value: z.ZodType, label: string): WireScalar => {\n const definition = value._zod.def;\n if (\n [\"optional\", \"nullable\", \"union\", \"default\", \"prefault\", \"pipe\", \"catch\"].includes(\n definition.type,\n )\n )\n throw new Error(\n `${label}: optional, nullable, transformed and defaulted wire fields are unsupported.`,\n );\n if (definition.type === \"enum\") {\n const entries = z\n .record(z.string(), z.union([z.number().int(), z.string()]))\n .parse(Reflect.get(definition, \"entries\"));\n const members: Record<string, number> = {};\n // A TypeScript numeric enum also maps each value back to its name; those entries are skipped.\n for (const [name, value] of Object.entries(entries)) {\n if (typeof value === \"number\") members[name] = value;\n else if (identifier.test(name) && entries[value] !== name)\n throw new Error(`${label}: PLC enums require only numeric members.`);\n }\n return { kind: \"enum\", members };\n }\n const json = jsonFor(value, { io: \"input\", unrepresentable: \"throw\" });\n if (json.type === \"boolean\") return { kind: \"boolean\" };\n if (json.type === \"string\" && json.maxLength !== undefined)\n return { kind: \"string\", capacity: json.maxLength };\n const type = metadata(value).iec;\n if (\n json.type === \"integer\" &&\n type !== undefined &&\n isIntegerType(type) &&\n json.minimum !== undefined &&\n json.maximum !== undefined\n )\n return { kind: \"integer\", iec: type, minimum: json.minimum, maximum: json.maximum };\n throw new Error(\n `${label}: use z.boolean(), z.enum() of a numeric enum, iec.STRING(), or an integer such as iec.INT(). Optional, nullable, union, float and dynamic layouts are unsupported.`,\n );\n};\nconst record = (declared: z.ZodType, label: string, output: boolean): WireRecord => {\n // An input record may `.transform()` into the handler's shape; the wire keeps the record.\n const piped = declared._zod.def;\n const value =\n !output && piped.type === \"pipe\" ? schema(Reflect.get(piped, \"in\"), label) : declared;\n const def = value._zod.def;\n if (def.type !== \"object\")\n throw new Error(`${label}: HTTP services require a closed object schema.`);\n const shape = z\n .record(z.string(), z.custom<z.ZodType>(schemaValue))\n .parse(Reflect.get(def, \"shape\"));\n const fields: Record<string, WireField> = {};\n for (const [key, item] of Object.entries(shape)) {\n if (item._zod.def.type !== \"array\") {\n fields[key] = scalar(item, `${label}.${key}`);\n continue;\n }\n const bounds = jsonFor(item, { io: \"input\" });\n if (bounds.maxItems === undefined || bounds.maxItems !== bounds.minItems)\n throw new Error(`${label}.${key}: arrays require one fixed length from 1 to 256.`);\n fields[key] = {\n kind: \"array\",\n length: bounds.maxItems,\n element: scalar(\n schema(Reflect.get(item._zod.def, \"element\"), `${label}.${key}`),\n `${label}.${key}`,\n ),\n };\n }\n return wireRecord(fields, label, output);\n};\n\n/** Reflection is isolated here because tRPC v11 keeps parsers on procedure._def. */\nexport const serviceContracts = (router: AnyRouter): ServiceContract[] => {\n const contracts: ServiceContract[] = [];\n const procedures = z.record(z.string(), z.unknown()).parse(router._def.procedures);\n for (const [path, procedure] of Object.entries(procedures)) {\n if (typeof procedure !== \"function\" || !(\"_def\" in procedure))\n throw new Error(\"Unsupported tRPC router metadata.\");\n const reflected = z\n .custom<AnyProcedure>((value) => typeof value === \"function\" && \"_def\" in value)\n .parse(procedure);\n const def = reflected._def;\n if (\n typeof def.meta !== \"object\" ||\n def.meta === null ||\n !(\"plc\" in def.meta) ||\n def.meta.plc !== true\n )\n continue;\n if (def.type !== \"query\" && def.type !== \"mutation\")\n throw new Error(\n `${path}: subscriptions use a different transport; expose a query or mutation to the PLC.`,\n );\n if (def.inputs.length > 1)\n throw new Error(`${path}: merge the input into one bounded object schema.`);\n const input = record(\n def.inputs.length ? schema(def.inputs[0], `${path}.input`) : z.strictObject({}),\n `${path}.Input`,\n false,\n );\n const output = record(\n schema(Reflect.get(def, \"output\"), `${path}.output`),\n `${path}.Output`,\n true,\n );\n contracts.push(serviceContract(path, def.type, input, output));\n }\n return contracts.sort((a, b) => a.path.localeCompare(b.path));\n};\n\nexport const servicePath = (contract: ServiceContract) =>\n `/tsplc/services/${contract.version}/${contract.path}`;\n\nexport const validateWireRecord = (record: WireRecord, value: unknown): void => {\n const scalarValue = (field: WireScalar, value: unknown): boolean => {\n switch (field.kind) {\n case \"boolean\":\n return typeof value === \"boolean\";\n case \"integer\":\n return (\n typeof value === \"number\" &&\n Number.isInteger(value) &&\n value >= field.minimum &&\n value <= field.maximum\n );\n case \"string\":\n return (\n typeof value === \"string\" &&\n !value.includes(\"\\0\") &&\n new TextEncoder().encode(value).length <= field.capacity\n );\n case \"enum\":\n return typeof value === \"number\" && Object.values(field.members).includes(value);\n }\n };\n if (typeof value !== \"object\" || value === null || Array.isArray(value))\n throw new Error(\"Expected a closed wire record.\");\n const entries = Object.entries(value);\n if (entries.length !== Object.keys(record.fields).length)\n throw new Error(\"Wire record fields differ from the contract.\");\n for (const [name, field] of Object.entries(record.fields)) {\n const item: unknown = Reflect.get(value, name);\n const valid =\n field.kind === \"array\"\n ? Array.isArray(item) &&\n item.length === field.length &&\n item.every((value) => scalarValue(field.element, value))\n : scalarValue(field, item);\n if (!valid) throw new Error(`Wire field ${name} violates its PLC representation.`);\n }\n if (new TextEncoder().encode(JSON.stringify(value)).length > 2048)\n throw new Error(\"Wire record exceeds the 2048-byte native JSON limit.\");\n};\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "80B5AB46DA3DEA5364756E2164756E21",
10
+ "names": []
11
+ }
package/dist/index.js ADDED
@@ -0,0 +1,320 @@
1
+ // packages/server/src/index.ts
2
+ import { TRPCError } from "@trpc/server";
3
+
4
+ // packages/server/src/contract.ts
5
+ import { createHash } from "node:crypto";
6
+ import { z } from "zod";
7
+ var integerTypes = {
8
+ SINT: [-128, 127],
9
+ USINT: [0, 255],
10
+ BYTE: [0, 255],
11
+ INT: [-32768, 32767],
12
+ UINT: [0, 65535],
13
+ WORD: [0, 65535],
14
+ DINT: [-2147483648, 2147483647],
15
+ UDINT: [0, 4294967295],
16
+ DWORD: [0, 4294967295]
17
+ };
18
+ var wireLayout = "~plc";
19
+ var integer = (type) => (bounds = {}) => z.number().int().min(bounds.min ?? integerTypes[type][0]).max(bounds.max ?? integerTypes[type][1]).meta({ iec: type });
20
+ var iec = {
21
+ SINT: integer("SINT"),
22
+ USINT: integer("USINT"),
23
+ BYTE: integer("BYTE"),
24
+ INT: integer("INT"),
25
+ UINT: integer("UINT"),
26
+ WORD: integer("WORD"),
27
+ DINT: integer("DINT"),
28
+ UDINT: integer("UDINT"),
29
+ DWORD: integer("DWORD"),
30
+ STRING: (capacity) => z.string().max(capacity),
31
+ FixedArray: (element, length) => z.array(element).length(length)
32
+ };
33
+ var identifier = /^(?!__proto__$)[A-Za-z_][A-Za-z0-9_]*$/;
34
+ var isIntegerType = (value) => Object.hasOwn(integerTypes, value);
35
+ var wireScalar = (field, label) => {
36
+ switch (field.kind) {
37
+ case "boolean":
38
+ return { kind: "boolean" };
39
+ case "string":
40
+ if (!Number.isInteger(field.capacity) || field.capacity < 1 || field.capacity > 2048)
41
+ throw new Error(`${label}: string capacity must be 1..2048 bytes.`);
42
+ return { kind: "string", capacity: field.capacity };
43
+ case "integer": {
44
+ const [minimum, maximum] = integerTypes[field.iec];
45
+ if (!Number.isInteger(field.minimum) || !Number.isInteger(field.maximum))
46
+ throw new Error(`${label}: integer bounds must be integers.`);
47
+ if (field.minimum > field.maximum)
48
+ throw new Error(`${label}: integer bounds are contradictory.`);
49
+ if (field.minimum < minimum || field.maximum > maximum)
50
+ throw new Error(`${label}: integer bounds do not fit ${field.iec}.`);
51
+ return { kind: "integer", iec: field.iec, minimum: field.minimum, maximum: field.maximum };
52
+ }
53
+ case "enum": {
54
+ const values = Object.entries(field.members).sort((left, right) => {
55
+ if (left[1] === 0)
56
+ return -1;
57
+ if (right[1] === 0)
58
+ return 1;
59
+ return left[1] - right[1];
60
+ });
61
+ if (!values.length || values.some(([name, value]) => !identifier.test(name) || !Number.isInteger(value) || value < -2147483648 || value > 2147483647))
62
+ throw new Error(`${label}: PLC enums require named DINT values.`);
63
+ if (new Set(values.map(([, value]) => value)).size !== values.length)
64
+ throw new Error(`${label}: PLC enum values must be distinct.`);
65
+ return { kind: "enum", members: Object.fromEntries(values) };
66
+ }
67
+ }
68
+ };
69
+ var wireRecord = (fields, label, output) => {
70
+ const checked = {};
71
+ for (const key of Object.keys(fields).sort()) {
72
+ const field = fields[key];
73
+ if (!identifier.test(key))
74
+ throw new Error(`${label}.${key}: use an IEC field identifier.`);
75
+ if (field.kind !== "array") {
76
+ checked[key] = wireScalar(field, `${label}.${key}`);
77
+ continue;
78
+ }
79
+ if (output)
80
+ throw new Error(`${label}.${key}: the native HTTP decoder currently supports scalar response fields only.`);
81
+ if (!Number.isInteger(field.length) || field.length < 1 || field.length > 256)
82
+ throw new Error(`${label}.${key}: arrays require one fixed length from 1 to 256.`);
83
+ checked[key] = {
84
+ kind: "array",
85
+ length: field.length,
86
+ element: wireScalar(field.element, `${label}.${key}`)
87
+ };
88
+ }
89
+ if (Object.keys(checked).length > 31 || output && !Object.keys(checked).length)
90
+ throw new Error(`${label}: response records require 1..31 fields; requests allow 0..31.`);
91
+ return { fields: checked };
92
+ };
93
+ var serviceContract = (path, kind, input, output) => {
94
+ if (path.split(".").some((part) => !identifier.test(part)))
95
+ throw new Error(`${path}: PLC procedure paths require identifier segments.`);
96
+ const version = createHash("sha256").update(JSON.stringify({ abi: 2, path, kind, input, output })).digest("hex").slice(0, 24);
97
+ return { path, kind, input, output, version };
98
+ };
99
+ var schemaValue = (value) => typeof value === "object" && value !== null && ("_zod" in value) && ("safeParse" in value);
100
+ var schema = (value, label) => {
101
+ if (!schemaValue(value))
102
+ throw new Error(`${label}: provide a bounded Zod input/output schema; TypeScript number/string inference has no IEC memory layout.`);
103
+ return value;
104
+ };
105
+ var jsonSchema = z.looseObject({
106
+ type: z.string().optional(),
107
+ maxLength: z.number().optional(),
108
+ minItems: z.number().optional(),
109
+ maxItems: z.number().optional(),
110
+ minimum: z.number().optional(),
111
+ maximum: z.number().optional(),
112
+ iec: z.string().optional()
113
+ });
114
+ var metadata = (value) => jsonSchema.parse(value.meta() ?? {});
115
+ var refSchema = z.looseObject({
116
+ $ref: z.string().optional(),
117
+ $defs: z.record(z.string(), z.unknown()).optional()
118
+ });
119
+ var jsonFor = (value, options) => {
120
+ const json = refSchema.parse(z.toJSONSchema(value, options));
121
+ const definition = json.$ref?.startsWith("#/$defs/") ? json.$defs?.[json.$ref.slice("#/$defs/".length)] : json;
122
+ return jsonSchema.parse(definition);
123
+ };
124
+ var scalar = (value, label) => {
125
+ const definition = value._zod.def;
126
+ if (["optional", "nullable", "union", "default", "prefault", "pipe", "catch"].includes(definition.type))
127
+ throw new Error(`${label}: optional, nullable, transformed and defaulted wire fields are unsupported.`);
128
+ if (definition.type === "enum") {
129
+ const entries = z.record(z.string(), z.union([z.number().int(), z.string()])).parse(Reflect.get(definition, "entries"));
130
+ const members = {};
131
+ for (const [name, value] of Object.entries(entries)) {
132
+ if (typeof value === "number")
133
+ members[name] = value;
134
+ else if (identifier.test(name) && entries[value] !== name)
135
+ throw new Error(`${label}: PLC enums require only numeric members.`);
136
+ }
137
+ return { kind: "enum", members };
138
+ }
139
+ const json = jsonFor(value, { io: "input", unrepresentable: "throw" });
140
+ if (json.type === "boolean")
141
+ return { kind: "boolean" };
142
+ if (json.type === "string" && json.maxLength !== undefined)
143
+ return { kind: "string", capacity: json.maxLength };
144
+ const type = metadata(value).iec;
145
+ if (json.type === "integer" && type !== undefined && isIntegerType(type) && json.minimum !== undefined && json.maximum !== undefined)
146
+ return { kind: "integer", iec: type, minimum: json.minimum, maximum: json.maximum };
147
+ throw new Error(`${label}: use z.boolean(), z.enum() of a numeric enum, iec.STRING(), or an integer such as iec.INT(). Optional, nullable, union, float and dynamic layouts are unsupported.`);
148
+ };
149
+ var record = (declared, label, output) => {
150
+ const piped = declared._zod.def;
151
+ const value = !output && piped.type === "pipe" ? schema(Reflect.get(piped, "in"), label) : declared;
152
+ const def = value._zod.def;
153
+ if (def.type !== "object")
154
+ throw new Error(`${label}: HTTP services require a closed object schema.`);
155
+ const shape = z.record(z.string(), z.custom(schemaValue)).parse(Reflect.get(def, "shape"));
156
+ const fields = {};
157
+ for (const [key, item] of Object.entries(shape)) {
158
+ if (item._zod.def.type !== "array") {
159
+ fields[key] = scalar(item, `${label}.${key}`);
160
+ continue;
161
+ }
162
+ const bounds = jsonFor(item, { io: "input" });
163
+ if (bounds.maxItems === undefined || bounds.maxItems !== bounds.minItems)
164
+ throw new Error(`${label}.${key}: arrays require one fixed length from 1 to 256.`);
165
+ fields[key] = {
166
+ kind: "array",
167
+ length: bounds.maxItems,
168
+ element: scalar(schema(Reflect.get(item._zod.def, "element"), `${label}.${key}`), `${label}.${key}`)
169
+ };
170
+ }
171
+ return wireRecord(fields, label, output);
172
+ };
173
+ var serviceContracts = (router) => {
174
+ const contracts = [];
175
+ const procedures = z.record(z.string(), z.unknown()).parse(router._def.procedures);
176
+ for (const [path, procedure] of Object.entries(procedures)) {
177
+ if (typeof procedure !== "function" || !("_def" in procedure))
178
+ throw new Error("Unsupported tRPC router metadata.");
179
+ const reflected = z.custom((value) => typeof value === "function" && ("_def" in value)).parse(procedure);
180
+ const def = reflected._def;
181
+ if (typeof def.meta !== "object" || def.meta === null || !("plc" in def.meta) || def.meta.plc !== true)
182
+ continue;
183
+ if (def.type !== "query" && def.type !== "mutation")
184
+ throw new Error(`${path}: subscriptions use a different transport; expose a query or mutation to the PLC.`);
185
+ if (def.inputs.length > 1)
186
+ throw new Error(`${path}: merge the input into one bounded object schema.`);
187
+ const input = record(def.inputs.length ? schema(def.inputs[0], `${path}.input`) : z.strictObject({}), `${path}.Input`, false);
188
+ const output = record(schema(Reflect.get(def, "output"), `${path}.output`), `${path}.Output`, true);
189
+ contracts.push(serviceContract(path, def.type, input, output));
190
+ }
191
+ return contracts.sort((a, b) => a.path.localeCompare(b.path));
192
+ };
193
+ var servicePath = (contract) => `/tsplc/services/${contract.version}/${contract.path}`;
194
+ var validateWireRecord = (record, value) => {
195
+ const scalarValue = (field, value) => {
196
+ switch (field.kind) {
197
+ case "boolean":
198
+ return typeof value === "boolean";
199
+ case "integer":
200
+ return typeof value === "number" && Number.isInteger(value) && value >= field.minimum && value <= field.maximum;
201
+ case "string":
202
+ return typeof value === "string" && !value.includes("\x00") && new TextEncoder().encode(value).length <= field.capacity;
203
+ case "enum":
204
+ return typeof value === "number" && Object.values(field.members).includes(value);
205
+ }
206
+ };
207
+ if (typeof value !== "object" || value === null || Array.isArray(value))
208
+ throw new Error("Expected a closed wire record.");
209
+ const entries = Object.entries(value);
210
+ if (entries.length !== Object.keys(record.fields).length)
211
+ throw new Error("Wire record fields differ from the contract.");
212
+ for (const [name, field] of Object.entries(record.fields)) {
213
+ const item = Reflect.get(value, name);
214
+ const valid = field.kind === "array" ? Array.isArray(item) && item.length === field.length && item.every((value) => scalarValue(field.element, value)) : scalarValue(field, item);
215
+ if (!valid)
216
+ throw new Error(`Wire field ${name} violates its PLC representation.`);
217
+ }
218
+ if (new TextEncoder().encode(JSON.stringify(value)).length > 2048)
219
+ throw new Error("Wire record exceeds the 2048-byte native JSON limit.");
220
+ };
221
+
222
+ // packages/server/src/index.ts
223
+ var createServiceHandler = (options) => {
224
+ const contracts = serviceContracts(options.router);
225
+ const paths = new Map(contracts.map((contract) => [servicePath(contract), contract]));
226
+ const response = (value, status = 200) => {
227
+ const body = JSON.stringify(value);
228
+ return new Response(body, {
229
+ status,
230
+ headers: {
231
+ "Content-Type": "application/json; charset=utf-8",
232
+ "Content-Length": String(new TextEncoder().encode(body).length),
233
+ "Cache-Control": "no-store"
234
+ }
235
+ });
236
+ };
237
+ return async (request) => {
238
+ const path = new URL(request.url).pathname;
239
+ const contract = paths.get(path);
240
+ if (!contract)
241
+ return response({ error: "Unknown procedure or incompatible contract" }, 409);
242
+ if (request.method !== "POST")
243
+ return response({ error: "Use POST" }, 405);
244
+ const length = request.headers.get("content-length");
245
+ if (length !== null && (!/^\d+$/.test(length) || Number(length) > 2048))
246
+ return response({ error: "Request exceeds the PLC wire limit" }, 413);
247
+ const reader = request.body?.getReader();
248
+ const chunks = [];
249
+ let size = 0;
250
+ if (reader)
251
+ try {
252
+ for (;; ) {
253
+ const chunk = await reader.read();
254
+ if (chunk.done)
255
+ break;
256
+ size += chunk.value.length;
257
+ if (size > 2048) {
258
+ await reader.cancel();
259
+ return response({ error: "Request exceeds the PLC wire limit" }, 413);
260
+ }
261
+ chunks.push(chunk.value);
262
+ }
263
+ } finally {
264
+ reader.releaseLock();
265
+ }
266
+ const bytes = new Uint8Array(size);
267
+ let offset = 0;
268
+ for (const chunk of chunks) {
269
+ bytes.set(chunk, offset);
270
+ offset += chunk.length;
271
+ }
272
+ let input;
273
+ try {
274
+ input = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
275
+ validateWireRecord(contract.input, input);
276
+ } catch {
277
+ return response({ error: "Invalid procedure input" }, 400);
278
+ }
279
+ try {
280
+ const context = await options.createContext(request);
281
+ const caller = options.router.createCaller(context, { signal: request.signal });
282
+ const invoke = Reflect.get(caller, contract.path);
283
+ if (typeof invoke !== "function")
284
+ throw new Error("Invalid tRPC caller.");
285
+ const output = await invoke(input);
286
+ validateWireRecord(contract.output, output);
287
+ return response(output);
288
+ } catch (cause) {
289
+ if (cause instanceof TRPCError) {
290
+ const statuses = {
291
+ UNAUTHORIZED: 401,
292
+ FORBIDDEN: 403,
293
+ BAD_REQUEST: 400,
294
+ NOT_FOUND: 404,
295
+ CONFLICT: 409,
296
+ TOO_MANY_REQUESTS: 429
297
+ };
298
+ const status = statuses[cause.code];
299
+ if (status === undefined)
300
+ options.onError?.(cause);
301
+ return response({ error: cause.code }, status ?? 500);
302
+ }
303
+ options.onError?.(cause);
304
+ return response({ error: "Procedure failed" }, 500);
305
+ }
306
+ };
307
+ };
308
+ export {
309
+ createServiceHandler,
310
+ iec,
311
+ isIntegerType,
312
+ serviceContract,
313
+ serviceContracts,
314
+ servicePath,
315
+ wireLayout,
316
+ wireRecord
317
+ };
318
+
319
+ //# debugId=F8FF43EA2E94C95264756E2164756E21
320
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts", "../src/contract.ts"],
4
+ "sourcesContent": [
5
+ "import { TRPCError, type AnyRouter, type inferRouterContext } from \"@trpc/server\";\nimport { serviceContracts, servicePath, validateWireRecord } from \"./contract\";\nexport {\n iec,\n isIntegerType,\n serviceContract,\n serviceContracts,\n servicePath,\n wireLayout,\n wireRecord,\n type IntegerType,\n type PlcMeta,\n type Wire,\n type ServiceContract,\n type WireField,\n type WireRecord,\n type WireScalar,\n} from \"./contract\";\n\n/** Mount in an authenticated Fetch host; createContext must resolve the caller's identity. */\nexport const createServiceHandler = <Router extends AnyRouter>(options: {\n router: Router;\n createContext: (\n request: Request,\n ) => inferRouterContext<Router> | Promise<inferRouterContext<Router>>;\n onError?: (error: unknown) => void;\n}) => {\n const contracts = serviceContracts(options.router);\n const paths = new Map(contracts.map((contract) => [servicePath(contract), contract]));\n const response = (value: unknown, status = 200) => {\n const body = JSON.stringify(value);\n return new Response(body, {\n status,\n headers: {\n \"Content-Type\": \"application/json; charset=utf-8\",\n \"Content-Length\": String(new TextEncoder().encode(body).length),\n \"Cache-Control\": \"no-store\",\n },\n });\n };\n return async (request: Request): Promise<Response> => {\n const path = new URL(request.url).pathname;\n const contract = paths.get(path);\n if (!contract) return response({ error: \"Unknown procedure or incompatible contract\" }, 409);\n if (request.method !== \"POST\") return response({ error: \"Use POST\" }, 405);\n const length = request.headers.get(\"content-length\");\n if (length !== null && (!/^\\d+$/.test(length) || Number(length) > 2048))\n return response({ error: \"Request exceeds the PLC wire limit\" }, 413);\n const reader = request.body?.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n if (reader)\n try {\n for (;;) {\n const chunk = await reader.read();\n if (chunk.done) break;\n size += chunk.value.length;\n if (size > 2048) {\n await reader.cancel();\n return response({ error: \"Request exceeds the PLC wire limit\" }, 413);\n }\n chunks.push(chunk.value);\n }\n } finally {\n reader.releaseLock();\n }\n const bytes = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n bytes.set(chunk, offset);\n offset += chunk.length;\n }\n let input: unknown;\n try {\n input = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes));\n validateWireRecord(contract.input, input);\n } catch {\n return response({ error: \"Invalid procedure input\" }, 400);\n }\n try {\n const context = await options.createContext(request);\n const caller = options.router.createCaller(context, { signal: request.signal });\n const invoke: unknown = Reflect.get(caller, contract.path);\n if (typeof invoke !== \"function\") throw new Error(\"Invalid tRPC caller.\");\n const output: unknown = await invoke(input);\n validateWireRecord(contract.output, output);\n return response(output);\n } catch (cause) {\n if (cause instanceof TRPCError) {\n const statuses: Partial<Record<TRPCError[\"code\"], number>> = {\n UNAUTHORIZED: 401,\n FORBIDDEN: 403,\n BAD_REQUEST: 400,\n NOT_FOUND: 404,\n CONFLICT: 409,\n TOO_MANY_REQUESTS: 429,\n };\n const status = statuses[cause.code];\n if (status === undefined) options.onError?.(cause);\n return response({ error: cause.code }, status ?? 500);\n }\n options.onError?.(cause);\n return response({ error: \"Procedure failed\" }, 500);\n }\n };\n};\n",
6
+ "import { createHash } from \"node:crypto\";\nimport type { AnyRouter, AnyProcedure } from \"@trpc/server\";\nimport { z } from \"zod\";\n\n/** Marks a tRPC procedure as callable by the PLC; the PLC calls it through `createServiceClient`. */\nexport interface PlcMeta {\n plc?: boolean;\n}\n\nexport type WireScalar =\n | { kind: \"boolean\" }\n | { kind: \"integer\"; iec: IntegerType; minimum: number; maximum: number }\n | { kind: \"string\"; capacity: number }\n | { kind: \"enum\"; members: Record<string, number> };\nexport type WireField = WireScalar | { kind: \"array\"; length: number; element: WireScalar };\nexport interface WireRecord {\n fields: Record<string, WireField>;\n}\nexport interface ServiceContract {\n path: string;\n kind: \"query\" | \"mutation\";\n input: WireRecord;\n output: WireRecord;\n version: string;\n}\n\nconst integerTypes = {\n SINT: [-128, 127],\n USINT: [0, 255],\n BYTE: [0, 255],\n INT: [-32768, 32767],\n UINT: [0, 65535],\n WORD: [0, 65535],\n DINT: [-2147483648, 2147483647],\n UDINT: [0, 4294967295],\n DWORD: [0, 4294967295],\n} as const;\nexport type IntegerType = keyof typeof integerTypes;\n\n/** The type-only property that carries a wire value's PLC layout through tRPC's inference. */\nexport const wireLayout = \"~plc\";\n/** A wire value whose type names its PLC layout, so the PLC compiler reads it without running the server. */\nexport type Wire<T, Layout> = T & { readonly [wireLayout]?: Layout };\ntype Schema<T> = z.ZodType<T, T>;\n\nconst integer =\n <const Type extends IntegerType>(type: Type) =>\n <\n const Minimum extends number = (typeof integerTypes)[Type][0],\n const Maximum extends number = (typeof integerTypes)[Type][1],\n >(\n bounds: { min?: Minimum; max?: Maximum } = {},\n ): Schema<Wire<number, { iec: Type; minimum: Minimum; maximum: Maximum }>> =>\n z\n .number()\n .int()\n .min(bounds.min ?? integerTypes[type][0])\n .max(bounds.max ?? integerTypes[type][1])\n .meta({ iec: type });\n\n/**\n * Zod schemas for PLC wire values. Booleans are `z.boolean()` and enums are `z.enum()` of a\n * numeric TypeScript enum the PLC also imports; records are `z.strictObject()` of these.\n */\nexport const iec = {\n SINT: integer(\"SINT\"),\n USINT: integer(\"USINT\"),\n BYTE: integer(\"BYTE\"),\n INT: integer(\"INT\"),\n UINT: integer(\"UINT\"),\n WORD: integer(\"WORD\"),\n DINT: integer(\"DINT\"),\n UDINT: integer(\"UDINT\"),\n DWORD: integer(\"DWORD\"),\n /** A UTF-8 string of at most `capacity` bytes. */\n STRING: <const N extends number>(capacity: N): Schema<Wire<string, { capacity: N }>> =>\n z.string().max(capacity),\n FixedArray: <T, const N extends number>(\n element: Schema<T>,\n length: N,\n ): Schema<Wire<T[], { length: N }>> => z.array(element).length(length),\n};\n\nconst identifier = /^(?!__proto__$)[A-Za-z_][A-Za-z0-9_]*$/;\nexport const isIntegerType = (value: string): value is IntegerType =>\n Object.hasOwn(integerTypes, value);\n\n/**\n * Checks a scalar's layout and rebuilds it in canonical key order, with enum members zero first,\n * so the version digest does not depend on which side built it or on declaration order.\n */\nconst wireScalar = (field: WireScalar, label: string): WireScalar => {\n switch (field.kind) {\n case \"boolean\":\n return { kind: \"boolean\" };\n case \"string\":\n if (!Number.isInteger(field.capacity) || field.capacity < 1 || field.capacity > 2048)\n throw new Error(`${label}: string capacity must be 1..2048 bytes.`);\n return { kind: \"string\", capacity: field.capacity };\n case \"integer\": {\n const [minimum, maximum] = integerTypes[field.iec];\n if (!Number.isInteger(field.minimum) || !Number.isInteger(field.maximum))\n throw new Error(`${label}: integer bounds must be integers.`);\n if (field.minimum > field.maximum)\n throw new Error(`${label}: integer bounds are contradictory.`);\n if (field.minimum < minimum || field.maximum > maximum)\n throw new Error(`${label}: integer bounds do not fit ${field.iec}.`);\n return { kind: \"integer\", iec: field.iec, minimum: field.minimum, maximum: field.maximum };\n }\n case \"enum\": {\n const values = Object.entries(field.members).sort((left, right) => {\n if (left[1] === 0) return -1;\n if (right[1] === 0) return 1;\n return left[1] - right[1];\n });\n if (\n !values.length ||\n values.some(\n ([name, value]) =>\n !identifier.test(name) ||\n !Number.isInteger(value) ||\n value < -2147483648 ||\n value > 2147483647,\n )\n )\n throw new Error(`${label}: PLC enums require named DINT values.`);\n if (new Set(values.map(([, value]) => value)).size !== values.length)\n throw new Error(`${label}: PLC enum values must be distinct.`);\n return { kind: \"enum\", members: Object.fromEntries(values) };\n }\n }\n};\n\n/** Checks a record against the native JSON codec's limits, in canonical field order. */\nexport const wireRecord = (\n fields: Record<string, WireField>,\n label: string,\n output: boolean,\n): WireRecord => {\n const checked: Record<string, WireField> = {};\n for (const key of Object.keys(fields).sort()) {\n const field = fields[key]!;\n if (!identifier.test(key)) throw new Error(`${label}.${key}: use an IEC field identifier.`);\n if (field.kind !== \"array\") {\n checked[key] = wireScalar(field, `${label}.${key}`);\n continue;\n }\n if (output)\n throw new Error(\n `${label}.${key}: the native HTTP decoder currently supports scalar response fields only.`,\n );\n if (!Number.isInteger(field.length) || field.length < 1 || field.length > 256)\n throw new Error(`${label}.${key}: arrays require one fixed length from 1 to 256.`);\n checked[key] = {\n kind: \"array\",\n length: field.length,\n element: wireScalar(field.element, `${label}.${key}`),\n };\n }\n if (Object.keys(checked).length > 31 || (output && !Object.keys(checked).length))\n throw new Error(`${label}: response records require 1..31 fields; requests allow 0..31.`);\n return { fields: checked };\n};\n\n/** One owner of the path digest, so the PLC compiler and the host agree without sharing a file. */\nexport const serviceContract = (\n path: string,\n kind: ServiceContract[\"kind\"],\n input: WireRecord,\n output: WireRecord,\n): ServiceContract => {\n if (path.split(\".\").some((part) => !identifier.test(part)))\n throw new Error(`${path}: PLC procedure paths require identifier segments.`);\n const version = createHash(\"sha256\")\n .update(JSON.stringify({ abi: 2, path, kind, input, output }))\n .digest(\"hex\")\n .slice(0, 24);\n return { path, kind, input, output, version };\n};\n\nconst schemaValue = (value: unknown): value is z.ZodType =>\n typeof value === \"object\" && value !== null && \"_zod\" in value && \"safeParse\" in value;\nconst schema = (value: unknown, label: string): z.ZodType => {\n if (!schemaValue(value))\n throw new Error(\n `${label}: provide a bounded Zod input/output schema; TypeScript number/string inference has no IEC memory layout.`,\n );\n return value;\n};\nconst jsonSchema = z.looseObject({\n type: z.string().optional(),\n maxLength: z.number().optional(),\n minItems: z.number().optional(),\n maxItems: z.number().optional(),\n minimum: z.number().optional(),\n maximum: z.number().optional(),\n iec: z.string().optional(),\n});\n/** Schema metadata from the schema's own Zod instance; JSON Schema output varies by Zod version. */\nconst metadata = (value: z.ZodType) => jsonSchema.parse(value.meta() ?? {});\nconst refSchema = z.looseObject({\n $ref: z.string().optional(),\n $defs: z.record(z.string(), z.unknown()).optional(),\n});\n/** The JSON Schema of `value` itself, following the top-level `$ref` newer Zod emits for schemas with an id. */\nconst jsonFor = (value: z.ZodType, options: Parameters<typeof z.toJSONSchema>[1]) => {\n const json = refSchema.parse(z.toJSONSchema(value, options));\n const definition = json.$ref?.startsWith(\"#/$defs/\")\n ? json.$defs?.[json.$ref.slice(\"#/$defs/\".length)]\n : json;\n return jsonSchema.parse(definition);\n};\nconst scalar = (value: z.ZodType, label: string): WireScalar => {\n const definition = value._zod.def;\n if (\n [\"optional\", \"nullable\", \"union\", \"default\", \"prefault\", \"pipe\", \"catch\"].includes(\n definition.type,\n )\n )\n throw new Error(\n `${label}: optional, nullable, transformed and defaulted wire fields are unsupported.`,\n );\n if (definition.type === \"enum\") {\n const entries = z\n .record(z.string(), z.union([z.number().int(), z.string()]))\n .parse(Reflect.get(definition, \"entries\"));\n const members: Record<string, number> = {};\n // A TypeScript numeric enum also maps each value back to its name; those entries are skipped.\n for (const [name, value] of Object.entries(entries)) {\n if (typeof value === \"number\") members[name] = value;\n else if (identifier.test(name) && entries[value] !== name)\n throw new Error(`${label}: PLC enums require only numeric members.`);\n }\n return { kind: \"enum\", members };\n }\n const json = jsonFor(value, { io: \"input\", unrepresentable: \"throw\" });\n if (json.type === \"boolean\") return { kind: \"boolean\" };\n if (json.type === \"string\" && json.maxLength !== undefined)\n return { kind: \"string\", capacity: json.maxLength };\n const type = metadata(value).iec;\n if (\n json.type === \"integer\" &&\n type !== undefined &&\n isIntegerType(type) &&\n json.minimum !== undefined &&\n json.maximum !== undefined\n )\n return { kind: \"integer\", iec: type, minimum: json.minimum, maximum: json.maximum };\n throw new Error(\n `${label}: use z.boolean(), z.enum() of a numeric enum, iec.STRING(), or an integer such as iec.INT(). Optional, nullable, union, float and dynamic layouts are unsupported.`,\n );\n};\nconst record = (declared: z.ZodType, label: string, output: boolean): WireRecord => {\n // An input record may `.transform()` into the handler's shape; the wire keeps the record.\n const piped = declared._zod.def;\n const value =\n !output && piped.type === \"pipe\" ? schema(Reflect.get(piped, \"in\"), label) : declared;\n const def = value._zod.def;\n if (def.type !== \"object\")\n throw new Error(`${label}: HTTP services require a closed object schema.`);\n const shape = z\n .record(z.string(), z.custom<z.ZodType>(schemaValue))\n .parse(Reflect.get(def, \"shape\"));\n const fields: Record<string, WireField> = {};\n for (const [key, item] of Object.entries(shape)) {\n if (item._zod.def.type !== \"array\") {\n fields[key] = scalar(item, `${label}.${key}`);\n continue;\n }\n const bounds = jsonFor(item, { io: \"input\" });\n if (bounds.maxItems === undefined || bounds.maxItems !== bounds.minItems)\n throw new Error(`${label}.${key}: arrays require one fixed length from 1 to 256.`);\n fields[key] = {\n kind: \"array\",\n length: bounds.maxItems,\n element: scalar(\n schema(Reflect.get(item._zod.def, \"element\"), `${label}.${key}`),\n `${label}.${key}`,\n ),\n };\n }\n return wireRecord(fields, label, output);\n};\n\n/** Reflection is isolated here because tRPC v11 keeps parsers on procedure._def. */\nexport const serviceContracts = (router: AnyRouter): ServiceContract[] => {\n const contracts: ServiceContract[] = [];\n const procedures = z.record(z.string(), z.unknown()).parse(router._def.procedures);\n for (const [path, procedure] of Object.entries(procedures)) {\n if (typeof procedure !== \"function\" || !(\"_def\" in procedure))\n throw new Error(\"Unsupported tRPC router metadata.\");\n const reflected = z\n .custom<AnyProcedure>((value) => typeof value === \"function\" && \"_def\" in value)\n .parse(procedure);\n const def = reflected._def;\n if (\n typeof def.meta !== \"object\" ||\n def.meta === null ||\n !(\"plc\" in def.meta) ||\n def.meta.plc !== true\n )\n continue;\n if (def.type !== \"query\" && def.type !== \"mutation\")\n throw new Error(\n `${path}: subscriptions use a different transport; expose a query or mutation to the PLC.`,\n );\n if (def.inputs.length > 1)\n throw new Error(`${path}: merge the input into one bounded object schema.`);\n const input = record(\n def.inputs.length ? schema(def.inputs[0], `${path}.input`) : z.strictObject({}),\n `${path}.Input`,\n false,\n );\n const output = record(\n schema(Reflect.get(def, \"output\"), `${path}.output`),\n `${path}.Output`,\n true,\n );\n contracts.push(serviceContract(path, def.type, input, output));\n }\n return contracts.sort((a, b) => a.path.localeCompare(b.path));\n};\n\nexport const servicePath = (contract: ServiceContract) =>\n `/tsplc/services/${contract.version}/${contract.path}`;\n\nexport const validateWireRecord = (record: WireRecord, value: unknown): void => {\n const scalarValue = (field: WireScalar, value: unknown): boolean => {\n switch (field.kind) {\n case \"boolean\":\n return typeof value === \"boolean\";\n case \"integer\":\n return (\n typeof value === \"number\" &&\n Number.isInteger(value) &&\n value >= field.minimum &&\n value <= field.maximum\n );\n case \"string\":\n return (\n typeof value === \"string\" &&\n !value.includes(\"\\0\") &&\n new TextEncoder().encode(value).length <= field.capacity\n );\n case \"enum\":\n return typeof value === \"number\" && Object.values(field.members).includes(value);\n }\n };\n if (typeof value !== \"object\" || value === null || Array.isArray(value))\n throw new Error(\"Expected a closed wire record.\");\n const entries = Object.entries(value);\n if (entries.length !== Object.keys(record.fields).length)\n throw new Error(\"Wire record fields differ from the contract.\");\n for (const [name, field] of Object.entries(record.fields)) {\n const item: unknown = Reflect.get(value, name);\n const valid =\n field.kind === \"array\"\n ? Array.isArray(item) &&\n item.length === field.length &&\n item.every((value) => scalarValue(field.element, value))\n : scalarValue(field, item);\n if (!valid) throw new Error(`Wire field ${name} violates its PLC representation.`);\n }\n if (new TextEncoder().encode(JSON.stringify(value)).length > 2048)\n throw new Error(\"Wire record exceeds the 2048-byte native JSON limit.\");\n};\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "F8FF43EA2E94C95264756E2164756E21",
10
+ "names": []
11
+ }