@withloops.ai/contracts 0.3.0 → 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,12 +23,53 @@ 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({
28
68
  type: z.literal('oauth2'),
29
69
  authorizeUrl: HttpsUrl,
30
70
  tokenUrl: HttpsUrl,
71
+ /** The developer's OAuth client id at their own service (public). The secret never goes in the manifest: it lives in the vault (APP_CLIENT_SECRET_REF). Added in 0.3.1. */
72
+ clientId: z.string().min(1).max(256).optional(),
31
73
  scopes: z.array(z.string().min(1)).default([]),
32
74
  refresh: z.boolean().default(true),
33
75
  }),
@@ -35,14 +77,21 @@ export const SyncAuth = z.discriminatedUnion('type', [
35
77
  type: z.literal('apiKey'),
36
78
  header: z.string().min(1).default('Authorization'),
37
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(),
38
83
  }),
39
84
  ]);
40
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. */
41
86
  export const FieldOwner = z.enum(['theirs', 'ours']);
42
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`). */
43
89
  theirs: z.string().min(1),
90
+ /** The Loops field (`LOOPS_FIELDS` in sync.ts) or `custom.<key>` for a custom field. */
44
91
  ours: z.string().min(1),
45
92
  owner: FieldOwner,
93
+ /** Added in 0.4.0. Applied in order to their value when reading it. */
94
+ transform: z.array(ValueTransform).optional(),
46
95
  });
47
96
  export const SyncObject = z.object({
48
97
  theirs: z.string().min(1),
@@ -53,30 +102,95 @@ export const SyncObject = z.object({
53
102
  cursorParam: z.string().optional(),
54
103
  nextCursorPath: z.string().optional(),
55
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(),
56
111
  }),
57
112
  fields: z.array(SyncField).min(1),
58
113
  /** Dedupe keys, in order: e.g. ["email"] for contacts, ["domain"] for companies. External id always matches first. */
59
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(),
60
125
  });
61
126
  export const SyncLink = z.object({
62
127
  from: RecordKind,
63
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`). */
64
130
  theirs: z.string().min(1),
131
+ /** The role on the Loops record link (`recordLinks`), e.g. `decision maker`, `employee`. */
65
132
  role: z.string().optional(),
66
133
  });
67
134
  export const ChangeFeed = z
68
135
  .object({
69
136
  webhook: z
70
137
  .object({
71
- 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(),
72
174
  })
73
175
  .optional(),
74
- 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(),
75
181
  })
76
182
  .refine((f) => f.webhook || f.polling, 'Declare a webhook, polling, or both');
77
183
  /** What Loops writes back to the CRM timeline (Loops owns these). */
78
184
  export const WriteBackKind = z.enum(['call', 'transcript', 'meeting', 'note', 'action', 'campaignReply']);
79
- 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
+ }));
80
194
  export const SyncLimits = z.object({
81
195
  requestsPerSecond: z.number().positive().max(1000),
82
196
  burst: z.number().int().positive().max(10000).optional(),
@@ -90,6 +204,8 @@ export const SyncManifest = z.object({
90
204
  changeFeed: ChangeFeed,
91
205
  writeBack: WriteBack.default({}),
92
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(),
93
209
  });
94
210
  // ── ui: the LoopKit half ────────────────────────────────────────────────────────
95
211
  export const SLOTS = [
@@ -174,6 +290,8 @@ export const ManifestV1 = z
174
290
  if (new Set(m.scopes).size !== m.scopes.length)
175
291
  ctx.addIssue({ code: 'custom', path: ['scopes'], message: 'Scopes are listed twice' });
176
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);
177
295
  if (m.ui) {
178
296
  m.ui.uses.forEach((f, i) => need(RECORD_SCOPE[f.split('.')[0]].read, ['ui', 'uses', i], `Reading ${f}`));
179
297
  m.ui.does.forEach((d, i) => need(DOES[d], ['ui', 'does', i], `Doing ${d}`));
@@ -204,6 +322,48 @@ export const ManifestV1 = z
204
322
  }
205
323
  }
206
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
+ }
207
367
  /** The manifest file name in an app folder. */
208
368
  export const MANIFEST_FILE = 'loops.manifest.json';
209
369
  /** Parse a manifest; errors come back as plain lines ("ui.uses.0: Reading contact.email needs the contacts:read scope"). */
