@automate.ax/integration-contracts 0.148.0 → 0.150.1

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,97 @@
1
+ import * as z from "zod";
2
+ const DATE_TIME_SCHEMA = z.iso.datetime({ offset: true });
3
+ /** Krisp profile represented by a personal API key. */
4
+ export const KRISP_PROFILE_SCHEMA = z.object({
5
+ avatar: z.url().nullable().prefault(null),
6
+ email: z.email(),
7
+ firstName: z.string().nullable().prefault(null),
8
+ id: z.number().int().positive(),
9
+ lastName: z.string().nullable().prefault(null),
10
+ teamId: z.number().int().positive(),
11
+ workspaceId: z.number().int().positive(),
12
+ });
13
+ /** Calendar attendee or diarized speaker attached to a Krisp meeting. */
14
+ export const KRISP_PARTICIPANT_SCHEMA = z.object({
15
+ email: z.string().nullable().prefault(null),
16
+ firstName: z.string().nullable().prefault(null),
17
+ lastName: z.string().nullable().prefault(null),
18
+ photo: z.url().nullable().prefault(null),
19
+ status: z.string().nullable().prefault(null),
20
+ });
21
+ /** Meeting metadata returned by Krisp list and detail endpoints. */
22
+ export const KRISP_MEETING_SCHEMA = z.object({
23
+ duration: z.number().int().nonnegative().nullable(),
24
+ id: z.string().min(1),
25
+ ownership: z.enum(["owned", "shared"]),
26
+ participants: KRISP_PARTICIPANT_SCHEMA.array().optional(),
27
+ source: z.string().nullable(),
28
+ startedAt: DATE_TIME_SCHEMA.nullable(),
29
+ status: z.string(),
30
+ tags: z.string().array(),
31
+ title: z.string(),
32
+ });
33
+ /** One diarized transcript segment. Times are seconds from recording start. */
34
+ export const KRISP_TRANSCRIPT_SEGMENT_SCHEMA = z.object({
35
+ end: z.number().nonnegative(),
36
+ speaker: z.number().int().nonnegative(),
37
+ start: z.number().nonnegative(),
38
+ text: z.string(),
39
+ });
40
+ const KRISP_NOTE_ASSIGNEE_SCHEMA = z.record(z.string(), z.json());
41
+ /** Recursive block returned by Krisp's fixed and custom meeting-note sections. */
42
+ export const KRISP_NOTE_BLOCK_SCHEMA = z.lazy(() => z.object({
43
+ assignee: KRISP_NOTE_ASSIGNEE_SCHEMA.nullable().optional(),
44
+ children: KRISP_NOTE_BLOCK_SCHEMA.array().optional(),
45
+ completed: z.boolean().nullable().optional(),
46
+ dueDate: z.string().nullable().optional(),
47
+ text: z.string().optional(),
48
+ type: z.string(),
49
+ }));
50
+ /** Complete Krisp meeting detail with optional heavy transcript and note data. */
51
+ export const KRISP_MEETING_DETAIL_SCHEMA = KRISP_MEETING_SCHEMA.extend({
52
+ language: z.string().nullable().prefault(null),
53
+ meetingType: z.string().nullable().prefault(null),
54
+ notes: z
55
+ .object({ blocks: KRISP_NOTE_BLOCK_SCHEMA.array() })
56
+ .nullable()
57
+ .optional(),
58
+ transcript: z
59
+ .object({
60
+ language: z.string().nullable().prefault(null),
61
+ segments: KRISP_TRANSCRIPT_SEGMENT_SCHEMA.array().prefault([]),
62
+ speakers: z.record(z.string(), KRISP_PARTICIPANT_SCHEMA).prefault({}),
63
+ })
64
+ .nullable()
65
+ .optional(),
66
+ });
67
+ /** One action item extracted from a Krisp meeting. */
68
+ export const KRISP_ACTION_ITEM_SCHEMA = z.object({
69
+ assignee: KRISP_PARTICIPANT_SCHEMA.nullable().prefault(null),
70
+ completed: z.boolean().nullable().prefault(null),
71
+ dueDate: z.string().nullable().prefault(null),
72
+ id: z.string().min(1),
73
+ meetingId: z.string().min(1),
74
+ meetingStartedAt: DATE_TIME_SCHEMA.nullable().prefault(null),
75
+ meetingTitle: z.string(),
76
+ title: z.string(),
77
+ });
78
+ /** Tag used to organize Krisp meetings. */
79
+ export const KRISP_TAG_SCHEMA = z.object({
80
+ color: z.string().nullable(),
81
+ createdAt: DATE_TIME_SCHEMA,
82
+ id: z.string().min(1),
83
+ name: z.string(),
84
+ });
85
+ /** Recording-import identity returned after Krisp issues an upload URL. */
86
+ export const KRISP_IMPORT_SCHEMA = z.object({
87
+ expiresAt: DATE_TIME_SCHEMA,
88
+ importId: z.string().min(1),
89
+ url: z.url(),
90
+ });
91
+ /** Current processing state for a Krisp recording import. */
92
+ export const KRISP_IMPORT_STATUS_SCHEMA = z.object({
93
+ error: z.string().nullable().prefault(null),
94
+ importId: z.string().min(1),
95
+ meetingId: z.string().min(1).nullable().prefault(null),
96
+ status: z.enum(["uploading", "processing", "ready", "failed"]),
97
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automate.ax/integration-contracts",
3
- "version": "0.148.0",
3
+ "version": "0.150.1",
4
4
  "description": "Shared integration payload contracts and provider primitives for Automate.ax.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -47,6 +47,7 @@
47
47
  "./highlevel": "./src/highlevel/index.ts",
48
48
  "./jobnimbus": "./src/jobnimbus/index.ts",
49
49
  "./linear": "./src/linear/index.ts",
50
+ "./krisp": "./src/krisp/index.ts",
50
51
  "./millionverifier": "./src/millionverifier/index.ts",
51
52
  "./notion": "./src/notion/index.ts",
52
53
  "./outlook": "./src/outlook/index.ts",
@@ -70,7 +71,7 @@
70
71
  },
71
72
  "dependencies": {
72
73
  "@anthropic-ai/sdk": "0.123.0",
73
- "@automate.ax/codec": "0.148.0",
74
+ "@automate.ax/codec": "0.150.1",
74
75
  "@cfworker/json-schema": "^4.1.1",
75
76
  "@googleapis/calendar": "^16.0.0",
76
77
  "@googleapis/forms": "^6.0.1",
@@ -272,6 +273,11 @@
272
273
  "types": "./dist/linear/index.d.ts",
273
274
  "default": "./dist/linear/index.js"
274
275
  },
276
+ "./krisp": {
277
+ "bun": "./src/krisp/index.ts",
278
+ "types": "./dist/krisp/index.d.ts",
279
+ "default": "./dist/krisp/index.js"
280
+ },
275
281
  "./millionverifier": {
276
282
  "bun": "./src/millionverifier/index.ts",
277
283
  "types": "./dist/millionverifier/index.d.ts",
@@ -330,7 +330,42 @@ export type CalcomEvent<
330
330
  }
331
331
  : never
332
332
 
333
- export const CALCOM_TRIGGER_CONFIG_SCHEMA = z.object({})
333
+ export const CALCOM_USER_WEBHOOK_SCOPE_SCHEMA = z.object({
334
+ type: z.literal("user"),
335
+ })
336
+ export const CALCOM_TEAM_WEBHOOK_SCOPE_SCHEMA = z.object({
337
+ eventTypeId: z.number().int().positive(),
338
+ teamId: z.number().int().positive(),
339
+ type: z.literal("team"),
340
+ })
341
+ export const CALCOM_ORGANIZATION_WEBHOOK_SCOPE_SCHEMA = z.object({
342
+ organizationId: z.number().int().positive(),
343
+ type: z.literal("organization"),
344
+ })
345
+ export const CALCOM_WEBHOOK_SCOPE_SCHEMA = z.discriminatedUnion("type", [
346
+ CALCOM_USER_WEBHOOK_SCOPE_SCHEMA,
347
+ CALCOM_TEAM_WEBHOOK_SCOPE_SCHEMA,
348
+ CALCOM_ORGANIZATION_WEBHOOK_SCOPE_SCHEMA,
349
+ ])
350
+ export type CalcomWebhookScope = z.output<typeof CALCOM_WEBHOOK_SCOPE_SCHEMA>
351
+
352
+ export const CALCOM_USER_TRIGGER_CONFIG_SCHEMA = z.object({
353
+ scope: CALCOM_USER_WEBHOOK_SCOPE_SCHEMA.optional(),
354
+ })
355
+ export const CALCOM_TEAM_TRIGGER_CONFIG_SCHEMA = z.object({
356
+ scope: z
357
+ .discriminatedUnion("type", [
358
+ CALCOM_USER_WEBHOOK_SCOPE_SCHEMA,
359
+ CALCOM_TEAM_WEBHOOK_SCOPE_SCHEMA,
360
+ ])
361
+ .optional(),
362
+ })
363
+ export const CALCOM_ORGANIZATION_TRIGGER_CONFIG_SCHEMA = z.object({
364
+ scope: CALCOM_ORGANIZATION_WEBHOOK_SCOPE_SCHEMA,
365
+ })
366
+ export const CALCOM_BROAD_TRIGGER_CONFIG_SCHEMA = z.object({
367
+ scope: CALCOM_WEBHOOK_SCOPE_SCHEMA.optional(),
368
+ })
334
369
 
335
370
  export const CALCOM_EVENT_SCHEMA: z.ZodType<CalcomEvent> =
336
371
  z.custom<CalcomEvent>((value) => {
@@ -398,8 +433,14 @@ function isCalcomWrappedTriggerType(
398
433
  return Object.hasOwn(CALCOM_PAYLOAD_SCHEMAS, type)
399
434
  }
400
435
 
436
+ type CalcomTriggerConfigSchema =
437
+ | typeof CALCOM_BROAD_TRIGGER_CONFIG_SCHEMA
438
+ | typeof CALCOM_ORGANIZATION_TRIGGER_CONFIG_SCHEMA
439
+ | typeof CALCOM_TEAM_TRIGGER_CONFIG_SCHEMA
440
+ | typeof CALCOM_USER_TRIGGER_CONFIG_SCHEMA
441
+
401
442
  type CalcomTriggerContract<TEvent> = {
402
- readonly configSchema: typeof CALCOM_TRIGGER_CONFIG_SCHEMA
443
+ readonly configSchema: CalcomTriggerConfigSchema
403
444
  readonly eventSchema: z.ZodType<TEvent>
404
445
  }
405
446
 
@@ -444,59 +485,137 @@ type CalcomSemanticContracts = {
444
485
  >
445
486
  } & { readonly "calcom.event": CalcomTriggerContract<CalcomEvent> }
446
487
 
447
- export const calcomTriggerContracts: CalcomSemanticContracts = {
488
+ export const calcomTriggerContracts = {
448
489
  "calcom.afterGuestsCalVideoNoShow": contract(
449
490
  "AFTER_GUESTS_CAL_VIDEO_NO_SHOW",
491
+ CALCOM_USER_TRIGGER_CONFIG_SCHEMA,
492
+ ),
493
+ "calcom.afterHostsCalVideoNoShow": contract(
494
+ "AFTER_HOSTS_CAL_VIDEO_NO_SHOW",
495
+ CALCOM_USER_TRIGGER_CONFIG_SCHEMA,
496
+ ),
497
+ "calcom.booking.cancelled": contract(
498
+ "BOOKING_CANCELLED",
499
+ CALCOM_TEAM_TRIGGER_CONFIG_SCHEMA,
500
+ ),
501
+ "calcom.booking.created": contract(
502
+ "BOOKING_CREATED",
503
+ CALCOM_TEAM_TRIGGER_CONFIG_SCHEMA,
504
+ ),
505
+ "calcom.booking.locationUpdated": contract(
506
+ "BOOKING_LOCATION_UPDATED",
507
+ CALCOM_TEAM_TRIGGER_CONFIG_SCHEMA,
508
+ ),
509
+ "calcom.booking.noShowUpdated": contract(
510
+ "BOOKING_NO_SHOW_UPDATED",
511
+ CALCOM_TEAM_TRIGGER_CONFIG_SCHEMA,
512
+ ),
513
+ "calcom.booking.paid": contract(
514
+ "BOOKING_PAID",
515
+ CALCOM_TEAM_TRIGGER_CONFIG_SCHEMA,
516
+ ),
517
+ "calcom.booking.paymentInitiated": contract(
518
+ "BOOKING_PAYMENT_INITIATED",
519
+ CALCOM_TEAM_TRIGGER_CONFIG_SCHEMA,
520
+ ),
521
+ "calcom.booking.reassigned": contract(
522
+ "BOOKING_REASSIGNED",
523
+ CALCOM_TEAM_TRIGGER_CONFIG_SCHEMA,
524
+ ),
525
+ "calcom.booking.rejected": contract(
526
+ "BOOKING_REJECTED",
527
+ CALCOM_TEAM_TRIGGER_CONFIG_SCHEMA,
528
+ ),
529
+ "calcom.booking.requested": contract(
530
+ "BOOKING_REQUESTED",
531
+ CALCOM_TEAM_TRIGGER_CONFIG_SCHEMA,
532
+ ),
533
+ "calcom.booking.rescheduled": contract(
534
+ "BOOKING_RESCHEDULED",
535
+ CALCOM_TEAM_TRIGGER_CONFIG_SCHEMA,
536
+ ),
537
+ "calcom.calendarEntryRejected": contract(
538
+ "CALENDAR_ENTRY_REJECTED",
539
+ CALCOM_USER_TRIGGER_CONFIG_SCHEMA,
540
+ ),
541
+ "calcom.delegationCredential.error": contract(
542
+ "DELEGATION_CREDENTIAL_ERROR",
543
+ CALCOM_ORGANIZATION_TRIGGER_CONFIG_SCHEMA,
450
544
  ),
451
- "calcom.afterHostsCalVideoNoShow": contract("AFTER_HOSTS_CAL_VIDEO_NO_SHOW"),
452
- "calcom.booking.cancelled": contract("BOOKING_CANCELLED"),
453
- "calcom.booking.created": contract("BOOKING_CREATED"),
454
- "calcom.booking.locationUpdated": contract("BOOKING_LOCATION_UPDATED"),
455
- "calcom.booking.noShowUpdated": contract("BOOKING_NO_SHOW_UPDATED"),
456
- "calcom.booking.paid": contract("BOOKING_PAID"),
457
- "calcom.booking.paymentInitiated": contract("BOOKING_PAYMENT_INITIATED"),
458
- "calcom.booking.reassigned": contract("BOOKING_REASSIGNED"),
459
- "calcom.booking.rejected": contract("BOOKING_REJECTED"),
460
- "calcom.booking.requested": contract("BOOKING_REQUESTED"),
461
- "calcom.booking.rescheduled": contract("BOOKING_RESCHEDULED"),
462
- "calcom.calendarEntryRejected": contract("CALENDAR_ENTRY_REJECTED"),
463
- "calcom.delegationCredential.error": contract("DELEGATION_CREDENTIAL_ERROR"),
464
545
  "calcom.delegationCredential.rotationRequired": contract(
465
546
  "DELEGATION_CREDENTIAL_ROTATION_REQUIRED",
547
+ CALCOM_ORGANIZATION_TRIGGER_CONFIG_SCHEMA,
466
548
  ),
467
549
  "calcom.delegationCredential.secretRotated": contract(
468
550
  "DELEGATION_CREDENTIAL_SECRET_ROTATED",
551
+ CALCOM_ORGANIZATION_TRIGGER_CONFIG_SCHEMA,
469
552
  ),
470
553
  "calcom.delegationCredential.secretRotationFailed": contract(
471
554
  "DELEGATION_CREDENTIAL_SECRET_ROTATION_FAILED",
555
+ CALCOM_ORGANIZATION_TRIGGER_CONFIG_SCHEMA,
472
556
  ),
473
557
  "calcom.event": {
474
- configSchema: CALCOM_TRIGGER_CONFIG_SCHEMA,
558
+ configSchema: CALCOM_BROAD_TRIGGER_CONFIG_SCHEMA,
475
559
  eventSchema: CALCOM_EVENT_SCHEMA,
476
560
  },
477
- "calcom.form.submitted": contract("FORM_SUBMITTED"),
478
- "calcom.form.submittedWithoutEvent": contract("FORM_SUBMITTED_NO_EVENT"),
479
- "calcom.instantMeeting.accepted": contract("INSTANT_MEETING_ACCEPTED"),
480
- "calcom.instantMeeting.created": contract("INSTANT_MEETING"),
481
- "calcom.meeting.ended": contract("MEETING_ENDED"),
482
- "calcom.meeting.started": contract("MEETING_STARTED"),
483
- "calcom.outOfOffice.created": contract("OOO_CREATED"),
484
- "calcom.recording.ready": contract("RECORDING_READY"),
561
+ "calcom.form.submitted": contract(
562
+ "FORM_SUBMITTED",
563
+ CALCOM_USER_TRIGGER_CONFIG_SCHEMA,
564
+ ),
565
+ "calcom.form.submittedWithoutEvent": contract(
566
+ "FORM_SUBMITTED_NO_EVENT",
567
+ CALCOM_USER_TRIGGER_CONFIG_SCHEMA,
568
+ ),
569
+ "calcom.instantMeeting.accepted": contract(
570
+ "INSTANT_MEETING_ACCEPTED",
571
+ CALCOM_USER_TRIGGER_CONFIG_SCHEMA,
572
+ ),
573
+ "calcom.instantMeeting.created": contract(
574
+ "INSTANT_MEETING",
575
+ CALCOM_USER_TRIGGER_CONFIG_SCHEMA,
576
+ ),
577
+ "calcom.meeting.ended": contract(
578
+ "MEETING_ENDED",
579
+ CALCOM_USER_TRIGGER_CONFIG_SCHEMA,
580
+ ),
581
+ "calcom.meeting.started": contract(
582
+ "MEETING_STARTED",
583
+ CALCOM_USER_TRIGGER_CONFIG_SCHEMA,
584
+ ),
585
+ "calcom.outOfOffice.created": contract(
586
+ "OOO_CREATED",
587
+ CALCOM_USER_TRIGGER_CONFIG_SCHEMA,
588
+ ),
589
+ "calcom.recording.ready": contract(
590
+ "RECORDING_READY",
591
+ CALCOM_USER_TRIGGER_CONFIG_SCHEMA,
592
+ ),
485
593
  "calcom.recording.transcriptionGenerated": contract(
486
594
  "RECORDING_TRANSCRIPTION_GENERATED",
595
+ CALCOM_USER_TRIGGER_CONFIG_SCHEMA,
487
596
  ),
488
- "calcom.routingForm.fallbackHit": contract("ROUTING_FORM_FALLBACK_HIT"),
489
- "calcom.wrongAssignmentReported": contract("WRONG_ASSIGNMENT_REPORT"),
490
- }
597
+ "calcom.routingForm.fallbackHit": contract(
598
+ "ROUTING_FORM_FALLBACK_HIT",
599
+ CALCOM_USER_TRIGGER_CONFIG_SCHEMA,
600
+ ),
601
+ "calcom.wrongAssignmentReported": contract(
602
+ "WRONG_ASSIGNMENT_REPORT",
603
+ CALCOM_USER_TRIGGER_CONFIG_SCHEMA,
604
+ ),
605
+ } as const satisfies CalcomSemanticContracts
491
606
 
492
607
  /**
493
608
  * Builds one trigger contract for an exact Cal.com provider event.
494
609
  *
495
610
  * @param type Exact Cal.com provider event type.
611
+ * @param configSchema Trigger configuration schema.
496
612
  */
497
- function contract<TType extends CalcomWebhookTriggerType>(type: TType) {
613
+ function contract<
614
+ TType extends CalcomWebhookTriggerType,
615
+ TConfigSchema extends CalcomTriggerConfigSchema,
616
+ >(type: TType, configSchema: TConfigSchema) {
498
617
  return {
499
- configSchema: CALCOM_TRIGGER_CONFIG_SCHEMA,
618
+ configSchema,
500
619
  eventSchema: calcomSemanticEventSchema(type),
501
620
  }
502
621
  }
@@ -32,6 +32,12 @@ const operationData = KIT_OPENAPI_DATA as {
32
32
  operations: Record<string, unknown>
33
33
  wireDefinitions: Record<string, Record<string, unknown>>
34
34
  }
35
+ for (const operation of Object.values(operationData.operations)) {
36
+ if (typeof operation !== "object" || operation === null) continue
37
+ const record = operation as Record<string, unknown>
38
+ normalizeObservedKitNulls(record.outputSchema)
39
+ normalizeObservedKitNulls(record.outputWireSchema)
40
+ }
35
41
  const OPERATIONS = operationData.operations as unknown as Record<
36
42
  KitOperationKey,
37
43
  KitRuntimeOperation
@@ -91,3 +97,61 @@ export function kitOperationOutputSchema<TKey extends KitOperationKey>(
91
97
 
92
98
  /** Provider-native component schemas used for schema-guided key mapping. */
93
99
  export const KIT_WIRE_DEFINITIONS = operationData.wireDefinitions
100
+
101
+ /**
102
+ * Accepts nullable fields observed in successful Kit responses but omitted from
103
+ * the provider's OpenAPI nullability metadata.
104
+ *
105
+ * @param value Generated operation metadata.
106
+ */
107
+ function normalizeObservedKitNulls(value: unknown) {
108
+ if (Array.isArray(value)) {
109
+ for (const item of value) normalizeObservedKitNulls(item)
110
+ return
111
+ }
112
+ if (typeof value !== "object" || value === null) return
113
+
114
+ const record = value as Record<string, unknown>
115
+ const properties = record.properties
116
+ if (typeof properties === "object" && properties !== null) {
117
+ const propertyRecord = properties as Record<string, unknown>
118
+ for (const name of [
119
+ "bio",
120
+ "byline",
121
+ "endCursor",
122
+ "end_cursor",
123
+ "startCursor",
124
+ "start_cursor",
125
+ ]) {
126
+ const property = propertyRecord[name]
127
+ if (
128
+ typeof property !== "object" ||
129
+ property === null ||
130
+ isNullableSchema(property)
131
+ ) {
132
+ continue
133
+ }
134
+ propertyRecord[name] = { anyOf: [property, { type: "null" }] }
135
+ }
136
+ }
137
+
138
+ for (const child of Object.values(record)) normalizeObservedKitNulls(child)
139
+ }
140
+
141
+ /**
142
+ * Returns whether one JSON Schema fragment already accepts null.
143
+ *
144
+ * @param value JSON Schema fragment.
145
+ */
146
+ function isNullableSchema(value: object) {
147
+ const anyOf = (value as { anyOf?: unknown }).anyOf
148
+ return (
149
+ Array.isArray(anyOf) &&
150
+ anyOf.some(
151
+ (branch) =>
152
+ typeof branch === "object" &&
153
+ branch !== null &&
154
+ (branch as { type?: unknown }).type === "null",
155
+ )
156
+ )
157
+ }
package/src/kit/types.ts CHANGED
@@ -60,6 +60,20 @@ type EncodableValue<TValue> = unknown extends TValue
60
60
  : { [TKey in keyof TValue]: EncodableValue<TValue[TKey]> }
61
61
  : never
62
62
 
63
+ /** Fields whose live Kit nullability is missing from its OpenAPI document. */
64
+ type ObservedNullableField = "bio" | "byline" | "endCursor" | "startCursor"
65
+
66
+ /** Adds nulls returned by Kit but omitted from its generated OpenAPI types. */
67
+ type NormalizeObservedKitNulls<TValue> = TValue extends readonly (infer TItem)[]
68
+ ? NormalizeObservedKitNulls<TItem>[]
69
+ : TValue extends object
70
+ ? {
71
+ [TKey in keyof TValue]:
72
+ | NormalizeObservedKitNulls<TValue[TKey]>
73
+ | (TKey extends ObservedNullableField ? null : never)
74
+ }
75
+ : TValue
76
+
63
77
  /** Flattens one generated operation's path, query, and body fields. */
64
78
  type RawKitOperationInput<TKey extends KitOperationKey> = Simplify<
65
79
  Parameters<Operation<TKey>, "path"> &
@@ -83,5 +97,5 @@ export type KitOperationInput<TKey extends KitOperationKey> = PublicInput<
83
97
 
84
98
  /** CamelCase successful response for one frozen Kit V4 operation. */
85
99
  export type KitOperationOutput<TKey extends KitOperationKey> = EncodableValue<
86
- SuccessResponse<Operation<TKey>>
100
+ NormalizeObservedKitNulls<SuccessResponse<Operation<TKey>>>
87
101
  >
@@ -0,0 +1,215 @@
1
+ import type { Encodable } from "@automate.ax/codec"
2
+ import { encodableSchema } from "@automate.ax/codec"
3
+ import * as z from "zod"
4
+
5
+ const KRISP_API_BASE_URL = "https://meeting-api.krisp.ai/v1/"
6
+ const KRISP_API_ORIGIN = new URL(KRISP_API_BASE_URL).origin
7
+ const KRISP_SECRET_SCHEMA = z.object({ apiKey: z.string().min(1) })
8
+
9
+ /** Options for one authenticated Krisp REST request. */
10
+ export interface KrispRequestOptions<TSchema extends z.ZodType> {
11
+ /** Public camelCase JSON body. */
12
+ body?: Encodable
13
+
14
+ /** HTTP verb. Defaults to `GET`. */
15
+ method?: "GET" | "POST"
16
+
17
+ /** Public camelCase query parameters. */
18
+ query?: Record<
19
+ string,
20
+ boolean | number | readonly string[] | string | undefined
21
+ >
22
+
23
+ /** Schema for the normalized provider response. */
24
+ responseSchema: TSchema
25
+ }
26
+
27
+ /** Structured Krisp REST failure. */
28
+ export class KrispApiError extends Error {
29
+ /** Normalized provider error payload, when valid JSON was returned. */
30
+ readonly body?: Encodable
31
+
32
+ /** Retry delay in seconds, when Krisp returned one. */
33
+ readonly retryAfter?: number
34
+
35
+ /** HTTP status returned by Krisp. */
36
+ readonly status: number
37
+
38
+ /**
39
+ * Creates a structured Krisp API error.
40
+ *
41
+ * @param options - Provider status, body, and retry metadata.
42
+ * @param options.body - Normalized provider error body.
43
+ * @param options.retryAfter - Retry delay in seconds.
44
+ * @param options.status - HTTP status.
45
+ */
46
+ constructor(options: {
47
+ body?: Encodable
48
+ retryAfter?: number
49
+ status: number
50
+ }) {
51
+ super(
52
+ getErrorMessage(options.body) ??
53
+ `Krisp API request failed with status ${options.status}.`,
54
+ )
55
+ this.name = "KrispApiError"
56
+ this.body = options.body
57
+ this.retryAfter = options.retryAfter
58
+ this.status = options.status
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Creates an authenticated Krisp Meeting Assistant REST client.
64
+ *
65
+ * @param secret - Resolved Krisp API-key secret.
66
+ */
67
+ export function getKrispApi(secret: Record<string, unknown>) {
68
+ const { apiKey } = KRISP_SECRET_SCHEMA.parse(secret)
69
+
70
+ return {
71
+ /**
72
+ * Runs one Krisp request and returns its normalized response.
73
+ *
74
+ * @param path - API path relative to the v1 root.
75
+ * @param options - Method, parameters, and response schema.
76
+ */
77
+ async request<TSchema extends z.ZodType>(
78
+ path: string,
79
+ options: KrispRequestOptions<TSchema>,
80
+ ): Promise<z.output<TSchema>> {
81
+ const url = new URL(path.replace(/^\//, ""), KRISP_API_BASE_URL)
82
+ if (url.origin !== KRISP_API_ORIGIN) {
83
+ throw new Error("Krisp API paths must use the Krisp API origin.")
84
+ }
85
+ for (const [name, value] of Object.entries(options.query ?? {})) {
86
+ if (value === undefined) continue
87
+ url.searchParams.set(
88
+ toSnakeCase(name),
89
+ Array.isArray(value) ? value.join(",") : String(value),
90
+ )
91
+ }
92
+ const response = await fetch(url, {
93
+ body:
94
+ options.body === undefined
95
+ ? undefined
96
+ : JSON.stringify(toKrisp(options.body)),
97
+ headers: {
98
+ Accept: "application/json",
99
+ Authorization: `Bearer ${apiKey}`,
100
+ ...(options.body === undefined
101
+ ? {}
102
+ : { "Content-Type": "application/json" }),
103
+ },
104
+ method: options.method ?? "GET",
105
+ })
106
+ const normalized = fromKrisp(parseJson(await response.text()))
107
+ if (!response.ok) {
108
+ const body = encodableSchema.safeParse(normalized)
109
+ throw new KrispApiError({
110
+ ...(body.success && { body: body.data }),
111
+ retryAfter: parseFiniteNumber(response.headers.get("Retry-After")),
112
+ status: response.status,
113
+ })
114
+ }
115
+ return options.responseSchema.parse(normalized)
116
+ },
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Converts public camelCase JSON to Krisp wire keys.
122
+ *
123
+ * @param value - Public value to normalize.
124
+ */
125
+ export function toKrisp(value: Encodable): Encodable {
126
+ if (value instanceof Date) return value.toISOString()
127
+ if (Array.isArray(value)) return value.map(toKrisp)
128
+ if (!isPlainObject(value)) return value
129
+ return Object.fromEntries(
130
+ Object.entries(value).map(([key, item]) => [
131
+ toSnakeCase(key),
132
+ toKrisp(item),
133
+ ]),
134
+ )
135
+ }
136
+
137
+ /**
138
+ * Converts Krisp wire JSON to public camelCase keys.
139
+ *
140
+ * @param value - Provider value to normalize.
141
+ */
142
+ export function fromKrisp(value: unknown): unknown {
143
+ if (Array.isArray(value)) return value.map(fromKrisp)
144
+ if (!isPlainObject(value)) return value
145
+ return Object.fromEntries(
146
+ Object.entries(value).map(([key, item]) => [
147
+ toCamelCase(key),
148
+ fromKrisp(item),
149
+ ]),
150
+ )
151
+ }
152
+
153
+ /**
154
+ * Checks whether a value is a key-value object.
155
+ *
156
+ * @param value - Candidate value.
157
+ */
158
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
159
+ return typeof value === "object" && value !== null && !Array.isArray(value)
160
+ }
161
+
162
+ /**
163
+ * Parses a JSON response body without obscuring the HTTP status on failure.
164
+ *
165
+ * @param value - Raw response text.
166
+ */
167
+ function parseJson(value: string): unknown {
168
+ try {
169
+ return JSON.parse(value)
170
+ } catch {
171
+ return undefined
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Parses an optional finite numeric header.
177
+ *
178
+ * @param value - Raw header value.
179
+ */
180
+ function parseFiniteNumber(value: string | null) {
181
+ if (value === null) return undefined
182
+ const number = Number(value)
183
+ return Number.isFinite(number) ? number : undefined
184
+ }
185
+
186
+ /**
187
+ * Extracts Krisp's human-readable error string.
188
+ *
189
+ * @param body - Normalized provider error body.
190
+ */
191
+ function getErrorMessage(body: Encodable | undefined) {
192
+ return isPlainObject(body) && typeof body.error === "string"
193
+ ? body.error
194
+ : undefined
195
+ }
196
+
197
+ /**
198
+ * Converts one public camelCase key to Krisp snake_case.
199
+ *
200
+ * @param value - Public key.
201
+ */
202
+ function toSnakeCase(value: string) {
203
+ return value.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
204
+ }
205
+
206
+ /**
207
+ * Converts one Krisp snake_case key to public camelCase.
208
+ *
209
+ * @param value - Provider key.
210
+ */
211
+ function toCamelCase(value: string) {
212
+ return value.replace(/_([a-z0-9])/g, (_, letter: string) =>
213
+ letter.toUpperCase(),
214
+ )
215
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./api"
2
+ export * from "./schemas"