@adzenai/ai 1.1.1 → 1.2.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.
package/README.md CHANGED
@@ -51,8 +51,8 @@ dispatchPlacementEvents(events);
51
51
  | Path | Environment | Provides |
52
52
  | --- | --- | --- |
53
53
  | `@adzenai/ai` | Any | Core type re-exports (`AdzenPlacement`, `ProcessResponse`, `ProcessResponseAd`) |
54
- | `@adzenai/ai/copilotkit` | Server or client | `AdzenAsyncMiddleware`, `AdzenAsyncConfig` |
55
- | `@adzenai/ai/copilotkit/react` | Browser (React 18+) | `AdzenCard`, `useAdzenPlacement`, `dispatchPlacementEvent(s)` |
54
+ | `@adzenai/ai/copilotkit` | Async: server or client. Stream: server only. | `AdzenAsyncMiddleware`, `AdzenAsyncConfig`, `AdzenStreamMiddleware`, `AdzenStreamConfig` |
55
+ | `@adzenai/ai/copilotkit/react` | Browser (React 18+) | `AdzenCard`, `InlineAd`, `useAdzenPlacement`, `useAdzenInlineAds`, `buildInlineAdSegments`, `dispatchPlacementEvent(s)` |
56
56
 
57
57
  ## How It Works
58
58
 
@@ -66,19 +66,74 @@ Impression beacons are fired as client-side GET requests directly to the deliver
66
66
 
67
67
  The middleware is **additive only** — it never modifies, blocks, or delays the original event stream. All failures degrade silently.
68
68
 
69
+ ## Streaming Enrichment
70
+
71
+ `AdzenStreamMiddleware` proxies the LLM response stream through Adzen's `/stream` endpoint for in-message ads, instead of waiting for the completed message. Tee the upstream response so one branch feeds AG-UI parsing and the other is piped to Adzen:
72
+
73
+ ```typescript
74
+ const [parseBranch, proxyBranch] = res.body!.tee();
75
+
76
+ for await (const evt of adzen.processStream(parseIntoAgUiEvents(parseBranch), {
77
+ prompt,
78
+ upstreamBody: proxyBranch,
79
+ })) {
80
+ sendToClient(evt);
81
+ }
82
+ ```
83
+
84
+ **Server-side only (Node 18+).** It passes a `ReadableStream` as the fetch request body; browsers other than Chromium do not support this and coerce the stream to the string `"[object ReadableStream]"`. The SDK feature-detects and emits an `adzen_error` rather than sending a corrupted body.
85
+
86
+ ### Rendering inline ads
87
+
88
+ Inline ads carry a `content_offset` — the number of message characters streamed before the ad arrived — so they render at that point in the text rather than at the end of the message. `getInlineAdSegments()` does the splitting:
89
+
90
+ ```tsx
91
+ const { getInlineAdSegments } = useAdzenInlineAds();
92
+
93
+ <div style={{ whiteSpace: "pre-wrap" }}>
94
+ {getInlineAdSegments(msg.id, msg.content).map((segment, i) =>
95
+ segment.kind === "text" ? (
96
+ <span key={i}>{segment.text}</span>
97
+ ) : (
98
+ <InlineAd key={segment.ad.ad_id} ad={segment.ad} />
99
+ ),
100
+ )}
101
+ </div>
102
+ ```
103
+
104
+ `InlineAd` renders as a plain text snippet — an "Ad" pill, the advertiser name, and the CTA as the only link (`[AD] Brooks Brothers clothing sale`) — and is block-level, so it appears on its own line directly below the text it follows. The advertiser text uses `creative.advertiser_name`, falling back to `creative.headline`.
105
+
106
+ Anchors are snapped forward to the next paragraph break, so segments are always whole paragraphs and each one can be passed to a Markdown renderer (`<Markdown>{segment.text}</Markdown>`) without a split breaking `**bold text**`, a list, or a code fence.
107
+
108
+ At most one ad is placed per paragraph break. Ads that would land on an already-used break move to the next free one (the end of the message counts as a slot), and any that still have nowhere to go are held back until the message grows, rather than stacking in one spot.
109
+
69
110
  ## Configuration
70
111
 
