@absolutejs/voice 0.0.22-beta.114 → 0.0.22-beta.115

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,52 @@
1
+ import type { VoiceCampaignDialer, VoiceCampaignDialerInput } from './campaign';
2
+ type VoiceCampaignDialerFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
3
+ type VoiceCampaignDialerMetadataInput = VoiceCampaignDialerInput & {
4
+ provider: 'plivo' | 'telnyx' | 'twilio';
5
+ };
6
+ type VoiceCampaignDialerMetadata = Record<string, string | number | boolean | undefined> | ((input: VoiceCampaignDialerMetadataInput) => Record<string, string | number | boolean | undefined> | Promise<Record<string, string | number | boolean | undefined>>);
7
+ type VoiceCampaignURLInput = {
8
+ attempt: VoiceCampaignDialerInput['attempt'];
9
+ campaign: VoiceCampaignDialerInput['campaign'];
10
+ recipient: VoiceCampaignDialerInput['recipient'];
11
+ };
12
+ type VoiceCampaignURLResolver = string | ((input: VoiceCampaignURLInput) => string | Promise<string>);
13
+ export type VoiceTwilioCampaignDialerOptions = {
14
+ accountSid: string;
15
+ answerMethod?: 'GET' | 'POST';
16
+ answerUrl: VoiceCampaignURLResolver;
17
+ apiBaseUrl?: string;
18
+ authToken: string;
19
+ fetch?: VoiceCampaignDialerFetch;
20
+ from: string;
21
+ machineDetection?: string;
22
+ metadata?: VoiceCampaignDialerMetadata;
23
+ statusCallbackEvents?: string[];
24
+ statusCallbackMethod?: 'GET' | 'POST';
25
+ statusCallbackUrl?: VoiceCampaignURLResolver;
26
+ };
27
+ export type VoiceTelnyxCampaignDialerOptions = {
28
+ apiBaseUrl?: string;
29
+ apiKey: string;
30
+ clientState?: VoiceCampaignDialerMetadata;
31
+ connectionId: string;
32
+ fetch?: VoiceCampaignDialerFetch;
33
+ from: string;
34
+ webhookUrl?: VoiceCampaignURLResolver;
35
+ webhookUrlMethod?: 'GET' | 'POST';
36
+ };
37
+ export type VoicePlivoCampaignDialerOptions = {
38
+ answerMethod?: 'GET' | 'POST';
39
+ answerUrl: VoiceCampaignURLResolver;
40
+ apiBaseUrl?: string;
41
+ authId: string;
42
+ authToken: string;
43
+ callbackMethod?: 'GET' | 'POST';
44
+ callbackUrl?: VoiceCampaignURLResolver;
45
+ fetch?: VoiceCampaignDialerFetch;
46
+ from: string;
47
+ metadata?: VoiceCampaignDialerMetadata;
48
+ };
49
+ export declare const createVoiceTwilioCampaignDialer: (options: VoiceTwilioCampaignDialerOptions) => VoiceCampaignDialer;
50
+ export declare const createVoiceTelnyxCampaignDialer: (options: VoiceTelnyxCampaignDialerOptions) => VoiceCampaignDialer;
51
+ export declare const createVoicePlivoCampaignDialer: (options: VoicePlivoCampaignDialerOptions) => VoiceCampaignDialer;
52
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { voice } from './plugin';
2
2
  export { createVoiceAppKit, createVoiceAppKitRoutes, summarizeVoiceAppKitStatus } from './appKit';
3
3
  export { applyVoiceCampaignTelephonyOutcome, buildVoiceCampaignObservabilityReport, createVoiceCampaignTelephonyOutcomeHandler, createVoiceCampaign, createVoiceCampaignRoutes, createVoiceCampaignWorker, createVoiceCampaignWorkerLoop, createVoiceMemoryCampaignStore, renderVoiceCampaignObservabilityHTML, renderVoiceCampaignsHTML, runVoiceCampaignProof, summarizeVoiceCampaigns } from './campaign';
