Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean up operator backend proxy endpoints #23

Merged
merged 9 commits into from
Mar 10, 2022
36 changes: 23 additions & 13 deletions paf-mvp-core-js/src/endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
// TODO refactor to group endpoints and params
export const proxyEndpoints = {
verifyRedirectRead: '/verify/redirectRead',
signWrite: '/sign/write',
signPrefs: '/sign/prefs',
}

export const proxyUriParams = {
returnUrl: 'returnUrl',
message: 'message'
}
// TODO refactor to group by operator / operator proxy

// Endpoints exposed by the operator API
export const redirectEndpoints = {
read: '/v1/redirect/get-ids-prefs',
write: "/v1/redirect/post-ids-prefs"
write: '/v1/redirect/post-ids-prefs'
}
export const jsonEndpoints = {
read: '/v1/ids-prefs',
write: "/v1/ids-prefs",
write: '/v1/ids-prefs',
verify3PC: '/v1/3pc',
newId: '/v1/new-id',
identity: '/v1/identity'
}

// Endpoints exposed by the operator proxy
const proxyPrefix = '/paf-proxy'
export const jsonProxyEndpoints = {
verifyRead: `${proxyPrefix}/v1/verify/read`,
signWrite: `${proxyPrefix}/v1/sign/write`,
signPrefs: `${proxyPrefix}/v1/sign/prefs`,
read: `${proxyPrefix}${jsonEndpoints.read}`,
write: `${proxyPrefix}${jsonEndpoints.write}`,
verify3PC: `${proxyPrefix}${jsonEndpoints.verify3PC}`,
}
export const redirectProxyEndpoints = {
read: `${proxyPrefix}${redirectEndpoints.read}`,
write: `${proxyPrefix}${redirectEndpoints.write}`,
}

