@withloops.ai/contracts 0.3.1 → 0.4.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/dist/manifest.js CHANGED
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { AppId, HttpsUrl, SemVer } from "./ids.js";
3
3
  import { EventTypeSchema, scopeForEvent } from "./events.js";
4
4
  import { ScopeSchema, expandScopes } from "./scopes.js";
5
+ import { LOOPS_FIELD_NAMES } from "./fields.js";
5
6
  /**
6
7
  * App manifest v1 (`loops.manifest.json`). Everything is a Loops App: one manifest, two optional halves —
7
8
  * `sync` (declarative CRM connector, run by the one Loops sync engine) and `ui` (LoopKit). Skill §2 #1–#3, §3.
@@ -22,6 +23,45 @@ const RECORD_SCOPE = {
22
23
  loop: { read: 'loops:read', write: 'loops:write' },
23
24
  };
24
25
  const PathTemplate = z.string().regex(/^\/[A-Za-z0-9_\-./{}:]*$/, 'A path starting with /, e.g. /crm/v3/objects/contacts');
26
+ /**
27
+ * Added in 0.4.0 (L2). A JSON body template. A string that is exactly `{{name}}` takes the value as it is (type kept);
28
+ * `{{name}}` inside longer text is written as text. A key whose value comes out null or missing is left out, so is a null
29
+ * array item and an object left empty. Names: see `TEMPLATE_VARIABLES` in sync.ts. Pure: no code, no loops, no conditions.
30
+ */
31
+ export const BodyTemplate = z.json();
32
+ /** Added in 0.4.0. A dotted path into a JSON answer: `data`, `values.name[0].first_name`, `values.associated_people[*].target_record_id`. */
33
+ export const JsonPath = z.string().regex(/^[A-Za-z0-9_$-]+(\[(\d+|\*)\])?(\.[A-Za-z0-9_$-]+(\[(\d+|\*)\])?)*$/, 'A dotted path like data.items or values.name[0].first_name');
34
+ /** Added in 0.4.0. Small pure value transforms, applied in order when reading their value. */
35
+ export const ValueTransform = z.enum(['trim', 'lower', 'upper', 'string', 'number', 'boolean', 'isoDate', 'first', 'domainFromUrl']);
36
+ /** Added in 0.4.0. How a list call pages. `cursor`: their answer holds the next cursor; `offset`/`page`: count up until a short page. */
37
+ export const Paging = z.discriminatedUnion('style', [
38
+ z.object({ style: z.literal('cursor'), param: z.string().min(1), nextPath: JsonPath, in: z.enum(['query', 'body']).default('query') }),
39
+ z.object({
40
+ style: z.literal('offset'),
41
+ param: z.string().min(1),
42
+ sizeParam: z.string().min(1),
43
+ size: z.number().int().min(1).max(1000).default(100),
44
+ in: z.enum(['query', 'body']).default('query'),
45
+ }),
46
+ z.object({
47
+ style: z.literal('page'),
48
+ param: z.string().min(1),
49
+ sizeParam: z.string().min(1),
50
+ size: z.number().int().min(1).max(1000).default(100),
51
+ startAt: z.number().int().min(0).max(1).default(1),
52
+ in: z.enum(['query', 'body']).default('query'),
53
+ }),
54
+ ]);
55
+ /** Added in 0.4.0. One HTTP call against their API. `{id}` in the path is their record id. */
56
+ export const RecordCall = z.object({
57
+ method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).default('GET'),
58
+ path: PathTemplate,
59
+ body: BodyTemplate.optional(),
60
+ /** Where the record sits in the answer (`data`); the whole answer when left out. */
61
+ itemPath: JsonPath.optional(),
62
+ /** Where their id sits in the answer of a create (`data.id.record_id`); defaults to the object's idPath under itemPath. */
63
+ idPath: JsonPath.optional(),
64
+ });
25
65
  // ── sync: the connector half ────────────────────────────────────────────────────