4
+ export { createVoicePlivoCampaignDialer, createVoiceTelnyxCampaignDialer, createVoiceTwilioCampaignDialer } from './campaignDialers';
4
5
  export { createVoiceAssistant, createVoiceExperiment, summarizeVoiceAssistantRuns } from './assistant';
5
6
  export { createVoiceAssistantHealthHTMLHandler, createVoiceAssistantHealthJSONHandler, createVoiceAssistantHealthRoutes, renderVoiceAssistantHealthHTML, summarizeVoiceAssistantHealth } from './assistantHealth';
6
7
  export { createVoiceBargeInRoutes, renderVoiceBargeInHTML, summarizeVoiceBargeIn } from './bargeInRoutes';
package/dist/index.js CHANGED
@@ -11256,6 +11256,201 @@ var createVoiceCampaignRoutes = (options) => {
11256
11256
  }
11257
11257
  return app;
11258
11258
  };
11259
+ // src/campaignDialers.ts
11260
+ import { Buffer as Buffer3 } from "buffer";
11261
+ var resolveFetch = (fetcher) => {
11262
+ const activeFetch = fetcher ?? globalThis.fetch;
11263
+ if (!activeFetch) {
11264
+ throw new Error("Campaign dialer requires fetch or globalThis.fetch.");
11265
+ }
11266
+ return activeFetch;
11267
+ };
11268
+ var resolveURL = async (resolver, input) => {
11269
+ if (!resolver) {
11270
+ return;
11271
+ }
11272
+ return typeof resolver === "function" ? await resolver(input) : resolver;
11273
+ };
11274
+ var baseCampaignMetadata = (input) => ({
11275
+ attemptId: input.attempt.id,
11276
+ campaignId: input.campaign.id,
11277
+ recipientId: input.recipient.id,
11278
+ voiceCampaignAttemptId: input.attempt.id,
11279
+ voiceCampaignId: input.campaign.id,
11280
+ voiceCampaignRecipientId: input.recipient.id
11281
+ });
11282
+ var resolveMetadata = async (metadata, input) => ({
11283
+ ...baseCampaignMetadata(input),
11284
+ ...typeof metadata === "function" ? await metadata(input) : metadata
11285
+ });
11286
+ var appendQuery = (url, params) => {
11287
+ if (!url) {
11288
+ return;
11289
+ }
11290
+ const parsed = new URL(url);
11291
+ for (const [key, value] of Object.entries(params)) {
11292
+ if (value !== undefined) {
11293
+ parsed.searchParams.set(key, String(value));
11294
+ }
11295
+ }
11296
+ return parsed.toString();
11297
+ };
11298
+ var readJson = async (response) => {
11299
+ const text = await response.text();
11300
+ if (!text.trim()) {
11301
+ return {};
11302
+ }
11303
+ try {
11304
+ return JSON.parse(text);
11305
+ } catch {
11306
+ return {
11307
+ text
11308
+ };
11309
+ }
11310
+ };
11311
+ var assertOk = async (response, provider) => {
11312
+ const body = await readJson(response);
11313
+ if (!response.ok) {
11314
+ throw new Error(`${provider} campaign dialer failed with ${response.status}: ${JSON.stringify(body)}`);
11315
+ }
11316
+ return body;
11317
+ };
11318
+ var firstString2 = (values) => {
11319
+ for (const value of values) {
11320
+ if (typeof value === "string" && value.trim()) {
11321
+ return value.trim();
11322
+ }
11323
+ }
11324
+ };
11325
+ var formBody = (values) => {
11326
+ const body = new URLSearchParams;
11327
+ for (const [key, value] of Object.entries(values)) {
11328
+ if (Array.isArray(value)) {
11329
+ for (const item of value) {
11330
+ if (item !== undefined) {
11331
+ body.append(key, String(item));
11332
+ }
11333
+ }
11334
+ continue;
11335
+ }
11336
+ if (value !== undefined) {
11337
+ body.set(key, String(value));
11338
+ }
11339
+ }
11340
+ return body;
11341
+ };
11342
+ var createVoiceTwilioCampaignDialer = (options) => async (input) => {
11343
+ const fetcher = resolveFetch(options.fetch);
11344
+ const metadata = await resolveMetadata(options.metadata, {
11345
+ ...input,
11346
+ provider: "twilio"
11347
+ });
11348
+ const answerUrl = appendQuery(await resolveURL(options.answerUrl, input), metadata);
11349
+ const statusCallbackUrl = appendQuery(await resolveURL(options.statusCallbackUrl, input), metadata);
11350
+ const response = await fetcher(`${options.apiBaseUrl ?? "https://api.twilio.com"}/2010-04-01/Accounts/${encodeURIComponent(options.accountSid)}/Calls.json`, {
11351
+ body: formBody({
11352
+ From: options.from,
11353
+ MachineDetection: options.machineDetection,
11354
+ Method: options.answerMethod,
11355
+ StatusCallback: statusCallbackUrl,
11356
+ StatusCallbackEvent: options.statusCallbackEvents,
11357
+ StatusCallbackMethod: options.statusCallbackMethod,
11358
+ To: input.recipient.phone,
11359
+ Url: answerUrl
11360
+ }),
11361
+ headers: {
11362
+ authorization: `Basic ${Buffer3.from(`${options.accountSid}:${options.authToken}`).toString("base64")}`,
11363
+ "content-type": "application/x-www-form-urlencoded"
11364
+ },
11365
+ method: "POST"
11366
+ });
11367
+ const body = await assertOk(response, "Twilio");
11368
+ return {
11369
+ externalCallId: firstString2([body.sid, body.callSid, body.call_sid]),
11370
+ metadata: {
11371
+ provider: "twilio",
11372
+ response: body
11373
+ },
11374
+ status: "running"
11375
+ };
11376
+ };
11377
+ var createVoiceTelnyxCampaignDialer = (options) => async (input) => {
11378
+ const fetcher = resolveFetch(options.fetch);
11379
+ const metadata = await resolveMetadata(options.clientState, {
11380
+ ...input,
11381
+ provider: "telnyx"
11382
+ });
11383
+ const webhookUrl = appendQuery(await resolveURL(options.webhookUrl, input), metadata);
11384
+ const response = await fetcher(`${options.apiBaseUrl ?? "https://api.telnyx.com"}/v2/calls`, {
11385
+ body: JSON.stringify({
11386
+ client_state: Buffer3.from(JSON.stringify(metadata)).toString("base64"),
11387
+ connection_id: options.connectionId,
11388
+ from: options.from,
11389
+ to: input.recipient.phone,
11390
+ webhook_url: webhookUrl,
11391
+ webhook_url_method: options.webhookUrlMethod
11392
+ }),
11393
+ headers: {
11394
+ authorization: `Bearer ${options.apiKey}`,
11395
+ "content-type": "application/json"
11396
+ },
11397
+ method: "POST"
11398
+ });
11399
+ const body = await assertOk(response, "Telnyx");
11400
+ const data = typeof body.data === "object" && body.data !== null ? body.data : body;
11401
+ return {
11402
+ externalCallId: firstString2([
11403
+ data.call_control_id,
11404
+ data.call_session_id,
11405
+ data.call_leg_id,
11406
+ body.call_control_id,
11407
+ body.call_session_id
11408
+ ]),
11409
+ metadata: {
11410
+ provider: "telnyx",
11411
+ response: body
11412
+ },
11413
+ status: "running"
11414
+ };
11415
+ };
11416
+ var createVoicePlivoCampaignDialer = (options) => async (input) => {
11417
+ const fetcher = resolveFetch(options.fetch);
11418
+ const metadata = await resolveMetadata(options.metadata, {
11419
+ ...input,
11420
+ provider: "plivo"
11421
+ });
11422
+ const answerUrl = appendQuery(await resolveURL(options.answerUrl, input), metadata);
11423
+ const callbackUrl = appendQuery(await resolveURL(options.callbackUrl, input), metadata);
11424
+ const response = await fetcher(`${options.apiBaseUrl ?? "https://api.plivo.com"}/v1/Account/${encodeURIComponent(options.authId)}/Call/`, {
11425
+ body: formBody({
11426
+ answer_method: options.answerMethod,
11427
+ answer_url: answerUrl,
11428
+ callback_method: options.callbackMethod,
11429
+ callback_url: callbackUrl,
11430
+ from: options.from,
11431
+ to: input.recipient.phone
11432
+ }),
11433
+ headers: {
11434
+ authorization: `Basic ${Buffer3.from(`${options.authId}:${options.authToken}`).toString("base64")}`,
11435
+ "content-type": "application/x-www-form-urlencoded"
11436
+ },
11437
+ method: "POST"
11438
+ });
11439
+ const body = await assertOk(response, "Plivo");
11440
+ return {
11441
+ externalCallId: firstString2([
11442
+ body.request_uuid,
11443
+ body.requestUuid,
11444
+ body.call_uuid,
11445
+ body.callUuid
11446
+ ]),
11447
+ metadata: {
11448
+ provider: "plivo",
11449
+ response: body
11450
+ },
11451
+ status: "running"
11452
+ };
11453
+ };
11259
11454
  // src/simulationSuite.ts
