@acid-cat/spec-ts 0.1.0 → 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.
Files changed (2) hide show
  1. package/dist/index.js +193 -153
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,174 +1,214 @@
1
- // src/index.ts
2
- var CALL_FAILURE_KINDS = ["cancelled", "unknown-capability", "unavailable", "invalid-input", "internal"];
3
- var ok = (value) => ({ ok: true, value });
4
- var fail = (tag, data) => ({ ok: false, error: { _tag: tag, ...data ?? {} } });
5
- var callFailure = (kind, message) => ({ kind, message });
6
- var isCallFailure = (outcome) => !("ok" in outcome);
1
+ //#region src/index.ts
2
+ /**
3
+ * The fixed details of the AcidCat specification, as TypeScript data and pure functions, for the TypeScript reference
4
+ * implementation. This package has no dependencies.
5
+ *
6
+ * - Outcomes and call failures: spec/invocation.md
7
+ * - Names, views and bundling: spec/catalog.md
8
+ * - Exit codes, status codes and MCP results: spec/bindings/
9
+ */
10
+ /** The kinds of call failure, in order of precedence: when more than one applies, the first one is used. */
11
+ const CALL_FAILURE_KINDS = [
12
+ "cancelled",
13
+ "unknown-capability",
14
+ "unavailable",
15
+ "invalid-input",
16
+ "internal"
17
+ ];
18
+ const ok = (value) => ({
19
+ ok: true,
20
+ value
21
+ });
22
+ /** A failure with its tag, as a result. */
23
+ const fail = (tag, data) => ({
24
+ ok: false,
25
+ error: {
26
+ _tag: tag,
27
+ ...data ?? {}
28
+ }
29
+ });
30
+ const callFailure = (kind, message) => ({
31
+ kind,
32
+ message
33
+ });
34
+ const isCallFailure = (outcome) => !("ok" in outcome);
35
+ /** The JavaScript error for a call failure (spec/bindings/typescript.md#handles). */
7
36
  var CallFailureError = class extends Error {
8
- kind;
9
- constructor(failure) {
10
- super(failure.message);
11
- this.kind = failure.kind;
12
- this.name = failure.kind === "cancelled" ? "AbortError" : failure.kind === "invalid-input" ? "TypeError" : "Error";
13
- }
37
+ kind;
38
+ constructor(failure) {
39
+ super(failure.message);
40
+ this.kind = failure.kind;
41
+ this.name = failure.kind === "cancelled" ? "AbortError" : failure.kind === "invalid-input" ? "TypeError" : "Error";
42
+ }
14
43
  };
