@frockbot/connection-core 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/models.ts ADDED
@@ -0,0 +1,497 @@
1
+ import type {
2
+ ConnectionAuthorizationKind,
3
+ CredentialDescriptorV1,
4
+ } from "./credentials.js";
5
+
6
+ export interface ModelCapabilityV1 {
7
+ tools: boolean;
8
+ vision: boolean;
9
+ reasoning: boolean;
10
+ }
11
+
12
+ export const MAX_CONNECTION_MODELS_V1 = 100;
13
+
14
+ export interface ConnectionModelV1 {
15
+ providerModelId: string;
16
+ displayName: string;
17
+ contextWindow?: number;
18
+ capabilities: ModelCapabilityV1;
19
+ source: "discovered" | "exact-resolution";
20
+ }
21
+
22
+ export interface ConnectionModelCatalogV1 {
23
+ schemaVersion: 1;
24
+ generation: string;
25
+ state: "fresh" | "stale" | "refreshing" | "failed";
26
+ models: ConnectionModelV1[];
27
+ refreshedAt?: string;
28
+ refreshAfter?: string;
29
+ failure?: string;
30
+ }
31
+
32
+ export interface ConnectionAuthorizationViewV1 {
33
+ schemaVersion: 1;
34
+ kind: ConnectionAuthorizationKind;
35
+ credential: CredentialDescriptorV1;
36
+ }
37
+
38
+ /**
39
+ * The Connection-scoped settings a Connection carries beside its credential:
40
+ * the values a Connection Type's manifest declares (manifest v4), such as an
41
+ * MCP server's URL and transport. They are configuration, never secrets — a
42
+ * secret reaches the keyring through `apiKey` and never through here.
43
+ */
44
+ export type ConnectionSettingsV1 = Record<
45
+ string,
46
+ string | number | boolean | null
47
+ >;
48
+
49
+ export const MAX_CONNECTION_SETTINGS_V1 = 32;
50
+
51
+ export interface CreateApiKeyConnectionCommandV1 {
52
+ schemaVersion: 1;
53
+ type: "connection/create-api-key";
54
+ commandId: string;
55
+ packageId: string;
56
+ connectionTypeId: string;
57
+ label: string;
58
+ apiKey: string;
59
+ settings?: ConnectionSettingsV1;
60
+ }
61
+
62
+ /**
63
+ * Creating a Connection of a Connection Type whose authorization kind is
64
+ * `none`: it has no credential at all, so there is nothing for
65
+ * `connection/create-api-key` to carry. Its settings are the whole of its
66
+ * configuration.
67
+ */
68
+ export interface CreateConnectionCommandV1 {
69
+ schemaVersion: 1;
70
+ type: "connection/create";
71
+ commandId: string;
72
+ packageId: string;
73
+ connectionTypeId: string;
74
+ label: string;
75
+ settings?: ConnectionSettingsV1;
76
+ }
77
+
78
+ export interface RotateApiKeyConnectionCommandV1 {
79
+ schemaVersion: 1;
80
+ type: "connection/rotate-api-key";
81
+ commandId: string;
82
+ connectionId: string;
83
+ apiKey: string;
84
+ }
85
+
86
+ export interface UpdateConnectionLabelCommandV1 {
87
+ schemaVersion: 1;
88
+ type: "connection/update-label";
89
+ commandId: string;
90
+ connectionId: string;
91
+ label: string;
92
+ }
93
+
94
+ export interface RefreshConnectionCatalogCommandV1 {
95
+ schemaVersion: 1;
96
+ type: "connection/refresh-models";
97
+ commandId: string;
98
+ connectionId: string;
99
+ }
100
+
101
+ export interface SetConnectionEnabledCommandV1 {
102
+ schemaVersion: 1;
103
+ type: "connection/set-enabled";
104
+ commandId: string;
105
+ connectionId: string;
106
+ enabled: boolean;
107
+ }
108
+
109
+ export interface DisconnectConnectionCommandV1 {
110
+ schemaVersion: 1;
111
+ type: "connection/disconnect";
112
+ commandId: string;
113
+ connectionId: string;
114
+ revokeUpstream: boolean;
115
+ }
116
+
117
+ export type ConnectionCommandV1 =
118
+ | CreateConnectionCommandV1
119
+ | CreateApiKeyConnectionCommandV1
120
+ | RotateApiKeyConnectionCommandV1
121
+ | UpdateConnectionLabelCommandV1
122
+ | RefreshConnectionCatalogCommandV1
123
+ | SetConnectionEnabledCommandV1
124
+ | DisconnectConnectionCommandV1;
125
+
126
+ export interface ConnectionCommandReceiptV1 {
127
+ schemaVersion: 1;
128
+ commandId: string;
129
+ connectionId: string;
130
+ status: "applied" | "failed" | "reconciliation-required";
131
+ }
132
+
133
+ function record(value: unknown, label: string): Record<string, unknown> {
134
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
135
+ throw new Error(`${label} must be an object`);
136
+ }
137
+ return value as Record<string, unknown>;
138
+ }
139
+
140
+ function exact(
141
+ value: Record<string, unknown>,
142
+ required: readonly string[],
143
+ ): void {
144
+ const expected = new Set(required);
145
+ if (
146
+ !required.every((key) => Object.hasOwn(value, key)) ||
147
+ Object.keys(value).some((key) => !expected.has(key))
148
+ ) {
149
+ throw new Error("Connection command has invalid fields");
150
+ }
151
+ }
152
+
153
+ const CONNECTION_COMMAND_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
154
+
155
+ export function decodeConnectionCommandIdV1(value: unknown): string {
156
+ if (typeof value !== "string" || !CONNECTION_COMMAND_ID_PATTERN.test(value)) {
157
+ throw new Error("commandId is invalid");
158
+ }
159
+ return value;
160
+ }
161
+
162
+ function text(value: unknown, label: string, maximum: number): string {
163
+ if (
164
+ typeof value !== "string" ||
165
+ value.trim().length === 0 ||
166
+ value.length > maximum
167
+ ) {
168
+ throw new Error(`${label} must be a non-empty string`);
169
+ }
170
+ return value;
171
+ }
172
+
173
+ function connectionSettings(value: unknown): ConnectionSettingsV1 {
174
+ const settings = record(value, "settings");
175
+ const entries = Object.entries(settings);
176
+ if (entries.length > MAX_CONNECTION_SETTINGS_V1) {
177
+ throw new Error("Connection settings are too many");
178
+ }
179
+ return Object.fromEntries(
180
+ entries.map(([key, item]) => {
181
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(key)) {
182
+ throw new Error(`Connection setting "${key}" is invalid`);
183
+ }
184
+ if (
185
+ item === null ||
186
+ typeof item === "boolean" ||
187
+ (typeof item === "number" && Number.isFinite(item)) ||
188
+ (typeof item === "string" && item.length <= 2_048)
189
+ ) {
190
+ return [key, item as string | number | boolean | null];
191
+ }
192
+ throw new Error(`Connection setting "${key}" is invalid`);
193
+ }),
194
+ );
195
+ }
196
+
197
+ function common(value: Record<string, unknown>): {
198
+ schemaVersion: 1;
199
+ commandId: string;
200
+ } {
201
+ if (value.schemaVersion !== 1) {
202
+ throw new Error("Connection command schemaVersion must be 1");
203
+ }
204
+ return {
205
+ schemaVersion: 1,
206
+ commandId: decodeConnectionCommandIdV1(value.commandId),
207
+ };
208
+ }
209
+
210
+ function optionalTimestamp(value: unknown, label: string): string | undefined {
211
+ if (value === undefined) return undefined;
212
+ const decoded = text(value, label, 64);
213
+ if (!Number.isFinite(Date.parse(decoded))) {
214
+ throw new Error(`${label} must be a timestamp`);
215
+ }
216
+ return decoded;
217
+ }
218
+
219
+ export function decodeConnectionAuthorizationViewV1(
220
+ input: unknown,
221
+ ): ConnectionAuthorizationViewV1 {
222
+ const value = record(input, "Connection authorization");
223
+ exact(value, ["schemaVersion", "kind", "credential"]);
224
+ const kinds: ConnectionAuthorizationKind[] = [
225
+ "none",
226
+ "api-key",
227
+ "ambient-native",
228
+ "grant",
229
+ ];
230
+ if (value.schemaVersion !== 1 || !kinds.includes(value.kind as never)) {
231
+ throw new Error("Connection authorization is invalid");
232
+ }
233
+ const credential = record(value.credential, "credential");
234
+ const allowed = new Set([
235
+ "schemaVersion",
236
+ "configured",
237
+ "source",
238
+ "writable",
239
+ "generation",
240
+ "updatedAt",
241
+ ]);
242
+ if (
243
+ credential.schemaVersion !== 1 ||
244
+ typeof credential.configured !== "boolean" ||
245
+ typeof credential.writable !== "boolean" ||
246
+ !kinds.includes(credential.source as never) ||
247
+ Object.keys(credential).some((key) => !allowed.has(key))
248
+ ) {
249
+ throw new Error("credential descriptor is invalid");
250
+ }
251
+ return {
252
+ schemaVersion: 1,
253
+ kind: value.kind as ConnectionAuthorizationKind,
254
+ credential: {
255
+ schemaVersion: 1,
256
+ configured: credential.configured,
257
+ source: credential.source as ConnectionAuthorizationKind,
258
+ writable: credential.writable,
259
+ ...(credential.generation === undefined
260
+ ? {}
261
+ : { generation: text(credential.generation, "generation", 128) }),
262
+ ...(credential.updatedAt === undefined
263
+ ? {}
264
+ : {
265
+ updatedAt: optionalTimestamp(
266
+ credential.updatedAt,
267
+ "credential.updatedAt",
268
+ ),
269
+ }),
270
+ },
271
+ };
272
+ }
273
+
274
+ export function decodeConnectionModelCatalogV1(
275
+ input: unknown,
276
+ ): ConnectionModelCatalogV1 {
277
+ const value = record(input, "Connection model catalog");
278
+ const allowed = new Set([
279
+ "schemaVersion",
280
+ "generation",
281
+ "state",
282
+ "models",
283
+ "refreshedAt",
284
+ "refreshAfter",
285
+ "failure",
286
+ ]);
287
+ const states: ConnectionModelCatalogV1["state"][] = [
288
+ "fresh",
289
+ "stale",
290
+ "refreshing",
291
+ "failed",
292
+ ];
293
+ if (
294
+ value.schemaVersion !== 1 ||
295
+ !states.includes(value.state as never) ||
296
+ !Array.isArray(value.models) ||
297
+ value.models.length > MAX_CONNECTION_MODELS_V1 ||
298
+ Object.keys(value).some((key) => !allowed.has(key))
299
+ ) {
300
+ throw new Error("Connection model catalog is invalid");
301
+ }
302
+ const models = value.models.map((candidate) => {
303
+ const model = record(candidate, "Connection model");
304
+ exact(model, [
305
+ "providerModelId",
306
+ "displayName",
307
+ "capabilities",
308
+ "source",
309
+ ...(Object.hasOwn(model, "contextWindow") ? ["contextWindow"] : []),
310
+ ]);
311
+ const capabilities = record(model.capabilities, "model capabilities");
312
+ exact(capabilities, ["tools", "vision", "reasoning"]);
313
+ if (
314
+ typeof capabilities.tools !== "boolean" ||
315
+ typeof capabilities.vision !== "boolean" ||
316
+ typeof capabilities.reasoning !== "boolean" ||
317
+ (model.source !== "discovered" && model.source !== "exact-resolution") ||
318
+ (model.contextWindow !== undefined &&
319
+ (typeof model.contextWindow !== "number" ||
320
+ !Number.isSafeInteger(model.contextWindow) ||
321
+ model.contextWindow <= 0))
322
+ ) {
323
+ throw new Error("Connection model is invalid");
324
+ }
325
+ return {
326
+ providerModelId: text(model.providerModelId, "providerModelId", 256),
327
+ displayName: text(model.displayName, "displayName", 256),
328
+ ...(model.contextWindow === undefined
329
+ ? {}
330
+ : { contextWindow: model.contextWindow as number }),
331
+ capabilities: {
332
+ tools: capabilities.tools,
333
+ vision: capabilities.vision,
334
+ reasoning: capabilities.reasoning,
335
+ },
336
+ source: model.source,
337
+ } satisfies ConnectionModelV1;
338
+ });
339
+ return {
340
+ schemaVersion: 1,
341
+ generation: text(value.generation, "generation", 128),
342
+ state: value.state as ConnectionModelCatalogV1["state"],
343
+ models,
344
+ ...(value.refreshedAt === undefined
345
+ ? {}
346
+ : { refreshedAt: optionalTimestamp(value.refreshedAt, "refreshedAt") }),
347
+ ...(value.refreshAfter === undefined
348
+ ? {}
349
+ : {
350
+ refreshAfter: optionalTimestamp(value.refreshAfter, "refreshAfter"),
351
+ }),
352
+ ...(value.failure === undefined
353
+ ? {}
354
+ : { failure: text(value.failure, "failure", 2_000) }),
355
+ };
356
+ }
357
+
358
+ export function decodeConnectionCommandReceiptV1(
359
+ input: unknown,
360
+ ): ConnectionCommandReceiptV1 {
361
+ const value = record(input, "Connection command receipt");
362
+ exact(value, ["schemaVersion", "commandId", "connectionId", "status"]);
363
+ const statuses: ConnectionCommandReceiptV1["status"][] = [
364
+ "applied",
365
+ "failed",
366
+ "reconciliation-required",
367
+ ];
368
+ if (value.schemaVersion !== 1 || !statuses.includes(value.status as never)) {
369
+ throw new Error("Connection command receipt is invalid");
370
+ }
371
+ return {
372
+ schemaVersion: 1,
373
+ commandId: decodeConnectionCommandIdV1(value.commandId),
374
+ connectionId: text(value.connectionId, "connectionId", 128),
375
+ status: value.status as ConnectionCommandReceiptV1["status"],
376
+ };
377
+ }
378
+
379
+ export function decodeConnectionCommandV1(input: unknown): ConnectionCommandV1 {
380
+ const value = record(input, "Connection command");
381
+ const base = common(value);
382
+ switch (value.type) {
383
+ case "connection/create":
384
+ exact(value, [
385
+ "schemaVersion",
386
+ "type",
387
+ "commandId",
388
+ "packageId",
389
+ "connectionTypeId",
390
+ "label",
391
+ ...(Object.hasOwn(value, "settings") ? ["settings"] : []),
392
+ ]);
393
+ return {
394
+ ...base,
395
+ type: value.type,
396
+ packageId: text(value.packageId, "packageId", 128),
397
+ connectionTypeId: text(value.connectionTypeId, "connectionTypeId", 128),
398
+ label: text(value.label, "label", 120),
399
+ ...(value.settings === undefined
400
+ ? {}
401
+ : { settings: connectionSettings(value.settings) }),
402
+ };
403
+ case "connection/create-api-key":
404
+ exact(value, [
405
+ "schemaVersion",
406
+ "type",
407
+ "commandId",
408
+ "packageId",
409
+ "connectionTypeId",
410
+ "label",
411
+ "apiKey",
412
+ ...(Object.hasOwn(value, "settings") ? ["settings"] : []),
413
+ ]);
414
+ return {
415
+ ...base,
416
+ type: value.type,
417
+ packageId: text(value.packageId, "packageId", 128),
418
+ connectionTypeId: text(value.connectionTypeId, "connectionTypeId", 128),
419
+ label: text(value.label, "label", 120),
420
+ apiKey: text(value.apiKey, "apiKey", 16_384),
421
+ ...(value.settings === undefined
422
+ ? {}
423
+ : { settings: connectionSettings(value.settings) }),
424
+ };
425
+ case "connection/rotate-api-key":
426
+ exact(value, [
427
+ "schemaVersion",
428
+ "type",
429
+ "commandId",
430
+ "connectionId",
431
+ "apiKey",
432
+ ]);
433
+ return {
434
+ ...base,
435
+ type: value.type,
436
+ connectionId: text(value.connectionId, "connectionId", 128),
437
+ apiKey: text(value.apiKey, "apiKey", 16_384),
438
+ };
439
+ case "connection/update-label":
440
+ exact(value, [
441
+ "schemaVersion",
442
+ "type",
443
+ "commandId",
444
+ "connectionId",
445
+ "label",
446
+ ]);
447
+ return {
448
+ ...base,
449
+ type: value.type,
450
+ connectionId: text(value.connectionId, "connectionId", 128),
451
+ label: text(value.label, "label", 120),
452
+ };
453
+ case "connection/refresh-models":
454
+ exact(value, ["schemaVersion", "type", "commandId", "connectionId"]);
455
+ return {
456
+ ...base,
457
+ type: value.type,
458
+ connectionId: text(value.connectionId, "connectionId", 128),
459
+ };
460
+ case "connection/set-enabled":
461
+ exact(value, [
462
+ "schemaVersion",
463
+ "type",
464
+ "commandId",
465
+ "connectionId",
466
+ "enabled",
467
+ ]);
468
+ if (typeof value.enabled !== "boolean") {
469
+ throw new Error("enabled must be a boolean");
470
+ }
471
+ return {
472
+ ...base,
473
+ type: value.type,
474
+ connectionId: text(value.connectionId, "connectionId", 128),
475
+ enabled: value.enabled,
476
+ };
477
+ case "connection/disconnect":
478
+ exact(value, [
479
+ "schemaVersion",
480
+ "type",
481
+ "commandId",
482
+ "connectionId",
483
+ "revokeUpstream",
484
+ ]);
485
+ if (typeof value.revokeUpstream !== "boolean") {
486
+ throw new Error("revokeUpstream must be a boolean");
487
+ }
488
+ return {
489
+ ...base,
490
+ type: value.type,
491
+ connectionId: text(value.connectionId, "connectionId", 128),
492
+ revokeUpstream: value.revokeUpstream,
493
+ };
494
+ default:
495
+ throw new Error("Connection command type is unsupported");
496
+ }
497
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "skipLibCheck": true,
9
+ "lib": ["ES2023"],
10
+ "types": ["bun"]
11
+ },
12
+ "include": ["src/**/*.ts"]
13
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # @frockbot/connection-core
2
-
3
- Placeholder reserving this name. See https://github.com/timoconnellaus/frockbot.