@@ -74,12 +74,15 @@ export declare const AppVersionRecord: z.ZodObject<{
74
74
  type: z.ZodLiteral<"oauth2">;
75
75
  authorizeUrl: z.ZodURL;
76
76
  tokenUrl: z.ZodURL;
77
+ clientId: z.ZodOptional<z.ZodString>;
77
78
  scopes: z.ZodDefault<z.ZodArray<z.ZodString>>;
78
79
  refresh: z.ZodDefault<z.ZodBoolean>;
79
80
  }, z.core.$strip>, z.ZodObject<{
80
81
  type: z.ZodLiteral<"apiKey">;
81
82
  header: z.ZodDefault<z.ZodString>;
82
83
  prefix: z.ZodOptional<z.ZodString>;
84
+ label: z.ZodOptional<z.ZodString>;
85
+ helpUrl: z.ZodOptional<z.ZodURL>;
83
86
  }, z.core.$strip>], "type">;
84
87
  objects: z.ZodArray<z.ZodObject<{
85
88
  theirs: z.ZodString;
@@ -95,6 +98,39 @@ export declare const AppVersionRecord: z.ZodObject<{
95
98
  cursorParam: z.ZodOptional<z.ZodString>;
96
99
  nextCursorPath: z.ZodOptional<z.ZodString>;
97
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">>;
98
134
  }, z.core.$strip>;
99
135
  fields: z.ZodArray<z.ZodObject<{
100
136
  theirs: z.ZodString;
@@ -103,8 +139,60 @@ export declare const AppVersionRecord: z.ZodObject<{
103
139
  theirs: "theirs";
104
140
  ours: "ours";
105
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
+ }>>>;
106
153
  }, z.core.$strip>>;
107
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>>;
108
196
  }, z.core.$strip>>;
109
197
  links: z.ZodDefault<z.ZodArray<z.ZodObject<{
110
198
  from: z.ZodEnum<{
@@ -130,10 +218,58 @@ export declare const AppVersionRecord: z.ZodObject<{
130
218
  "hmac-sha1": "hmac-sha1";
131
219
  }>;
132
220
  header: z.ZodString;
221
+ encoding: z.ZodOptional<z.ZodEnum<{
222
+ hex: "hex";
223
+ base64: "base64";
224
+ }>>;
225
+ prefix: z.ZodOptional<z.ZodString>;
133
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>>;
134
266
  }, z.core.$strip>>;
135
267
  polling: z.ZodOptional<z.ZodObject<{
136
268
  everyMinutes: z.ZodNumber;
269
+ mode: z.ZodOptional<z.ZodEnum<{
270
+ updatedSince: "updatedSince";
271
+ full: "full";
272
+ }>>;
137
273
  }, z.core.$strip>>;
138
274
  }, z.core.$strip>;
139
275
  writeBack: z.ZodDefault<z.ZodRecord<z.ZodEnum<{
@@ -150,12 +286,27 @@ export declare const AppVersionRecord: z.ZodObject<{
150
286
  PATCH: "PATCH";
151
287
  }>;
152
288
  path: z.ZodString;
289
+ body: z.ZodOptional<z.ZodJSONSchema>;
290
+ idPath: z.ZodOptional<z.ZodString>;
153
291
  }, z.core.$strip>>>;
154
292
  limits: z.ZodObject<{
155
293
  requestsPerSecond: z.ZodNumber;
156
294
  burst: z.ZodOptional<z.ZodNumber>;
157
295
  retryAfterHeader: z.ZodDefault<z.ZodString>;
158
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>>;
159
310
  }, z.core.$strip>>;
160
311
  ui: z.ZodOptional<z.ZodObject<{
161
312
  apiVersion: z.ZodLiteral<1>;
@@ -212,10 +363,10 @@ export declare const AppVersionRecord: z.ZodObject<{
212
363
  }, z.core.$strip>], "type">>>;
213
364
  endpoints: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
214
365
  method: z.ZodEnum<{
366
+ GET: "GET";
215
367
  POST: "POST";
216
368
  PUT: "PUT";
217
369
  PATCH: "PATCH";
218
- GET: "GET";
219
370
  DELETE: "DELETE";
220
371
  }>;
221
372
  url: z.ZodURL;