71
112
  ```typescript
72
113
  new AdzenAsyncMiddleware({
73
114
  apiKey: string; // required — Adzen API key
74
- endpointUrl?: string; // default: "https://api.adzen.ai/v1/ai"
115
+ endpointUrl?: string; // default: "https://api.adzen.ai/v1"
75
116
  timeoutMs?: number; // default: 3000
76
117
  adUnitPosition?: string; // default: "chin"
77
118
  location?: string; // optional — DMA location code for geo-targeting
78
119
  conversationId?: string; // optional — links impressions to conversation context
120
+ profileId?: string; // optional — sent as X-Profile-Id where APIM does not inject it
79
121
  });
80
122
  ```
81
123
 
124
+ `endpointUrl` is a base URL; the SDK appends `/process` and `/stream`. Include any environment-specific path prefix (such as `/ai` on stage and prod) in the value you pass.
125
+
126
+ `AdzenStreamMiddleware` accepts the same options plus `prefetch` (POST `/process` with the prompt before opening the stream), `placementPollIntervalMs`, `sidecar`, and the `onInlineAd` / `onPlacement` / `onError` / `onStreamStart` / `onStreamEnd` callbacks.
127
+
128
+ <!-- INTERNAL / DEBUGGING-MODE ONLY — do not surface in the public shared-vision/developer-docs.
129
+ There is also an optional `headers` (`Record<string, string>`) option, merged into every outbound
130
+ Adzen request (`/stream`, `/process`, placement polling) for internal debugging (e.g. `X-Adzen-Debug-*`).
131
+ SDK-managed headers (auth, content-type, correlation IDs) always take precedence. -->
132
+
133
+
134
+
135
+ The middleware never makes the assistant message wait on the ad pipeline: the prefetch is dispatched without being awaited, and content events are forwarded as they arrive whether or not an ad has been matched yet.
136
+
82
137
  ## License
83
138
 
84
139
  MIT
@@ -20,7 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/copilotkit/index.ts
21
21
  var copilotkit_exports = {};
22
22
  __export(copilotkit_exports, {
23
- AdzenAsyncMiddleware: () => AdzenAsyncMiddleware
23
+ AdzenAsyncMiddleware: () => AdzenAsyncMiddleware,
24
+ AdzenStreamMiddleware: () => AdzenStreamMiddleware
24
25
  });
25
26
  module.exports = __toCommonJS(copilotkit_exports);
26
27
 
@@ -63,7 +64,7 @@ var AdzenAsyncMiddleware = class {
63
64
  constructor(config) {
64
65
  this.config = {
65
66
  ...config,
66
- endpointUrl: config.endpointUrl ?? "https://api.adzen.ai/v1/ai",
67
+ endpointUrl: config.endpointUrl ?? "https://api.adzen.ai/v1",
67
68
  timeoutMs: config.timeoutMs ?? 3e3,
68
69
  adUnitPosition: config.adUnitPosition ?? "chin"
69
70
  };
@@ -176,15 +177,17 @@ var AdzenAsyncMiddleware = class {
176
177
  };
177
178
  if (this.config.location) body.location = this.config.location;
178
179
  if (this.conversationId) body.conversation_id = this.conversationId;
180
+ const headers = {
181
+ "Content-Type": "application/json",
182
+ "X-API-Key": this.config.apiKey,
183
+ // AI-API requires an Idempotency-Key on /process (400 otherwise).
184
+ // One per ad fetch — each completed message is a distinct request.
185
+ "Idempotency-Key": generateIdempotencyKey()
186
+ };
187
+ if (this.config.profileId) headers["X-Profile-Id"] = this.config.profileId;
179
188
  const res = await fetch(`${this.config.endpointUrl}/process`, {
180
189
  method: "POST",
181
- headers: {
182
- "Content-Type": "application/json",
183
- "X-API-Key": this.config.apiKey,
184
- // AI-API requires an Idempotency-Key on /process (400 otherwise).
185
- // One per ad fetch — each completed message is a distinct request.
186
- "Idempotency-Key": generateIdempotencyKey()
187
- },
190
+ headers,
188
191
  body: JSON.stringify(body),
189
192
  signal: controller.signal
190
193
  });
@@ -229,8 +232,476 @@ var AdzenAsyncMiddleware = class {
229
232
  };
230
233
  }
231
234
  };
