@withloops.ai/contracts 0.1.0 → 0.2.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/api.d.ts ADDED
@@ -0,0 +1,148 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * The public REST API envelope: `api.withloops.ai/v1`. One envelope for every resource, cursor paging, one error shape.
4
+ * Only ever ADD fields (versioning promise). Hono + @hono/zod-openapi build the OpenAPI spec from these schemas.
5
+ */
6
+ export declare const API_VERSION: "v1";
7
+ /** Every error code the API returns, with its HTTP status. */
8
+ export declare const ERROR_CODES: {
9
+ readonly bad_request: 400;
10
+ readonly validation_failed: 422;
11
+ readonly unauthorized: 401;
12
+ readonly forbidden: 403;
13
+ readonly missing_scope: 403;
14
+ readonly not_found: 404;
15
+ readonly conflict: 409;
16
+ readonly rate_limited: 429;
17
+ readonly internal: 500;
18
+ readonly upstream_timeout: 504;
19
+ readonly unavailable: 503;
20
+ };
21
+ export type ErrorCode = keyof typeof ERROR_CODES;
22
+ export declare const ErrorCodeSchema: z.ZodEnum<{
23
+ bad_request: "bad_request";
24
+ validation_failed: "validation_failed";
25
+ unauthorized: "unauthorized";
26
+ forbidden: "forbidden";
27
+ missing_scope: "missing_scope";
28
+ not_found: "not_found";
29
+ conflict: "conflict";
30
+ rate_limited: "rate_limited";
31
+ internal: "internal";
32
+ upstream_timeout: "upstream_timeout";
33
+ unavailable: "unavailable";
34
+ }>;
35
+ export declare const ApiError: z.ZodObject<{
36
+ code: z.ZodEnum<{
37
+ bad_request: "bad_request";
38
+ validation_failed: "validation_failed";
39
+ unauthorized: "unauthorized";
40
+ forbidden: "forbidden";
41
+ missing_scope: "missing_scope";
42
+ not_found: "not_found";
43
+ conflict: "conflict";
44
+ rate_limited: "rate_limited";
45
+ internal: "internal";
46
+ upstream_timeout: "upstream_timeout";
47
+ unavailable: "unavailable";
48
+ }>;
49
+ message: z.ZodString;
50
+ fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
51
+ scope: z.ZodOptional<z.ZodString>;
52
+ requestId: z.ZodString;
53
+ }, z.core.$strip>;
54
+ export type ApiError = z.infer<typeof ApiError>;
55
+ export declare const ErrorEnvelope: z.ZodObject<{
56
+ ok: z.ZodLiteral<false>;
57
+ error: z.ZodObject<{
58
+ code: z.ZodEnum<{
59
+ bad_request: "bad_request";
60
+ validation_failed: "validation_failed";
61
+ unauthorized: "unauthorized";
62
+ forbidden: "forbidden";
63
+ missing_scope: "missing_scope";
64
+ not_found: "not_found";
65
+ conflict: "conflict";
66
+ rate_limited: "rate_limited";
67
+ internal: "internal";
68
+ upstream_timeout: "upstream_timeout";
69
+ unavailable: "unavailable";
70
+ }>;
71
+ message: z.ZodString;
72
+ fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
73
+ scope: z.ZodOptional<z.ZodString>;
74
+ requestId: z.ZodString;
75
+ }, z.core.$strip>;
76
+ }, z.core.$strip>;
77
+ export type ErrorEnvelope = z.infer<typeof ErrorEnvelope>;
78
+ export declare function errorStatus(code: ErrorCode): number;
79
+ export declare const Page: z.ZodObject<{
80
+ nextCursor: z.ZodNullable<z.ZodString>;
81
+ hasMore: z.ZodBoolean;
82
+ }, z.core.$strip>;
83
+ export type Page = z.infer<typeof Page>;
84
+ /** Single object: `{ ok: true, data }`. */
85
+ export declare function dataEnvelope<T extends z.ZodType>(data: T): z.ZodObject<{
86
+ ok: z.ZodLiteral<true>;
87
+ data: T;
88
+ }, z.core.$strip>;
89
+ /** List: `{ ok: true, data: [...], page: { nextCursor, hasMore } }`. */
90
+ export declare function listEnvelope<T extends z.ZodType>(item: T): z.ZodObject<{
91
+ ok: z.ZodLiteral<true>;
92
+ data: z.ZodArray<T>;
93
+ page: z.ZodObject<{
94
+ nextCursor: z.ZodNullable<z.ZodString>;
95
+ hasMore: z.ZodBoolean;
96
+ }, z.core.$strip>;
97
+ }, z.core.$strip>;
98
+ export type DataEnvelope<T> = {
99
+ ok: true;
100
+ data: T;
101
+ };
102
+ export type ListEnvelope<T> = {
103
+ ok: true;
104
+ data: T[];
105
+ page: Page;
106
+ };
107
+ export declare const PAGE_LIMIT_DEFAULT = 25;
108
+ export declare const PAGE_LIMIT_MAX = 100;
109
+ export declare const ListQuery: z.ZodObject<{
110
+ cursor: z.ZodOptional<z.ZodString>;
111
+ limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
112
+ updatedSince: z.ZodOptional<z.ZodISODateTime>;
113
+ }, z.core.$strip>;
114
+ export type ListQuery = z.infer<typeof ListQuery>;
115
+ /** A Loops contact as the public API shows it. camelCase, ids as strings, dates ISO. */
116
+ export declare const Contact: z.ZodObject<{
117
+ id: z.ZodString;
118
+ firstName: z.ZodNullable<z.ZodString>;
119
+ lastName: z.ZodNullable<z.ZodString>;
120
+ email: z.ZodNullable<z.ZodEmail>;
121
+ phone: z.ZodNullable<z.ZodString>;
122
+ jobTitle: z.ZodNullable<z.ZodString>;
123
+ companyId: z.ZodNullable<z.ZodString>;
124
+ ownerId: z.ZodNullable<z.ZodString>;
125
+ createdAt: z.ZodISODateTime;
126
+ updatedAt: z.ZodISODateTime;
127
+ }, z.core.$strip>;
128
+ export type Contact = z.infer<typeof Contact>;
129
+ export declare const ContactCreate: z.ZodObject<{
130
+ firstName: z.ZodOptional<z.ZodString>;
131
+ lastName: z.ZodOptional<z.ZodString>;
132
+ email: z.ZodOptional<z.ZodEmail>;
133
+ phone: z.ZodOptional<z.ZodString>;
134
+ jobTitle: z.ZodOptional<z.ZodString>;
135
+ companyId: z.ZodOptional<z.ZodString>;
136
+ ownerId: z.ZodOptional<z.ZodString>;
137
+ }, z.core.$strip>;
138
+ export type ContactCreate = z.infer<typeof ContactCreate>;
139
+ export declare const ContactUpdate: z.ZodObject<{
140
+ firstName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
141
+ lastName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
142
+ email: z.ZodOptional<z.ZodNullable<z.ZodEmail>>;
143
+ phone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
144
+ jobTitle: z.ZodOptional<z.ZodNullable<z.ZodString>>;
145
+ companyId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
146
+ ownerId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
147
+ }, z.core.$strip>;
148
+ export type ContactUpdate = z.infer<typeof ContactUpdate>;
package/dist/api.js ADDED
@@ -0,0 +1,95 @@
1
+ import { z } from 'zod';
2
+ import { Timestamp } from "./ids.js";
3
+ /**
4
+ * The public REST API envelope: `api.withloops.ai/v1`. One envelope for every resource, cursor paging, one error shape.
5
+ * Only ever ADD fields (versioning promise). Hono + @hono/zod-openapi build the OpenAPI spec from these schemas.
6
+ */
7
+ export const API_VERSION = 'v1';
8
+ // ── Errors ──────────────────────────────────────────────────────────────────────
9
+ /** Every error code the API returns, with its HTTP status. */
10
+ export const ERROR_CODES = {
11
+ bad_request: 400,
12
+ validation_failed: 422,
13
+ unauthorized: 401,
14
+ forbidden: 403,
15
+ missing_scope: 403,
16
+ not_found: 404,
17
+ conflict: 409,
18
+ rate_limited: 429,
19
+ internal: 500,
20
+ upstream_timeout: 504,
21
+ unavailable: 503,
22
+ };
23
+ export const ErrorCodeSchema = z.enum(Object.keys(ERROR_CODES));
24
+ export const ApiError = z.object({
25
+ code: ErrorCodeSchema,
26
+ /** Plain words a developer can act on. Never a stack trace, never a secret. */
27
+ message: z.string(),
28
+ /** Field-level problems for validation_failed: { "email": "Not an email" }. */
29
+ fields: z.record(z.string(), z.string()).optional(),
30
+ /** The scope that was missing, for missing_scope. */
31
+ scope: z.string().optional(),
32
+ /** Echoed in the `x-request-id` header; quote it to support. */
33
+ requestId: z.string(),
34
+ });
35
+ export const ErrorEnvelope = z.object({ ok: z.literal(false), error: ApiError });
36
+ export function errorStatus(code) {
37
+ return ERROR_CODES[code];
38
+ }
39
+ // ── Success ─────────────────────────────────────────────────────────────────────
40
+ export const Page = z.object({
41
+ /** Pass as `cursor` to get the next page; null on the last page. */
42
+ nextCursor: z.string().nullable(),
43
+ hasMore: z.boolean(),
44
+ });
45
+ /** Single object: `{ ok: true, data }`. */
46
+ export function dataEnvelope(data) {
47
+ return z.object({ ok: z.literal(true), data });
48
+ }
49
+ /** List: `{ ok: true, data: [...], page: { nextCursor, hasMore } }`. */
50
+ export function listEnvelope(item) {
51
+ return z.object({ ok: z.literal(true), data: z.array(item), page: Page });
52
+ }
53
+ // ── Paging query ────────────────────────────────────────────────────────────────
54
+ export const PAGE_LIMIT_DEFAULT = 25;
55
+ export const PAGE_LIMIT_MAX = 100;
56
+ export const ListQuery = z.object({
57
+ cursor: z.string().min(1).optional(),
58
+ limit: z.coerce.number().int().min(1).max(PAGE_LIMIT_MAX).default(PAGE_LIMIT_DEFAULT),
59
+ /** Only records changed at or after this time (sync and polling). */
60
+ updatedSince: Timestamp.optional(),
61
+ });
62
+ // ── Resources (L0: contacts) ────────────────────────────────────────────────────
63
+ /** A Loops contact as the public API shows it. camelCase, ids as strings, dates ISO. */
64
+ export const Contact = z.object({
65
+ id: z.string(),
66
+ firstName: z.string().nullable(),
67
+ lastName: z.string().nullable(),
68
+ email: z.email().nullable(),
69
+ phone: z.string().nullable(),
70
+ jobTitle: z.string().nullable(),
71
+ companyId: z.string().nullable(),
72
+ ownerId: z.string().nullable(),
73
+ createdAt: Timestamp,
74
+ updatedAt: Timestamp,
75
+ });
76
+ export const ContactCreate = z
77
+ .object({
78
+ firstName: z.string().max(120).optional(),
79
+ lastName: z.string().max(120).optional(),
80
+ email: z.email().optional(),
81
+ phone: z.string().max(40).optional(),
82
+ jobTitle: z.string().max(200).optional(),
83
+ companyId: z.string().optional(),
84
+ ownerId: z.string().optional(),
85
+ })
86
+ .refine((c) => c.firstName || c.lastName || c.email, 'Give a name or an email');
87
+ export const ContactUpdate = z.object({
88
+ firstName: z.string().max(120).nullable().optional(),
89
+ lastName: z.string().max(120).nullable().optional(),
90
+ email: z.email().nullable().optional(),
91
+ phone: z.string().max(40).nullable().optional(),
92
+ jobTitle: z.string().max(200).nullable().optional(),
93
+ companyId: z.string().nullable().optional(),
94
+ ownerId: z.string().nullable().optional(),
95
+ });
@@ -0,0 +1,102 @@
1
+ import { z } from 'zod';
2
+ import type { Scope } from './scopes.ts';
3
+ /**
4
+ * Every event an app can hear (LoopKit `hears`, webhooks out, the engine's queues). Each names the scope that lets an app
5
+ * hear it. Only ever ADD here (versioning promise).
6
+ */
7
+ export declare const EVENTS: {
8
+ readonly 'contact.created': "contacts:read";
9
+ readonly 'contact.updated': "contacts:read";
10
+ readonly 'contact.deleted': "contacts:read";
11
+ readonly 'company.created': "companies:read";
12
+ readonly 'company.updated': "companies:read";
13
+ readonly 'company.deleted': "companies:read";
14
+ readonly 'lead.created': "leads:read";
15
+ readonly 'lead.updated': "leads:read";
16
+ readonly 'lead.deleted': "leads:read";
17
+ readonly 'deal.created': "deals:read";
18
+ readonly 'deal.updated': "deals:read";
19
+ readonly 'deal.deleted': "deals:read";
20
+ readonly 'loop.message': "loops:read";
21
+ readonly 'loop.call.ended': "calls:read";
22
+ readonly 'meeting.ended': "meetings:read";
23
+ readonly 'app.installed': null;
24
+ readonly 'app.uninstalled': null;
25
+ };
26
+ export type EventType = keyof typeof EVENTS;
27
+ export declare const EVENT_TYPES: [EventType, ...EventType[]];
28
+ export declare const EventTypeSchema: z.ZodEnum<{
29
+ "contact.created": "contact.created";
30
+ "contact.updated": "contact.updated";
31
+ "contact.deleted": "contact.deleted";
32
+ "company.created": "company.created";
33
+ "company.updated": "company.updated";
34
+ "company.deleted": "company.deleted";
35
+ "lead.created": "lead.created";
36
+ "lead.updated": "lead.updated";
37
+ "lead.deleted": "lead.deleted";
38
+ "deal.created": "deal.created";
39
+ "deal.updated": "deal.updated";
40
+ "deal.deleted": "deal.deleted";
41
+ "loop.message": "loop.message";
42
+ "loop.call.ended": "loop.call.ended";
43
+ "meeting.ended": "meeting.ended";
44
+ "app.installed": "app.installed";
45
+ "app.uninstalled": "app.uninstalled";
46
+ }>;
47
+ /** The scope needed to hear an event; null = every installed app hears it (about itself only). */
48
+ export declare function scopeForEvent(type: EventType): Scope | null;
49
+ /** Who caused a change. Apps never hear their own echo (`causedBy.kind === 'app'` and same app id). */
50
+ export declare const CausedBy: z.ZodDiscriminatedUnion<[z.ZodObject<{
51
+ kind: z.ZodLiteral<"user">;
52
+ id: z.ZodString;
53
+ }, z.core.$strip>, z.ZodObject<{
54
+ kind: z.ZodLiteral<"app">;
55
+ id: z.ZodString;
56
+ }, z.core.$strip>, z.ZodObject<{
57
+ kind: z.ZodLiteral<"system">;
58
+ }, z.core.$strip>], "kind">;
59
+ export type CausedBy = z.infer<typeof CausedBy>;
60
+ /**
61
+ * The one event envelope. `data` carries ids plus only the fields the app was granted; the engine trims it per install,
62
+ * so `data` is open here and each consumer narrows it.
63
+ */
64
+ export declare const EventEnvelope: z.ZodObject<{
65
+ id: z.ZodString;
66
+ type: z.ZodEnum<{
67
+ "contact.created": "contact.created";
68
+ "contact.updated": "contact.updated";
69
+ "contact.deleted": "contact.deleted";
70
+ "company.created": "company.created";
71
+ "company.updated": "company.updated";
72
+ "company.deleted": "company.deleted";
73
+ "lead.created": "lead.created";
74
+ "lead.updated": "lead.updated";
75
+ "lead.deleted": "lead.deleted";
76
+ "deal.created": "deal.created";
77
+ "deal.updated": "deal.updated";
78
+ "deal.deleted": "deal.deleted";
79
+ "loop.message": "loop.message";
80
+ "loop.call.ended": "loop.call.ended";
81
+ "meeting.ended": "meeting.ended";
82
+ "app.installed": "app.installed";
83
+ "app.uninstalled": "app.uninstalled";
84
+ }>;
85
+ version: z.ZodLiteral<1>;
86
+ occurredAt: z.ZodISODateTime;
87
+ workspaceId: z.ZodString;
88
+ installId: z.ZodOptional<z.ZodString>;
89
+ causedBy: z.ZodDiscriminatedUnion<[z.ZodObject<{
90
+ kind: z.ZodLiteral<"user">;
91
+ id: z.ZodString;
92
+ }, z.core.$strip>, z.ZodObject<{
93
+ kind: z.ZodLiteral<"app">;
94
+ id: z.ZodString;
95
+ }, z.core.$strip>, z.ZodObject<{
96
+ kind: z.ZodLiteral<"system">;
97
+ }, z.core.$strip>], "kind">;
98
+ data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
99
+ }, z.core.$strip>;
100
+ export type EventEnvelope = z.infer<typeof EventEnvelope>;
101
+ /** True when an app should not get this event back (it caused it). */
102
+ export declare function isEcho(event: Pick<EventEnvelope, 'causedBy'>, appId: string): boolean;
package/dist/events.js ADDED
@@ -0,0 +1,55 @@
1
+ import { z } from 'zod';
2
+ import { AppId, EventId, InstallId, Timestamp, UserId, WorkspaceId } from "./ids.js";
3
+ /**
4
+ * Every event an app can hear (LoopKit `hears`, webhooks out, the engine's queues). Each names the scope that lets an app
5
+ * hear it. Only ever ADD here (versioning promise).
6
+ */
7
+ export const EVENTS = {
8
+ 'contact.created': 'contacts:read',
9
+ 'contact.updated': 'contacts:read',
10
+ 'contact.deleted': 'contacts:read',
11
+ 'company.created': 'companies:read',
12
+ 'company.updated': 'companies:read',
13
+ 'company.deleted': 'companies:read',
14
+ 'lead.created': 'leads:read',
15
+ 'lead.updated': 'leads:read',
16
+ 'lead.deleted': 'leads:read',
17
+ 'deal.created': 'deals:read',
18
+ 'deal.updated': 'deals:read',
19
+ 'deal.deleted': 'deals:read',
20
+ 'loop.message': 'loops:read',
21
+ 'loop.call.ended': 'calls:read',
22
+ 'meeting.ended': 'meetings:read',
23
+ 'app.installed': null,
24
+ 'app.uninstalled': null,
25
+ };
26
+ export const EVENT_TYPES = Object.keys(EVENTS);
27
+ export const EventTypeSchema = z.enum(EVENT_TYPES);
28
+ /** The scope needed to hear an event; null = every installed app hears it (about itself only). */
29
+ export function scopeForEvent(type) {
30
+ return EVENTS[type];
31
+ }
32
+ /** Who caused a change. Apps never hear their own echo (`causedBy.kind === 'app'` and same app id). */
33
+ export const CausedBy = z.discriminatedUnion('kind', [
34
+ z.object({ kind: z.literal('user'), id: UserId }),
35
+ z.object({ kind: z.literal('app'), id: AppId }),
36
+ z.object({ kind: z.literal('system') }),
37
+ ]);
38
+ /**
39
+ * The one event envelope. `data` carries ids plus only the fields the app was granted; the engine trims it per install,
40
+ * so `data` is open here and each consumer narrows it.
41
+ */
42
+ export const EventEnvelope = z.object({
43
+ id: EventId,
44
+ type: EventTypeSchema,
45
+ version: z.literal(1),
46
+ occurredAt: Timestamp,
47
+ workspaceId: WorkspaceId,
48
+ installId: InstallId.optional(),
49
+ causedBy: CausedBy,
50
+ data: z.record(z.string(), z.unknown()),
51
+ });
52
+ /** True when an app should not get this event back (it caused it). */
53
+ export function isEcho(event, appId) {
54
+ return event.causedBy.kind === 'app' && event.causedBy.id === appId;
55
+ }
package/dist/ids.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ /** An app's id: the slug chosen at `loops create`, unique across Loops, never changes. */
3
+ export declare const AppId: z.ZodString;
4
+ export type AppId = z.infer<typeof AppId>;
5
+ /** Semantic version of one app release (`1.0.0`, `1.2.0-beta.1`). */
6
+ export declare const SemVer: z.ZodString;
7
+ export type SemVer = z.infer<typeof SemVer>;
8
+ /** A Loops workspace (the customer side; Vista tenant id underneath). */
9
+ export declare const WorkspaceId: z.ZodString;
10
+ export type WorkspaceId = z.infer<typeof WorkspaceId>;
11
+ /** A person in a workspace (Vista user id underneath). */
12
+ export declare const UserId: z.ZodString;
13
+ export type UserId = z.infer<typeof UserId>;
14
+ /** Opaque ids minted by the registry. Prefix shows the kind at a glance in logs. */
15
+ export declare const InstallId: z.ZodString;
16
+ export type InstallId = z.infer<typeof InstallId>;
17
+ export declare const VersionId: z.ZodString;
18
+ export type VersionId = z.infer<typeof VersionId>;
19
+ export declare const EventId: z.ZodString;
20
+ export type EventId = z.infer<typeof EventId>;
21
+ /** ISO 8601 timestamp with offset, e.g. 2026-11-11T09:30:00Z. */
22
+ export declare const Timestamp: z.ZodISODateTime;
23
+ export type Timestamp = z.infer<typeof Timestamp>;
24
+ /** Public https URL. http is refused everywhere in the platform. */
25
+ export declare const HttpsUrl: z.ZodURL;
26
+ export type HttpsUrl = z.infer<typeof HttpsUrl>;
package/dist/ids.js ADDED
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ /** An app's id: the slug chosen at `loops create`, unique across Loops, never changes. */
3
+ export const AppId = z
4
+ .string()
5
+ .regex(/^[a-z][a-z0-9-]{1,38}[a-z0-9]$/, 'App id: 3–40 characters, lowercase letters, digits and dashes, starting with a letter');
6
+ /** Semantic version of one app release (`1.0.0`, `1.2.0-beta.1`). */
7
+ export const SemVer = z
8
+ .string()
9
+ .regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z.-]+)?$/, 'Version must look like 1.0.0');
10
+ /** A Loops workspace (the customer side; Vista tenant id underneath). */
11
+ export const WorkspaceId = z.string().min(1).max(64);
12
+ /** A person in a workspace (Vista user id underneath). */
13
+ export const UserId = z.string().min(1).max(64);
14
+ /** Opaque ids minted by the registry. Prefix shows the kind at a glance in logs. */
15
+ export const InstallId = z.string().regex(/^ins_[A-Za-z0-9]{8,40}$/);
16
+ export const VersionId = z.string().regex(/^ver_[A-Za-z0-9]{8,40}$/);
17
+ export const EventId = z.string().regex(/^evt_[A-Za-z0-9]{8,40}$/);
18
+ /** ISO 8601 timestamp with offset, e.g. 2026-11-11T09:30:00Z. */
19
+ export const Timestamp = z.iso.datetime({ offset: true });
20
+ /** Public https URL. http is refused everywhere in the platform. */
21
+ export const HttpsUrl = z.url({ protocol: /^https$/ });
@@ -0,0 +1,6 @@
1
+ export * from './ids.ts';
2
+ export * from './scopes.ts';
3
+ export * from './events.ts';
4
+ export * from './manifest.ts';
5
+ export * from './api.ts';
6
+ export * from './registry.ts';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./ids.js";
2
+ export * from "./scopes.js";
3
+ export * from "./events.js";
4
+ export * from "./manifest.js";
5
+ export * from "./api.js";
6
+ export * from "./registry.js";