11260
11455
  import { Elysia as Elysia20 } from "elysia";
11261
11456
 
@@ -12690,7 +12885,7 @@ var createMemoryVoiceTelephonyWebhookIdempotencyStore = () => {
12690
12885
  };
12691
12886
  };
12692
12887
  var normalizeToken = (value) => typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, "-").replace(/_+/g, "-") : undefined;
12693
- var firstString2 = (source, keys) => {
12888
+ var firstString3 = (source, keys) => {
12694
12889
  for (const key of keys) {
12695
12890
  const value = source[key];
12696
12891
  if (typeof value === "string" && value.trim()) {
@@ -13062,8 +13257,8 @@ var verifyVoiceTelephonyWebhook = async (input) => {
13062
13257
  var durationMsFromSeconds = (value) => typeof value === "number" ? value * 1000 : undefined;
13063
13258
  var parseVoiceTelephonyWebhookEvent = (input) => {
13064
13259
  const payload = flattenPayload(input.body);
13065
- const provider = firstString2(payload, ["provider", "Provider"]) ?? input.provider;
13066
- const status = firstString2(payload, [
13260
+ const provider = firstString3(payload, ["provider", "Provider"]) ?? input.provider;
13261
+ const status = firstString3(payload, [
13067
13262
  "CallStatus",
13068
13263
  "call_status",
13069
13264
  "callStatus",
@@ -13088,9 +13283,9 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
13088
13283
  "sip_code",
13089
13284
  "hangupCauseCode"
13090
13285
  ]);
13091
- const from = firstString2(payload, ["From", "from", "caller_id", "callerId"]);
13092
- const to = firstString2(payload, ["To", "to", "called_number", "calledNumber"]);
13093
- const target = firstString2(payload, [
13286
+ const from = firstString3(payload, ["From", "from", "caller_id", "callerId"]);
13287
+ const to = firstString3(payload, ["To", "to", "called_number", "calledNumber"]);
13288
+ const target = firstString3(payload, [
13094
13289
  "transferTarget",
13095
13290
  "TransferTarget",
13096
13291
  "target",
@@ -13098,7 +13293,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
13098
13293
  "department"
13099
13294
  ]);
13100
13295
  return {
13101
- answeredBy: firstString2(payload, [
13296
+ answeredBy: firstString3(payload, [
13102
13297
  "AnsweredBy",
13103
13298
  "answered_by",
13104
13299
  "answeredBy",
@@ -13107,9 +13302,12 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
13107
13302
  ]),
13108
13303
  durationMs,
13109
13304
  from,
13110
- metadata: payload,
13305
+ metadata: {
13306
+ ...input.query,
13307
+ ...payload
13308
+ },
13111
13309
  provider,
13112
- reason: firstString2(payload, [
13310
+ reason: firstString3(payload, [
13113
13311
  "Reason",
13114
13312
  "reason",
13115
13313
  "HangupCause",
@@ -13125,7 +13323,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
13125
13323
  var defaultSessionId = (input) => {
13126
13324
  const payload = flattenPayload(input.body);
13127
13325
  const metadataSessionId = input.event.metadata?.sessionId;
13128
- return firstString2(input.query, ["sessionId", "session_id"]) ?? firstString2(payload, [
13326
+ return firstString3(input.query, ["sessionId", "session_id"]) ?? firstString3(payload, [
13129
13327
  "sessionId",
13130
13328
  "session_id",
13131
13329
  "SessionId",
@@ -13140,7 +13338,7 @@ var defaultSessionId = (input) => {
13140
13338
  };
13141
13339
  var defaultIdempotencyKey = (input) => {
13142
13340
  const payload = flattenPayload(input.body);
13143
- const eventId = firstString2(payload, [
13341
+ const eventId = firstString3(payload, [
13144
13342
  "id",
13145
13343
  "event_id",
13146
13344
  "eventId",
@@ -13301,11 +13499,11 @@ var createVoiceTelephonyWebhookRoutes = (options = {}) => {
13301
13499
  import { Elysia as Elysia28 } from "elysia";
13302
13500
 
13303
13501
  // src/telephony/plivo.ts
13304
- import { Buffer as Buffer4 } from "buffer";
13502
+ import { Buffer as Buffer5 } from "buffer";
13305
13503
  import { Elysia as Elysia26 } from "elysia";
13306
13504
 
13307
13505
  // src/telephony/twilio.ts
13308
- import { Buffer as Buffer3 } from "buffer";
13506
+ import { Buffer as Buffer4 } from "buffer";
13309
13507
  import { Elysia as Elysia25 } from "elysia";
13310
13508
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
13311
13509
  var VOICE_PCM_SAMPLE_RATE = 16000;
@@ -13596,7 +13794,7 @@ var bytesToInt16Array = (bytes) => {
13596
13794
  return output;
13597
13795
  };
13598
13796
  var decodeTwilioMulawBase64 = (payload) => {
13599
- const bytes = Uint8Array.from(Buffer3.from(payload, "base64"));
13797
+ const bytes = Uint8Array.from(Buffer4.from(payload, "base64"));
13600
13798
  const samples = new Int16Array(bytes.length);
13601
13799
  for (let index = 0;index < bytes.length; index += 1) {
13602
13800
  samples[index] = decodeMulawSample(bytes[index] ?? 0);
@@ -13608,7 +13806,7 @@ var encodeTwilioMulawBase64 = (samples) => {
13608
13806
  for (let index = 0;index < samples.length; index += 1) {
13609
13807
  bytes[index] = encodeMulawSample(samples[index] ?? 0);
13610
13808
  }
13611
- return Buffer3.from(bytes).toString("base64");
13809
+ return Buffer4.from(bytes).toString("base64");
13612
13810
  };
13613
13811
  var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
13614
13812
  const narrowband = decodeTwilioMulawBase64(payload);
@@ -13617,7 +13815,7 @@ var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
13617
13815
  };
13618
13816
  var transcodePCMToTwilioOutboundPayload = (chunk, format) => {
13619
13817
  if (format.container === "raw" && format.encoding === "mulaw" && format.channels === 1 && format.sampleRateHz === TWILIO_MULAW_SAMPLE_RATE) {
13620
- return Buffer3.from(chunk).toString("base64");
13818
+ return Buffer4.from(chunk).toString("base64");
13621
13819
  }
13622
13820
  if (format.encoding !== "pcm_s16le") {
13623
13821
  throw new Error(`Unsupported outbound telephony audio format: ${format.container}/${format.encoding}`);
@@ -13658,7 +13856,7 @@ var createTwilioSocketAdapter = (socket, getState) => ({
13658
13856
  return;
13659
13857
  }
13660
13858
  if (message.type === "audio") {
13661
- const payload = transcodePCMToTwilioOutboundPayload(Uint8Array.from(Buffer3.from(message.chunkBase64, "base64")), message.format);
13859
+ const payload = transcodePCMToTwilioOutboundPayload(Uint8Array.from(Buffer4.from(message.chunkBase64, "base64")), message.format);
13662
13860
  state.hasOutboundAudioSinceLastInbound = true;
13663
13861
  state.reviewRecorder?.recordTwilioOutbound({
13664
13862
  bytes: payload.length,
@@ -14184,7 +14382,7 @@ var createPlivoMediaStreamBridge = (socket, options) => {
14184
14382
  }
14185
14383
  };
14186
14384
  };
14187
- var toBase642 = (bytes) => Buffer4.from(new Uint8Array(bytes)).toString("base64");
14385
+ var toBase642 = (bytes) => Buffer5.from(new Uint8Array(bytes)).toString("base64");
14188
14386
  var timingSafeEqual2 = (left, right) => {
14189
14387
  const encoder = new TextEncoder;
14190
14388
  const leftBytes = encoder.encode(left);
@@ -14486,7 +14684,7 @@ var createPlivoVoiceRoutes = (options = {}) => {
14486
14684
  };
14487
14685
 
14488
14686
  // src/telephony/telnyx.ts
14489
- import { Buffer as Buffer5 } from "buffer";
14687
+ import { Buffer as Buffer6 } from "buffer";
14490
14688
  import { Elysia as Elysia27 } from "elysia";
14491
14689
  var escapeXml4 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
14492
14690
  var escapeHtml27 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
@@ -14636,7 +14834,7 @@ var createTelnyxMediaStreamBridge = (socket, options) => {
14636
14834
  }
14637
14835
  };
14638
14836
  };
14639
- var decodeBase64 = (value) => Uint8Array.from(Buffer5.from(value, "base64"));
14837
+ var decodeBase64 = (value) => Uint8Array.from(Buffer6.from(value, "base64"));
14640
14838
  var verifyVoiceTelnyxWebhookSignature = async (input) => {
14641
14839
  if (!input.publicKey) {
14642
14840
  return { ok: false, reason: "missing-secret" };
@@ -19346,6 +19544,7 @@ export {
19346
19544
  createVoiceWebhookDeliveryWorkerLoop,
19347
19545
  createVoiceWebhookDeliveryWorker,
19348
19546
  createVoiceTwilioRedirectHandoffAdapter,
19547
+ createVoiceTwilioCampaignDialer,
19349
19548
  createVoiceTurnQualityRoutes,
19350
19549
  createVoiceTurnQualityJSONHandler,
19351
19550
  createVoiceTurnQualityHTMLHandler,
@@ -19368,6 +19567,7 @@ export {
19368
19567
  createVoiceToolContractJSONHandler,
19369
19568
  createVoiceToolContractHTMLHandler,
19370
19569
  createVoiceToolContract,
19570
+ createVoiceTelnyxCampaignDialer,
19371
19571
  createVoiceTelephonyWebhookRoutes,
19372
19572
  createVoiceTelephonyWebhookHandler,
19373
19573
  createVoiceTelephonyOutcomePolicy,
@@ -19424,6 +19624,7 @@ export {
19424
19624
  createVoicePostgresIntegrationEventStore,
19425
19625
  createVoicePostgresExternalObjectMapStore,
19426
19626
  createVoicePostgresCampaignStore,
19627
+ createVoicePlivoCampaignDialer,
19427
19628
  createVoicePhoneAgent,
19428
19629
  createVoiceOutcomeContractRoutes,
19429
19630
  createVoiceOutcomeContractJSONHandler,
@@ -8434,7 +8434,10 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
8434
8434
  ]),
8435
8435
  durationMs,
8436
8436
  from,
8437
- metadata: payload,
8437
+ metadata: {
8438
+ ...input.query,
8439
+ ...payload
8440
+ },
8438
8441
  provider,
8439
8442
  reason: firstString(payload, [
8440
8443
  "Reason",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.114",
3
+ "version": "0.0.22-beta.115",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",