@wavebirdai/api 0.1.0-beta.10 → 0.1.0-beta.11

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
@@ -9,7 +9,7 @@ You need a saved Wavebird Test project and placement in Dashboard, a Server Test
9
9
  1. Install the package and generate the route files for your app directory:
10
10
 
11
11
  ```sh
12
- npm install @wavebirdai/api@0.1.0-beta.10
12
+ npm install @wavebirdai/api@0.1.0-beta.11
13
13
  ./node_modules/.bin/wavebird-next init --lang ts --root . --app-dir src/app
14
14
  ```
15
15
 
@@ -85,7 +85,7 @@ With `sessionIdForRequest` and `turnJournal` configured, the package validates t
85
85
 
86
86
  Call `reportGeneration` only from trusted server code with a job ID returned by an authorized placement. Its events are `started`, `finished`, and `failed`. The server key scopes the request at wavebird; invalid IDs or events are rejected locally, and each report has a five-second deadline and a parallel request limit. Reporting errors reject the returned promise so the caller can handle them outside the model response path.
87
87
 
88
- `@wavebirdai/api/browser` exports the existing tested `createPublisherAds` and `connectPublisherGeneration` lifecycle helper. It loads the hosted renderer and attaches only to a reserved container selected by the publisher. Initialize it with the current app user and conversation IDs; pass optional same-origin `placementEndpoint` and `consentEndpoint` paths when using the server routes in this package. The helper forwards those paths to the hosted renderer. Keep the Server Key and preference signing secret in the server configuration; never pass them as browser options. Use the real wavebird consent dialog or the publisher's actual CMP. Consent on an idle page does not request an ad. For `during`, the helper displays only during an active generation. Its generation request and response stream run without waiting for consent synchronization, the ad response, or renderer readiness. It clears late responses on completion, error, abort, conversation change, GPC, or withdrawal.
88
+ `@wavebirdai/api/browser` exports the existing tested `createPublisherAds` and `connectPublisherGeneration` lifecycle helper. Pass the mount's `ads` instance as the fourth `connectPublisherGeneration` argument when more than one surface can run; the original three-argument global lookup remains for older integrations. It loads the hosted renderer and attaches only to a reserved container selected by the publisher. Initialize it with the current app user and conversation IDs; pass optional same-origin `placementEndpoint` and `consentEndpoint` paths when using the server routes in this package. The helper forwards those paths to the hosted renderer. Keep the Server Key and preference signing secret in the server configuration; never pass them as browser options. Use the real wavebird consent dialog or the publisher's actual CMP. Consent on an idle page does not request an ad. For `during`, the helper displays only during an active generation. Its generation request and response stream run without waiting for consent synchronization, the ad response, or renderer readiness. It clears late responses on completion, error, abort, conversation change, GPC, or withdrawal.
89
89
 
90
90
  For `after`, resolve the generation consumer only after your app presents the result. The browser helper waits for both the transport finish and that consumer before starting the placement. If presentation fails, reject the consumer so the placement does not start.
91
91
 
package/dist/browser.d.ts CHANGED
@@ -1,31 +1,37 @@
1
1
  /** Resolve after the publisher has presented the result; After timing starts then. */
2
2
  export type GenerationConsumer<T> = (response: Response, lifecycle: { onDelta(): void }) => T | Promise<T>;
