@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/sync.d.ts ADDED
@@ -0,0 +1,338 @@
1
+ import { z } from 'zod';
2
+ import { RecordKind, type SyncManifest } from './manifest.ts';
3
+ /**
4
+ * Added in 0.4.0 (L2 Sync). The engine core's vocabulary: connector kinds, the CRM of record, external refs, health,
5
+ * import progress, mapping preview, write-back items, the driver interface both connector kinds implement, and the RPC the
6
+ * engine adds. Skill devops-loops-developer-platform §2 #1 #4 #5, §3. Only ever ADD.
7
+ */
8
+ /** `native` = first-party code we build (HubSpot, Salesforce…); `manifest` = a developer app's `sync`, run by the manifest driver. */
9
+ export declare const ConnectorKind: z.ZodEnum<{
10
+ manifest: "manifest";
11
+ native: "native";
12
+ }>;
13
+ export type ConnectorKind = z.infer<typeof ConnectorKind>;
14
+ /** `TenantApps.crmConnector` in Vista. Null there = the native Loops CRM is the CRM of record. One per workspace. */
15
+ export declare const CrmConnector: z.ZodObject<{
16
+ appId: z.ZodString;
17
+ installId: z.ZodString;
18
+ kind: z.ZodEnum<{
19
+ manifest: "manifest";
20
+ native: "native";
21
+ }>;
22
+ setBy: z.ZodString;
23
+ setAt: z.ZodISODateTime;
24
+ }, z.core.$strip>;
25
+ export type CrmConnector = z.infer<typeof CrmConnector>;
26
+ /** A Loops record's identity at the other side (the link table in Vista). */
27
+ export declare const ExternalRef: z.ZodObject<{
28
+ appId: z.ZodString;
29
+ installId: z.ZodString;
30
+ object: z.ZodString;
31
+ externalId: z.ZodString;
32
+ lockedFields: z.ZodDefault<z.ZodArray<z.ZodString>>;
33
+ linkedAt: z.ZodISODateTime;
34
+ }, z.core.$strip>;
35
+ export type ExternalRef = z.infer<typeof ExternalRef>;
36
+ /**
37
+ * `connecting` (no credentials yet) · `needs_auth` (token refused; an admin reconnects) · `importing` (first import running)
38
+ * · `healthy` · `degraded` (calls failing or rate limited, retrying) · `paused` (auto-pause or an admin paused it).
39
+ */
40
+ export declare const SyncHealth: z.ZodEnum<{
41
+ paused: "paused";
42
+ connecting: "connecting";
43
+ needs_auth: "needs_auth";
44
+ importing: "importing";
45
+ healthy: "healthy";
46
+ degraded: "degraded";
47
+ }>;
48
+ export type SyncHealth = z.infer<typeof SyncHealth>;
49
+ export declare const ObjectProgress: z.ZodObject<{
50
+ object: z.ZodString;
51
+ ours: z.ZodEnum<{
52
+ CONTACT: "CONTACT";
53
+ COMPANY: "COMPANY";
54
+ LEAD: "LEAD";
55
+ DEAL: "DEAL";
56
+ }>;
57
+ enabled: z.ZodBoolean;
58
+ read: z.ZodNumber;
59
+ created: z.ZodNumber;
60
+ matched: z.ZodNumber;
61
+ failed: z.ZodNumber;
62
+ total: z.ZodNullable<z.ZodNumber>;
63
+ lastChangeAt: z.ZodNullable<z.ZodISODateTime>;
64
+ }, z.core.$strip>;
65
+ export type ObjectProgress = z.infer<typeof ObjectProgress>;
66
+ export declare const ImportState: z.ZodEnum<{
67
+ failed: "failed";
68
+ not_started: "not_started";
69
+ running: "running";
70
+ done: "done";
71
+ }>;
72
+ export declare const SyncStatus: z.ZodObject<{
73
+ installId: z.ZodString;
74
+ appId: z.ZodString;
75
+ kind: z.ZodEnum<{
76
+ manifest: "manifest";
77
+ native: "native";
78
+ }>;
79
+ health: z.ZodEnum<{
80
+ paused: "paused";
81
+ connecting: "connecting";
82
+ needs_auth: "needs_auth";
83
+ importing: "importing";
84
+ healthy: "healthy";
85
+ degraded: "degraded";
86
+ }>;
87
+ import: z.ZodObject<{
88
+ state: z.ZodEnum<{
89
+ failed: "failed";
90
+ not_started: "not_started";
91
+ running: "running";
92
+ done: "done";
93
+ }>;
94
+ runId: z.ZodNullable<z.ZodString>;
95
+ startedAt: z.ZodNullable<z.ZodISODateTime>;
96
+ finishedAt: z.ZodNullable<z.ZodISODateTime>;
97
+ }, z.core.$strip>;
98
+ objects: z.ZodArray<z.ZodObject<{
99
+ object: z.ZodString;
100
+ ours: z.ZodEnum<{
101
+ CONTACT: "CONTACT";
102
+ COMPANY: "COMPANY";
103
+ LEAD: "LEAD";
104
+ DEAL: "DEAL";
105
+ }>;
106
+ enabled: z.ZodBoolean;
107
+ read: z.ZodNumber;
108
+ created: z.ZodNumber;
109
+ matched: z.ZodNumber;
110
+ failed: z.ZodNumber;
111
+ total: z.ZodNullable<z.ZodNumber>;
112
+ lastChangeAt: z.ZodNullable<z.ZodISODateTime>;
113
+ }, z.core.$strip>>;
114
+ changes: z.ZodObject<{
115
+ inLastHour: z.ZodNumber;
116
+ outLastHour: z.ZodNumber;
117
+ echoesDropped: z.ZodNumber;
118
+ deadLettered: z.ZodNumber;
119
+ }, z.core.$strip>;
120
+ lastError: z.ZodNullable<z.ZodObject<{
121
+ at: z.ZodISODateTime;
122
+ message: z.ZodString;
123
+ }, z.core.$strip>>;
124
+ nextPollAt: z.ZodNullable<z.ZodISODateTime>;
125
+ }, z.core.$strip>;
126
+ export type SyncStatus = z.infer<typeof SyncStatus>;
127
+ /** A few of their records shown beside what Loops would make of them, before the first import. */
128
+ export declare const MappingPreview: z.ZodObject<{
129
+ object: z.ZodString;
130
+ ours: z.ZodEnum<{
131
+ CONTACT: "CONTACT";
132
+ COMPANY: "COMPANY";
133
+ LEAD: "LEAD";
134
+ DEAL: "DEAL";
135
+ }>;
136
+ fields: z.ZodArray<z.ZodObject<{
137
+ theirs: z.ZodString;
138
+ ours: z.ZodString;
139
+ owner: z.ZodEnum<{
140
+ theirs: "theirs";
141
+ ours: "ours";
142
+ }>;
143
+ }, z.core.$strip>>;
144
+ rows: z.ZodArray<z.ZodObject<{
145
+ externalId: z.ZodString;
146
+ values: z.ZodRecord<z.ZodString, z.ZodUnknown>;
147
+ links: z.ZodArray<z.ZodObject<{
148
+ to: z.ZodEnum<{
149
+ CONTACT: "CONTACT";
150
+ COMPANY: "COMPANY";
151
+ LEAD: "LEAD";
152
+ DEAL: "DEAL";
153
+ }>;
154
+ role: z.ZodNullable<z.ZodString>;
155
+ externalIds: z.ZodArray<z.ZodString>;
156
+ }, z.core.$strip>>;
157
+ }, z.core.$strip>>;
158
+ }, z.core.$strip>;
159
+ export type MappingPreview = z.infer<typeof MappingPreview>;
160
+ /** What the admin picked in the bring-your-own-CRM setup. */
161
+ export declare const SyncSettings: z.ZodObject<{
162
+ objects: z.ZodDefault<z.ZodArray<z.ZodString>>;
163
+ crmOfRecord: z.ZodDefault<z.ZodBoolean>;
164
+ }, z.core.$strip>;
165
+ export type SyncSettings = z.infer<typeof SyncSettings>;
166
+ /** One thing Loops owns, written to their timeline. `{{item.*}}` in a write-back body template. */
167
+ export declare const WriteBackItem: z.ZodObject<{
168
+ kind: z.ZodEnum<{
169
+ call: "call";
170
+ transcript: "transcript";
171
+ meeting: "meeting";
172
+ note: "note";
173
+ action: "action";
174
+ campaignReply: "campaignReply";
175
+ }>;
176
+ loopsId: z.ZodString;
177
+ title: z.ZodString;
178
+ text: z.ZodString;
179
+ markdown: z.ZodNullable<z.ZodString>;
180
+ occurredAt: z.ZodISODateTime;
181
+ durationSeconds: z.ZodNullable<z.ZodNumber>;
182
+ direction: z.ZodNullable<z.ZodEnum<{
183
+ inbound: "inbound";
184
+ outbound: "outbound";
185
+ }>>;
186
+ url: z.ZodNullable<z.ZodString>;
187
+ byEmail: z.ZodNullable<z.ZodString>;
188
+ }, z.core.$strip>;
189
+ export type WriteBackItem = z.infer<typeof WriteBackItem>;
190
+ /**
191
+ * Names a body template may use. In paths only `{id}` and `{object}`.
192
+ * - list body: `{{limit}}`, `{{offset}}`, `{{page}}`, `{{cursor}}`, `{{updatedSince}}`
193
+ * - create / update body: `{{ours.<field>}}` (a Loops field from the object's map), `{{record.id}}`
194
+ * - write-back body: `{{item.<field>}}` (WriteBackItem), `{{record.id}}`, `{{record.object}}`
195
+ * - webhook register body: `{{hookUrl}}`, `{{object}}`
196
+ */
197
+ export declare const TEMPLATE_VARIABLES: {
198
+ readonly list: readonly ["limit", "offset", "page", "cursor", "updatedSince"];
199
+ readonly record: readonly ["ours.*", "record.id"];
200
+ readonly writeBack: readonly ["item.*", "record.id", "record.object"];
201
+ readonly register: readonly ["hookUrl", "object"];
202
+ };
203
+ /** One of their records, already mapped to Loops field names (ownership is the core's job, not the driver's). */
204
+ export interface ExternalRecord {
205
+ object: string;
206
+ externalId: string;
207
+ updatedAt: string | null;
208
+ /** Loops field → value, every mapped field present (null when empty). */
209
+ values: Record<string, unknown>;
210
+ links: {
211
+ to: RecordKind;
212
+ role: string | null;
213
+ externalIds: string[];
214
+ }[];
215
+ /** SHA-256 of the mapped values (sorted keys): the echo guard and full-mode polling compare this. */
216
+ hash: string;
217
+ }
218
+ export interface ListPage {
219
+ records: ExternalRecord[];
220
+ /** Opaque; pass back for the next page. Null on the last page. */
221
+ next: string | null;
222
+ /** Their total when the API says it. */
223
+ total: number | null;
224
+ }
225
+ /** A change their webhook announced. `record` is filled when the driver could read it (created/updated). */
226
+ export interface HookChange {
227
+ type: 'created' | 'updated' | 'deleted';
228
+ object: string;
229
+ externalId: string;
230
+ record: ExternalRecord | null;
231
+ }
232
+ /**
233
+ * The only door a driver has to their API. The core adds the base URL, the credentials (from the vault, refreshed when
234
+ * expired), the rate limit and `Retry-After`; it answers 401/403 by marking the install `needs_auth`. A driver never sees a secret.
235
+ */
236
+ export interface ConnectorHttp {
237
+ request(method: string, path: string, init?: {
238
+ query?: Record<string, string>;
239
+ body?: unknown;
240
+ }): Promise<{
241
+ status: number;
242
+ headers: Headers;
243
+ json: unknown;
244
+ }>;
245
+ }
246
+ export interface ConnectorDriver {
247
+ readonly kind: ConnectorKind;
248
+ /** Their objects this connector syncs, in import order (companies before the people who link to them). */
249
+ objects(): {
250
+ object: string;
251
+ ours: RecordKind;
252
+ fields: {
253
+ theirs: string;
254
+ ours: string;
255
+ owner: 'theirs' | 'ours';
256
+ }[];
257
+ match: string[];
258
+ }[];
259
+ test(http: ConnectorHttp): Promise<{
260
+ ok: boolean;
261
+ message: string;
262
+ }>;
263
+ list(http: ConnectorHttp, object: string, page: {
264
+ next: string | null;
265
+ updatedSince: string | null;
266
+ }): Promise<ListPage>;
267
+ get(http: ConnectorHttp, object: string, externalId: string): Promise<ExternalRecord | null>;
268
+ /** Null when the connector cannot create this object. */
269
+ create(http: ConnectorHttp, object: string, values: Record<string, unknown>): Promise<{
270
+ externalId: string;
271
+ } | null>;
272
+ /** `values` holds only Loops-owned fields. False when the connector cannot update this object. */
273
+ update(http: ConnectorHttp, object: string, externalId: string, values: Record<string, unknown>): Promise<boolean>;
274
+ /** Null when the connector does not declare this kind. */
275
+ writeBack(http: ConnectorHttp, target: {
276
+ object: string;
277
+ externalId: string;
278
+ }, item: WriteBackItem): Promise<{
279
+ externalId: string | null;
280
+ } | null>;
281
+ verifyHook(rawBody: string, headers: Headers, secret: string): Promise<boolean>;
282
+ /** After verifyHook. Reads created/updated records with `get`. */
283
+ parseHook(http: ConnectorHttp, rawBody: string): Promise<HookChange[]>;
284
+ /** Null when the connector does not register webhooks itself. */
285
+ registerHook(http: ConnectorHttp, hookUrl: string): Promise<{
286
+ hookId: string | null;
287
+ secret: string;
288
+ } | null>;
289
+ unregisterHook(http: ConnectorHttp, hookId: string): Promise<void>;
290
+ }
291
+ /** The manifest driver's constructor (`@withloops.ai/contracts/connector`). */
292
+ export type CreateManifestDriver = (sync: SyncManifest) => ConnectorDriver;
293
+ export declare const SyncCaller: z.ZodObject<{
294
+ workspaceId: z.ZodString;
295
+ userId: z.ZodString;
296
+ }, z.core.$strip>;
297
+ /** `backend-apps-engine` default WorkerEntrypoint implements this as well as EngineRpc + EngineVaultRpc. */
298
+ export interface EngineSyncRpc {
299
+ syncStatus(installId: string): Promise<SyncStatus | null>;
300
+ /** Save the admin's picks; starts nothing. */
301
+ configureSync(installId: string, settings: SyncSettings): Promise<{
302
+ ok: true;
303
+ } | {
304
+ ok: false;
305
+ error: string;
306
+ }>;
307
+ previewMapping(installId: string, object: string, limit?: number): Promise<{
308
+ ok: true;
309
+ preview: MappingPreview;
310
+ } | {
311
+ ok: false;
312
+ error: string;
313
+ }>;
314
+ /** Starts the first import as a Workflow (resumable). Safe to call twice: a running import is returned, not restarted. */
315
+ startImport(installId: string): Promise<{
316
+ ok: true;
317
+ runId: string;
318
+ } | {
319
+ ok: false;
320
+ error: string;
321
+ }>;
322
+ pauseSync(installId: string, paused: boolean): Promise<{
323
+ ok: true;
324
+ }>;
325
+ /** Loops wrote something it owns (call, note…) on a record linked to this install; queued, written to their timeline. */
326
+ writeBack(installId: string, record: {
327
+ kind: z.infer<typeof RecordKind>;
328
+ loopsId: string;
329
+ }, item: WriteBackItem): Promise<{
330
+ ok: true;
331
+ queued: true;
332
+ }>;
333
+ /** Check the credentials with the connector's test call. */
334
+ testConnection(installId: string): Promise<{
335
+ ok: boolean;
336
+ message: string;
337
+ }>;
338
+ }
package/dist/sync.js ADDED
@@ -0,0 +1,123 @@
1
+ import { z } from 'zod';
2
+ import { AppId, InstallId, Timestamp, UserId, WorkspaceId } from "./ids.js";
3
+ import { RecordKind, WriteBackKind } from "./manifest.js";
4
+ /**
5
+ * Added in 0.4.0 (L2 Sync). The engine core's vocabulary: connector kinds, the CRM of record, external refs, health,
6
+ * import progress, mapping preview, write-back items, the driver interface both connector kinds implement, and the RPC the
7
+ * engine adds. Skill devops-loops-developer-platform §2 #1 #4 #5, §3. Only ever ADD.
8
+ */
9
+ // ── Connector kinds and the CRM of record ───────────────────────────────────────
10
+ /** `native` = first-party code we build (HubSpot, Salesforce…); `manifest` = a developer app's `sync`, run by the manifest driver. */
11
+ export const ConnectorKind = z.enum(['native', 'manifest']);
12
+ /** `TenantApps.crmConnector` in Vista. Null there = the native Loops CRM is the CRM of record. One per workspace. */
13
+ export const CrmConnector = z.object({
14
+ appId: AppId,
15
+ installId: InstallId,
16
+ kind: ConnectorKind,
17
+ setBy: UserId,
18
+ setAt: Timestamp,
19
+ });
20
+ /** A Loops record's identity at the other side (the link table in Vista). */
21
+ export const ExternalRef = z.object({
22
+ appId: AppId,
23
+ installId: InstallId,
24
+ /** Their object name (`people`). */
25
+ object: z.string().min(1),
26
+ externalId: z.string().min(1).max(200),
27
+ /** Loops fields their CRM owns on this record: locked in Loops ("Edited in Attio"). */
28
+ lockedFields: z.array(z.string()).default([]),
29
+ linkedAt: Timestamp,
30
+ });
31
+ // ── Health, import, preview ─────────────────────────────────────────────────────
32
+ /**
33
+ * `connecting` (no credentials yet) · `needs_auth` (token refused; an admin reconnects) · `importing` (first import running)
34
+ * · `healthy` · `degraded` (calls failing or rate limited, retrying) · `paused` (auto-pause or an admin paused it).
35
+ */
36
+ export const SyncHealth = z.enum(['connecting', 'needs_auth', 'importing', 'healthy', 'degraded', 'paused']);
37
+ export const ObjectProgress = z.object({
38
+ object: z.string(),
39
+ ours: RecordKind,
40
+ enabled: z.boolean(),
41
+ /** Records read from their side in the first import. */
42
+ read: z.number().int().min(0),
43
+ created: z.number().int().min(0),
44
+ matched: z.number().int().min(0),
45
+ failed: z.number().int().min(0),
46
+ /** Their total when their API says it; null otherwise. */
47
+ total: z.number().int().min(0).nullable(),
48
+ lastChangeAt: Timestamp.nullable(),
49
+ });
50
+ export const ImportState = z.enum(['not_started', 'running', 'done', 'failed']);
51
+ export const SyncStatus = z.object({
52
+ installId: InstallId,
53
+ appId: AppId,
54
+ kind: ConnectorKind,
55
+ health: SyncHealth,
56
+ import: z.object({
57
+ state: ImportState,
58
+ runId: z.string().nullable(),
59
+ startedAt: Timestamp.nullable(),
60
+ finishedAt: Timestamp.nullable(),
61
+ }),
62
+ objects: z.array(ObjectProgress),
63
+ changes: z.object({
64
+ inLastHour: z.number().int().min(0),
65
+ outLastHour: z.number().int().min(0),
66
+ echoesDropped: z.number().int().min(0),
67
+ deadLettered: z.number().int().min(0),
68
+ }),
69
+ /** Plain words, the last problem only. Never a secret, never a stack trace. */
70
+ lastError: z.object({ at: Timestamp, message: z.string() }).nullable(),
71
+ nextPollAt: Timestamp.nullable(),
72
+ });
73
+ /** A few of their records shown beside what Loops would make of them, before the first import. */
74
+ export const MappingPreview = z.object({
75
+ object: z.string(),
76
+ ours: RecordKind,
77
+ fields: z.array(z.object({ theirs: z.string(), ours: z.string(), owner: z.enum(['theirs', 'ours']) })),
78
+ rows: z.array(z.object({
79
+ externalId: z.string(),
80
+ values: z.record(z.string(), z.unknown()),
81
+ links: z.array(z.object({ to: RecordKind, role: z.string().nullable(), externalIds: z.array(z.string()) })),
82
+ })),
83
+ });
84
+ /** What the admin picked in the bring-your-own-CRM setup. */
85
+ export const SyncSettings = z.object({
86
+ /** Their object names to sync; every object in the manifest when empty. */
87
+ objects: z.array(z.string()).default([]),
88
+ /** Make this install the workspace's CRM of record (`TenantApps.crmConnector`). */
89
+ crmOfRecord: z.boolean().default(false),
90
+ });
91
+ // ── Write-back ──────────────────────────────────────────────────────────────────
92
+ /** One thing Loops owns, written to their timeline. `{{item.*}}` in a write-back body template. */
93
+ export const WriteBackItem = z.object({
94
+ kind: WriteBackKind,
95
+ /** Loops id of the thing (call, note…), for the echo guard and to find it again. */
96
+ loopsId: z.string(),
97
+ title: z.string().max(300),
98
+ /** Plain text. */
99
+ text: z.string().max(100_000),
100
+ markdown: z.string().max(100_000).nullable(),
101
+ occurredAt: Timestamp,
102
+ durationSeconds: z.number().int().min(0).nullable(),
103
+ direction: z.enum(['inbound', 'outbound']).nullable(),
104
+ /** Link back to the item in Loops. */
105
+ url: z.string().nullable(),
106
+ byEmail: z.string().nullable(),
107
+ });
108
+ // ── Template variables (manifest BodyTemplate) ──────────────────────────────────
109
+ /**
110
+ * Names a body template may use. In paths only `{id}` and `{object}`.
111
+ * - list body: `{{limit}}`, `{{offset}}`, `{{page}}`, `{{cursor}}`, `{{updatedSince}}`
112
+ * - create / update body: `{{ours.<field>}}` (a Loops field from the object's map), `{{record.id}}`
113
+ * - write-back body: `{{item.<field>}}` (WriteBackItem), `{{record.id}}`, `{{record.object}}`
114
+ * - webhook register body: `{{hookUrl}}`, `{{object}}`
115
+ */
116
+ export const TEMPLATE_VARIABLES = {
117
+ list: ['limit', 'offset', 'page', 'cursor', 'updatedSince'],
118
+ record: ['ours.*', 'record.id'],
119
+ writeBack: ['item.*', 'record.id', 'record.object'],
120
+ register: ['hookUrl', 'object'],
121
+ };
122
+ // ── Engine RPC added in L2 ──────────────────────────────────────────────────────
123
+ export const SyncCaller = z.object({ workspaceId: WorkspaceId, userId: UserId });
package/dist/trust.d.ts CHANGED
@@ -136,9 +136,9 @@ export declare function appClientSecretRef(appId: string, ownerWorkspaceId: stri
136
136
  /** Engine-side code exchange for an app connecting OUT (the handoff at api.thesoogroup.com/oauth/callback). Added in 0.3.1. */
137
137
  export declare const OAuthCodeExchange: z.ZodObject<{
138
138
  install: z.ZodObject<{
139
+ appId: z.ZodString;
139
140
  workspaceId: z.ZodString;
140
141
  installId: z.ZodString;
141
- appId: z.ZodString;
142
142
  }, z.core.$strip>;
143
143
  ownerWorkspaceId: z.ZodString;
144
144
  tokenUrl: z.ZodString;
@@ -0,0 +1,146 @@
1
+ {
2
+ "manifestVersion": 1,
3
+ "id": "attio-sync",
4
+ "name": "Attio sync",
5
+ "version": "0.1.0",
6
+ "summary": "Use Attio as your CRM of record: people and companies come into Loops, calls and notes go to their Attio timeline.",
7
+ "developer": { "name": "Loops developer platform proof", "email": "developers@withloops.ai", "website": "https://withloops.ai" },
8
+ "visibility": "private",
9
+ "scopes": ["contacts:write", "companies:write"],
10
+ "sync": {
11
+ "baseUrl": "https://api.attio.com",
12
+ "auth": {
13
+ "type": "apiKey",
14
+ "header": "Authorization",
15
+ "prefix": "Bearer ",
16
+ "label": "Attio access token",
17
+ "helpUrl": "https://attio.com/help/apps/other-apps/generating-an-api-key"
18
+ },
19
+ "test": { "method": "GET", "path": "/v2/self" },
20
+ "objects": [
21
+ {
22
+ "theirs": "companies",
23
+ "ours": "COMPANY",
24
+ "idPath": "id.record_id",
25
+ "list": {
26
+ "path": "/v2/objects/companies/records/query",
27
+ "method": "POST",
28
+ "itemsPath": "data",
29
+ "body": { "limit": "{{limit}}", "offset": "{{offset}}" },
30
+ "paging": { "style": "offset", "param": "offset", "sizeParam": "limit", "size": 100, "in": "body" }
31
+ },
32
+ "get": { "method": "GET", "path": "/v2/objects/companies/records/{id}", "itemPath": "data" },
33
+ "create": {
34
+ "method": "POST",
35
+ "path": "/v2/objects/companies/records",
36
+ "itemPath": "data",
37
+ "body": { "data": { "values": { "name": "{{ours.name}}", "domains": ["{{ours.domain}}"] } } }
38
+ },
39
+ "fields": [
40
+ { "theirs": "values.name[0].value", "ours": "name", "owner": "theirs", "transform": ["trim"] },
41
+ { "theirs": "values.domains[0].domain", "ours": "domain", "owner": "theirs", "transform": ["lower"] }
42
+ ],
43
+ "match": ["domain"]
44
+ },
45
+ {
46
+ "theirs": "people",
47
+ "ours": "CONTACT",
48
+ "idPath": "id.record_id",
49
+ "list": {
50
+ "path": "/v2/objects/people/records/query",
51
+ "method": "POST",
52
+ "itemsPath": "data",
53
+ "body": { "limit": "{{limit}}", "offset": "{{offset}}" },
54
+ "paging": { "style": "offset", "param": "offset", "sizeParam": "limit", "size": 100, "in": "body" }
55
+ },
56
+ "get": { "method": "GET", "path": "/v2/objects/people/records/{id}", "itemPath": "data" },
57
+ "create": {
58
+ "method": "POST",
59
+ "path": "/v2/objects/people/records",
60
+ "itemPath": "data",
61
+ "body": {
62
+ "data": {
63
+ "values": {
64
+ "name": { "first_name": "{{ours.firstName}}", "last_name": "{{ours.lastName}}", "full_name": "{{ours.firstName}} {{ours.lastName}}" },
65
+ "email_addresses": ["{{ours.email}}"],
66
+ "job_title": "{{ours.jobTitle}}"
67
+ }
68
+ }
69
+ }
70
+ },
71
+ "fields": [
72
+ { "theirs": "values.name[0].first_name", "ours": "firstName", "owner": "theirs", "transform": ["trim"] },
73
+ { "theirs": "values.name[0].last_name", "ours": "lastName", "owner": "theirs", "transform": ["trim"] },
74
+ { "theirs": "values.email_addresses[0].email_address", "ours": "email", "owner": "theirs", "transform": ["lower"] },
75
+ { "theirs": "values.phone_numbers[0].original_phone_number", "ours": "phone", "owner": "theirs" },
76
+ { "theirs": "values.job_title[0].value", "ours": "jobTitle", "owner": "theirs" }
77
+ ],
78
+ "match": ["email"]
79
+ }
80
+ ],
81
+ "links": [
82
+ { "from": "CONTACT", "to": "COMPANY", "theirs": "values.company[0].target_record_id", "role": "employee" }
83
+ ],
84
+ "changeFeed": {
85
+ "webhook": {
86
+ "signature": { "scheme": "hmac-sha256", "header": "Attio-Signature", "encoding": "hex" },
87
+ "eventsPath": "events",
88
+ "typePath": "event_type",
89
+ "idPath": "id.record_id",
90
+ "objectPath": "id.object_id",
91
+ "types": { "created": ["record.created"], "updated": ["record.updated"], "deleted": ["record.deleted"] },
92
+ "register": {
93
+ "call": {
94
+ "method": "POST",
95
+ "path": "/v2/webhooks",
96
+ "body": {
97
+ "data": {
98
+ "target_url": "{{hookUrl}}",
99
+ "subscriptions": [
100
+ { "event_type": "record.created", "filter": null },
101
+ { "event_type": "record.updated", "filter": null },
102
+ { "event_type": "record.deleted", "filter": null }
103
+ ]
104
+ }
105
+ }
106
+ },
107
+ "secretPath": "data.secret",
108
+ "idPath": "data.id.webhook_id",
109
+ "unregister": { "method": "DELETE", "path": "/v2/webhooks/{id}" }
110
+ }
111
+ },
112
+ "polling": { "everyMinutes": 60, "mode": "full" }
113
+ },
114
+ "writeBack": {
115
+ "note": {
116
+ "method": "POST",
117
+ "path": "/v2/notes",
118
+ "idPath": "data.id.note_id",
119
+ "body": {
120
+ "data": {
121
+ "parent_object": "{{record.object}}",
122
+ "parent_record_id": "{{record.id}}",
123
+ "title": "{{item.title}}",
124
+ "format": "markdown",
125
+ "content": "{{item.text}}"
126
+ }
127
+ }
128
+ },
129
+ "call": {
130
+ "method": "POST",
131
+ "path": "/v2/notes",
132
+ "idPath": "data.id.note_id",
133
+ "body": {
134
+ "data": {
135
+ "parent_object": "{{record.object}}",
136
+ "parent_record_id": "{{record.id}}",
137
+ "title": "{{item.title}}",
138
+ "format": "plaintext",
139
+ "content": "{{item.text}}"
140
+ }
141
+ }
142
+ }
143
+ },
144
+ "limits": { "requestsPerSecond": 20, "burst": 25, "retryAfterHeader": "Retry-After" }
145
+ }
146
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withloops.ai/contracts",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "The Loops developer platform contracts (zod): app manifest v1, scopes, events, the /v1 API envelope and registry records. Everything else builds on these.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",