export const proxyUriParams = {
returnUrl: 'returnUrl',
message: 'message'
}
28 changes: 22 additions & 6 deletions paf-mvp-core-js/src/model/generated-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@ export interface _ {
"ids-and-optional-preferences"?: IdsAndOptionalPreferences;
"ids-and-preferences"?: IdsAndPreferences;
"message-base"?: MessageBase;
"new-unsigned-preferences"?: NewUnsignedPreferences;
"post-ids-prefs-request"?: PostIdsPrefsRequest;
"post-ids-prefs-response"?: PostIdsPrefsResponse;
"preferences-data"?: PreferencesData;
preferences?: Preferences;
"redirect-get-ids-prefs-request"?: RedirectGetIdsPrefsRequest;
"redirect-get-ids-prefs-response"?: RedirectGetIdsPrefsResponse;
Expand Down Expand Up @@ -165,14 +167,18 @@ export interface IdsAndOptionalPreferences {
*/
export interface Preferences {
version: Version;
data: {
/**
* `true` if the user accepted the usage of browsing history for ad personalization, `false` otherwise
*/
use_browsing_for_personalization: boolean;
};
data: PreferencesData;
source: Source;
}
/**
* Preferences data
*/
export interface PreferencesData {
/**
* Whether the user accepts (`true`) or not (`false`) that their browsing is used for personalization
*/
use_browsing_for_personalization: boolean;
}
/**
* Source of data representing what contracting party created and signed the data
*/
Expand Down Expand Up @@ -256,6 +262,16 @@ export interface MessageBase {
timestamp: Timestamp;
signature: Signature;
}
/**
* A list of identifiers and a new (unsigned) value for preferences
*/
export interface NewUnsignedPreferences {
unsignedPreferences?: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, the semantics and compositions of the Preferences models are confusing. If I translate this line: "new unsigned preferences object that potentially contains unsigned preferences"...

Can you describe how this class helps us, please? It will help to find an alternative.

More generally speaking, in this file, we have many entities that express the Preferences concept: Preferences, PreferenceData, NewUnsignedPreferences, IdsAndPreferences. I have the impression that we generate classes based on the JSON-Schema and so the readability of the models is impacted by how the JSON-Schema works.

Can it be simplified with less class and more semantics? I want to be constructive so here are a few examples but I may not have all the corner cases in mind.

Example 1, the simplest

interface Preferences {
  version: Version;
  data: {
    use_browsing_for_personalization: boolean;
  };
  /** Has a source if the preferences is signed */
  source?: Source;
  /** Weak but explicit relationship to the identifiers. */
  identifiers: Identifier[];
}
// That's it.

Exemple 2, more atomical approach

interface UnsignedPreferences {
  version: Version;
  data: {
    use_browsing_for_personalization: boolean;
  };
}

interface Preferences : UnsignedPreference {
  source: Source;
}

interface PrebidData { 
  preferences: Preferences;
  identifiers: Identifier[];
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

I get your point and I agree this model is becoming confusing.
I will rework to rename and refine some models.

However, a few comments:

  • The example you commented is a mistake from me: unsignedPreferences shouldn't be optional, I'll fix it, thanks for the heads up.
  • It is complex by nature: here, we want to model an unsigned preferences object, with a list of signed identifiers. I don't have an obvious solution to even name this object. Defining an API often means defining an object per request and response which ends up being quite verbose.
  • I want to avoid using optional attributes like in your suggestion 1. I think strong typing is always safer and more practical than weak typing + validation methods. (I don't want to add a method "if should be signed, then source should exist, otherwise it can be null")
  • I think example 2 that you propose is actually my current approach, except that you mix inheritance and composition, whereas I only used composition. Another difference is that you used names such as PrebidData where I favored explicit (but sometimes pretty long) names such as IdsAndPreferences. I'm not sure PrebidData is more explicit, WDYT?

To sum up, I'll rework these interfaces to make names more explicit and when possible, simplify it, in line with your suggestion 2.

version: Version;
data: PreferencesData;
};
identifiers: Identifier[];
}
/**
* POST /v1/ids-prefs request
*/
Expand Down
67 changes: 67 additions & 0 deletions paf-mvp-core-js/src/model/proxy-request-builders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
GetIdsPrefsResponse,
Identifiers,
IdsAndPreferences,
NewUnsignedPreferences,
Preferences,
PreferencesData
} from "./generated-model";
import {jsonProxyEndpoints} from "../endpoints";
import {setInQueryString} from "../express";

export abstract class ProxyRestRequestBuilder<T extends object | undefined> {
constructor(public proxyHost: string, protected restEndpoint: string) {
}

protected getProxyUrl(endpoint: string, pafQuery: object | undefined = undefined): URL {
let url = new URL(`https://${this.proxyHost}${endpoint}`);

if (pafQuery) {
url = setInQueryString(url, pafQuery)
}

return url
}

getRestUrl(request: T): URL {
return this.getProxyUrl(this.restEndpoint, request)
}
}

export class ProxyRestSignPreferencesRequestBuilder extends ProxyRestRequestBuilder<NewUnsignedPreferences> {

constructor(proxyHost: string) {
super(proxyHost, jsonProxyEndpoints.signPrefs);
}

buildRequest(identifiers: Identifiers, data: PreferencesData): NewUnsignedPreferences {
return {
identifiers,
unsignedPreferences: {
version: "0.1",
data
}
}
}
}

export class ProxyRestSignPostIdsPrefsRequestBuilder extends ProxyRestRequestBuilder<IdsAndPreferences> {

constructor(proxyHost: string) {
super(proxyHost, jsonProxyEndpoints.signWrite);
}

buildRequest(identifiers: Identifiers, preferences: Preferences): IdsAndPreferences {
return {
identifiers,
preferences
}
}
}

export class ProxyRestVerifyGetIdsPrefsRequestBuilder extends ProxyRestRequestBuilder<GetIdsPrefsResponse> {

constructor(proxyHost: string) {
super(proxyHost, jsonProxyEndpoints.verifyRead);
}
}
81 changes: 57 additions & 24 deletions paf-mvp-demo-express/scripts/generate-examples.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,55 @@
import {
Error,
Get3PcResponse,
GetIdentityResponse,
GetIdsPrefsRequest,
GetIdsPrefsResponse,
RedirectGetIdsPrefsRequest,
RedirectGetIdsPrefsResponse,
GetNewIdRequest,
GetNewIdResponse,
Get3PcResponse,
GetIdentityResponse,
Identifier,
Identifiers,
NewUnsignedPreferences,
PostIdsPrefsRequest,
RedirectPostIdsPrefsRequest,
RedirectPostIdsPrefsResponse,
PostIdsPrefsResponse,
Preferences,
Identifiers,
Test3Pc,
Error
RedirectGetIdsPrefsRequest,
RedirectGetIdsPrefsResponse,
RedirectPostIdsPrefsRequest,
RedirectPostIdsPrefsResponse,
Test3Pc
} from "@core/model/generated-model";
import {toIdsCookie, toPrefsCookie, toTest3pcCookie} from "@core/cookies";
import {getTimeStampInSec} from "@core/timestamp";
import {advertiser, cmp, operator, publisher} from "../src/config";
import path from "path";
import {OperatorClient} from "@operator-client/operator-client";
import {
GetIdsPrefsRequestBuilder,
GetNewIdRequestBuilder,
Get3PCRequestBuilder,
GetIdentityRequestBuilder,
GetIdsPrefsRequestBuilder,
GetNewIdRequestBuilder,
PostIdsPrefsRequestBuilder
} from "@core/model/request-builders";
} from "@core/model/operator-request-builders";
import {OperatorApi} from "@operator/operator-api";
import {GetNewIdResponseBuilder, GetIdsPrefsResponseBuilder, PostIdsPrefsResponseBuilder, Get3PCResponseBuilder, GetIdentityResponseBuilder} from "@core/model/response-builders";
import {
Get3PCResponseBuilder,
GetIdentityResponseBuilder,
GetIdsPrefsResponseBuilder,
GetNewIdResponseBuilder,
PostIdsPrefsResponseBuilder
} from "@core/model/operator-response-builders";
import {Validator} from "jsonschema";
import {publicKeys} from "../src/public-keys";
import * as fs from "fs";
import {
ProxyRestSignPostIdsPrefsRequestBuilder,
ProxyRestSignPreferencesRequestBuilder,
ProxyRestVerifyGetIdsPrefsRequestBuilder
} from "@core/model/proxy-request-builders";

const getTimestamp = (dateString: string) => getTimeStampInSec(new Date(dateString))
const getUrl = (method: "POST"|"GET", url: URL): string => `${method} ${url.pathname}${url.search}\nHost: ${url.host}`
const getGetUrl = (url: URL): string => getUrl("GET", url)
const getUrl = (method: "POST" | "GET", url: URL): string => `${method} ${url.pathname}${url.search}\nHost: ${url.host}`
const getGETUrl = (url: URL): string => getUrl("GET", url)
const getPOSTUrl = (url: URL): string => getUrl("POST", url)
const getRedirect = (url: URL): string => `303 ${url}`

Expand Down Expand Up @@ -125,6 +137,14 @@ class Examples {
getIdentityRequest_cmpHttp: string
getIdentityResponse_cmpJson: GetIdentityResponse

// **************************** Proxy
signPreferencesHttp: string
signPreferencesJson: NewUnsignedPreferences
signPostIdsPrefsHttp: string
signPostIdsPrefsJson: NewUnsignedPreferences
verifyGetIdsPrefsHttp: string
verifyGetIdsPrefs_invalidJson: Error

constructor() {
const operatorAPI = new OperatorApi(operator.host, operator.privateKey)
const originalAdvertiserUrl = new URL(`https://${advertiser.host}/news/2022/02/07/something-crazy-happened?utm_content=campaign%20content`)
Expand All @@ -138,7 +158,7 @@ class Examples {
this.idJson = operatorAPI.signId("7435313e-caee-4889-8ad7-0acd0114ae3c", getTimestamp("2022/01/18 12:13"));

const cmpClient = new OperatorClient(operator.host, cmp.host, cmp.privateKey, publicKeys)
this.preferencesJson = cmpClient.buildPreferences([this.idJson], true, getTimestamp("2022/01/18 12:16"))
this.preferencesJson = cmpClient.buildPreferences([this.idJson], {use_browsing_for_personalization: true}, getTimestamp("2022/01/18 12:16"))

// **************************** Cookies
this['ids_cookie-prettyJson'] = [this.idJson]
Expand All @@ -156,7 +176,7 @@ class Examples {
const getIdsPrefsRequestBuilder = new GetIdsPrefsRequestBuilder(operator.host, cmp.host, cmp.privateKey)
const getIdsPrefsResponseBuilder = new GetIdsPrefsResponseBuilder(operator.host, cmp.privateKey)
this.getIdsPrefsRequestJson = getIdsPrefsRequestBuilder.buildRequest(getTimestamp("2022/01/24 17:19"))
this.getIdsPrefsRequestHttp = getGetUrl(getIdsPrefsRequestBuilder.getRestUrl(this.getIdsPrefsRequestJson))
this.getIdsPrefsRequestHttp = getGETUrl(getIdsPrefsRequestBuilder.getRestUrl(this.getIdsPrefsRequestJson))
this.getIdsPrefsResponse_knownJson = getIdsPrefsResponseBuilder.buildResponse(
advertiser.host,
{
Expand All @@ -173,7 +193,7 @@ class Examples {
)

this.redirectGetIdsPrefsRequestJson = getIdsPrefsRequestBuilder.toRedirectRequest(this.getIdsPrefsRequestJson, originalAdvertiserUrl)
this.redirectGetIdsPrefsRequestHttp = getGetUrl(getIdsPrefsRequestBuilder.getRedirectUrl(this.redirectGetIdsPrefsRequestJson))
this.redirectGetIdsPrefsRequestHttp = getGETUrl(getIdsPrefsRequestBuilder.getRedirectUrl(this.redirectGetIdsPrefsRequestJson))

this.redirectGetIdsPrefsResponse_knownJson = getIdsPrefsResponseBuilder.toRedirectResponse(this.getIdsPrefsResponse_knownJson, 200)
this.redirectGetIdsPrefsResponse_knownTxt = getRedirect(getIdsPrefsResponseBuilder.getRedirectUrl(originalAdvertiserUrl, this.redirectGetIdsPrefsResponse_knownJson))
Expand All @@ -194,30 +214,30 @@ class Examples {
}, getTimestamp("2022/01/25 09:01:03"))

this.redirectPostIdsPrefsRequestJson = postIdsPrefsRequestBuilder.toRedirectRequest(this.postIdsPrefsRequestJson, originalAdvertiserUrl)
this.redirectPostIdsPrefsRequestHttp = getGetUrl(postIdsPrefsRequestBuilder.getRedirectUrl(this.redirectPostIdsPrefsRequestJson))
this.redirectPostIdsPrefsRequestHttp = getGETUrl(postIdsPrefsRequestBuilder.getRedirectUrl(this.redirectPostIdsPrefsRequestJson))
this.redirectPostIdsPrefsResponseJson = postIdsPrefsResponseBuilder.toRedirectResponse(this.postIdsPrefsResponseJson, 200)
this.redirectPostIdsPrefsResponseTxt = getRedirect(postIdsPrefsResponseBuilder.getRedirectUrl(originalAdvertiserUrl, this.redirectPostIdsPrefsResponseJson))

// **************************** Get new ID
const getNewIdRequestBuilder = new GetNewIdRequestBuilder(operator.host, cmp.host, cmp.privateKey)
const getNewIdResponseBuilder = new GetNewIdResponseBuilder(operator.host, operator.privateKey)
this.getNewIdRequestJson = getNewIdRequestBuilder.buildRequest(getTimestamp("2022/03/01 19:04"))
this.getNewIdRequestHttp = getGetUrl(getNewIdRequestBuilder.getRestUrl(this.getNewIdRequestJson))
this.getNewIdRequestHttp = getGETUrl(getNewIdRequestBuilder.getRestUrl(this.getNewIdRequestJson))

this.getNewIdResponseJson = getNewIdResponseBuilder.buildResponse(cmp.host, newId, getTimestamp("2022/03/01 19:04:47"))

// **************************** Verify 3PC
const get3PCRequestBuilder = new Get3PCRequestBuilder(operator.host, cmp.host, cmp.privateKey)
const get3PCResponseBuilder = new Get3PCResponseBuilder(operator.host, operator.privateKey)
this.get3pcRequestHttp = getGetUrl(get3PCRequestBuilder.getRestUrl())
this.get3pcRequestHttp = getGETUrl(get3PCRequestBuilder.getRestUrl())

this.get3pcResponse_supportedJson = get3PCResponseBuilder.buildResponse(this["test_3pc_cookie-prettyJson"]) as Get3PcResponse
this.get3pcResponse_unsupportedJson = get3PCResponseBuilder.buildResponse(undefined) as Error

// **************************** Identity
const getIdentityRequestBuilder_operator = new GetIdentityRequestBuilder(operator.host, advertiser.host, cmp.privateKey)
const getIdentityResponseBuilder_operator = new GetIdentityResponseBuilder(operator.host, operator.privateKey, operator.name, operator.type)
this.getIdentityRequest_operatorHttp = getGetUrl(getIdentityRequestBuilder_operator.getRestUrl(undefined))
this.getIdentityRequest_operatorHttp = getGETUrl(getIdentityRequestBuilder_operator.getRestUrl(undefined))
this.getIdentityResponse_operatorJson = getIdentityResponseBuilder_operator.buildResponse([
{
publicKey: operator.publicKey,
Expand All @@ -229,13 +249,26 @@ class Examples {
// TODO add examples with multiple keys
const getIdentityRequestBuilder_cmp = new GetIdentityRequestBuilder(cmp.host, advertiser.host, cmp.privateKey)
const getIdentityResponseBuilder_cmp = new GetIdentityResponseBuilder(cmp.host, cmp.privateKey, cmp.name, cmp.type)
this.getIdentityRequest_cmpHttp = getGetUrl(getIdentityRequestBuilder_cmp.getRestUrl(undefined))
this.getIdentityRequest_cmpHttp = getGETUrl(getIdentityRequestBuilder_cmp.getRestUrl(undefined))
this.getIdentityResponse_cmpJson = getIdentityResponseBuilder_cmp.buildResponse([
{
publicKey: cmp.publicKey,
start: new Date("2022/01/15 11:50")
}
])

// **************************** Proxy
const signPreferencesRequestBuilder = new ProxyRestSignPreferencesRequestBuilder(cmp.host)
this.signPreferencesHttp = getPOSTUrl(signPreferencesRequestBuilder.getRestUrl(undefined)) // Notice is POST url
this.signPreferencesJson = signPreferencesRequestBuilder.buildRequest([this.idJson], {use_browsing_for_personalization: true})

const signPostIdsPrefsRequestBuilder = new ProxyRestSignPostIdsPrefsRequestBuilder(cmp.host)
this.signPostIdsPrefsHttp = getPOSTUrl(signPostIdsPrefsRequestBuilder.getRestUrl(undefined)) // Notice is POST url
this.signPostIdsPrefsJson = signPostIdsPrefsRequestBuilder.buildRequest([this.idJson], this.preferencesJson)

const verifyGetIdsPrefsRequestBuilder = new ProxyRestVerifyGetIdsPrefsRequestBuilder(cmp.host)
this.verifyGetIdsPrefsHttp = getPOSTUrl(verifyGetIdsPrefsRequestBuilder.getRestUrl(undefined)) // Notice is POST url
this.verifyGetIdsPrefs_invalidJson = {message: 'Invalid signature'}
}
}

Expand All @@ -247,7 +280,7 @@ class SchemasValidator {
async initValidator(): Promise<this> {

// FIXME use a parameter to validate examples. Or ignore validation
const inputDir = path.join(__dirname, '..', '..','paf-mvp-core-js', 'json-schemas');
const inputDir = path.join(__dirname, '..', '..', 'paf-mvp-core-js', 'json-schemas');
const files = await fs.promises.readdir(inputDir);
const schemas = await Promise.all(files
.map(async (f: string) => JSON.parse(await fs.promises.readFile(path.join(inputDir, f), 'utf-8')))
Expand Down
19 changes: 14 additions & 5 deletions paf-mvp-demo-express/src/cmp/js/cmp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ declare const PAF: {
}

// Using the CMP backend as a PAF operator proxy
const proxyBase = `https://${cmp.host}`;
const proxyHostName = cmp.host;

export const cmpCheck = async () => {
const pafData = await PAF.refreshIdsAndPreferences({proxyBase, triggerRedirectIfNeeded: true});
const pafData = await PAF.refreshIdsAndPreferences({proxyHostName, triggerRedirectIfNeeded: true});

if (pafData === undefined) {
// Will trigger a redirect
return;
}

const returnedId = pafData.identifiers?.[0]
const hasPersistedId = returnedId?.persisted === undefined || returnedId?.persisted

Expand All @@ -27,10 +27,19 @@ export const cmpCheck = async () => {
Please confirm if you want to opt-in, otherwise click cancel`)

// 1. sign preferences
const signedPreferences = await PAF.signPreferences({proxyBase}, {identifier: returnedId, optIn})
const unsignedPreferences = {
version: "0.1",
data: {
use_browsing_for_personalization: optIn
}
};
const signedPreferences = await PAF.signPreferences({proxyHostName}, {
identifiers: pafData.identifiers,
unsignedPreferences
})

// 2. write
await PAF.writeIdsAndPref({proxyBase}, {
await PAF.writeIdsAndPref({proxyHostName}, {
identifiers: pafData.identifiers,
preferences: signedPreferences
})
Expand Down
Loading