26
66
  export const SyncAuth = z.discriminatedUnion('type', [
27
67
  z.object({
@@ -37,14 +77,21 @@ export const SyncAuth = z.discriminatedUnion('type', [
37
77
  type: z.literal('apiKey'),
38
78
  header: z.string().min(1).default('Authorization'),
39
79
  prefix: z.string().optional(),
80
+ /** Added in 0.4.0. What the admin is asked for at install ("Attio access token"), and where to find it. */
81
+ label: z.string().min(1).max(80).optional(),
82
+ helpUrl: HttpsUrl.optional(),
40
83
  }),
41
84
  ]);
42
85
  /** Who owns a field. The customer's CRM owns its fields (locked in Loops); Loops owns its own. Owner wins, never last-write-wins. */
43
86
  export const FieldOwner = z.enum(['theirs', 'ours']);
44
87
  export const SyncField = z.object({
88
+ /** Where the value sits in their record: a field name or a JsonPath (`values.email_addresses[0].email_address`). */
45
89
  theirs: z.string().min(1),
90
+ /** The Loops field (`LOOPS_FIELDS` in sync.ts) or `custom.<key>` for a custom field. */
46
91
  ours: z.string().min(1),
47
92
  owner: FieldOwner,
93
+ /** Added in 0.4.0. Applied in order to their value when reading it. */
94
+ transform: z.array(ValueTransform).optional(),
48
95
  });
49
96
  export const SyncObject = z.object({
50
97
  theirs: z.string().min(1),
@@ -55,30 +102,95 @@ export const SyncObject = z.object({
55
102
  cursorParam: z.string().optional(),
56
103
  nextCursorPath: z.string().optional(),
57
104
  updatedSinceParam: z.string().optional(),
105
+ /** Added in 0.4.0. GET by default; POST for APIs that query with a body. */
106
+ method: z.enum(['GET', 'POST']).optional(),
107
+ /** Added in 0.4.0. Body for a POST list (`{{limit}}`, `{{offset}}`, `{{cursor}}`, `{{updatedSince}}` fill in). */
108
+ body: BodyTemplate.optional(),
109
+ /** Added in 0.4.0. Replaces cursorParam/nextCursorPath when set. */
110
+ paging: Paging.optional(),
58
111
  }),
59
112
  fields: z.array(SyncField).min(1),
60
113
  /** Dedupe keys, in order: e.g. ["email"] for contacts, ["domain"] for companies. External id always matches first. */
61
114
  match: z.array(z.string().min(1)).default([]),
115
+ /** Added in 0.4.0. Where their id sits in one item (default `id`). */
116
+ idPath: JsonPath.optional(),
117
+ /** Added in 0.4.0. Where their last-changed time sits; without it, polling compares each record with what it saw last. */
118
+ updatedAtPath: JsonPath.optional(),
119
+ /** Added in 0.4.0. Read one record (`/v1/people/{id}`); needed when their webhook sends only ids. */
120
+ get: RecordCall.optional(),
121
+ /** Added in 0.4.0. Create a record Loops made (all mapped fields are filled). Without it, Loops never creates records there. */
122
+ create: RecordCall.optional(),
123
+ /** Added in 0.4.0. Update a record: only fields owned by Loops (`owner: ours`) are filled; their fields are left out. */
124
+ update: RecordCall.optional(),
62
125
  });
63
126
  export const SyncLink = z.object({
64
127
  from: RecordKind,
65
128
  to: RecordKind,
129
+ /** Where the other record's id sits on `from`'s item: a field or a JsonPath; `[*]` for many (`values.associated_people[*].target_record_id`). */
66
130
  theirs: z.string().min(1),
131
+ /** The role on the Loops record link (`recordLinks`), e.g. `decision maker`, `employee`. */
67
132
  role: z.string().optional(),
68
133
  });
69
134
  export const ChangeFeed = z
70
135
  .object({
71
136
  webhook: z
72
137
  .object({
73
- signature: z.object({ scheme: z.enum(['hmac-sha256', 'hmac-sha1']), header: z.string().min(1) }),
138
+ signature: z.object({
139
+ scheme: z.enum(['hmac-sha256', 'hmac-sha1']),
140
+ header: z.string().min(1),
141
+ /** Added in 0.4.0. How the signature is written (default hex). */
142
+ encoding: z.enum(['hex', 'base64']).optional(),
143
+ /** Added in 0.4.0. Text before the signature in the header, e.g. `sha256=`. */
144
+ prefix: z.string().max(40).optional(),
145
+ }),
146
+ /** Added in 0.4.0. Where the events sit in one delivery (`events`); the body is one event when left out. */
147
+ eventsPath: JsonPath.optional(),
148
+ /** Added in 0.4.0. Where an event says what happened (`event_type`). */
149
+ typePath: JsonPath.optional(),
150
+ /** Added in 0.4.0. Where an event names the record (`id.record_id`). */
151
+ idPath: JsonPath.optional(),
152
+ /** Added in 0.4.0. Where an event names the object; when it matches no object's `theirs`, the engine asks each object's `get`. */
153
+ objectPath: JsonPath.optional(),
154
+ /** Added in 0.4.0. Their event names for each change. A created or updated record is read again with the object's `get`. */
155
+ types: z
156
+ .object({
157
+ created: z.array(z.string().min(1)).default([]),
158
+ updated: z.array(z.string().min(1)).default([]),
159
+ deleted: z.array(z.string().min(1)).default([]),
160
+ })
161
+ .optional(),
162
+ /** Added in 0.4.0. Register the webhook through their API at install (`{{hookUrl}}` in the body); the secret is kept in the vault. */
163
+ register: z
164
+ .object({
165
+ call: RecordCall,
166
+ /** Where the signing secret sits in the answer (`data.secret`). */
167
+ secretPath: JsonPath,
168
+ /** Where the webhook's id sits in the answer, to remove it at uninstall. */
169
+ idPath: JsonPath.optional(),
170
+ /** Remove it at uninstall; `{id}` is the webhook id. */
171
+ unregister: RecordCall.optional(),
172
+ })
173
+ .optional(),
74
174
  })
75
175
  .optional(),
76
- polling: z.object({ everyMinutes: z.number().int().min(5).max(1440) }).optional(),
176
+ polling: z.object({
177
+ everyMinutes: z.number().int().min(5).max(1440),
178
+ /** Added in 0.4.0. `updatedSince` asks only for changed records (needs list.updatedSinceParam); `full` reads every record and keeps the changed ones. Default: updatedSince when the object has the param, else full. */
179
+ mode: z.enum(['updatedSince', 'full']).optional(),
180
+ }).optional(),
77
181
  })
78
182
  .refine((f) => f.webhook || f.polling, 'Declare a webhook, polling, or both');
79
183
  /** What Loops writes back to the CRM timeline (Loops owns these). */
80
184
  export const WriteBackKind = z.enum(['call', 'transcript', 'meeting', 'note', 'action', 'campaignReply']);
81
- export const WriteBack = z.partialRecord(WriteBackKind, z.object({ method: z.enum(['POST', 'PUT', 'PATCH']), path: PathTemplate }));
185
+ export const WriteBack = z.partialRecord(WriteBackKind, z.object({
186
+ method: z.enum(['POST', 'PUT', 'PATCH']),
187
+ /** `{id}` = their id of the record the item belongs to, `{object}` = their object name. */
188
+ path: PathTemplate,
189
+ /** Added in 0.4.0. Body template; `{{item.*}}` and `{{record.*}}` fill in (sync.ts TEMPLATE_VARIABLES). */
190
+ body: BodyTemplate.optional(),
191
+ /** Added in 0.4.0. Where their id of the written item sits in the answer, so Loops can find it again. */
192
+ idPath: JsonPath.optional(),
193
+ }));
82
194
  export const SyncLimits = z.object({
83
195
  requestsPerSecond: z.number().positive().max(1000),
84
196
  burst: z.number().int().positive().max(10000).optional(),
@@ -92,6 +204,8 @@ export const SyncManifest = z.object({
92
204
  changeFeed: ChangeFeed,
93
205
  writeBack: WriteBack.default({}),
94
206
  limits: SyncLimits,
207
+ /** Added in 0.4.0. A cheap call that proves the credentials work (`GET /v2/self`). `loops test` and the health check use it. */
208
+ test: RecordCall.optional(),
95
209
  });
96
210
  // ── ui: the LoopKit half ────────────────────────────────────────────────────────
97
211
  export const SLOTS = [
@@ -176,6 +290,8 @@ export const ManifestV1 = z
176
290
  if (new Set(m.scopes).size !== m.scopes.length)
177
291
  ctx.addIssue({ code: 'custom', path: ['scopes'], message: 'Scopes are listed twice' });
178
292
  m.sync?.objects.forEach((o, i) => need(RECORD_SCOPE[o.ours].write, ['sync', 'objects', i, 'ours'], `Syncing ${o.ours}`));
293
+ if (m.sync)
294
+ checkSync(m.sync, ctx);
179
295
  if (m.ui) {
180
296
  m.ui.uses.forEach((f, i) => need(RECORD_SCOPE[f.split('.')[0]].read, ['ui', 'uses', i], `Reading ${f}`));
181
297
  m.ui.does.forEach((d, i) => need(DOES[d], ['ui', 'does', i], `Doing ${d}`));
@@ -206,6 +322,48 @@ export const ManifestV1 = z
206
322
  }
207
323
  }
208
324
  });
325
+ /** Connector checks added in 0.4.0: Loops field names, `{id}` where a record is named, webhook pieces that need each other. */
326
+ function checkSync(sync, ctx) {
327
+ const issue = (path, message) => ctx.addIssue({ code: 'custom', path: ['sync', ...path], message });
328
+ const seen = new Set();
329
+ sync.objects.forEach((o, i) => {
330
+ if (seen.has(o.theirs))
331
+ issue(['objects', i, 'theirs'], `${o.theirs} is listed twice`);
332
+ seen.add(o.theirs);
333
+ const known = LOOPS_FIELD_NAMES[o.ours];
334
+ o.fields.forEach((f, j) => {
335
+ if (!f.ours.startsWith('custom.') && !known.includes(f.ours))
336
+ issue(['objects', i, 'fields', j, 'ours'], `${f.ours} is not a Loops ${o.ours} field (use one of ${known.join(', ')}, or custom.<key>)`);
337
+ });
338
+ o.match.forEach((k, j) => {
339
+ // A match key names a Loops field (preferred) or, as in 0.3.x manifests, their field; either must be mapped.
340
+ if (!o.fields.some((f) => f.ours === k || f.theirs === k))
341
+ issue(['objects', i, 'match', j], `Matching on ${k} needs a field mapped to ${k}`);
342
+ });
343
+ for (const call of ['get', 'update']) {
344
+ if (o[call] && !o[call].path.includes('{id}'))
345
+ issue(['objects', i, call, 'path'], `The ${call} path needs {id}`);
346
+ }
347
+ if (o.list.paging && (o.list.paging.in === 'body') !== (o.list.method === 'POST'))
348
+ issue(['objects', i, 'list', 'paging', 'in'], 'Paging in the body needs list.method POST, and a POST list pages in the body');
349
+ });
350
+ sync.links.forEach((l, i) => {
351
+ for (const end of ['from', 'to'])
352
+ if (!sync.objects.some((o) => o.ours === l[end]))
353
+ issue(['links', i, end], `${l[end]} is linked but no object syncs it`);
354
+ });
355
+ const hook = sync.changeFeed.webhook;
356
+ if (hook?.types && !hook.idPath)
357
+ issue(['changeFeed', 'webhook', 'idPath'], 'Webhook event types need idPath');
358
+ if (hook?.types && (hook.types.created.length || hook.types.updated.length))
359
+ sync.objects.forEach((o, i) => { if (!o.get)
360
+ issue(['objects', i, 'get'], 'Webhooks that send ids need a get call to read the record'); });
361
+ if (sync.changeFeed.polling?.mode === 'updatedSince')
362
+ sync.objects.forEach((o, i) => {
363
+ if (!o.list.updatedSinceParam)
364
+ issue(['objects', i, 'list', 'updatedSinceParam'], 'Polling by updatedSince needs list.updatedSinceParam');
365
+ });
366
+ }
209
367
  /** The manifest file name in an app folder. */
210
368
  export const MANIFEST_FILE = 'loops.manifest.json';
211
369
  /** Parse a manifest; errors come back as plain lines ("ui.uses.0: Reading contact.email needs the contacts:read scope"). */
@@ -81,6 +81,8 @@ export declare const AppVersionRecord: z.ZodObject<{
81
81
  type: z.ZodLiteral<"apiKey">;
82
82
  header: z.ZodDefault<z.ZodString>;
83
83
  prefix: z.ZodOptional<z.ZodString>;
84
+ label: z.ZodOptional<z.ZodString>;
85
+ helpUrl: z.ZodOptional<z.ZodURL>;
84
86
  }, z.core.$strip>], "type">;
85
87
  objects: z.ZodArray<z.ZodObject<{
86
88
  theirs: z.ZodString;
@@ -96,6 +98,39 @@ export declare const AppVersionRecord: z.ZodObject<{
96
98
  cursorParam: z.ZodOptional<z.ZodString>;
97
99
  nextCursorPath: z.ZodOptional<z.ZodString>;
98
100
  updatedSinceParam: z.ZodOptional<z.ZodString>;
101
+ method: z.ZodOptional<z.ZodEnum<{
102
+ GET: "GET";
103
+ POST: "POST";
104
+ }>>;
105
+ body: z.ZodOptional<z.ZodJSONSchema>;
106
+ paging: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
107
+ style: z.ZodLiteral<"cursor">;
108
+ param: z.ZodString;
109
+ nextPath: z.ZodString;
110
+ in: z.ZodDefault<z.ZodEnum<{
111
+ query: "query";
112
+ body: "body";
113
+ }>>;
114
+ }, z.core.$strip>, z.ZodObject<{
115
+ style: z.ZodLiteral<"offset">;
116
+ param: z.ZodString;
117
+ sizeParam: z.ZodString;
118
+ size: z.ZodDefault<z.ZodNumber>;
119
+ in: z.ZodDefault<z.ZodEnum<{
120
+ query: "query";
121
+ body: "body";
122
+ }>>;
123
+ }, z.core.$strip>, z.ZodObject<{
124
+ style: z.ZodLiteral<"page">;
125
+ param: z.ZodString;
126
+ sizeParam: z.ZodString;
127
+ size: z.ZodDefault<z.ZodNumber>;
128
+ startAt: z.ZodDefault<z.ZodNumber>;
129
+ in: z.ZodDefault<z.ZodEnum<{
130
+ query: "query";
131
+ body: "body";
132
+ }>>;
133
+ }, z.core.$strip>], "style">>;
99
134
  }, z.core.$strip>;
