@wavebirdai/api 0.1.0-beta.14 → 0.1.0-beta.15
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 +1 -1
- package/dist/server.js +23 -5
- package/docs/web/generation.md +1 -1
- package/docs/web/nextjs.md +1 -1
- package/docs/web/reference.md +3 -1
- package/package.json +1 -1
- package/src/server.js +23 -5
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ 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.15
|
|
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`.
|
package/dist/server.js
CHANGED
|
@@ -388,21 +388,39 @@ export function createPublisherServer(inputOptions) {
|
|
|
388
388
|
const headers = { "Cache-Control": "no-store", Vary: "Cookie, Sec-GPC" };
|
|
389
389
|
if (!validOrigin(request, options, false)) return response("invalid_origin", 403, headers);
|
|
390
390
|
if (gpc(request)) return Response.json({ preference: null }, { headers });
|
|
391
|
+
const revision = {};
|
|
392
|
+
if (options.sessionIdForRequest) {
|
|
393
|
+
let sessionId;
|
|
394
|
+
try { sessionId = await journalCall(() => options.sessionIdForRequest(request)); }
|
|
395
|
+
catch { return response("publisher_session_unavailable", 503, headers); }
|
|
396
|
+
if (!ID.test(sessionId || "")) return response("publisher_session_unavailable", 401, headers);
|
|
397
|
+
try {
|
|
398
|
+
const query = new URLSearchParams({ client_id: options.clientId, session_id: sessionId });
|
|
399
|
+
const result = await consentFetch(`${options.base}/consent?${query}`, {
|
|
400
|
+
method: "GET", headers: { Authorization: `Bearer ${options.secretKey}` }, cache: "no-store"
|
|
401
|
+
});
|
|
402
|
+
const state = await result.json();
|
|
403
|
+
if (!result.ok || !Number.isSafeInteger(state.current_revision) || state.current_revision < 0 ||
|
|
404
|
+
state.current_revision >= Number.MAX_SAFE_INTEGER) throw new Error("consent_revision_unavailable");
|
|
405
|
+
revision.current_revision = state.current_revision;
|
|
406
|
+
} catch { return response("consent_revision_unavailable", 503, headers); }
|
|
407
|
+
}
|
|
408
|
+
const preferenceResponse = value => Response.json({ ...value, ...revision }, { headers });
|
|
391
409
|
try {
|
|
392
410
|
const value = (request.headers.get("cookie") || "").split(";").map(value => value.trim())
|
|
393
411
|
.find(value => value.startsWith(cookieName + "="))?.slice(cookieName.length + 1) || "";
|
|
394
412
|
if (value.length > 2048) throw new Error("preference_invalid");
|
|
395
413
|
const [payload, signature, extra] = value.split(".");
|
|
396
|
-
if (!payload || !signature || extra || !/^[A-Za-z0-9_-]+$/.test(payload) || !/^[A-Za-z0-9_-]+$/.test(signature)) return
|
|
397
|
-
if (!await crypto.subtle.verify("HMAC", await preferenceKey(), Buffer.from(signature, "base64url"), new TextEncoder().encode(payload))) return
|
|
414
|
+
if (!payload || !signature || extra || !/^[A-Za-z0-9_-]+$/.test(payload) || !/^[A-Za-z0-9_-]+$/.test(signature)) return preferenceResponse({ preference: null });
|
|
415
|
+
if (!await crypto.subtle.verify("HMAC", await preferenceKey(), Buffer.from(signature, "base64url"), new TextEncoder().encode(payload))) return preferenceResponse({ preference: null });
|
|
398
416
|
const record = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
399
417
|
const now = Date.now();
|
|
400
418
|
if (Object.entries(scope(request)).some(([key, value]) => record[key] !== value) ||
|
|
401
419
|
!["granted", "denied", "withdrawn"].includes(record.state) || !Number.isSafeInteger(record.issued_at) ||
|
|
402
420
|
!Number.isSafeInteger(record.expires_at) || record.issued_at > now || record.expires_at <= now ||
|
|
403
|
-
record.expires_at - record.issued_at !== PREFERENCE_TTL * 1000) return
|
|
404
|
-
return
|
|
405
|
-
} catch { return
|
|
421
|
+
record.expires_at - record.issued_at !== PREFERENCE_TTL * 1000) return preferenceResponse({ preference: null });
|
|
422
|
+
return preferenceResponse({ preference: { state: record.state, expires_at: record.expires_at } });
|
|
423
|
+
} catch { return preferenceResponse({ preference: null, error: "preference_unavailable" }); }
|
|
406
424
|
}
|
|
407
425
|
async function consentPOST(request) {
|
|
408
426
|
if (!await authorized(request)) return response("authentication_required", 401);
|
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.15` 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
|
@@ -7,7 +7,7 @@ Use this path instead of copying Dashboard's route files. It supports an existin
|
|
|
7
7
|
From the folder containing `package.json`:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npm install @wavebirdai/api@0.1.0-beta.
|
|
10
|
+
npm install @wavebirdai/api@0.1.0-beta.15 server-only
|
|
11
11
|
./node_modules/.bin/wavebird-next init --lang ts --root . --app-dir src/app
|
|
12
12
|
```
|
|
13
13
|
|
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.15`. [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.
|
|
@@ -22,6 +22,8 @@ For `after`, resolve the generation consumer only after your app presents the re
|
|
|
22
22
|
|
|
23
23
|
## Troubleshooting
|
|
24
24
|
|
|
25
|
+
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.
|
|
26
|
+
|
|
25
27
|
- **401:** check the app session and server authorization before changing the wavebird key.
|
|
26
28
|
- **Origin rejected:** compare scheme, hostname and port with the saved project and server allowlist. `www.example.com` and `example.com` are separate origins.
|
|
27
29
|
- **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.
|
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.15",
|
|
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/server.js
CHANGED
|
@@ -388,21 +388,39 @@ export function createPublisherServer(inputOptions) {
|
|
|
388
388
|
const headers = { "Cache-Control": "no-store", Vary: "Cookie, Sec-GPC" };
|
|
389
389
|
if (!validOrigin(request, options, false)) return response("invalid_origin", 403, headers);
|
|
390
390
|
if (gpc(request)) return Response.json({ preference: null }, { headers });
|
|
391
|
+
const revision = {};
|
|
392
|
+
if (options.sessionIdForRequest) {
|
|
393
|
+
let sessionId;
|
|
394
|
+
try { sessionId = await journalCall(() => options.sessionIdForRequest(request)); }
|
|
395
|
+
catch { return response("publisher_session_unavailable", 503, headers); }
|
|
396
|
+
if (!ID.test(sessionId || "")) return response("publisher_session_unavailable", 401, headers);
|
|
397
|
+
try {
|
|
398
|
+
const query = new URLSearchParams({ client_id: options.clientId, session_id: sessionId });
|
|
399
|
+
const result = await consentFetch(`${options.base}/consent?${query}`, {
|
|
400
|
+
method: "GET", headers: { Authorization: `Bearer ${options.secretKey}` }, cache: "no-store"
|
|
401
|
+
});
|
|
402
|
+
const state = await result.json();
|
|
403
|
+
if (!result.ok || !Number.isSafeInteger(state.current_revision) || state.current_revision < 0 ||
|
|
404
|
+
state.current_revision >= Number.MAX_SAFE_INTEGER) throw new Error("consent_revision_unavailable");
|
|
405
|
+
revision.current_revision = state.current_revision;
|
|
406
|
+
} catch { return response("consent_revision_unavailable", 503, headers); }
|
|
407
|
+
}
|
|
408
|
+
const preferenceResponse = value => Response.json({ ...value, ...revision }, { headers });
|
|
391
409
|
try {
|
|
392
410
|
const value = (request.headers.get("cookie") || "").split(";").map(value => value.trim())
|
|
393
411
|
.find(value => value.startsWith(cookieName + "="))?.slice(cookieName.length + 1) || "";
|
|
394
412
|
if (value.length > 2048) throw new Error("preference_invalid");
|
|
395
413
|
const [payload, signature, extra] = value.split(".");
|
|
396
|
-
if (!payload || !signature || extra || !/^[A-Za-z0-9_-]+$/.test(payload) || !/^[A-Za-z0-9_-]+$/.test(signature)) return
|
|
397
|
-
if (!await crypto.subtle.verify("HMAC", await preferenceKey(), Buffer.from(signature, "base64url"), new TextEncoder().encode(payload))) return
|
|
414
|
+
if (!payload || !signature || extra || !/^[A-Za-z0-9_-]+$/.test(payload) || !/^[A-Za-z0-9_-]+$/.test(signature)) return preferenceResponse({ preference: null });
|
|
415
|
+
if (!await crypto.subtle.verify("HMAC", await preferenceKey(), Buffer.from(signature, "base64url"), new TextEncoder().encode(payload))) return preferenceResponse({ preference: null });
|
|
398
416
|
const record = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
399
417
|
const now = Date.now();
|
|
400
418
|
if (Object.entries(scope(request)).some(([key, value]) => record[key] !== value) ||
|
|
401
419
|
!["granted", "denied", "withdrawn"].includes(record.state) || !Number.isSafeInteger(record.issued_at) ||
|
|
402
420
|
!Number.isSafeInteger(record.expires_at) || record.issued_at > now || record.expires_at <= now ||
|
|
403
|
-
record.expires_at - record.issued_at !== PREFERENCE_TTL * 1000) return
|
|
404
|
-
return
|
|
405
|
-
} catch { return
|
|
421
|
+
record.expires_at - record.issued_at !== PREFERENCE_TTL * 1000) return preferenceResponse({ preference: null });
|
|
422
|
+
return preferenceResponse({ preference: { state: record.state, expires_at: record.expires_at } });
|
|
423
|
+
} catch { return preferenceResponse({ preference: null, error: "preference_unavailable" }); }
|
|
406
424
|
}
|
|
407
425
|
async function consentPOST(request) {
|
|
408
426
|
if (!await authorized(request)) return response("authentication_required", 401);
|