235
+
236
+ // src/copilotkit/AdzenStreamClient.ts
237
+ var import_core = require("@adzenai/core");
238
+
239
+ // src/copilotkit/parseSSE.ts
240
+ async function* parseSSE(stream) {
241
+ const reader = stream.getReader();
242
+ const decoder = new TextDecoder();
243
+ let buffer = "";
244
+ let currentEvent;
245
+ let dataLines = [];
246
+ try {
247
+ while (true) {
248
+ const { done, value } = await reader.read();
249
+ if (done) break;
250
+ buffer += decoder.decode(value, { stream: true });
251
+ const lines = buffer.split("\n");
252
+ buffer = lines.pop();
253
+ for (const raw of lines) {
254
+ const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
255
+ if (line === "") {
256
+ if (dataLines.length > 0) {
257
+ yield { event: currentEvent, data: dataLines.join("\n") };
258
+ dataLines = [];
259
+ currentEvent = void 0;
260
+ }
261
+ continue;
262
+ }
263
+ if (line.startsWith(":")) continue;
264
+ const colonIdx = line.indexOf(":");
265
+ if (colonIdx === -1) continue;
266
+ const field = line.slice(0, colonIdx);
267
+ const value2 = line.slice(colonIdx + 1).replace(/^ /, "");
268
+ switch (field) {
269
+ case "data":
270
+ dataLines.push(value2);
271
+ break;
272
+ case "event":
273
+ currentEvent = value2;
274
+ break;
275
+ }
276
+ }
277
+ }
278
+ if (dataLines.length > 0) {
279
+ yield { event: currentEvent, data: dataLines.join("\n") };
280
+ }
281
+ } finally {
282
+ reader.releaseLock();
283
+ }
284
+ }
285
+
286
+ // src/copilotkit/AdzenStreamClient.ts
287
+ var DEFAULT_ENDPOINT = "https://api.adzen.ai/v1";
288
+ var DEFAULT_TIMEOUT_MS = 3e3;
289
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
290
+ var SUPPORTS_REQUEST_STREAMS = (() => {
291
+ try {
292
+ let duplexAccessed = false;
293
+ const hasContentType = new Request("http://localhost", {
294
+ method: "POST",
295
+ body: new ReadableStream(),
296
+ get duplex() {
297
+ duplexAccessed = true;
298
+ return "half";
299
+ }
300
+ }).headers.has("Content-Type");
301
+ return duplexAccessed && !hasContentType;
302
+ } catch {
303
+ return false;
304
+ }
305
+ })();
306
+ var AdzenStreamClient = class {
307
+ endpointUrl;
308
+ apiKey;
309
+ timeoutMs;
310
+ location;
311
+ profileId;
312
+ placementPollIntervalMs;
313
+ prefetchEnabled;
314
+ sidecarEnabled;
315
+ customHeaders;
316
+ conversationId;
317
+ abortController = null;
318
+ placementEndpoint = null;
319
+ pollTimer = null;
320
+ streamEnded = false;
321
+ inlineAdQueue = [];
322
+ streamPromise = null;
323
+ prefetchPromise = null;
324
+ onInlineAd;
325
+ onPlacement;
326
+ onError;
327
+ onStreamStart;
328
+ onStreamEnd;
329
+ constructor(config) {
330
+ this.endpointUrl = config.endpointUrl ?? DEFAULT_ENDPOINT;
331
+ this.apiKey = config.apiKey;
332
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
333
+ this.location = config.location;
334
+ this.profileId = config.profileId;
335
+ this.placementPollIntervalMs = config.placementPollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
336
+ this.prefetchEnabled = config.prefetch ?? false;
337
+ this.sidecarEnabled = config.sidecar ?? true;
338
+ this.customHeaders = config.headers ?? {};
339
+ this.conversationId = config.conversationId ?? (0, import_core.uuidv4)();
340
+ this.onInlineAd = config.onInlineAd;
341
+ this.onPlacement = config.onPlacement;
342
+ this.onError = config.onError;
343
+ this.onStreamStart = config.onStreamStart;
344
+ this.onStreamEnd = config.onStreamEnd;
345
+ }
346
+ getConversationId() {
347
+ return this.conversationId;
348
+ }
349
+ setConversationId(id) {
350
+ this.conversationId = id;
351
+ }
352
+ /**
353
+ * Dispatches the prefetch without waiting for it.
354
+ *
355
+ * The request leaves before `connect()` opens `/stream`, which is all the
356
+ * 2-step chained flow needs — the two calls are correlated server-side by
357
+ * `conversation_id`. Awaiting the response here would stall the assistant
358
+ * message behind a full ad-matching round trip while the LLM keeps
359
+ * streaming, so the buffered tokens would then arrive in one burst.
360
+ */
361
+ startPrefetch(prompt) {
362
+ if (!this.prefetchEnabled) return;
363
+ this.prefetchPromise = this.prefetchProcess(prompt);
364
+ }
365
+ /**
366
+ * 2-step chained flow: POST /process with the user prompt and
367
+ * conversationId. Awaits the response (or timeout) but discards the
368
+ * result. If it fails, logs and continues — the stream will proceed
369
+ * in degraded mode.
370
+ */
371
+ async prefetchProcess(prompt) {
372
+ if (!this.prefetchEnabled) return;
373
+ const controller = new AbortController();
374
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
375
+ try {
376
+ const body = {
377
+ content: prompt,
378
+ message_id: (0, import_core.uuidv4)(),
379
+ conversation_id: this.conversationId
380
+ };
381
+ if (this.location) body.location = this.location;
382
+ const headers = {
383
+ "Content-Type": "application/json",
384
+ "X-API-Key": this.apiKey,
385
+ "Idempotency-Key": (0, import_core.uuidv4)()
386
+ };
387
+ if (this.profileId) headers["X-Profile-Id"] = this.profileId;
388
+ await fetch(`${this.endpointUrl}/process`, {
389
+ method: "POST",
390
+ headers: this.mergeHeaders(headers),
391
+ body: JSON.stringify(body),
392
+ signal: controller.signal
393
+ });
394
+ } catch {
395
+ } finally {
396
+ clearTimeout(timeout);
397
+ }
398
+ }
399
+ /**
400
+ * Opens a streaming POST to /stream, piping the upstream LLM response
401
+ * body directly. The API reads the raw LLM stream in real time and
402
+ * sends SSE ad events back on the response side.
403
+ *
404
+ * @param messageId Unique ID for this assistant message
405
+ * @param body The upstream LLM response body (one branch of a tee)
406
+ */
407
+ connect(messageId, body) {
408
+ this.streamEnded = false;
409
+ this.inlineAdQueue = [];
410
+ this.placementEndpoint = null;
411
+ this.streamPromise = null;
412
+ this.abortController = new AbortController();
413
+ if (!SUPPORTS_REQUEST_STREAMS) {
414
+ this.streamEnded = true;
415
+ this.onError?.({
416
+ type: "adzen_error",
417
+ code: "request_streams_unsupported",
418
+ message: "This runtime cannot send a ReadableStream as a request body, so the LLM stream cannot be proxied. Run AdzenStreamMiddleware server-side (Node 18+) \u2014 browsers other than Chromium do not support fetch upload streams."
419
+ });
420
+ void body.cancel().catch(() => {
421
+ });
422
+ return;
423
+ }
424
+ const headers = {
425
+ "Content-Type": "application/octet-stream",
426
+ "X-API-Key": this.apiKey,
427
+ "Idempotency-Key": (0, import_core.uuidv4)(),
428
+ "X-Message-Id": messageId,
429
+ "X-Conversation-Id": this.conversationId
430
+ };
431
+ if (this.profileId) headers["X-Profile-Id"] = this.profileId;
432
+ if (this.location) headers["X-Location"] = this.location;
433
+ this.streamPromise = this.executeStreamFetch(body, this.mergeHeaders(headers));
434
+ }
435
+ /**
436
+ * Waits for the SSE response stream from the API to finish, along with any
437
+ * prefetch still in flight. Neither can reject — both swallow their errors.
438
+ */
439
+ async finalize() {
440
+ await Promise.all([this.prefetchPromise, this.streamPromise]);
441
+ }
442
+ /**
443
+ * Merges caller-provided passthrough headers with the SDK's fixed headers.
444
+ * Fixed headers are spread last so SDK-managed keys (auth, content-type,
445
+ * correlation IDs) always win over any passthrough value.
446
+ */
447
+ mergeHeaders(fixed) {
448
+ return { ...this.customHeaders, ...fixed };
449
+ }
450
+ async executeStreamFetch(body, headers) {
451
+ const timeout = setTimeout(
452
+ () => this.abortController?.abort(),
453
+ this.timeoutMs
454
+ );
455
+ try {
456
+ const res = await fetch(`${this.endpointUrl}/stream`, {
457
+ method: "POST",
458
+ headers,
459
+ body,
460
+ signal: this.abortController?.signal,
461
+ // @ts-expect-error -- required for streaming request bodies (Node/undici)
462
+ duplex: "half"
463
+ });
464
+ clearTimeout(timeout);
465
+ if (!res.ok || !res.body) return;
466
+ await this.readStream(res.body);
467
+ } catch {
468
+ clearTimeout(timeout);
469
+ }
470
+ }
471
+ /** Drains and returns any queued inline ad events. */
472
+ drainInlineAds() {
473
+ const ads = this.inlineAdQueue.splice(0);
474
+ return ads;
475
+ }
476
+ /** Cancels pending requests and cleans up state. */
477
+ abort() {
478
+ this.abortController?.abort();
479
+ this.stopPolling();
480
+ this.streamEnded = true;
481
+ }
482
+ /** Polls the placement endpoint once. Called on an interval internally. */
483
+ async pollPlacements() {
484
+ if (!this.sidecarEnabled || !this.placementEndpoint) return;
485
+ try {
486
+ const res = await fetch(this.placementEndpoint, {
487
+ method: "GET",
488
+ headers: this.mergeHeaders({ "X-API-Key": this.apiKey })
489
+ });
490
+ if (!res.ok) return;
491
+ const data = await res.json();
492
+ for (const placement of data.placements) {
493
+ this.onPlacement?.(placement);
494
+ }
495
+ } catch {
496
+ }
497
+ }
498
+ async readStream(body) {
499
+ try {
500
+ for await (const frame of parseSSE(body)) {
501
+ if (this.abortController?.signal.aborted) break;
502
+ this.handleFrame(frame.event, frame.data);
503
+ }
504
+ } catch {
505
+ } finally {
506
+ this.streamEnded = true;
507
+ this.stopPolling();
508
+ }
509
+ }
510
+ handleFrame(event, data) {
511
+ switch (event) {
512
+ case "adzen_stream_start": {
513
+ try {
514
+ const parsed = JSON.parse(data);
515
+ const evt = { type: "adzen_stream_start", ...parsed };
516
+ this.placementEndpoint = parsed.placement_endpoint;
517
+ this.onStreamStart?.(evt);
518
+ this.startPolling();
519
+ } catch {
520
+ }
521
+ break;
522
+ }
523
+ case "adzen_inline": {
524
+ try {
525
+ const ad = JSON.parse(data);
526
+ this.inlineAdQueue.push(ad);
527
+ this.onInlineAd?.(ad);
528
+ } catch {
529
+ }
530
+ break;
531
+ }
532
+ case "adzen_error": {
533
+ try {
534
+ const parsed = JSON.parse(data);
535
+ this.onError?.({ type: "adzen_error", ...parsed });
536
+ } catch {
537
+ }
538
+ break;
539
+ }
540
+ case "adzen_stream_end": {
541
+ try {
542
+ const parsed = JSON.parse(data);
543
+ const evt = { type: "adzen_stream_end", ...parsed };
544
+ this.onStreamEnd?.(evt);
545
+ } catch {
546
+ }
547
+ this.streamEnded = true;
548
+ this.stopPolling();
549
+ break;
550
+ }
551
+ default: {
552
+ if (event?.startsWith("adzen_debug_")) break;
553
+ break;
554
+ }
555
+ }
556
+ }
557
+ startPolling() {
558
+ if (!this.sidecarEnabled) return;
559
+ if (this.pollTimer || !this.placementEndpoint) return;
560
+ this.pollTimer = setInterval(
561
+ () => this.pollPlacements(),
562
+ this.placementPollIntervalMs
563
+ );
564
+ }
565
+ stopPolling() {
566
+ if (this.pollTimer) {
567
+ clearInterval(this.pollTimer);
568
+ this.pollTimer = null;
569
+ }
570
+ }
571
+ };
572
+
573
+ // src/copilotkit/streamMiddleware.ts
574
+ var AdzenStreamMiddleware = class {
575
+ config;
576
+ adUnitPosition;
577
+ constructor(config) {
578
+ this.config = config;
579
+ this.adUnitPosition = config.adUnitPosition ?? "chin";
580
+ }
581
+ async *processStream(events, options) {
582
+ const sidecarPlacements = [];
583
+ let client = null;
584
+ let currentMessageId = "";
585
+ let contentOffset = 0;
586
+ const onPlacement = (record) => {
587
+ sidecarPlacements.push(this.mapPlacementRecord(record, currentMessageId));
588
+ };
589
+ const onError = (error) => {
590
+ console.warn(`[adzen-stream] ${error.code}: ${error.message}`);
591
+ };
592
+ for await (const event of events) {
593
+ const type = String(event.type ?? "");
594
+ switch (type) {
595
+ case "RUN_STARTED":
596
+ case "RunStarted": {
597
+ const threadId = String(event.threadId ?? "");
598
+ client = new AdzenStreamClient({
599
+ ...this.config,
600
+ conversationId: threadId || this.config.conversationId,
601
+ onPlacement,
602
+ onError
603
+ });
604
+ if (this.config.prefetch && options?.prompt) {
605
+ client.startPrefetch(options.prompt);
606
+ }
607
+ yield event;
608
+ break;
609
+ }
610
+ case "TEXT_MESSAGE_START":
611
+ case "TextMessageStart": {
612
+ currentMessageId = String(event.messageId ?? "");
613
+ contentOffset = 0;
614
+ if (client && options?.upstreamBody) {
615
+ client.connect(currentMessageId, options.upstreamBody);
616
+ }
617
+ yield event;
618
+ break;
619
+ }
620
+ case "TEXT_MESSAGE_CONTENT":
621
+ case "TextMessageContent": {
622
+ yield event;
623
+ contentOffset += String(event.delta ?? "").length;
624
+ if (client) {
625
+ for (const ad of client.drainInlineAds()) {
626
+ yield this.makeInlineAdEvent(ad, currentMessageId, contentOffset);
627
+ }
628
+ }
629
+ break;
630
+ }
631
+ case "TEXT_MESSAGE_END":
632
+ case "TextMessageEnd": {
633
+ if (client) {
634
+ await client.finalize();
635
+ for (const ad of client.drainInlineAds()) {
636
+ yield this.makeInlineAdEvent(ad, currentMessageId, contentOffset);
637
+ }
638
+ }
639
+ yield event;
640
+ break;
641
+ }
642
+ case "RUN_FINISHED":
643
+ case "RunFinished": {
644
+ if (client) {
645
+ await client.pollPlacements();
646
+ client.abort();
647
+ }
648
+ for (const placement of sidecarPlacements) {
649
+ yield this.makePlacementEvent(placement, placement.ad_id);
650
+ }
651
+ yield event;
652
+ break;
653
+ }
654
+ default:
655
+ yield event;
656
+ }
657
+ }
658
+ }
659
+ /**
660
+ * Wraps an inline ad as an AG-UI custom event anchored to `contentOffset`,
661
+ * the number of message characters emitted before the ad arrived. Clients
662
+ * use it to render the ad directly after that content instead of at the
663
+ * end of the message.
664
+ */
665
+ makeInlineAdEvent(ad, messageId, contentOffset) {
666
+ const value = {
667
+ ...ad,
668
+ message_id: messageId,
669
+ content_offset: contentOffset
670
+ };
671
+ return {
672
+ type: "CUSTOM_EVENT",
673
+ name: "adzen_inline_ad",
674
+ messageId,
675
+ value
676
+ };
677
+ }
678
+ makePlacementEvent(placement, adId) {
679
+ return {
680
+ type: "CUSTOM_EVENT",
681
+ name: "adzen_placement",
682
+ messageId: adId,
683
+ value: {
684
+ ...placement,
685
+ message_id: placement.ad_id
686
+ }
687
+ };
688
+ }
689
+ mapPlacementRecord(record, messageId) {
690
+ return {
691
+ ad_id: String(record.ad_id),
692
+ advertiser_name: "",
693
+ headline: record.headline,
694
+ cta_text: record.cta_text,
695
+ destination_url: record.destination_url,
696
+ adUnitPosition: record.placement || this.adUnitPosition,
697
+ render_impression_url: record.tracking.impression_url,
698
+ view_impression_url: record.tracking.impression_url
699
+ };
700
+ }
701
+ };
232
702
  // Annotate the CommonJS export names for ESM import in node:
233
703
  0 && (module.exports = {
234
- AdzenAsyncMiddleware
704
+ AdzenAsyncMiddleware,
705
+ AdzenStreamMiddleware
235
706
  });
236
707
  //# sourceMappingURL=index.cjs.map