100
135
  fields: z.ZodArray<z.ZodObject<{
101
136
  theirs: z.ZodString;
@@ -104,8 +139,60 @@ export declare const AppVersionRecord: z.ZodObject<{
104
139
  theirs: "theirs";
105
140
  ours: "ours";
106
141
  }>;
142
+ transform: z.ZodOptional<z.ZodArray<z.ZodEnum<{
143
+ string: "string";
144
+ number: "number";
145
+ boolean: "boolean";
146
+ trim: "trim";
147
+ lower: "lower";
148
+ upper: "upper";
149
+ isoDate: "isoDate";
150
+ first: "first";
151
+ domainFromUrl: "domainFromUrl";
152
+ }>>>;
107
153
  }, z.core.$strip>>;
108
154
  match: z.ZodDefault<z.ZodArray<z.ZodString>>;
155
+ idPath: z.ZodOptional<z.ZodString>;
156
+ updatedAtPath: z.ZodOptional<z.ZodString>;
157
+ get: z.ZodOptional<z.ZodObject<{
158
+ method: z.ZodDefault<z.ZodEnum<{
159
+ GET: "GET";
160
+ POST: "POST";
161
+ PUT: "PUT";
162
+ PATCH: "PATCH";
163
+ DELETE: "DELETE";
164
+ }>>;
165
+ path: z.ZodString;
166
+ body: z.ZodOptional<z.ZodJSONSchema>;
167
+ itemPath: z.ZodOptional<z.ZodString>;
168
+ idPath: z.ZodOptional<z.ZodString>;
169
+ }, z.core.$strip>>;
170
+ create: z.ZodOptional<z.ZodObject<{
171
+ method: z.ZodDefault<z.ZodEnum<{
172
+ GET: "GET";
173
+ POST: "POST";
174
+ PUT: "PUT";
175
+ PATCH: "PATCH";
176
+ DELETE: "DELETE";
177
+ }>>;
178
+ path: z.ZodString;
179
+ body: z.ZodOptional<z.ZodJSONSchema>;
180
+ itemPath: z.ZodOptional<z.ZodString>;
181
+ idPath: z.ZodOptional<z.ZodString>;
182
+ }, z.core.$strip>>;
183
+ update: z.ZodOptional<z.ZodObject<{
184
+ method: z.ZodDefault<z.ZodEnum<{
185
+ GET: "GET";
186
+ POST: "POST";
187
+ PUT: "PUT";
188
+ PATCH: "PATCH";
189
+ DELETE: "DELETE";
190
+ }>>;
191
+ path: z.ZodString;
192
+ body: z.ZodOptional<z.ZodJSONSchema>;
193
+ itemPath: z.ZodOptional<z.ZodString>;
194
+ idPath: z.ZodOptional<z.ZodString>;
195
+ }, z.core.$strip>>;
109
196
  }, z.core.$strip>>;
