@wavebirdai/api 0.1.0-beta.15 → 0.1.0-beta.16
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 +2 -2
- package/bin/wavebird-next.mjs +1 -1
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +19 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.js +2 -2
- package/dist/turn-journal.js +1 -1
- package/docs/web/generation.md +1 -1
- package/docs/web/nextjs.md +5 -1
- package/docs/web/reference.md +12 -1
- package/package.json +1 -1
- package/src/browser.d.ts +2 -0
- package/src/browser.js +19 -1
- package/src/server.d.ts +2 -1
- package/src/server.js +2 -2
- package/src/turn-journal.js +1 -1
- package/templates/NEXT-SETUP.md +2 -0
package/README.md
CHANGED
|
@@ -22,11 +22,11 @@ Use Node.js 20 or later on your server.
|
|
|
22
22
|
2. **Install the package.** Run this in your app's terminal, in the folder containing `package.json`:
|
|
23
23
|
|
|
24
24
|
```sh
|
|
25
|
-
npm install @wavebirdai/api@0.1.0-beta.
|
|
25
|
+
npm install @wavebirdai/api@0.1.0-beta.16
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
3. **Save the Server ENV from Overview, Integration step 2.** The key-only download is not the full configuration. Keep `WAVEBIRD_SECRET_KEY` and the stable, project-specific `WAVEBIRD_PREFERENCE_SECRET` out of browser code and Git. Include the saved `WAVEBIRD_PUBLIC_ORIGINS`.
|
|
29
|
-
4. **Connect the backend and frontend code.** Follow Dashboard's generated files to connect your app session, durable turn storage, model stream and ad slot. Keep the saved formats, sizes and timing.
|
|
29
|
+
4. **Connect the backend and frontend code.** Follow Dashboard's generated files to connect your app session, durable turn storage, model stream and ad slot. The source ZIP contains code only; download secret Server ENV separately. Keep the saved formats, sizes and timing. For apps with a Content Security Policy, follow the [exact renderer rules](docs/web/nextjs.md#generate-files).
|
|
30
30
|
5. **Run your first Test ad.** Confirm that the ad appears and the request is recorded in Test Logs. Check that generation still works with declined consent, no available ad and Stop.
|
|
31
31
|
|
|
32
32
|
Test ads are nonpayable. Complete the [Live prerequisites](https://wavebird.ai/api/guides/going-live) before requesting Live activation.
|
package/bin/wavebird-next.mjs
CHANGED
|
@@ -46,7 +46,7 @@ export async function plan(argv) {
|
|
|
46
46
|
];
|
|
47
47
|
for (const [source, dest] of files) {
|
|
48
48
|
await access(source, constants.R_OK);
|
|
49
|
-
try { await access(dest); throw new Error(`Refusing to overwrite ${dest}`); }
|
|
49
|
+
try { await access(dest); throw new Error(`Refusing to overwrite ${dest}. Keep the existing file. Copy the matching connection hooks from Dashboard Overview into your app. See https://wavebird.ai/api/quickstart#backend`); }
|
|
50
50
|
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
51
51
|
}
|
|
52
52
|
return { lang, projectRoot, appDir, files };
|
package/dist/browser.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ type PublisherAdsOptionsBase = {
|
|
|
30
30
|
rendererUrl?: string;
|
|
31
31
|
privacySettingsTarget?: string;
|
|
32
32
|
getFormat?: () => "banner" | "native" | "clip" | "auto";
|
|
33
|
+
/** Fixed diagnostic codes only. Exceptions in this callback never interrupt the model. */
|
|
34
|
+
onDiagnostic?: (event: { code: "renderer_csp_blocked" | "renderer_unavailable" | "renderer_frame_unavailable" }) => void;
|
|
33
35
|
placementEndpoint?: string;
|
|
34
36
|
consentEndpoint?: string;
|
|
35
37
|
};
|
package/dist/browser.js
CHANGED
|
@@ -52,6 +52,8 @@ export function createPublisherAds(options) {
|
|
|
52
52
|
if (rendererUrl !== "https://api.wavebird.ai/v1/render.js") throw new Error("publisher_renderer_url_invalid");
|
|
53
53
|
if (options.getFormat !== undefined && typeof options.getFormat !== "function")
|
|
54
54
|
throw new Error("publisher_format_hook_invalid");
|
|
55
|
+
if (options.onDiagnostic !== undefined && typeof options.onDiagnostic !== "function")
|
|
56
|
+
throw new Error("publisher_diagnostic_hook_invalid");
|
|
55
57
|
let privacyTarget = null;
|
|
56
58
|
if (consentMode === "wavebird_consent" && options.privacySettingsTarget !== undefined) {
|
|
57
59
|
if (typeof options.privacySettingsTarget !== "string" || !options.privacySettingsTarget.trim())
|
|
@@ -62,7 +64,21 @@ export function createPublisherAds(options) {
|
|
|
62
64
|
let binding = { userId: options.userId, conversationId: options.conversationId,
|
|
63
65
|
sessionId: turnBinding ? options.sessionId : crypto.randomUUID() };
|
|
64
66
|
let sponsor = null, disposed = false, context = 0, active = null;
|
|
67
|
+
const diagnostic = code => {
|
|
68
|
+
if (!disposed) { try { Promise.resolve(options.onDiagnostic?.({ code })).catch(() => {}); } catch {} }
|
|
69
|
+
};
|
|
70
|
+
const onPolicyViolation = event => {
|
|
71
|
+
if (event.disposition !== "enforce" ||
|
|
72
|
+
!["script-src", "script-src-elem", "frame-src", "connect-src"].includes(event.effectiveDirective)) return;
|
|
73
|
+
try {
|
|
74
|
+
if (new URL(event.blockedURI).origin === "https://api.wavebird.ai") diagnostic("renderer_csp_blocked");
|
|
75
|
+
} catch {}
|
|
76
|
+
};
|
|
77
|
+
document.addEventListener?.("securitypolicyviolation", onPolicyViolation);
|
|
65
78
|
const slot = document.createElement("section");
|
|
79
|
+
const onRenderFailure = () => diagnostic("renderer_frame_unavailable");
|
|
80
|
+
// Renderer failures originate in a child mount and do not bubble.
|
|
81
|
+
slot.addEventListener?.("ad:render_failed", onRenderFailure, true);
|
|
66
82
|
slot.setAttribute("aria-label", "Sponsored placement");
|
|
67
83
|
slot.style.cssText = "width:100%;height:100%;overflow:hidden;margin:0 auto";
|
|
68
84
|
target.append(slot);
|
|
@@ -121,8 +137,10 @@ export function createPublisherAds(options) {
|
|
|
121
137
|
if (active) event(active, "abort");
|
|
122
138
|
disposed = true; context++; active = null;
|
|
123
139
|
sponsor?.dispose(); sponsor = null; slot.remove();
|
|
140
|
+
slot.removeEventListener?.("ad:render_failed", onRenderFailure, true);
|
|
124
141
|
privacyTarget?.removeEventListener("click", openPrivacySettings);
|
|
125
142
|
window.removeEventListener("pagehide", dispose);
|
|
143
|
+
document.removeEventListener?.("securitypolicyviolation", onPolicyViolation);
|
|
126
144
|
}
|
|
127
145
|
window.addEventListener("pagehide", dispose);
|
|
128
146
|
const ready = loadRenderer(rendererUrl).then(() => {
|
|
@@ -143,7 +161,7 @@ export function createPublisherAds(options) {
|
|
|
143
161
|
notify("onStarted", active.id); notify("onDone", active.id);
|
|
144
162
|
}
|
|
145
163
|
}
|
|
146
|
-
}).catch(() => {});
|
|
164
|
+
}).catch(() => { diagnostic("renderer_unavailable"); });
|
|
147
165
|
|
|
148
166
|
async function connectGeneration({ request, consume, signal }) {
|
|
149
167
|
const job = { id: crypto.randomUUID(), sessionId: binding.sessionId, context, state: "pending", cancelled: false,
|
package/dist/server.d.ts
CHANGED
|
@@ -47,7 +47,8 @@ type PublisherServerBaseOptions = {
|
|
|
47
47
|
gdprApplies?: boolean;
|
|
48
48
|
publisher: { app_name: string; app_domain: string; categories: string[] };
|
|
49
49
|
allowedFormats: Array<"banner" | "native" | "clip">;
|
|
50
|
-
|
|
50
|
+
/** Omit to inherit the project's saved position. Must match your physical layout when supplied. */
|
|
51
|
+
positionHint?: "above" | "below" | "sidebar" | "between";
|
|
51
52
|
/** Optional advanced caps; saved allowedSizes determine the normal envelope. */
|
|
52
53
|
maxWidth?: number;
|
|
53
54
|
maxHeight?: number;
|
package/dist/server.js
CHANGED
|
@@ -44,7 +44,7 @@ function normalizeOptions(options) {
|
|
|
44
44
|
if (!["wavebird_consent", "own_cmp"].includes(options.consentMode)) throw new Error("publisher_consent_mode_invalid");
|
|
45
45
|
if (options.gdprApplies !== undefined && typeof options.gdprApplies !== "boolean") throw new Error("publisher_gdpr_invalid");
|
|
46
46
|
if (!Array.isArray(options.allowedFormats) || options.allowedFormats.length === 0 || options.allowedFormats.some(value => !["banner", "native", "clip"].includes(value))) throw new Error("publisher_formats_invalid");
|
|
47
|
-
if (!["above", "below", "sidebar", "between"].includes(options.positionHint)) throw new Error("publisher_position_invalid");
|
|
47
|
+
if (options.positionHint !== undefined && !["above", "below", "sidebar", "between"].includes(options.positionHint)) throw new Error("publisher_position_invalid");
|
|
48
48
|
if (!Array.isArray(options.allowedSizes) || options.allowedSizes.length === 0 || options.allowedSizes.length > MAX_ALLOWED_SIZES || options.allowedSizes.some(size => !size || typeof size !== "object" || Object.keys(size).some(key => !["width", "height", "formats"].includes(key)) || !Number.isSafeInteger(size.width) || !Number.isSafeInteger(size.height) || size.width < 1 || size.height < 1 || size.width > MAX_SIZE_EDGE || size.height > MAX_SIZE_EDGE ||
|
|
49
49
|
(size.formats !== undefined && (!Array.isArray(size.formats) || size.formats.length === 0 || size.formats.some(format => !options.allowedFormats.includes(format)))))) throw new Error("publisher_sizes_invalid");
|
|
50
50
|
if (new Set(options.allowedSizes.map(size => `${size.width}x${size.height}`)).size !== options.allowedSizes.length)
|
|
@@ -256,7 +256,7 @@ export function createPublisherServer(inputOptions) {
|
|
|
256
256
|
const placementFetch = boundedFetcher();
|
|
257
257
|
const consentFetch = boundedFetcher();
|
|
258
258
|
const generationReportFetch = boundedGenerationReporter();
|
|
259
|
-
const diagnostic = code => { try { options.onDiagnostic?.(code); } catch {} };
|
|
259
|
+
const diagnostic = code => { try { Promise.resolve(options.onDiagnostic?.(code)).catch(() => {}); } catch {} };
|
|
260
260
|
if (typeof options.turnJournal?.reconcilePending === "function") {
|
|
261
261
|
void Promise.resolve().then(() => options.turnJournal.reconcilePending(options.clientId, options.surfaceId))
|
|
262
262
|
.catch(() => diagnostic("turn_journal_unavailable"));
|
package/dist/turn-journal.js
CHANGED
|
@@ -22,7 +22,7 @@ export function createPublisherTurnJournal({ storage, reportGeneration, keySecre
|
|
|
22
22
|
const tasks = new Set();
|
|
23
23
|
let activeReports = 0;
|
|
24
24
|
const reportWaiters = [];
|
|
25
|
-
const diagnostic = code => { try { onDiagnostic?.(code); } catch {} };
|
|
25
|
+
const diagnostic = code => { try { Promise.resolve(onDiagnostic?.(code)).catch(() => {}); } catch {} };
|
|
26
26
|
const verifiedScopes = new Set();
|
|
27
27
|
const versionKey = createHash("sha256").update("wavebird/turn-journal-version-row/v1").digest("hex");
|
|
28
28
|
const keyFingerprint = createHmac("sha256", keySecret).update("wavebird/turn-journal-key-version/v1").digest("hex");
|
package/docs/web/generation.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Connect generation to the displayed result
|
|
2
2
|
|
|
3
|
-
These are connection helpers, not complete apps. Use the same installed `@wavebirdai/api@0.1.0-beta.
|
|
3
|
+
These are connection helpers, not complete apps. Use the same installed `@wavebirdai/api@0.1.0-beta.16` as your server routes.
|
|
4
4
|
|
|
5
5
|
- `ads` is the instance for this surface, returned by `createPublisherAds` or the generated component's `onReady` callback.
|
|
6
6
|
- `sendExistingRequest` sends your existing authenticated model request. Merge the provided headers into its headers and preserve its body and abort signal. Call the route wrapped by `withGeneration`.
|
package/docs/web/nextjs.md
CHANGED
|
@@ -4,10 +4,14 @@ Use this path instead of copying Dashboard's route files. It supports an existin
|
|
|
4
4
|
|
|
5
5
|
## Generate files
|
|
6
6
|
|
|
7
|
+
The generator never overwrites existing files. If it reports a conflict, keep your app's file and merge the authentication, storage and generation hooks from [Dashboard Overview](https://dashboard.wavebird.ai/wavebird/overview). Do not delete your model implementation to make the generator run.
|
|
8
|
+
|
|
9
|
+
If your app sets Content Security Policy, allow `https://api.wavebird.ai` in its script, frame and connection rules. Update `script-src-elem` too if your policy uses it. Preserve existing nonces and protections. Do not use wildcards or disable CSP. The optional browser `onDiagnostic` callback reports `renderer_csp_blocked` only for an observed enforced violation, `renderer_unavailable` if loading fails, or `renderer_frame_unavailable` if the ad frame fails. Without a CSP event, the cause remains unknown. Diagnostics never include blocked URLs or credentials.
|
|
10
|
+
|
|
7
11
|
From the folder containing `package.json`:
|
|
8
12
|
|
|
9
13
|
```sh
|
|
10
|
-
npm install @wavebirdai/api@0.1.0-beta.
|
|
14
|
+
npm install @wavebirdai/api@0.1.0-beta.16 server-only
|
|
11
15
|
./node_modules/.bin/wavebird-next init --lang ts --root . --app-dir src/app
|
|
12
16
|
```
|
|
13
17
|
|
package/docs/web/reference.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Web integration reference
|
|
2
2
|
|
|
3
|
-
Applies to `@wavebirdai/api@0.1.0-beta.
|
|
3
|
+
Applies to `@wavebirdai/api@0.1.0-beta.16`. [Start with the integration steps](../../README.md#set-up-javascript-or-typescript).
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
`@wavebirdai/api/server` exports `createPublisherServer`. Configure it with server-only Project ID, Server Test or production Key, preference signing secret, the publisher's real request authorization function, exact `publicOrigins`, and the saved project/placement values shown in the dashboard. The origin list is required. The package compares the browser Origin with a configured public origin whose host matches the route Host, even when Next.js or a proxy gives the route an internal URL. `localhost` and `127.0.0.1` are separate origins, and each port and scheme must match exactly; HTTP loopback is available only with a Server Test Key. A production key also requires its separately approved project and wavebird server authority. Omit `bidfloor` and `bidfloorCurrency` to inherit saved pricing. An explicit override supplies both a nonnegative amount, including zero, and a three-letter currency; the API validates it against the saved project. It returns authenticated `placementPOST`, `consentGET`, `consentPOST`, and `withGeneration` handlers, plus the server-only `reportGeneration(jobId, event)` method. The helper constructs the existing `/v1/placements`, `/v1/consent`, and `/v1/jobs/{id}/generation/{event}` requests; it does not introduce a new wavebird protocol. The wavebird API remains authoritative for saved scope, creative formats, duration, fallback, and financial classification.
|
|
@@ -20,6 +20,16 @@ Call `reportGeneration` only from trusted server code with a job ID returned by
|
|
|
20
20
|
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.
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
## Placement and diagnosis
|
|
24
|
+
|
|
25
|
+
Place the component where the ad belongs in your layout. Set `positionHint` to `above`, `below`, `sidebar` or `between` to describe that physical position. Omit it to use the project's saved position. The server still enforces formats, timing, maximum sizes, project ownership and consent.
|
|
26
|
+
|
|
27
|
+
Allow the creative, disclosure and controls to determine the slot height. A fixed creative-only height can crop the ad. In a fixed-height chat, keep the composer separate and allow the ad area to scroll if its expanded information exceeds the available space. Empty or cleared slots should collapse.
|
|
28
|
+
|
|
29
|
+
For renderer failures, the browser's optional `onDiagnostic` receives only a fixed `code`. `renderer_csp_blocked` requires an observed enforced policy violation. `renderer_unavailable` and `renderer_frame_unavailable` do not identify the cause. See the [CSP setup](nextjs.md#generate-files). Callback errors never interrupt model output.
|
|
30
|
+
|
|
31
|
+
Use the existing `generationId` in the server `onLifecycle` observer to correlate generation events. It is not a credential. Retain only that ID, event category and relative timing, never session identities, request headers or response contents. A renderer error without an exact turn or slot link must stay uncorrelated.
|
|
32
|
+
|
|
23
33
|
## Troubleshooting
|
|
24
34
|
|
|
25
35
|
With the trusted-session integration, consent restoration reads the current server revision before synchronizing the saved choice. Reloading does not reset consent revisions. An unavailable revision keeps ads disabled until consent can be synchronized; it does not block the model response.
|
|
@@ -28,6 +38,7 @@ With the trusted-session integration, consent restoration reads the current serv
|
|
|
28
38
|
- **Origin rejected:** compare scheme, hostname and port with the saved project and server allowlist. `www.example.com` and `example.com` are separate origins.
|
|
29
39
|
- **No-Fill or `no_fit`:** keep the model result usable. Check saved formats and the measured slot size. Do not stretch a creative to force it to fit.
|
|
30
40
|
- **Empty frame:** record only the fixed error category and relative timing. HTTP 200 alone does not prove visible rendering. Never share signed URLs, headers, tokens or a HAR containing credentials.
|
|
41
|
+
- **Frequency limit:** limits apply to the supplied session. `frequency_cap_exceeded` is an actual cap denial. Test decision metadata identifies its policy and limit; an unknown reset time remains `null`. `frequency_session_required` means session data is missing. `frequency_service_unavailable`, `frequency_context_unavailable` and `frequency_configuration_invalid` are technical failures, not proof of repeated ad exposure.
|
|
31
42
|
- **After ad arrives too early:** resolve the result consumer after the UI has presented the result, not merely after the last network byte.
|
|
32
43
|
|
|
33
44
|
See [troubleshooting](https://wavebird.ai/api/troubleshooting) and [format behavior](https://wavebird.ai/api/concepts/ad-formats).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wavebirdai/api",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.16",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "JavaScript and TypeScript integration for wavebird Banner, Native and Clip ads, with consent and generation lifecycle handling.",
|
package/src/browser.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ type PublisherAdsOptionsBase = {
|
|
|
30
30
|
rendererUrl?: string;
|
|
31
31
|
privacySettingsTarget?: string;
|
|
32
32
|
getFormat?: () => "banner" | "native" | "clip" | "auto";
|
|
33
|
+
/** Fixed diagnostic codes only. Exceptions in this callback never interrupt the model. */
|
|
34
|
+
onDiagnostic?: (event: { code: "renderer_csp_blocked" | "renderer_unavailable" | "renderer_frame_unavailable" }) => void;
|
|
33
35
|
placementEndpoint?: string;
|
|
34
36
|
consentEndpoint?: string;
|
|
35
37
|
};
|
package/src/browser.js
CHANGED
|
@@ -52,6 +52,8 @@ export function createPublisherAds(options) {
|
|
|
52
52
|
if (rendererUrl !== "https://api.wavebird.ai/v1/render.js") throw new Error("publisher_renderer_url_invalid");
|
|
53
53
|
if (options.getFormat !== undefined && typeof options.getFormat !== "function")
|
|
54
54
|
throw new Error("publisher_format_hook_invalid");
|
|
55
|
+
if (options.onDiagnostic !== undefined && typeof options.onDiagnostic !== "function")
|
|
56
|
+
throw new Error("publisher_diagnostic_hook_invalid");
|
|
55
57
|
let privacyTarget = null;
|
|
56
58
|
if (consentMode === "wavebird_consent" && options.privacySettingsTarget !== undefined) {
|
|
57
59
|
if (typeof options.privacySettingsTarget !== "string" || !options.privacySettingsTarget.trim())
|
|
@@ -62,7 +64,21 @@ export function createPublisherAds(options) {
|
|
|
62
64
|
let binding = { userId: options.userId, conversationId: options.conversationId,
|
|
63
65
|
sessionId: turnBinding ? options.sessionId : crypto.randomUUID() };
|
|
64
66
|
let sponsor = null, disposed = false, context = 0, active = null;
|
|
67
|
+
const diagnostic = code => {
|
|
68
|
+
if (!disposed) { try { Promise.resolve(options.onDiagnostic?.({ code })).catch(() => {}); } catch {} }
|
|
69
|
+
};
|
|
70
|
+
const onPolicyViolation = event => {
|
|
71
|
+
if (event.disposition !== "enforce" ||
|
|
72
|
+
!["script-src", "script-src-elem", "frame-src", "connect-src"].includes(event.effectiveDirective)) return;
|
|
73
|
+
try {
|
|
74
|
+
if (new URL(event.blockedURI).origin === "https://api.wavebird.ai") diagnostic("renderer_csp_blocked");
|
|
75
|
+
} catch {}
|
|
76
|
+
};
|
|
77
|
+
document.addEventListener?.("securitypolicyviolation", onPolicyViolation);
|
|
65
78
|
const slot = document.createElement("section");
|
|
79
|
+
const onRenderFailure = () => diagnostic("renderer_frame_unavailable");
|
|
80
|
+
// Renderer failures originate in a child mount and do not bubble.
|
|
81
|
+
slot.addEventListener?.("ad:render_failed", onRenderFailure, true);
|
|
66
82
|
slot.setAttribute("aria-label", "Sponsored placement");
|
|
67
83
|
slot.style.cssText = "width:100%;height:100%;overflow:hidden;margin:0 auto";
|
|
68
84
|
target.append(slot);
|
|
@@ -121,8 +137,10 @@ export function createPublisherAds(options) {
|
|
|
121
137
|
if (active) event(active, "abort");
|
|
122
138
|
disposed = true; context++; active = null;
|
|
123
139
|
sponsor?.dispose(); sponsor = null; slot.remove();
|
|
140
|
+
slot.removeEventListener?.("ad:render_failed", onRenderFailure, true);
|
|
124
141
|
privacyTarget?.removeEventListener("click", openPrivacySettings);
|
|
125
142
|
window.removeEventListener("pagehide", dispose);
|
|
143
|
+
document.removeEventListener?.("securitypolicyviolation", onPolicyViolation);
|
|
126
144
|
}
|
|
127
145
|
window.addEventListener("pagehide", dispose);
|
|
128
146
|
const ready = loadRenderer(rendererUrl).then(() => {
|
|
@@ -143,7 +161,7 @@ export function createPublisherAds(options) {
|
|
|
143
161
|
notify("onStarted", active.id); notify("onDone", active.id);
|
|
144
162
|
}
|
|
145
163
|
}
|
|
146
|
-
}).catch(() => {});
|
|
164
|
+
}).catch(() => { diagnostic("renderer_unavailable"); });
|
|
147
165
|
|
|
148
166
|
async function connectGeneration({ request, consume, signal }) {
|
|
149
167
|
const job = { id: crypto.randomUUID(), sessionId: binding.sessionId, context, state: "pending", cancelled: false,
|
package/src/server.d.ts
CHANGED
|
@@ -47,7 +47,8 @@ type PublisherServerBaseOptions = {
|
|
|
47
47
|
gdprApplies?: boolean;
|
|
48
48
|
publisher: { app_name: string; app_domain: string; categories: string[] };
|
|
49
49
|
allowedFormats: Array<"banner" | "native" | "clip">;
|
|
50
|
-
|
|
50
|
+
/** Omit to inherit the project's saved position. Must match your physical layout when supplied. */
|
|
51
|
+
positionHint?: "above" | "below" | "sidebar" | "between";
|
|
51
52
|
/** Optional advanced caps; saved allowedSizes determine the normal envelope. */
|
|
52
53
|
maxWidth?: number;
|
|
53
54
|
maxHeight?: number;
|
package/src/server.js
CHANGED
|
@@ -44,7 +44,7 @@ function normalizeOptions(options) {
|
|
|
44
44
|
if (!["wavebird_consent", "own_cmp"].includes(options.consentMode)) throw new Error("publisher_consent_mode_invalid");
|
|
45
45
|
if (options.gdprApplies !== undefined && typeof options.gdprApplies !== "boolean") throw new Error("publisher_gdpr_invalid");
|
|
46
46
|
if (!Array.isArray(options.allowedFormats) || options.allowedFormats.length === 0 || options.allowedFormats.some(value => !["banner", "native", "clip"].includes(value))) throw new Error("publisher_formats_invalid");
|
|
47
|
-
if (!["above", "below", "sidebar", "between"].includes(options.positionHint)) throw new Error("publisher_position_invalid");
|
|
47
|
+
if (options.positionHint !== undefined && !["above", "below", "sidebar", "between"].includes(options.positionHint)) throw new Error("publisher_position_invalid");
|
|
48
48
|
if (!Array.isArray(options.allowedSizes) || options.allowedSizes.length === 0 || options.allowedSizes.length > MAX_ALLOWED_SIZES || options.allowedSizes.some(size => !size || typeof size !== "object" || Object.keys(size).some(key => !["width", "height", "formats"].includes(key)) || !Number.isSafeInteger(size.width) || !Number.isSafeInteger(size.height) || size.width < 1 || size.height < 1 || size.width > MAX_SIZE_EDGE || size.height > MAX_SIZE_EDGE ||
|
|
49
49
|
(size.formats !== undefined && (!Array.isArray(size.formats) || size.formats.length === 0 || size.formats.some(format => !options.allowedFormats.includes(format)))))) throw new Error("publisher_sizes_invalid");
|
|
50
50
|
if (new Set(options.allowedSizes.map(size => `${size.width}x${size.height}`)).size !== options.allowedSizes.length)
|
|
@@ -256,7 +256,7 @@ export function createPublisherServer(inputOptions) {
|
|
|
256
256
|
const placementFetch = boundedFetcher();
|
|
257
257
|
const consentFetch = boundedFetcher();
|
|
258
258
|
const generationReportFetch = boundedGenerationReporter();
|
|
259
|
-
const diagnostic = code => { try { options.onDiagnostic?.(code); } catch {} };
|
|
259
|
+
const diagnostic = code => { try { Promise.resolve(options.onDiagnostic?.(code)).catch(() => {}); } catch {} };
|
|
260
260
|
if (typeof options.turnJournal?.reconcilePending === "function") {
|
|
261
261
|
void Promise.resolve().then(() => options.turnJournal.reconcilePending(options.clientId, options.surfaceId))
|
|
262
262
|
.catch(() => diagnostic("turn_journal_unavailable"));
|
package/src/turn-journal.js
CHANGED
|
@@ -22,7 +22,7 @@ export function createPublisherTurnJournal({ storage, reportGeneration, keySecre
|
|
|
22
22
|
const tasks = new Set();
|
|
23
23
|
let activeReports = 0;
|
|
24
24
|
const reportWaiters = [];
|
|
25
|
-
const diagnostic = code => { try { onDiagnostic?.(code); } catch {} };
|
|
25
|
+
const diagnostic = code => { try { Promise.resolve(onDiagnostic?.(code)).catch(() => {}); } catch {} };
|
|
26
26
|
const verifiedScopes = new Set();
|
|
27
27
|
const versionKey = createHash("sha256").update("wavebird/turn-journal-version-row/v1").digest("hex");
|
|
28
28
|
const keyFingerprint = createHmac("sha256", keySecret).update("wavebird/turn-journal-key-version/v1").digest("hex");
|
package/templates/NEXT-SETUP.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Connect wavebird to this Next.js app
|
|
2
2
|
|
|
3
|
+
If your app sets Content Security Policy, merge `https://api.wavebird.ai` into `script-src`, `frame-src` and `connect-src`. Preserve nonces and existing protections. Do not disable CSP or allow wildcard origins. Keep secret Server ENV outside source downloads, Git and browser bundles.
|
|
4
|
+
|
|
3
5
|
The generator installed `lib/publisher`, `lib/auth`, `lib/turn-store` and `lib/model`; `app/api/generate`, `app/api/sponsor-slot`, `app/api/sponsor-consent` and `app/api/sponsor-session`; and `components/WavebirdPlacement` (under `src/` when `src/app` was selected). No existing page was replaced.
|
|
4
6
|
|
|
5
7
|
Install `server-only` in your app (`npm install server-only`). Install `pg` too if you use the PostgreSQL adapter. Use the full Server ENV from Dashboard's Integration step 2, including the saved preference secret and origins. A key-only ENV download is not the full configuration. Keep the preference secret unchanged across restarts.
|