15
- var AVAILABLE = { available: true };
16
- var unavailable = (reason) => ({ available: false, reason });
17
- var ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*(\.[a-z][a-z0-9]*(-[a-z0-9]+)*)+$/;
18
- var isId = (id) => id.length <= 128 && ID_PATTERN.test(id);
19
- var commandPath = (id) => id.split(".");
20
- var CLI_EXIT_CODES = {
21
- value: 0,
22
- failure: 1,
23
- "unknown-capability": 2,
24
- "invalid-input": 3,
25
- unavailable: 4,
26
- internal: 5,
27
- cancelled: 130
44
+ const AVAILABLE = { available: true };
45
+ const unavailable = (reason) => ({
46
+ available: false,
47
+ reason
48
+ });
49
+ const ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*(\.[a-z][a-z0-9]*(-[a-z0-9]+)*)+$/;
50
+ const isId = (id) => id.length <= 128 && ID_PATTERN.test(id);
51
+ /** The path of the CLI command for an id: `aircon.power.set` → `["aircon", "power", "set"]`. */
52
+ const commandPath = (id) => id.split(".");
53
+ const CLI_EXIT_CODES = {
54
+ value: 0,
55
+ failure: 1,
56
+ "unknown-capability": 2,
57
+ "invalid-input": 3,
58
+ unavailable: 4,
59
+ internal: 5,
60
+ cancelled: 130
28
61
  };
29
- var cliExitCode = (outcome) => isCallFailure(outcome) ? CLI_EXIT_CODES[outcome.kind] : outcome.ok ? CLI_EXIT_CODES.value : CLI_EXIT_CODES.failure;
30
- var HTTP_STATUS = {
31
- "unknown-capability": 404,
32
- "invalid-input": 422,
33
- unavailable: 503,
34
- cancelled: 409,
35
- internal: 500
62
+ const cliExitCode = (outcome) => isCallFailure(outcome) ? CLI_EXIT_CODES[outcome.kind] : outcome.ok ? CLI_EXIT_CODES.value : CLI_EXIT_CODES.failure;
63
+ const HTTP_STATUS = {
64
+ "unknown-capability": 404,
65
+ "invalid-input": 422,
66
+ unavailable: 503,
67
+ cancelled: 409,
68
+ internal: 500
36
69
  };
37
- var httpStatus = (outcome) => isCallFailure(outcome) ? HTTP_STATUS[outcome.kind] : 200;
70
+ const httpStatus = (outcome) => isCallFailure(outcome) ? HTTP_STATUS[outcome.kind] : 200;
71
+ /**
72
+ * The first rule that a request breaks, before its body is read: method, origin, media type, declared length.
73
+ * The body itself is checked as it arrives, with `readBounded`.
74
+ */
38
75
  function screenRequest(request, rules) {
39
- if (!rules.methods.includes(request.method)) return "method";
40
- const origin = request.headers.get("Origin");
41
- if (origin !== null && !rules.allowedOrigins.includes(origin)) return "origin";
42
- if (request.method === "GET" || request.method === "HEAD") return void 0;
43
- const mediaType = (request.headers.get("Content-Type") ?? "").split(";")[0].trim().toLowerCase();
44
- if (mediaType !== rules.mediaType) return "media-type";
45
- const declared = Number(request.headers.get("Content-Length") ?? "");
46
- if (Number.isFinite(declared) && declared > rules.maximumBytes) return "too-large";
47
- return void 0;
76
+ if (!rules.methods.includes(request.method)) return "method";
77
+ const origin = request.headers.get("Origin");
78
+ if (origin !== null && !rules.allowedOrigins.includes(origin)) return "origin";
79
+ if (request.method === "GET" || request.method === "HEAD") return void 0;
80
+ if ((request.headers.get("Content-Type") ?? "").split(";")[0].trim().toLowerCase() !== rules.mediaType) return "media-type";
81
+ const declared = Number(request.headers.get("Content-Length") ?? "");
82
+ if (Number.isFinite(declared) && declared > rules.maximumBytes) return "too-large";
48
83
  }
84
+ /** Read a request body as UTF-8 text. Count the bytes as they arrive, and stop at the limit. */
49
85
  async function readBounded(request, maximumBytes) {
50
- if (request.body === null) return { text: "" };
51
- const reader = request.body.getReader();
52
- const chunks = [];
53
- let length = 0;
54
- for (; ; ) {
55
- const { done, value } = await reader.read();
56
- if (done) break;
57
- length += value.byteLength;
58
- if (length > maximumBytes) {
59
- await reader.cancel().catch(() => void 0);
60
- return { rejection: "too-large" };
61
- }
62
- chunks.push(value);
63
- }
64
- const bytes = new Uint8Array(length);
65
- let offset = 0;
66
- for (const chunk of chunks) {
67
- bytes.set(chunk, offset);
68
- offset += chunk.byteLength;
69
- }
70
- try {
71
- return { text: new TextDecoder("utf-8", { fatal: true }).decode(bytes) };
72
- } catch {
73
- return { rejection: "not-utf8" };
74
- }
86
+ if (request.body === null) return { text: "" };
87
+ const reader = request.body.getReader();
88
+ const chunks = [];
89
+ let length = 0;
90
+ for (;;) {
91
+ const { done, value } = await reader.read();
92
+ if (done) break;
93
+ length += value.byteLength;
94
+ if (length > maximumBytes) {
95
+ await reader.cancel().catch(() => void 0);
96
+ return { rejection: "too-large" };
97
+ }
98
+ chunks.push(value);
99
+ }
100
+ const bytes = new Uint8Array(length);
101
+ let offset = 0;
102
+ for (const chunk of chunks) {
103
+ bytes.set(chunk, offset);
104
+ offset += chunk.byteLength;
105
+ }
106
+ try {
107
+ return { text: new TextDecoder("utf-8", { fatal: true }).decode(bytes) };
108
+ } catch {
109
+ return { rejection: "not-utf8" };
110
+ }
75
111
  }
76
- var MCP_PROTOCOL_VERSION = "2026-07-28";
77
- var MCP_UNKNOWN_TOOL = -32602;
78
- var MCP_REJECTIONS = {
79
- method: { status: 405 },
80
- origin: { status: 403 },
81
- "media-type": { status: 415 },
82
- "too-large": { status: 413, code: -32600 },
83
- "not-utf8": { status: 400, code: -32700 }
112
+ const MCP_PROTOCOL_VERSION = "2026-07-28";
113
+ const MCP_UNKNOWN_TOOL = -32602;
114
+ /** The HTTP status and JSON-RPC error code for each rejected MCP request. */
115
+ const MCP_REJECTIONS = {
116
+ method: { status: 405 },
117
+ origin: { status: 403 },
118
+ "media-type": { status: 415 },
119
+ "too-large": {
120
+ status: 413,
121
+ code: -32600
122
+ },
123
+ "not-utf8": {
124
+ status: 400,
125
+ code: -32700
126
+ }
84
127
  };
128
+ /** The MCP tool result for an outcome that is not `unknown-capability`. */
85
129
  function mcpToolResult(outcome, hasOutputSchema) {
86
- const text = (value) => [{ type: "text", text: JSON.stringify(value) }];
87
- if (isCallFailure(outcome)) return { content: text({ kind: outcome.kind, message: outcome.message }), isError: true };
88
- if (!outcome.ok) return { content: text({ ok: false, error: outcome.error }), isError: true };
89
- return hasOutputSchema ? { content: text(outcome.value), structuredContent: outcome.value } : { content: text(outcome.value) };
130
+ const text = (value) => [{
131
+ type: "text",
132
+ text: JSON.stringify(value)
133
+ }];
134
+ if (isCallFailure(outcome)) return {
135
+ content: text({
136
+ kind: outcome.kind,
137
+ message: outcome.message
138
+ }),
139
+ isError: true
140
+ };
141
+ if (!outcome.ok) return {
142
+ content: text({
143
+ ok: false,
144
+ error: outcome.error
145
+ }),
146
+ isError: true
147
+ };
148
+ return hasOutputSchema ? {
149
+ content: text(outcome.value),
150
+ structuredContent: outcome.value
151
+ } : { content: text(outcome.value) };
90
152
  }
91
- var REFERENCE = /^#\/\$defs\/([A-Za-z][A-Za-z0-9_]*)$/;
153
+ const REFERENCE = /^#\/\$defs\/([A-Za-z][A-Za-z0-9_]*)$/;
154
+ /** The schemas of a capability, each with the member that holds it. */
92
155
  function schemasOf(capability) {
93
- const members = [
94
- ["input", capability.input],
95
- ["output", capability.output],
96
- ["state", capability.state],
97
- ...Object.entries(capability.failures ?? {}).map(([tag, schema]) => [`failure ${tag}`, schema])
98
- ];
99
- return members.filter((entry) => entry[1] !== void 0);
156
+ return [
157
+ ["input", capability.input],
158
+ ["output", capability.output],
159
+ ["state", capability.state],
160
+ ...Object.entries(capability.failures ?? {}).map(([tag, schema]) => [`failure ${tag}`, schema])
161
+ ].filter((entry) => entry[1] !== void 0);
100
162
  }
163
+ /** Every object in a JSON value, at all levels. */
101
164
  function visitObjects(value, onObject) {
102
- if (Array.isArray(value)) {
103
- value.forEach((item) => visitObjects(item, onObject));
104
- } else if (value !== null && typeof value === "object") {
105
- onObject(value);
106
- Object.values(value).forEach((item) => visitObjects(item, onObject));
107
- }
165
+ if (Array.isArray(value)) value.forEach((item) => visitObjects(item, onObject));
166
+ else if (value !== null && typeof value === "object") {
167
+ onObject(value);
168
+ Object.values(value).forEach((item) => visitObjects(item, onObject));
169
+ }
108
170
  }
171
+ /** The types that a schema uses, directly or through other types. */
109
172
  function typesUsedBy(schema, types) {
110
- const used = /* @__PURE__ */ new Map();
111
- const collect = (node) => visitObjects(node, (inner) => {
112
- const name = typeof inner.$ref === "string" ? REFERENCE.exec(inner.$ref)?.[1] : void 0;
113
- const type = name === void 0 ? void 0 : types[name];
114
- if (name !== void 0 && type !== void 0 && !used.has(name)) {
115
- used.set(name, type);
116
- collect(type);
117
- }
118
- });
119
- collect(schema);
120
- return used;
173
+ const used = /* @__PURE__ */ new Map();
174
+ const collect = (node) => visitObjects(node, (inner) => {
175
+ const name = typeof inner.$ref === "string" ? REFERENCE.exec(inner.$ref)?.[1] : void 0;
176
+ const type = name === void 0 ? void 0 : types[name];
177
+ if (name !== void 0 && type !== void 0 && !used.has(name)) {
178
+ used.set(name, type);
179
+ collect(type);
180
+ }
181
+ });
182
+ collect(schema);
183
+ return used;
121
184
  }
185
+ /** Bundle a schema (spec/catalog.md#bundling). */
122
186
  function bundle(schema, types) {
123
- const only = Object.keys(schema).length === 1 ? REFERENCE.exec(String(schema.$ref))?.[1] : void 0;
124
- const base = only === void 0 ? schema : types[only] ?? schema;
125
- const used = typesUsedBy(base, types);
126
- return used.size === 0 ? base : { ...base, $defs: Object.fromEntries(used) };
187
+ const only = Object.keys(schema).length === 1 ? REFERENCE.exec(String(schema.$ref))?.[1] : void 0;
188
+ const base = only === void 0 ? schema : types[only] ?? schema;
189
+ const used = typesUsedBy(base, types);
190
+ return used.size === 0 ? base : {
191
+ ...base,
192
+ $defs: Object.fromEntries(used)
193
+ };
127
194
  }
195
+ /** The view of a target (spec/catalog.md#views). */
128
196
  function viewOf(catalog, target) {
129
- const types = catalog.$defs ?? {};
130
- const capabilities = Object.fromEntries(
131
- Object.entries(catalog.capabilities).filter(([, capability]) => (capability.audience ?? []).includes(target)).map(([id, capability]) => [id, { ...capability, audience: [target] }])
132
- );
133
- const used = /* @__PURE__ */ new Map();
134
- for (const capability of Object.values(capabilities)) {
135
- for (const [, schema] of schemasOf(capability)) for (const [name, type] of typesUsedBy(schema, types)) used.set(name, type);
136
- }
137
- const { $defs: _, ...rest } = catalog;
138
- const declared = catalog.targets[target];
139
- return {
140
- ...rest,
141
- targets: declared === void 0 ? {} : { [target]: declared },
142
- ...used.size > 0 ? { $defs: Object.fromEntries(used) } : {},
143
- capabilities
144
- };
197
+ const types = catalog.$defs ?? {};
198
+ const capabilities = Object.fromEntries(Object.entries(catalog.capabilities).filter(([, capability]) => (capability.audience ?? []).includes(target)).map(([id, capability]) => [id, {
199
+ ...capability,
200
+ audience: [target]
201
+ }]));
202
+ const used = /* @__PURE__ */ new Map();
203
+ for (const capability of Object.values(capabilities)) for (const [, schema] of schemasOf(capability)) for (const [name, type] of typesUsedBy(schema, types)) used.set(name, type);
204
+ const { $defs: _, ...rest } = catalog;
205
+ const declared = catalog.targets[target];
206
+ return {
207
+ ...rest,
208
+ targets: declared === void 0 ? {} : { [target]: declared },
209
+ ...used.size > 0 ? { $defs: Object.fromEntries(used) } : {},
210
+ capabilities
211
+ };
145
212
  }
146
- export {
147
- AVAILABLE,
148
- CALL_FAILURE_KINDS,
149
- CLI_EXIT_CODES,
150
- CallFailureError,
151
- HTTP_STATUS,
152
- ID_PATTERN,
153
- MCP_PROTOCOL_VERSION,
154
- MCP_REJECTIONS,
155
- MCP_UNKNOWN_TOOL,
156
- REFERENCE,
157
- bundle,
158
- callFailure,
159
- cliExitCode,
160
- commandPath,
161
- fail,
162
- httpStatus,
163
- isCallFailure,
164
- isId,
165
- mcpToolResult,
166
- ok,
167
- readBounded,
168
- schemasOf,
169
- screenRequest,
170
- typesUsedBy,
171
- unavailable,
172
- viewOf,
173
- visitObjects
174
- };
213
+ //#endregion
214
+ export { AVAILABLE, CALL_FAILURE_KINDS, CLI_EXIT_CODES, CallFailureError, HTTP_STATUS, ID_PATTERN, MCP_PROTOCOL_VERSION, MCP_REJECTIONS, MCP_UNKNOWN_TOOL, REFERENCE, bundle, callFailure, cliExitCode, commandPath, fail, httpStatus, isCallFailure, isId, mcpToolResult, ok, readBounded, schemasOf, screenRequest, typesUsedBy, unavailable, viewOf, visitObjects };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acid-cat/spec-ts",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The fixed details of the AcidCat specification as TypeScript data and pure functions, with no dependencies. Part of the TypeScript reference implementation.",
5
5
  "keywords": [
6
6
  "acidcat",