110
197
  links: z.ZodDefault<z.ZodArray<z.ZodObject<{
111
198
  from: z.ZodEnum<{
@@ -131,10 +218,58 @@ export declare const AppVersionRecord: z.ZodObject<{
131
218
  "hmac-sha1": "hmac-sha1";
132
219
  }>;
133
220
  header: z.ZodString;
221
+ encoding: z.ZodOptional<z.ZodEnum<{
222
+ hex: "hex";
223
+ base64: "base64";
224
+ }>>;
225
+ prefix: z.ZodOptional<z.ZodString>;
134
226
  }, z.core.$strip>;
227
+ eventsPath: z.ZodOptional<z.ZodString>;
228
+ typePath: z.ZodOptional<z.ZodString>;
229
+ idPath: z.ZodOptional<z.ZodString>;
230
+ objectPath: z.ZodOptional<z.ZodString>;
231
+ types: z.ZodOptional<z.ZodObject<{
232
+ created: z.ZodDefault<z.ZodArray<z.ZodString>>;
233
+ updated: z.ZodDefault<z.ZodArray<z.ZodString>>;
234
+ deleted: z.ZodDefault<z.ZodArray<z.ZodString>>;
235
+ }, z.core.$strip>>;
236
+ register: z.ZodOptional<z.ZodObject<{
237
+ call: z.ZodObject<{
238
+ method: z.ZodDefault<z.ZodEnum<{
239
+ GET: "GET";
240
+ POST: "POST";
241
+ PUT: "PUT";
242
+ PATCH: "PATCH";
243
+ DELETE: "DELETE";
244
+ }>>;
245
+ path: z.ZodString;
246
+ body: z.ZodOptional<z.ZodJSONSchema>;
247
+ itemPath: z.ZodOptional<z.ZodString>;
248
+ idPath: z.ZodOptional<z.ZodString>;
249
+ }, z.core.$strip>;
250
+ secretPath: z.ZodString;
251
+ idPath: z.ZodOptional<z.ZodString>;
252
+ unregister: z.ZodOptional<z.ZodObject<{
253
+ method: z.ZodDefault<z.ZodEnum<{
254
+ GET: "GET";
255
+ POST: "POST";
256
+ PUT: "PUT";
257
+ PATCH: "PATCH";
258
+ DELETE: "DELETE";
259
+ }>>;
260
+ path: z.ZodString;
261
+ body: z.ZodOptional<z.ZodJSONSchema>;
262
+ itemPath: z.ZodOptional<z.ZodString>;
263
+ idPath: z.ZodOptional<z.ZodString>;
264
+ }, z.core.$strip>>;
265
+ }, z.core.$strip>>;
135
266
  }, z.core.$strip>>;