3
- export type PublisherAds = {
3
+ export type PublisherAds<Bound extends boolean = boolean> = {
4
4
  ready: Promise<void>;
5
5
  connectGeneration<T>(options: { request: (headers: Record<string, string>) => Response | Promise<Response>; consume: GenerationConsumer<T>; signal?: AbortSignal }): Promise<T>;
6
- setConversation(binding: { userId: string; conversationId: string; sessionId?: string }): void;
6
+ setConversation(binding: Bound extends true
7
+ ? { userId: string; conversationId: string; sessionId: string }
8
+ : { userId: string; conversationId: string; sessionId?: string }): void;
7
9
  dispose(): void;
8
10
  openPrivacySettings(): void;
9
11
  grant(choice?: unknown): Promise<unknown>;
10
12
  deny(): Promise<unknown>;
11
13
  withdraw(): Promise<unknown>;
12
14
  };
13
- export function createPublisherAds(options: {
15
+ type PublisherAdsOptionsBase = {
14
16
  target: HTMLElement;
15
17
  clientId: string;
16
18
  surfaceId: string;
17
19
  jobType: "chat" | "code" | "image" | "voice" | "agent";
18
20
  userId: string;
19
21
  conversationId: string;
20
- /** Opt-in successor contract: a session ID supplied by the host's authenticated context. */
21
- sessionId?: string;
22
- turnBinding?: boolean;
23
22
  timing?: "before" | "during" | "after";
24
23
  consentMode?: "wavebird_consent" | "own_cmp";
24
+ /** Optional official renderer URL. Only https://api.wavebird.ai/v1/render.js is supported. */
25
25
  rendererUrl?: string;
26
26
  privacySettingsTarget?: string;
27
27
  getFormat?: () => "banner" | "native" | "clip" | "auto";
28
28
  placementEndpoint?: string;
29
29
  consentEndpoint?: string;
30
- }): PublisherAds;
31
- export function connectPublisherGeneration<T>(url: string, init: RequestInit, consume: GenerationConsumer<T>): Promise<T>;
30
+ };
31
+ export function createPublisherAds(options: PublisherAdsOptionsBase &
32
+ { turnBinding?: false; sessionId?: string }): PublisherAds<false>;
33
+ /** Opt-in successor: a trusted host session is required at mount and at each conversation change. */
34
+ export function createPublisherAds(options: PublisherAdsOptionsBase &
35
+ { turnBinding: true; sessionId: string }): PublisherAds<true>;
36
+ /** Pass the mount's ads instance to keep concurrent surfaces bound to their own generation. */
37
+ export function connectPublisherGeneration<T>(url: string, init: RequestInit, consume: GenerationConsumer<T>, ads?: PublisherAds): Promise<T>;
package/dist/browser.js CHANGED
@@ -21,19 +21,44 @@ function loadRenderer(url) {
21
21
  export function createPublisherAds(options) {
22
22
  const target = options.target;
23
23
  const requiredText = value => typeof value === "string" && value.trim().length > 0;
24
- if (!target || !requiredText(options.userId) || !requiredText(options.conversationId))
24
+ if (!target || typeof target.append !== "function" || !requiredText(options.userId) || !requiredText(options.conversationId))
25
25
  throw new Error("Application container and session are required");
26
26
  if (!["chat", "code", "image", "voice", "agent"].includes(options.jobType)) throw new Error("publisher_job_invalid");
27
27
  if (typeof options.clientId !== "string" || !options.clientId.startsWith("wbproj_"))
28
28
  throw new Error("publisher_project_invalid");
29
29
  if (typeof options.surfaceId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(options.surfaceId))
30
30
  throw new Error("publisher_placement_invalid");
31
+ if (options.turnBinding !== undefined && typeof options.turnBinding !== "boolean")
32
+ throw new Error("publisher_turn_binding_invalid");
31
33
  const turnBinding = options.turnBinding === true;
32
34
  const validId = value => typeof value === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(value);
33
35
  if (turnBinding && !validId(options.sessionId)) throw new Error("publisher_trusted_session_required");
34
36
  const timing = options.timing === undefined ? "during" : options.timing;
35
37
  const consentMode = options.consentMode === undefined ? "wavebird_consent" : options.consentMode;
36
38
  if (!["before", "during", "after"].includes(timing) || !["wavebird_consent", "own_cmp"].includes(consentMode)) throw new Error("Invalid publisher configuration");
39
+ const validEndpoint = value => {
40
+ if (typeof value !== "string" || !value || value.trim() !== value) return false;
41
+ try {
42
+ const base = new URL(window.location?.href ?? "https://publisher.invalid/");
43
+ const endpoint = new URL(value, base);
44
+ return ["http:", "https:"].includes(endpoint.protocol) && endpoint.origin === base.origin &&
45
+ !endpoint.username && !endpoint.password && !endpoint.hash;
46
+ } catch { return false; }
47
+ };
48
+ if ((options.placementEndpoint !== undefined && !validEndpoint(options.placementEndpoint)) ||
49
+ (options.consentEndpoint !== undefined && !validEndpoint(options.consentEndpoint)))
50
+ throw new Error("publisher_endpoint_invalid");
51
+ const rendererUrl = options.rendererUrl === undefined ? "https://api.wavebird.ai/v1/render.js" : options.rendererUrl;
52
+ if (rendererUrl !== "https://api.wavebird.ai/v1/render.js") throw new Error("publisher_renderer_url_invalid");
53
+ if (options.getFormat !== undefined && typeof options.getFormat !== "function")
54
+ throw new Error("publisher_format_hook_invalid");
55
+ let privacyTarget = null;
56
+ if (consentMode === "wavebird_consent" && options.privacySettingsTarget !== undefined) {
57
+ if (typeof options.privacySettingsTarget !== "string" || !options.privacySettingsTarget.trim())
58
+ throw new Error("publisher_privacy_target_invalid");
59
+ try { privacyTarget = document.querySelector(options.privacySettingsTarget); }
60
+ catch { throw new Error("publisher_privacy_target_invalid"); }
61
+ }
37
62
  let binding = { userId: options.userId, conversationId: options.conversationId,
38
63
  sessionId: turnBinding ? options.sessionId : crypto.randomUUID() };
39
64
  let sponsor = null, disposed = false, context = 0, active = null;
@@ -41,8 +66,6 @@ export function createPublisherAds(options) {
41
66
  slot.setAttribute("aria-label", "Sponsored placement");
42
67
  slot.style.cssText = "width:100%;height:100%;overflow:hidden;margin:0 auto";
43
68
  target.append(slot);
44
- const privacyTarget = consentMode === "wavebird_consent" && options.privacySettingsTarget
45
- ? document.querySelector(options.privacySettingsTarget) : null;
46
69
  const openPrivacySettings = () => { if (!disposed) sponsor?.openPrivacySettings(); };
47
70
  privacyTarget?.addEventListener("click", openPrivacySettings);
48
71
  const notify = (method, id) => {
@@ -92,7 +115,7 @@ export function createPublisherAds(options) {
92
115
  window.removeEventListener("pagehide", dispose);
93
116
  }
94
117
  window.addEventListener("pagehide", dispose);
95
- const ready = loadRenderer(options.rendererUrl || "https://api.wavebird.ai/v1/render.js").then(() => {
118
+ const ready = loadRenderer(rendererUrl).then(() => {
96
119
  if (disposed) return;
97
120
  sponsor = window.wavebird.createChatSponsor({ target: slot, ...binding,
98
121
  clientId: options.clientId, surfaceId: options.surfaceId, timing, consentMode,
@@ -123,8 +146,14 @@ export function createPublisherAds(options) {
123
146
  if (signal?.aborted) abort();
124
147
  const headers = { "x-wavebird-session-id": job.sessionId, "x-wavebird-generation-id": job.id };
125
148
  try {
149
+ if (signal?.aborted) throw new Error("generation_aborted");
126
150
  const response = await request(headers);
127
- if (response.headers.get("x-wavebird-generation-transport") !== "1") return await consume(response);
151
+ if (!current(job)) throw new Error("generation_aborted");
152
+ if (response.headers.get("x-wavebird-generation-transport") !== "1") {
153
+ const result = await consume(response, { onDelta() {} });
154
+ if (!current(job)) throw new Error("generation_aborted");
155
+ return result;
156
+ }
128
157
  if (!response.body) throw new Error("generation_transport_missing");
129
158
  reader = response.body.getReader();
130
159
  async function nextRecord() {
@@ -161,7 +190,7 @@ export function createPublisherAds(options) {
161
190
  if (typeof record.data !== "string" || record.data.length > 21848 || !/^[A-Za-z0-9+/]*={0,2}$/.test(record.data)) throw new Error("generation_bytes_invalid");
162
191
  const raw = atob(record.data), bytes = Uint8Array.from(raw, value => value.charCodeAt(0));
163
192
  controller.enqueue(bytes);
164
- if (timing !== "before" && options.jobType !== "image") event(job, "progress");
193
+ if (bytes.length && timing !== "before" && options.jobType !== "image") event(job, "progress");
165
194
  return;
166
195
  }
167
196
  if (["done", "error", "abort"].includes(record.type)) {
@@ -205,8 +234,7 @@ export function createPublisherAds(options) {
205
234
  withdraw: () => sponsor ? sponsor.withdraw() : Promise.resolve({ status: "renderer_unavailable" }) };
206
235
  }
207
236
 
208
- export async function connectPublisherGeneration(url, init, consume) {
209
- const ads = window.publisherAds;
237
+ export async function connectPublisherGeneration(url, init, consume, ads = window.publisherAds) {
210
238
  if (!ads) return consume(await fetch(url, init), { onDelta() {} });
211
239
  return ads.connectGeneration({
212
240
  signal: init.signal ?? undefined,
package/dist/server.d.ts CHANGED
@@ -30,15 +30,12 @@ export function createPublisherTurnJournal(options: {
30
30
  }): PublisherTurnJournal;
31
31
  /** Derive a stable, domain-separated journal key from the existing server-only preference secret. */
32
32
  export function derivePublisherTurnKeySecret(preferenceSecret: string): string;
33
- export type PublisherServerOptions = {
33
+ type PublisherServerBaseOptions = {
34
34
  apiBaseUrl?: string;
35
35
  clientId: string;
36
36
  secretKey: string;
37
37
  preferenceSecret: string;
38
38
  authorize: (request: Request) => boolean | Promise<boolean>;
39
- /** Opt-in successor: derive the exact browser session from the host's authenticated request. */
40
- sessionIdForRequest?: (request: Request) => string | null | Promise<string | null>;
41
- turnJournal?: PublisherTurnJournal;
42
39
  onDiagnostic?: (code: string) => void;
43
40
  publicOrigins: string[];
44
41
  surfaceId: string;
@@ -47,7 +44,7 @@ export type PublisherServerOptions = {
47
44
  locale: string;
48
45
  timing: "before" | "during" | "after";
49
46
  consentMode: "wavebird_consent" | "own_cmp";
50
- gdprApplies: boolean;
47
+ gdprApplies?: boolean;
51
48
  publisher: { app_name: string; app_domain: string; categories: string[] };
52
49
  allowedFormats: Array<"banner" | "native" | "clip">;
53
50
  positionHint: "above" | "below" | "sidebar" | "between";
@@ -60,6 +57,12 @@ export type PublisherServerOptions = {
60
57
  bidfloorCurrency?: string;
61
58
  blockedCategories?: string[];
62
59
  };
60
+ /** Trusted session resolution and the durable turn journal must be supplied together. */
61
+ export type PublisherServerOptions = PublisherServerBaseOptions & (
62
+ { sessionIdForRequest?: never; turnJournal?: never } |
63
+ { sessionIdForRequest: (request: Request) => string | null | Promise<string | null>;
64
+ turnJournal: PublisherTurnJournal }
65
+ );
63
66
  export type GenerationLifecycleEvent = {
64
67
  type: "started" | "done" | "error" | "abort";
65
68
  sessionId: string;
package/dist/server.js CHANGED
@@ -28,6 +28,8 @@ function normalizeOptions(options) {
28
28
  if (typeof document !== "undefined") throw new Error("publisher_server_only");
29
29
  if (options && Object.keys(options).some(key => !SERVER_OPTION_KEYS.has(key))) throw new Error("publisher_option_unknown");
30
30
  if (!options || typeof options.authorize !== "function") throw new Error("publisher_authorize_required");
31
+ if (options.onDiagnostic !== undefined && typeof options.onDiagnostic !== "function")
32
+ throw new Error("publisher_diagnostic_invalid");
31
33
  if (typeof options.preferenceSecret !== "string" || new TextEncoder().encode(options.preferenceSecret).byteLength < 32) throw new Error("preference_secret_required");
32
34
  if ((options.turnJournal === undefined) !== (options.sessionIdForRequest === undefined) ||
33
35
  (options.turnJournal !== undefined && (typeof options.sessionIdForRequest !== "function" ||
@@ -17,6 +17,8 @@ export function createPublisherTurnJournal({ storage, reportGeneration, keySecre
17
17
  typeof reportGeneration !== "function") throw new Error("publisher_turn_storage_invalid");
18
18
  if (typeof keySecret !== "string" || Buffer.byteLength(keySecret, "utf8") < 32)
19
19
  throw new Error("publisher_turn_key_secret_invalid");
20
+ if (onDiagnostic !== undefined && typeof onDiagnostic !== "function")
21
+ throw new Error("publisher_diagnostic_invalid");
20
22
  const tasks = new Set();
21
23
  let activeReports = 0;
22
24
  const reportWaiters = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavebirdai/api",
3
- "version": "0.1.0-beta.10",
3
+ "version": "0.1.0-beta.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Integrate Banner, Native, and Clip placements into authenticated chat apps with server-side keys, consent, and generation lifecycle handling",
package/src/browser.d.ts CHANGED
@@ -1,31 +1,37 @@
1
1
  /** Resolve after the publisher has presented the result; After timing starts then. */
2
2
  export type GenerationConsumer<T> = (response: Response, lifecycle: { onDelta(): void }) => T | Promise<T>;
3
- export type PublisherAds = {
3
+ export type PublisherAds<Bound extends boolean = boolean> = {
4
4
  ready: Promise<void>;
5
5
  connectGeneration<T>(options: { request: (headers: Record<string, string>) => Response | Promise<Response>; consume: GenerationConsumer<T>; signal?: AbortSignal }): Promise<T>;
6
- setConversation(binding: { userId: string; conversationId: string; sessionId?: string }): void;
6
+ setConversation(binding: Bound extends true
7
+ ? { userId: string; conversationId: string; sessionId: string }
8
+ : { userId: string; conversationId: string; sessionId?: string }): void;
7
9
  dispose(): void;
8
10
  openPrivacySettings(): void;
9
11
  grant(choice?: unknown): Promise<unknown>;
10
12
  deny(): Promise<unknown>;
11
13
  withdraw(): Promise<unknown>;
12
14
  };
13
- export function createPublisherAds(options: {
15
+ type PublisherAdsOptionsBase = {
14
16
  target: HTMLElement;
15
17
  clientId: string;
16
18
  surfaceId: string;
17
19
  jobType: "chat" | "code" | "image" | "voice" | "agent";
18
20
  userId: string;
19
21
  conversationId: string;
20
- /** Opt-in successor contract: a session ID supplied by the host's authenticated context. */
21
- sessionId?: string;
22
- turnBinding?: boolean;
23
22
  timing?: "before" | "during" | "after";
24
23
  consentMode?: "wavebird_consent" | "own_cmp";
24
+ /** Optional official renderer URL. Only https://api.wavebird.ai/v1/render.js is supported. */
25
25
  rendererUrl?: string;
26
26
  privacySettingsTarget?: string;
27
27
  getFormat?: () => "banner" | "native" | "clip" | "auto";
28
28
  placementEndpoint?: string;
29
29
  consentEndpoint?: string;
30
- }): PublisherAds;
31
- export function connectPublisherGeneration<T>(url: string, init: RequestInit, consume: GenerationConsumer<T>): Promise<T>;
30
+ };
31
+ export function createPublisherAds(options: PublisherAdsOptionsBase &
32
+ { turnBinding?: false; sessionId?: string }): PublisherAds<false>;
33
+ /** Opt-in successor: a trusted host session is required at mount and at each conversation change. */
34
+ export function createPublisherAds(options: PublisherAdsOptionsBase &
35
+ { turnBinding: true; sessionId: string }): PublisherAds<true>;
36
+ /** Pass the mount's ads instance to keep concurrent surfaces bound to their own generation. */
37
+ export function connectPublisherGeneration<T>(url: string, init: RequestInit, consume: GenerationConsumer<T>, ads?: PublisherAds): Promise<T>;
package/src/browser.js CHANGED
@@ -21,19 +21,44 @@ function loadRenderer(url) {
21
21
  export function createPublisherAds(options) {
22
22
  const target = options.target;
23
23
  const requiredText = value => typeof value === "string" && value.trim().length > 0;
24
- if (!target || !requiredText(options.userId) || !requiredText(options.conversationId))
24
+ if (!target || typeof target.append !== "function" || !requiredText(options.userId) || !requiredText(options.conversationId))
25
25
  throw new Error("Application container and session are required");
26
26
  if (!["chat", "code", "image", "voice", "agent"].includes(options.jobType)) throw new Error("publisher_job_invalid");
27
27
  if (typeof options.clientId !== "string" || !options.clientId.startsWith("wbproj_"))
28
28
  throw new Error("publisher_project_invalid");
29
29
  if (typeof options.surfaceId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(options.surfaceId))
30
30
  throw new Error("publisher_placement_invalid");
31
+ if (options.turnBinding !== undefined && typeof options.turnBinding !== "boolean")
32
+ throw new Error("publisher_turn_binding_invalid");
31
33
  const turnBinding = options.turnBinding === true;
32
34
  const validId = value => typeof value === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(value);
33
35
  if (turnBinding && !validId(options.sessionId)) throw new Error("publisher_trusted_session_required");
34
36
  const timing = options.timing === undefined ? "during" : options.timing;
35
37
  const consentMode = options.consentMode === undefined ? "wavebird_consent" : options.consentMode;
36
38
  if (!["before", "during", "after"].includes(timing) || !["wavebird_consent", "own_cmp"].includes(consentMode)) throw new Error("Invalid publisher configuration");
39
+ const validEndpoint = value => {
40
+ if (typeof value !== "string" || !value || value.trim() !== value) return false;
41
+ try {
42
+ const base = new URL(window.location?.href ?? "https://publisher.invalid/");
43
+ const endpoint = new URL(value, base);
44
+ return ["http:", "https:"].includes(endpoint.protocol) && endpoint.origin === base.origin &&
45
+ !endpoint.username && !endpoint.password && !endpoint.hash;
46
+ } catch { return false; }
47
+ };
48
+ if ((options.placementEndpoint !== undefined && !validEndpoint(options.placementEndpoint)) ||
49
+ (options.consentEndpoint !== undefined && !validEndpoint(options.consentEndpoint)))
50
+ throw new Error("publisher_endpoint_invalid");
51
+ const rendererUrl = options.rendererUrl === undefined ? "https://api.wavebird.ai/v1/render.js" : options.rendererUrl;
52
+ if (rendererUrl !== "https://api.wavebird.ai/v1/render.js") throw new Error("publisher_renderer_url_invalid");
53
+ if (options.getFormat !== undefined && typeof options.getFormat !== "function")
54
+ throw new Error("publisher_format_hook_invalid");
55
+ let privacyTarget = null;
56
+ if (consentMode === "wavebird_consent" && options.privacySettingsTarget !== undefined) {
57
+ if (typeof options.privacySettingsTarget !== "string" || !options.privacySettingsTarget.trim())
58
+ throw new Error("publisher_privacy_target_invalid");
59
+ try { privacyTarget = document.querySelector(options.privacySettingsTarget); }
60
+ catch { throw new Error("publisher_privacy_target_invalid"); }
61
+ }
37
62
  let binding = { userId: options.userId, conversationId: options.conversationId,
38
63
  sessionId: turnBinding ? options.sessionId : crypto.randomUUID() };
39
64
  let sponsor = null, disposed = false, context = 0, active = null;
@@ -41,8 +66,6 @@ export function createPublisherAds(options) {
41
66
  slot.setAttribute("aria-label", "Sponsored placement");
42
67
  slot.style.cssText = "width:100%;height:100%;overflow:hidden;margin:0 auto";
43
68
  target.append(slot);
44
- const privacyTarget = consentMode === "wavebird_consent" && options.privacySettingsTarget
45
- ? document.querySelector(options.privacySettingsTarget) : null;
46
69
  const openPrivacySettings = () => { if (!disposed) sponsor?.openPrivacySettings(); };
47
70
  privacyTarget?.addEventListener("click", openPrivacySettings);
48
71
  const notify = (method, id) => {
@@ -92,7 +115,7 @@ export function createPublisherAds(options) {
92
115
  window.removeEventListener("pagehide", dispose);
93
116
  }
94
117
  window.addEventListener("pagehide", dispose);
95
- const ready = loadRenderer(options.rendererUrl || "https://api.wavebird.ai/v1/render.js").then(() => {
118
+ const ready = loadRenderer(rendererUrl).then(() => {
96
119
  if (disposed) return;
97
120
  sponsor = window.wavebird.createChatSponsor({ target: slot, ...binding,
98
121
  clientId: options.clientId, surfaceId: options.surfaceId, timing, consentMode,
@@ -123,8 +146,14 @@ export function createPublisherAds(options) {
123
146
  if (signal?.aborted) abort();
124
147
  const headers = { "x-wavebird-session-id": job.sessionId, "x-wavebird-generation-id": job.id };
125
148
  try {
149
+ if (signal?.aborted) throw new Error("generation_aborted");
126
150
  const response = await request(headers);
127
- if (response.headers.get("x-wavebird-generation-transport") !== "1") return await consume(response);
151
+ if (!current(job)) throw new Error("generation_aborted");
152
+ if (response.headers.get("x-wavebird-generation-transport") !== "1") {
153
+ const result = await consume(response, { onDelta() {} });
154
+ if (!current(job)) throw new Error("generation_aborted");
155
+ return result;
156
+ }
128
157
  if (!response.body) throw new Error("generation_transport_missing");
129
158
  reader = response.body.getReader();
130
159
  async function nextRecord() {
@@ -161,7 +190,7 @@ export function createPublisherAds(options) {
161
190
  if (typeof record.data !== "string" || record.data.length > 21848 || !/^[A-Za-z0-9+/]*={0,2}$/.test(record.data)) throw new Error("generation_bytes_invalid");
162
191
  const raw = atob(record.data), bytes = Uint8Array.from(raw, value => value.charCodeAt(0));
163
192
  controller.enqueue(bytes);
164
- if (timing !== "before" && options.jobType !== "image") event(job, "progress");
193
+ if (bytes.length && timing !== "before" && options.jobType !== "image") event(job, "progress");
165
194
  return;
166
195
  }
167
196
  if (["done", "error", "abort"].includes(record.type)) {
@@ -205,8 +234,7 @@ export function createPublisherAds(options) {
205
234
  withdraw: () => sponsor ? sponsor.withdraw() : Promise.resolve({ status: "renderer_unavailable" }) };
206
235
  }
207
236
 
208
- export async function connectPublisherGeneration(url, init, consume) {
209
- const ads = window.publisherAds;
237
+ export async function connectPublisherGeneration(url, init, consume, ads = window.publisherAds) {
210
238
  if (!ads) return consume(await fetch(url, init), { onDelta() {} });
211
239
  return ads.connectGeneration({
212
240
  signal: init.signal ?? undefined,
package/src/server.d.ts CHANGED
@@ -30,15 +30,12 @@ export function createPublisherTurnJournal(options: {
30
30
  }): PublisherTurnJournal;
31
31
  /** Derive a stable, domain-separated journal key from the existing server-only preference secret. */
32
32
  export function derivePublisherTurnKeySecret(preferenceSecret: string): string;
33
- export type PublisherServerOptions = {
33
+ type PublisherServerBaseOptions = {
34
34
  apiBaseUrl?: string;
35
35
  clientId: string;
36
36
  secretKey: string;
37
37
  preferenceSecret: string;
38
38
  authorize: (request: Request) => boolean | Promise<boolean>;
39
- /** Opt-in successor: derive the exact browser session from the host's authenticated request. */
40
- sessionIdForRequest?: (request: Request) => string | null | Promise<string | null>;
41
- turnJournal?: PublisherTurnJournal;
42
39
  onDiagnostic?: (code: string) => void;
43
40
  publicOrigins: string[];
44
41
  surfaceId: string;
@@ -47,7 +44,7 @@ export type PublisherServerOptions = {
47
44
  locale: string;
48
45
  timing: "before" | "during" | "after";
49
46
  consentMode: "wavebird_consent" | "own_cmp";
50
- gdprApplies: boolean;
47
+ gdprApplies?: boolean;
51
48
  publisher: { app_name: string; app_domain: string; categories: string[] };
52
49
  allowedFormats: Array<"banner" | "native" | "clip">;
53
50
  positionHint: "above" | "below" | "sidebar" | "between";
@@ -60,6 +57,12 @@ export type PublisherServerOptions = {
60
57
  bidfloorCurrency?: string;
61
58
  blockedCategories?: string[];
62
59
  };
60
+ /** Trusted session resolution and the durable turn journal must be supplied together. */
61
+ export type PublisherServerOptions = PublisherServerBaseOptions & (
62
+ { sessionIdForRequest?: never; turnJournal?: never } |
63
+ { sessionIdForRequest: (request: Request) => string | null | Promise<string | null>;
64
+ turnJournal: PublisherTurnJournal }
65
+ );
63
66
  export type GenerationLifecycleEvent = {
64
67
  type: "started" | "done" | "error" | "abort";
65
68
  sessionId: string;
package/src/server.js CHANGED
@@ -28,6 +28,8 @@ function normalizeOptions(options) {
28
28
  if (typeof document !== "undefined") throw new Error("publisher_server_only");
29
29
  if (options && Object.keys(options).some(key => !SERVER_OPTION_KEYS.has(key))) throw new Error("publisher_option_unknown");
30
30
  if (!options || typeof options.authorize !== "function") throw new Error("publisher_authorize_required");
31
+ if (options.onDiagnostic !== undefined && typeof options.onDiagnostic !== "function")
32
+ throw new Error("publisher_diagnostic_invalid");
31
33
  if (typeof options.preferenceSecret !== "string" || new TextEncoder().encode(options.preferenceSecret).byteLength < 32) throw new Error("preference_secret_required");
32
34
  if ((options.turnJournal === undefined) !== (options.sessionIdForRequest === undefined) ||
33
35
  (options.turnJournal !== undefined && (typeof options.sessionIdForRequest !== "function" ||
@@ -17,6 +17,8 @@ export function createPublisherTurnJournal({ storage, reportGeneration, keySecre
17
17
  typeof reportGeneration !== "function") throw new Error("publisher_turn_storage_invalid");
18
18
  if (typeof keySecret !== "string" || Buffer.byteLength(keySecret, "utf8") < 32)
19
19
  throw new Error("publisher_turn_key_secret_invalid");
20
+ if (onDiagnostic !== undefined && typeof onDiagnostic !== "function")
21
+ throw new Error("publisher_diagnostic_invalid");
20
22
  const tasks = new Set();
21
23
  let activeReports = 0;
22
24
  const reportWaiters = [];