@ekanos/integration-schema 0.1.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.
@@ -0,0 +1,661 @@
1
+ /**
2
+ * The canonical integration-definition contract: ONE zod schema and ONE set
3
+ * of inferred/hand-written types (F9). `@ekanos/sdk`'s `defineIntegration()`
4
+ * validates against this schema at authoring time; `@kit/integrations-core`'s
5
+ * `registerPartnerIntegration()` re-validates against the SAME schema at the
6
+ * host trust boundary (F3). Both packages depend on this one; it depends on
7
+ * neither, so there is no cycle and no hand-written structural twin.
8
+ *
9
+ * Dependency-pure (zod only): partner component refs are checked
10
+ * structurally via `ComponentReference`, so no React dependency leaks in.
11
+ */
12
+ import { z } from 'zod';
13
+ import { isComponentReference } from './component-reference.js';
14
+ import { WorkspaceTargetListSchema, } from './workspace-target.js';
15
+ const jsonValueSchema = z.lazy(() => z.union([
16
+ z.string(),
17
+ // `.finite()` rejects Infinity/-Infinity AND NaN (Number.isFinite) — all
18
+ // of which JSON.stringify silently turns to `null`, so they are not
19
+ // JSON-compatible values (F4).
20
+ z.number().finite(),
21
+ z.boolean(),
22
+ z.null(),
23
+ z.array(jsonValueSchema),
24
+ z.record(jsonValueSchema),
25
+ ]));
26
+ // ---- Leaf validators -------------------------------------------------------
27
+ function componentRefSchema(what) {
28
+ return z.custom(isComponentReference, {
29
+ message: `${what} must be a React component reference (a function component, or a memo/forwardRef/lazy wrapper) — pass the component itself, not an element or a module path.`,
30
+ });
31
+ }
32
+ const zodSchemaRef = z.custom((value) => typeof (value === null || value === void 0 ? void 0 : value.safeParse) === 'function', {
33
+ message: 'Every storage key must declare a zod schema (e.g. z.object({ … })) — ctx.storage validates reads and writes against it (capability-context ruling 1).',
34
+ });
35
+ const nonEmpty = (what) => z.string().min(1, { message: `${what} must be a non-empty string.` });
36
+ const slugSchema = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {
37
+ message: 'slug must be kebab-case ([a-z0-9] segments separated by single hyphens), e.g. "acme-crm" — it becomes the product slug, route segment, and MCP namespace.',
38
+ });
39
+ const widgetIdSchema = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {
40
+ message: 'Widget ids are kebab-case and globally unique, e.g. "acme-crm-pipeline" — prefix with the integration slug to stay collision-free.',
41
+ });
42
+ const storageKeySchema = z.string().regex(/^[a-z0-9_-]+(?:\/[a-z0-9_-]+)?$/, {
43
+ message: 'Storage keys are "<dataType>" or "<dataType>/<subtype>" in lowercase [a-z0-9_-] — they map onto the account/user product-data columns (capability-context ruling 6).',
44
+ });
45
+ const toolNameSchema = z.string().regex(/^[a-z][a-z0-9_]*$/, {
46
+ message: 'Tool names are lowercase snake_case starting with a letter, e.g. "list_invoices" — the model calls them by this exact string.',
47
+ });
48
+ const webhookIdSchema = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {
49
+ message: 'Webhook ids are kebab-case, e.g. "payment-updated" — the host ingress route addresses the handler by this exact string.',
50
+ });
51
+ const scheduleIdSchema = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {
52
+ message: 'Schedule ids are kebab-case, e.g. "daily-reconcile" — the host scheduler addresses the handler by this exact string.',
53
+ });
54
+ // Origin-only egress entries (ruling 3): absolute https origins, optional
55
+ // `*.` subdomain wildcard, no path/query/hash/credentials/http. Kept as a
56
+ // pure regex here so this package depends on nothing; the SDK's shared
57
+ // `parseEgressEntry`/`isEgressAllowed` matcher enforces the identical shape
58
+ // at runtime.
59
+ const egressEntrySchema = z.string().superRefine((entry, ctx) => {
60
+ var _a;
61
+ const match = /^https:\/\/(\*\.)?([a-z0-9.-]+)(?::(\d+))?$/i.exec(entry);
62
+ if (!match) {
63
+ ctx.addIssue({
64
+ code: z.ZodIssueCode.custom,
65
+ message: `Egress entry "${entry}" is invalid. Entries are https origins only — ` +
66
+ `"https://api.example.com" (exact) or "https://*.example.com" ` +
67
+ `(subdomain wildcard): scheme + host + optional port, no path, query, ` +
68
+ `hash, credentials, or http. Fix it in the integration's egress list.`,
69
+ });
70
+ return;
71
+ }
72
+ const host = (_a = match[2]) !== null && _a !== void 0 ? _a : '';
73
+ if (host.includes('*') || host.startsWith('.') || host.endsWith('.')) {
74
+ ctx.addIssue({
75
+ code: z.ZodIssueCode.custom,
76
+ message: `Egress entry "${entry}" has a malformed host — wildcards are only supported as a single leading "*." (e.g. "https://*.example.com").`,
77
+ });
78
+ }
79
+ });
80
+ // ---- Sub-object schemas ----------------------------------------------------
81
+ const capabilitySchema = z
82
+ .object({
83
+ label: nonEmpty('capabilities[].label'),
84
+ description: nonEmpty('capabilities[].description'),
85
+ icon: componentRefSchema('capabilities[].icon').optional(),
86
+ })
87
+ .strict();
88
+ const permissionSchema = z
89
+ .object({
90
+ label: nonEmpty('permissions[].label'),
91
+ detail: nonEmpty('permissions[].detail'),
92
+ type: z.enum(['read', 'write']),
93
+ })
94
+ .strict();
95
+ const gridUnitSchema = z
96
+ .object({
97
+ cols: z.number().int().positive(),
98
+ rows: z.number().int().positive(),
99
+ })
100
+ .strict();
101
+ const layoutBoxSchema = z
102
+ .object({
103
+ x: z.number(),
104
+ y: z.number(),
105
+ w: z.number(),
106
+ h: z.number(),
107
+ maxHeight: z.number().optional(),
108
+ })
109
+ .strict();
110
+ /**
111
+ * F7: authorable widget fields ONLY. Host-resolved fields (`productId`,
112
+ * `widgetConfigId`, `workspaceId`, `collapsed`, `isPinned`, `health`,
113
+ * `integrationMetadata`) are absent by design and rejected at runtime by
114
+ * `.strict()` — the type and the runtime agree. The host adapter maps this
115
+ * into the full `WidgetConfig`.
116
+ */
117
+ const widgetSchema = z
118
+ .object({
119
+ id: widgetIdSchema,
120
+ name: nonEmpty('widgets[].name'),
121
+ component: componentRefSchema('widgets[].component'),
122
+ widgetState: z.enum(['active', 'inactive', 'disabled']),
123
+ gridSize: z.union([gridUnitSchema, z.array(gridUnitSchema)]).optional(),
124
+ gridPosition: z
125
+ .object({ col: z.number(), row: z.number() })
126
+ .strict()
127
+ .optional(),
128
+ layouts: z
129
+ .object({
130
+ lg: layoutBoxSchema.optional(),
131
+ md: layoutBoxSchema.optional(),
132
+ sm: layoutBoxSchema.optional(),
133
+ })
134
+ .strict()
135
+ .optional(),
136
+ category: z
137
+ .object({
138
+ id: z.string(),
139
+ name: z.string(),
140
+ slug: z.string(),
141
+ icon: z.string().nullable(),
142
+ })
143
+ .strict()
144
+ .optional(),
145
+ isCollapsible: z.boolean().optional(),
146
+ isPinnable: z.boolean().optional(),
147
+ aiFooterEnabled: z.boolean().optional(),
148
+ })
149
+ .strict();
150
+ const toolParametersSchema = z
151
+ .object({
152
+ type: z.literal('object'),
153
+ properties: z.record(jsonValueSchema).optional(),
154
+ required: z.array(z.string()).optional(),
155
+ additionalProperties: z.boolean().optional(),
156
+ })
157
+ .strict();
158
+ const toolRunSchema = z.custom((value) => typeof value === 'function', {
159
+ message: 'tools[].run must be a function (ctx, args) => Promise<result> — it receives the host-scoped IntegrationContext, never a raw client.',
160
+ });
161
+ const toolSchema = z
162
+ .object({
163
+ name: toolNameSchema,
164
+ description: nonEmpty('tools[].description'),
165
+ parameters: toolParametersSchema.optional(),
166
+ run: toolRunSchema,
167
+ outputExample: z.record(jsonValueSchema).optional(),
168
+ })
169
+ .strict();
170
+ // ---- Event surfaces (webhooks, schedules, OAuth) ---------------------------
171
+ //
172
+ // Declared exactly like MCP tools: metadata parsed strictly, handlers checked
173
+ // structurally as functions (`z.custom`) and carried through the parse
174
+ // untouched. The declarations are the CONTRACT; every transport — the local
175
+ // harness today, the host's public ingress/scheduler/hosted-callback later —
176
+ // binds to these same fields, so a partner package never changes when the
177
+ // real transports arrive.
178
+ function handlerSchema(what, shape) {
179
+ return z.custom((value) => typeof value === 'function', {
180
+ message: `${what} must be a function ${shape} — it receives the host-scoped IntegrationContext, never a raw request or client.`,
181
+ });
182
+ }
183
+ const payloadSchemaRef = z.custom((value) => typeof (value === null || value === void 0 ? void 0 : value.safeParse) === 'function', {
184
+ message: 'webhooks[].payloadSchema must be a zod schema (e.g. z.object({ … })) — the transport validates every delivery against it before the handler runs.',
185
+ });
186
+ /**
187
+ * How the TRANSPORT verifies a delivery. Verification is never the partner's
188
+ * job: the declaration names the signature header and the secret that signs
189
+ * it; the host ingress enforces it (the local harness logs it as skipped).
190
+ * `'none'` is an explicit statement that the source is unsigned.
191
+ */
192
+ const webhookSignatureSchema = z.union([
193
+ z.literal('none'),
194
+ z
195
+ .object({
196
+ header: nonEmpty('webhooks[].signature.header'),
197
+ secretName: nonEmpty('webhooks[].signature.secretName'),
198
+ })
199
+ .strict(),
200
+ ]);
201
+ const webhookSchema = z
202
+ .object({
203
+ id: webhookIdSchema,
204
+ description: nonEmpty('webhooks[].description'),
205
+ payloadSchema: payloadSchemaRef,
206
+ signature: webhookSignatureSchema,
207
+ examplePayload: z.record(jsonValueSchema).optional(),
208
+ handler: handlerSchema('webhooks[].handler', '(ctx, event) => Promise<WebhookResult>'),
209
+ })
210
+ .strict();
211
+ const scheduleSchema = z
212
+ .object({
213
+ id: scheduleIdSchema,
214
+ description: nonEmpty('schedules[].description'),
215
+ // Presence only here — the dependency-pure schema package stays zod-only,
216
+ // so the real 5-field cron syntax check lives in the SDK layer
217
+ // (`defineIntegration()`), the same way it layers cross-field rules today.
218
+ cron: nonEmpty('schedules[].cron'),
219
+ handler: handlerSchema('schedules[].handler', '(ctx, invocation) => Promise<ScheduleResult>'),
220
+ })
221
+ .strict();
222
+ const httpsUrlSchema = (what) => z.string().superRefine((value, ctx) => {
223
+ let parsed;
224
+ try {
225
+ parsed = new URL(value);
226
+ }
227
+ catch (_a) {
228
+ ctx.addIssue({
229
+ code: z.ZodIssueCode.custom,
230
+ message: `${what} must be an absolute URL, e.g. "https://provider.example/oauth/authorize".`,
231
+ });
232
+ return;
233
+ }
234
+ if (parsed.protocol !== 'https:') {
235
+ ctx.addIssue({
236
+ code: z.ZodIssueCode.custom,
237
+ message: `${what} must use https — OAuth endpoints are never plain http.`,
238
+ });
239
+ }
240
+ if (parsed.username !== '' || parsed.password !== '') {
241
+ ctx.addIssue({
242
+ code: z.ZodIssueCode.custom,
243
+ message: `${what} must not embed credentials.`,
244
+ });
245
+ }
246
+ });
247
+ const oauthSchema = z
248
+ .object({
249
+ provider: z
250
+ .object({
251
+ authorizationUrl: httpsUrlSchema('oauth.provider.authorizationUrl'),
252
+ tokenUrl: httpsUrlSchema('oauth.provider.tokenUrl'),
253
+ scopes: z.array(nonEmpty('oauth.provider.scopes[]')),
254
+ pkce: z.boolean().optional(),
255
+ })
256
+ .strict(),
257
+ credentials: z
258
+ .object({
259
+ clientIdSecretName: nonEmpty('oauth.credentials.clientIdSecretName'),
260
+ clientSecretSecretName: nonEmpty('oauth.credentials.clientSecretSecretName'),
261
+ })
262
+ .strict(),
263
+ onTokens: handlerSchema('oauth.onTokens', '(ctx, tokens) => Promise<void>'),
264
+ })
265
+ .strict();
266
+ const toolClassificationProposalSchema = z
267
+ .object({
268
+ effect: z.enum(['read', 'write']).optional(),
269
+ sensitivity: z.enum(['public', 'internal', 'pii', 'financial']).optional(),
270
+ })
271
+ .strict();
272
+ const proposalsSchema = z
273
+ .object({
274
+ credentialModel: z.enum(['account', 'user', 'source']).optional(),
275
+ tools: z.record(toolClassificationProposalSchema).optional(),
276
+ })
277
+ .strict();
278
+ /**
279
+ * A storage key's EXPLICIT declaration: the zod schema plus its exposure
280
+ * flags. `clientReadable` is the only way a declared key becomes readable by
281
+ * the browser through the generic storage route — and it defaults to false,
282
+ * so the bare-schema form stays server-only exactly as it always was.
283
+ * `.strict()` keeps an unrecognized flag (a typo like `clientReadible`) an
284
+ * error rather than a silently-ignored key whose author believes it is
285
+ * exposed — or, worse, believes it is not.
286
+ */
287
+ const storageKeyDeclarationSchema = z
288
+ .object({
289
+ schema: zodSchemaRef,
290
+ clientReadable: z.boolean().optional(),
291
+ })
292
+ .strict();
293
+ /** Duck-typed so a partner's own bundled zod copy still reads as a schema. */
294
+ function isZodSchemaLike(value) {
295
+ return (typeof (value === null || value === void 0 ? void 0 : value.safeParse) === 'function');
296
+ }
297
+ /**
298
+ * Either declaration form, hand-routed rather than expressed as `z.union` so
299
+ * the failure message stays specific. A union reports a bare "Invalid input"
300
+ * for every wrong shape, which would lose both the "declare a zod schema"
301
+ * guidance AND the strict-descriptor typo report — the two errors an author
302
+ * is actually going to hit.
303
+ */
304
+ const storageKeyDeclarationRef = z
305
+ .custom(() => true)
306
+ .superRefine((value, ctx) => {
307
+ if (isZodSchemaLike(value))
308
+ return;
309
+ const looksLikeDescriptor = value !== null && typeof value === 'object' && !Array.isArray(value);
310
+ // Neither form: name both, since the descriptor is the less obvious one.
311
+ if (!looksLikeDescriptor ||
312
+ !isZodSchemaLike(value.schema)) {
313
+ ctx.addIssue({
314
+ code: z.ZodIssueCode.custom,
315
+ message: 'Every storage key must declare either a zod schema (e.g. z.object({ … })) or a { schema, clientReadable } descriptor — ctx.storage validates reads and writes against the schema (capability-context ruling 1), and clientReadable (default false) is what opts the key in to the browser-readable storage route.',
316
+ });
317
+ return;
318
+ }
319
+ // A real descriptor with a real schema — report its own issues verbatim
320
+ // (an unrecognized flag, a non-boolean clientReadable) rather than the
321
+ // generic message, which would send the author looking in the wrong place.
322
+ const result = storageKeyDeclarationSchema.safeParse(value);
323
+ if (result.success)
324
+ return;
325
+ for (const issue of result.error.issues) {
326
+ ctx.addIssue({
327
+ code: z.ZodIssueCode.custom,
328
+ path: issue.path,
329
+ message: issue.message,
330
+ });
331
+ }
332
+ });
333
+ const storageScopeSchema = z.record(storageKeySchema, storageKeyDeclarationRef);
334
+ const componentsSchema = z
335
+ .object({
336
+ activationForm: componentRefSchema('components.activationForm').optional(),
337
+ marketplaceTile: componentRefSchema('components.marketplaceTile').optional(),
338
+ widgets: z.array(widgetSchema).optional(),
339
+ })
340
+ .strict();
341
+ /**
342
+ * THE canonical schema. Strict everywhere: an unrecognized key is an error,
343
+ * which is what keeps host-assigned fields (productId, kind, trust tier,
344
+ * credentialModel, per-tool effect/sensitivity, host-resolved widget fields)
345
+ * structurally un-settable at runtime, not merely absent from the type.
346
+ */
347
+ export const IntegrationDefinitionSchema = z
348
+ .object({
349
+ slug: slugSchema,
350
+ name: nonEmpty('name'),
351
+ description: nonEmpty('description'),
352
+ version: z
353
+ .string()
354
+ .regex(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/, {
355
+ message: 'version must be semver ("1.0.0", optionally with a prerelease/build suffix) — promotion diffs definitions by it.',
356
+ }),
357
+ capabilities: z.array(capabilitySchema).optional(),
358
+ permissions: z.array(permissionSchema).optional(),
359
+ components: componentsSchema.optional(),
360
+ workspaceTargets: WorkspaceTargetListSchema.optional(),
361
+ tools: z.array(toolSchema).optional(),
362
+ storage: z
363
+ .object({
364
+ account: storageScopeSchema.optional(),
365
+ user: storageScopeSchema.optional(),
366
+ })
367
+ .strict()
368
+ .optional(),
369
+ egress: z.array(egressEntrySchema).optional(),
370
+ webhooks: z.array(webhookSchema).optional(),
371
+ schedules: z.array(scheduleSchema).optional(),
372
+ oauth: oauthSchema.optional(),
373
+ proposes: proposalsSchema.optional(),
374
+ })
375
+ .strict()
376
+ .superRefine((definition, ctx) => {
377
+ var _a, _b, _c, _d, _e, _f, _g;
378
+ const webhookIds = ((_a = definition.webhooks) !== null && _a !== void 0 ? _a : []).map((w) => w.id);
379
+ for (const id of findDuplicates(webhookIds)) {
380
+ ctx.addIssue({
381
+ code: z.ZodIssueCode.custom,
382
+ path: ['webhooks'],
383
+ message: `Webhook id "${id}" is declared more than once — give every webhook a unique id.`,
384
+ });
385
+ }
386
+ const scheduleIds = ((_b = definition.schedules) !== null && _b !== void 0 ? _b : []).map((s) => s.id);
387
+ for (const id of findDuplicates(scheduleIds)) {
388
+ ctx.addIssue({
389
+ code: z.ZodIssueCode.custom,
390
+ path: ['schedules'],
391
+ message: `Schedule id "${id}" is declared more than once — give every schedule a unique id.`,
392
+ });
393
+ }
394
+ const widgetIds = ((_d = (_c = definition.components) === null || _c === void 0 ? void 0 : _c.widgets) !== null && _d !== void 0 ? _d : []).map((w) => w.id);
395
+ for (const id of findDuplicates(widgetIds)) {
396
+ ctx.addIssue({
397
+ code: z.ZodIssueCode.custom,
398
+ path: ['components', 'widgets'],
399
+ message: `Widget id "${id}" is declared more than once — give every widget a unique id.`,
400
+ });
401
+ }
402
+ const toolNames = ((_e = definition.tools) !== null && _e !== void 0 ? _e : []).map((tool) => tool.name);
403
+ for (const name of findDuplicates(toolNames)) {
404
+ ctx.addIssue({
405
+ code: z.ZodIssueCode.custom,
406
+ path: ['tools'],
407
+ message: `Tool "${name}" is declared more than once — give every tool a unique name.`,
408
+ });
409
+ }
410
+ const declaredTools = new Set(toolNames);
411
+ for (const proposedName of Object.keys((_g = (_f = definition.proposes) === null || _f === void 0 ? void 0 : _f.tools) !== null && _g !== void 0 ? _g : {})) {
412
+ if (!declaredTools.has(proposedName)) {
413
+ ctx.addIssue({
414
+ code: z.ZodIssueCode.custom,
415
+ path: ['proposes', 'tools', proposedName],
416
+ message: `proposes.tools["${proposedName}"] does not match any declared tool — proposals are keyed by the exact tool name in \`tools\`.`,
417
+ });
418
+ }
419
+ }
420
+ });
421
+ // ---- Shared helpers --------------------------------------------------------
422
+ function findDuplicates(values) {
423
+ const seen = new Set();
424
+ const duplicates = new Set();
425
+ for (const value of values) {
426
+ if (seen.has(value))
427
+ duplicates.add(value);
428
+ seen.add(value);
429
+ }
430
+ return [...duplicates];
431
+ }
432
+ function formatIssues(issues) {
433
+ return issues
434
+ .map((issue) => {
435
+ const path = issue.path.length > 0 ? issue.path.join('.') : '(root)';
436
+ return ` - ${path}: ${issue.message}`;
437
+ })
438
+ .join('\n');
439
+ }
440
+ const HOST_ASSIGNED_REMINDER = 'Host-assigned fields are never partner-authorable: productId, kind, trust ' +
441
+ 'tier, and machine exposure (credentialModel) do not exist on ' +
442
+ 'IntegrationDefinition, per-tool effect/sensitivity belong in the ' +
443
+ '`proposes` block, and host-resolved widget fields (productId, ' +
444
+ 'widgetConfigId, workspaceId, collapsed, isPinned, health, ' +
445
+ 'integrationMetadata) are populated by the platform at runtime — remove ' +
446
+ 'them from the definition.';
447
+ /**
448
+ * A zod schema (storage leaf) — detected via `instanceof` so we NEVER read a
449
+ * property (like `.safeParse`) that could be a malicious getter (F4). A React
450
+ * exotic (`memo`/`forwardRef`) is an object tagged with `$$typeof`; the `in`
451
+ * check uses [[HasProperty]], which does not invoke a getter either.
452
+ */
453
+ function isDeclarationLeaf(value) {
454
+ return value instanceof z.ZodType || '$$typeof' in value;
455
+ }
456
+ /**
457
+ * F4: reject accessor/proxy/class-instance/cyclic declaration containers
458
+ * before parsing. An object with getters (or a proxy) can return validated
459
+ * values during parse and different values later; a non-plain prototype can
460
+ * smuggle mutable state past `z.object()`; a cycle would recurse into zod
461
+ * rather than fail cleanly. We walk every CONTAINER (plain object / array),
462
+ * and stop at legitimate leaves: functions (component refs, `run`) and zod
463
+ * schemas (storage). Leaf detection happens BEFORE any own-property read, so
464
+ * a `safeParse` getter cannot execute. Proxy detection is best-effort — the
465
+ * re-parse at the host boundary (which materializes fresh values via zod) is
466
+ * the real guard.
467
+ *
468
+ * Cycle detection tracks the ANCESTOR chain only (add on enter, remove on
469
+ * exit): a genuine back-edge is a cycle, but the same object referenced from
470
+ * two sibling branches (a DAG — e.g. a shallow-cloned widget sharing a
471
+ * `layouts` object) is not, and must not be rejected.
472
+ */
473
+ export function assertPlainDeclaration(value, path = '(root)', ancestors = new WeakSet()) {
474
+ if (value === null || typeof value !== 'object')
475
+ return;
476
+ if (typeof value === 'function')
477
+ return;
478
+ // Leaf detection first — instanceof / HasProperty never invoke a getter.
479
+ if (isDeclarationLeaf(value))
480
+ return;
481
+ if (ancestors.has(value)) {
482
+ throw new Error(`Integration definition contains a cycle at ${path}. ` +
483
+ `Declarations must be finite plain data — remove the self-reference. ${HOST_ASSIGNED_REMINDER}`);
484
+ }
485
+ const proto = Object.getPrototypeOf(value);
486
+ const isArray = Array.isArray(value);
487
+ if (!isArray && proto !== Object.prototype && proto !== null) {
488
+ throw new Error(`Integration definition value at ${path} is a class/exotic instance, not a plain object. ` +
489
+ `Declaration containers must be plain object/array literals so their values cannot mutate after validation. ${HOST_ASSIGNED_REMINDER}`);
490
+ }
491
+ ancestors.add(value);
492
+ for (const key of Object.keys(value)) {
493
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
494
+ if (descriptor && (descriptor.get || descriptor.set)) {
495
+ throw new Error(`Integration definition property at ${path}.${key} is a getter/setter, not a data property. ` +
496
+ `Declaration values must be plain data — a getter can return a different value after validation. ${HOST_ASSIGNED_REMINDER}`);
497
+ }
498
+ assertPlainDeclaration(value[key], `${path}.${key}`, ancestors);
499
+ }
500
+ ancestors.delete(value);
501
+ }
502
+ const frozen = new WeakSet();
503
+ function isPlainObject(value) {
504
+ if (value === null || typeof value !== 'object')
505
+ return false;
506
+ const proto = Object.getPrototypeOf(value);
507
+ return proto === Object.prototype || proto === null;
508
+ }
509
+ /**
510
+ * Cycle-aware deep freeze (F4). Freezes plain objects and arrays; leaves zod
511
+ * schemas (freezing breaks their internal caches) and functions alone.
512
+ */
513
+ export function deepFreezeDefinition(value) {
514
+ if (value === null || typeof value !== 'object')
515
+ return;
516
+ if (frozen.has(value))
517
+ return;
518
+ if (Array.isArray(value)) {
519
+ frozen.add(value);
520
+ Object.freeze(value);
521
+ for (const item of value)
522
+ deepFreezeDefinition(item);
523
+ return;
524
+ }
525
+ if (isPlainObject(value)) {
526
+ frozen.add(value);
527
+ Object.freeze(value);
528
+ for (const item of Object.values(value))
529
+ deepFreezeDefinition(item);
530
+ }
531
+ }
532
+ /**
533
+ * The single validation entry point used by BOTH `defineIntegration()` (SDK,
534
+ * authoring time) and `registerPartnerIntegration()` (core, host trust
535
+ * boundary — F3). Rejects non-plain containers, then parses against the
536
+ * canonical schema, and returns the SANITIZED `result.data` (fresh, plain,
537
+ * strict-stripped — never the caller's original object). Throws an Error
538
+ * whose message is a remediation instruction.
539
+ */
540
+ export function parseIntegrationDefinition(input) {
541
+ assertPlainDeclaration(input);
542
+ const result = IntegrationDefinitionSchema.safeParse(input);
543
+ if (!result.success) {
544
+ const slug = typeof (input === null || input === void 0 ? void 0 : input.slug) === 'string'
545
+ ? ` for "${input.slug}"`
546
+ : '';
547
+ throw new Error(`Invalid integration definition${slug}:\n` +
548
+ `${formatIssues(result.error.issues)}\n` +
549
+ HOST_ASSIGNED_REMINDER);
550
+ }
551
+ // F4: freeze inside the canonical parser so BOTH defineIntegration() and
552
+ // registerPartnerIntegration() register immutable output — the host boundary
553
+ // no longer hands back mutable arrays. Function/zod leaves stay executable
554
+ // by design (the T1 model), so this is structural immutability, not
555
+ // behavioral.
556
+ deepFreezeDefinition(result.data);
557
+ // The schema's inferred output matches IntegrationDefinition in every
558
+ // non-generic field; the storage/tool generics default to the permissive
559
+ // base, which is exactly right for the loose registration boundary.
560
+ return result.data;
561
+ }
562
+ // ---- Cross-definition collision detection (F5) -----------------------------
563
+ /**
564
+ * THE canonical effective MCP tool name — the single source of truth for how
565
+ * discovery keys a tool. Tool discovery namespaces each raw tool name with the
566
+ * integration slug (`slug.replace(/-/g,'_')`), skipping the prefix when the
567
+ * name already carries it. Two collision-free RAW pairs can therefore collapse
568
+ * to the same EFFECTIVE name (`{slug:"foo",tool:"bar_baz"}` and
569
+ * `{slug:"foo-bar",tool:"baz"}` both become `foo_bar_baz`), so collision
570
+ * checking MUST compare effective names, and runtime discovery MUST throw on a
571
+ * duplicate assignment. Both call this one helper
572
+ * (`packages/agents/src/tools/tool-discovery.ts`).
573
+ */
574
+ export function getDiscoveredToolName(slug, rawName) {
575
+ const slugPrefix = slug.replace(/-/g, '_');
576
+ return rawName.startsWith(slugPrefix) ? rawName : `${slugPrefix}_${rawName}`;
577
+ }
578
+ /**
579
+ * Detects cross-definition collisions (duplicate slugs, widget ids, tool
580
+ * names across the partner set) AND collisions against the first-party
581
+ * inventory, throwing one error listing EVERY collision.
582
+ *
583
+ * This is the build-time gate the host registry deliberately lacks:
584
+ * `integrationRegistry.register()` keys by slug via `Map.set` and silently
585
+ * OVERWRITES, and duplicate widget/tool ids resolve last- or
586
+ * first-registration-wins by import order.
587
+ */
588
+ export function validateIntegrationDefinitions(definitions, firstParty = {}) {
589
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
590
+ const slugCounts = new Map();
591
+ const widgetOwners = new Map();
592
+ const toolOwners = new Map();
593
+ for (const definition of definitions) {
594
+ slugCounts.set(definition.slug, ((_a = slugCounts.get(definition.slug)) !== null && _a !== void 0 ? _a : 0) + 1);
595
+ for (const widget of (_c = (_b = definition.components) === null || _b === void 0 ? void 0 : _b.widgets) !== null && _c !== void 0 ? _c : []) {
596
+ widgetOwners.set(widget.id, [
597
+ ...((_d = widgetOwners.get(widget.id)) !== null && _d !== void 0 ? _d : []),
598
+ definition.slug,
599
+ ]);
600
+ }
601
+ for (const tool of (_e = definition.tools) !== null && _e !== void 0 ? _e : []) {
602
+ // EFFECTIVE (discovery) name, not the raw name — two collision-free raw
603
+ // names can collapse to the same effective key.
604
+ const effective = getDiscoveredToolName(definition.slug, tool.name);
605
+ toolOwners.set(effective, [
606
+ ...((_f = toolOwners.get(effective)) !== null && _f !== void 0 ? _f : []),
607
+ definition.slug,
608
+ ]);
609
+ }
610
+ }
611
+ const collisions = [];
612
+ for (const [slug, count] of slugCounts) {
613
+ if (count > 1) {
614
+ collisions.push(`slug "${slug}" is declared by ${count} partner definitions — slugs are the registry key and must be globally unique.`);
615
+ }
616
+ }
617
+ for (const [id, owners] of widgetOwners) {
618
+ if (owners.length > 1) {
619
+ collisions.push(`widget id "${id}" is declared by [${owners.join(', ')}] — widget ids are global (widget_config rows key on them); prefix yours with the integration slug.`);
620
+ }
621
+ }
622
+ for (const [name, owners] of toolOwners) {
623
+ if (owners.length > 1) {
624
+ collisions.push(`effective tool name "${name}" is declared by [${owners.join(', ')}] — discovery namespaces tool names by slug, so these collapse to one flat key and overwrite each other. Rename so the slug-prefixed names differ.`);
625
+ }
626
+ }
627
+ const reservedSlugs = new Set((_g = firstParty.slugs) !== null && _g !== void 0 ? _g : []);
628
+ const reservedWidgets = new Set((_h = firstParty.widgetIds) !== null && _h !== void 0 ? _h : []);
629
+ const reservedTools = new Set((_j = firstParty.toolNames) !== null && _j !== void 0 ? _j : []);
630
+ for (const [slug, owners] of groupOwners(definitions, (d) => [d.slug])) {
631
+ if (reservedSlugs.has(slug)) {
632
+ collisions.push(`slug "${slug}" (declared by [${owners.join(', ')}]) collides with a first-party integration — pick a slug no built-in product uses.`);
633
+ }
634
+ }
635
+ for (const [id, owners] of widgetOwners) {
636
+ if (reservedWidgets.has(id)) {
637
+ collisions.push(`widget id "${id}" (declared by [${owners.join(', ')}]) collides with a first-party widget — the dashboard resolves widgets by id, so this would hijack it. Prefix with the integration slug.`);
638
+ }
639
+ }
640
+ for (const [name, owners] of toolOwners) {
641
+ if (reservedTools.has(name)) {
642
+ collisions.push(`effective tool name "${name}" (declared by [${owners.join(', ')}]) collides with a first-party tool — the flat, slug-namespaced tool registry would overwrite one with the other. Rename it.`);
643
+ }
644
+ }
645
+ if (collisions.length > 0) {
646
+ throw new Error(`Integration definitions collide (${collisions.length} collision${collisions.length === 1 ? '' : 's'}):\n` +
647
+ collisions.map((line) => ` - ${line}`).join('\n') +
648
+ '\nRename until every slug, widget id, and tool name is unique — the host registry would otherwise silently overwrite or drop a registration.');
649
+ }
650
+ }
651
+ function groupOwners(definitions, keysOf) {
652
+ var _a;
653
+ const owners = new Map();
654
+ for (const definition of definitions) {
655
+ for (const key of keysOf(definition)) {
656
+ owners.set(key, [...((_a = owners.get(key)) !== null && _a !== void 0 ? _a : []), definition.slug]);
657
+ }
658
+ }
659
+ return owners;
660
+ }
661
+ //# sourceMappingURL=integration-definition.js.map