136
267
  polling: z.ZodOptional<z.ZodObject<{
137
268
  everyMinutes: z.ZodNumber;
269
+ mode: z.ZodOptional<z.ZodEnum<{
270
+ updatedSince: "updatedSince";
271
+ full: "full";
272
+ }>>;
138
273
  }, z.core.$strip>>;
139
274
  }, z.core.$strip>;
140
275
  writeBack: z.ZodDefault<z.ZodRecord<z.ZodEnum<{
@@ -151,12 +286,27 @@ export declare const AppVersionRecord: z.ZodObject<{
151
286
  PATCH: "PATCH";
152
287
  }>;
153
288
  path: z.ZodString;
289
+ body: z.ZodOptional<z.ZodJSONSchema>;
290
+ idPath: z.ZodOptional<z.ZodString>;
154
291
  }, z.core.$strip>>>;
155
292
  limits: z.ZodObject<{
156
293
  requestsPerSecond: z.ZodNumber;
157
294
  burst: z.ZodOptional<z.ZodNumber>;
158
295
  retryAfterHeader: z.ZodDefault<z.ZodString>;
159
296
  }, z.core.$strip>;
297
+ test: z.ZodOptional<z.ZodObject<{
298
+ method: z.ZodDefault<z.ZodEnum<{
299
+ GET: "GET";
300
+ POST: "POST";
301
+ PUT: "PUT";
302
+ PATCH: "PATCH";
303
+ DELETE: "DELETE";
304
+ }>>;
305
+ path: z.ZodString;
306
+ body: z.ZodOptional<z.ZodJSONSchema>;
307
+ itemPath: z.ZodOptional<z.ZodString>;
308
+ idPath: z.ZodOptional<z.ZodString>;
309
+ }, z.core.$strip>>;
160
310
  }, z.core.$strip>>;
161
311
  ui: z.ZodOptional<z.ZodObject<{
162
312
  apiVersion: z.ZodLiteral<1>;
@@ -213,10 +363,10 @@ export declare const AppVersionRecord: z.ZodObject<{
213
363
  }, z.core.$strip>], "type">>>;
214
364
  endpoints: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
215
365
  method: z.ZodEnum<{
366
+ GET: "GET";
216
367
  POST: "POST";
217
368
  PUT: "PUT";
218
369
  PATCH: "PATCH";
219
- GET: "GET";
220
370
  DELETE: "DELETE";
221
371
  }>;
222
372
  url: z.ZodURL;