@owlmeans/server-marketing-consent 0.1.18-rc.0
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 +15 -0
- package/agent-meta/manifest.json +16 -0
- package/agent-meta/skills/server-marketing-consent/SKILL.md +148 -0
- package/build/consts.d.ts +12 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +12 -0
- package/build/consts.js.map +1 -0
- package/build/handlers.d.ts +20 -0
- package/build/handlers.d.ts.map +1 -0
- package/build/handlers.js +23 -0
- package/build/handlers.js.map +1 -0
- package/build/helper.d.ts +19 -0
- package/build/helper.d.ts.map +1 -0
- package/build/helper.js +14 -0
- package/build/helper.js.map +1 -0
- package/build/index.d.ts +7 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/build/model.d.ts +64 -0
- package/build/model.d.ts.map +1 -0
- package/build/model.js +83 -0
- package/build/model.js.map +1 -0
- package/build/service.d.ts +56 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +146 -0
- package/build/service.js.map +1 -0
- package/build/subject.d.ts +21 -0
- package/build/subject.d.ts.map +1 -0
- package/build/subject.js +21 -0
- package/build/subject.js.map +1 -0
- package/package.json +54 -0
- package/src/consts.ts +11 -0
- package/src/handlers.ts +44 -0
- package/src/helper.ts +18 -0
- package/src/index.ts +6 -0
- package/src/model.ts +147 -0
- package/src/service.ts +226 -0
- package/src/subject.ts +32 -0
- package/tests/context.ts +52 -0
- package/tests/handlers.spec.ts +85 -0
- package/tests/service.spec.ts +164 -0
- package/tests/subject.spec.ts +38 -0
- package/tests/tsconfig.json +12 -0
- package/tsconfig.json +11 -0
package/build/service.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createLazyService } from '@owlmeans/context';
|
|
3
|
+
import { consentStatus, MARKETING_CONSENT_SERVICE, resolveMarketingConsents, UnknownMarketingConsentError, } from '@owlmeans/marketing-consent';
|
|
4
|
+
import { RecordExists } from '@owlmeans/resource';
|
|
5
|
+
import { RES_MARKETING_CONSENT_LOG, RES_MARKETING_CONSENT_STATE } from './consts.js';
|
|
6
|
+
import { subjectKey } from './subject.js';
|
|
7
|
+
const nowIso = () => new Date().toISOString();
|
|
8
|
+
/**
|
|
9
|
+
* Build the `MarketingConsentService`. Database-agnostic: both resources are resolved BY ALIAS
|
|
10
|
+
* from the context lazily, inside each method call — never imported or constructed here. A
|
|
11
|
+
* Mongo/Postgres extension package registers the actual resources at those aliases separately.
|
|
12
|
+
*/
|
|
13
|
+
export const makeMarketingConsentService = (opts = {}) => {
|
|
14
|
+
const alias = opts.alias ?? MARKETING_CONSENT_SERVICE;
|
|
15
|
+
const stateAlias = opts.state ?? RES_MARKETING_CONSENT_STATE;
|
|
16
|
+
const logAlias = opts.log ?? RES_MARKETING_CONSENT_LOG;
|
|
17
|
+
let memoizedDefinitions;
|
|
18
|
+
const definitions = () => {
|
|
19
|
+
memoizedDefinitions ??= resolveMarketingConsents(opts.config);
|
|
20
|
+
return memoizedDefinitions;
|
|
21
|
+
};
|
|
22
|
+
const listeners = [];
|
|
23
|
+
const ctx = () => service.assertCtx();
|
|
24
|
+
const states = () => ctx().resource(stateAlias);
|
|
25
|
+
const logs = () => ctx().resource(logAlias);
|
|
26
|
+
const appendLog = async (record) => {
|
|
27
|
+
await logs().create({ ...record, id: `log:${record.subject}:${randomUUID()}` });
|
|
28
|
+
};
|
|
29
|
+
const notify = async (subject, decisions) => {
|
|
30
|
+
for (const listener of listeners) {
|
|
31
|
+
try {
|
|
32
|
+
await listener({ subject, decisions });
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
// A listener's failure never fails the write that triggered it.
|
|
36
|
+
console.error(`${alias}: observer failed for ${subjectKey(subject)}`, error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const statusOf = (state, gpc) => consentStatus(definitions(), state?.decisions ?? [], {
|
|
41
|
+
gpc, termsAcceptedAt: state?.terms?.acceptedAt, termsVersion: state?.terms?.version,
|
|
42
|
+
});
|
|
43
|
+
/** Create-then-fall-back-to-update-on-`RecordExists`, the idiom `server-auth-session` uses for
|
|
44
|
+
* the same create-vs-update race on a natural-id resource. */
|
|
45
|
+
const upsertState = async (id, patch) => {
|
|
46
|
+
const existing = await states().load(id);
|
|
47
|
+
if (existing != null) {
|
|
48
|
+
return states().save({ ...existing, ...patch(existing) });
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return await states().create({ id, ...patch(null) });
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (!(error instanceof RecordExists))
|
|
55
|
+
throw error;
|
|
56
|
+
const raced = await states().get(id);
|
|
57
|
+
return states().save({ ...raced, ...patch(raced) });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const service = createLazyService(alias, {
|
|
61
|
+
definitions,
|
|
62
|
+
status: async (subject, statusOpts) => {
|
|
63
|
+
const state = await states().load(subjectKey(subject));
|
|
64
|
+
return statusOf(state, statusOpts?.gpc);
|
|
65
|
+
},
|
|
66
|
+
save: async (subject, request) => {
|
|
67
|
+
const known = new Set(definitions().map(def => def.key));
|
|
68
|
+
const unknown = request.decisions.find(decision => !known.has(decision.key));
|
|
69
|
+
if (unknown != null) {
|
|
70
|
+
throw new UnknownMarketingConsentError(unknown.key);
|
|
71
|
+
}
|
|
72
|
+
const now = nowIso();
|
|
73
|
+
const decided = request.decisions.map(decision => {
|
|
74
|
+
// Safe: every key was just checked against `known` above.
|
|
75
|
+
const definition = definitions().find(def => def.key === decision.key);
|
|
76
|
+
return {
|
|
77
|
+
key: decision.key, granted: decision.granted, revisedAt: definition.revisedAt,
|
|
78
|
+
mode: definition.mode, decidedAt: now, source: request.source,
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
for (const decision of decided) {
|
|
82
|
+
await appendLog({
|
|
83
|
+
subject: subjectKey(subject), userId: subject.userId, profileId: subject.profileId,
|
|
84
|
+
entityId: subject.entityId, kind: 'consent', key: decision.key, granted: decision.granted,
|
|
85
|
+
revisedAt: decision.revisedAt, mode: decision.mode, decidedAt: decision.decidedAt,
|
|
86
|
+
source: decision.source, locale: request.locale, gpc: request.gpc,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const id = subjectKey(subject);
|
|
90
|
+
const saved = await upsertState(id, existing => {
|
|
91
|
+
const merged = new Map((existing?.decisions ?? []).map(decision => [decision.key, decision]));
|
|
92
|
+
decided.forEach(decision => merged.set(decision.key, decision));
|
|
93
|
+
return {
|
|
94
|
+
subject: id, userId: subject.userId, profileId: subject.profileId, entityId: subject.entityId,
|
|
95
|
+
decisions: [...merged.values()], gpc: request.gpc ?? existing?.gpc,
|
|
96
|
+
createdAt: existing?.createdAt ?? now, updatedAt: now,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
await notify(subject, decided);
|
|
100
|
+
return { ok: true, status: statusOf(saved, request.gpc) };
|
|
101
|
+
},
|
|
102
|
+
recordTerms: async (subject, acceptance, recordOpts) => {
|
|
103
|
+
const now = nowIso();
|
|
104
|
+
const source = recordOpts?.source ?? 'sign-in';
|
|
105
|
+
await appendLog({
|
|
106
|
+
subject: subjectKey(subject), userId: subject.userId, profileId: subject.profileId,
|
|
107
|
+
entityId: subject.entityId, kind: 'terms', documents: acceptance.documents,
|
|
108
|
+
notices: acceptance.notices, version: acceptance.version, decidedAt: now, source,
|
|
109
|
+
locale: acceptance.locale,
|
|
110
|
+
});
|
|
111
|
+
const id = subjectKey(subject);
|
|
112
|
+
const terms = {
|
|
113
|
+
documents: acceptance.documents, notices: acceptance.notices, version: acceptance.version,
|
|
114
|
+
locale: acceptance.locale, acceptedAt: now,
|
|
115
|
+
};
|
|
116
|
+
await upsertState(id, existing => ({
|
|
117
|
+
subject: id, userId: subject.userId, profileId: subject.profileId, entityId: subject.entityId,
|
|
118
|
+
decisions: existing?.decisions ?? [], terms,
|
|
119
|
+
createdAt: existing?.createdAt ?? now, updatedAt: now,
|
|
120
|
+
}));
|
|
121
|
+
return { ok: true };
|
|
122
|
+
},
|
|
123
|
+
isGranted: async (subject, key, grantOpts) => {
|
|
124
|
+
const view = await service.status(subject, grantOpts);
|
|
125
|
+
const item = view.items.find(candidate => candidate.definition.key === key);
|
|
126
|
+
if (item == null || item.status !== 'current')
|
|
127
|
+
return false;
|
|
128
|
+
return item.granted;
|
|
129
|
+
},
|
|
130
|
+
purge: async (subject) => {
|
|
131
|
+
await states().delete(subjectKey(subject));
|
|
132
|
+
},
|
|
133
|
+
observe: listener => {
|
|
134
|
+
listeners.push(listener);
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
return service;
|
|
138
|
+
};
|
|
139
|
+
/** Register the service, unless the application already registered its own under this alias. */
|
|
140
|
+
export const appendMarketingConsentService = (context, opts = {}) => {
|
|
141
|
+
const alias = opts.alias ?? MARKETING_CONSENT_SERVICE;
|
|
142
|
+
if (!context.hasService(alias)) {
|
|
143
|
+
context.registerService(makeMarketingConsentService(opts));
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AbstractRequest } from '@owlmeans/entrypoint';
|
|
2
|
+
/** Who a consent decision or a terms acceptance is recorded for. */
|
|
3
|
+
export interface MarketingConsentSubject {
|
|
4
|
+
userId: string;
|
|
5
|
+
profileId?: string;
|
|
6
|
+
entityId?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Read the subject off an authenticated request.
|
|
10
|
+
*
|
|
11
|
+
* `entityId` is read ONLY from `req.entity?.id` — never `requireEntityKey`, never a slug. A
|
|
12
|
+
* deployment with no organization concept at all (an end-user-facing generated target app, say)
|
|
13
|
+
* registers no entity resolver, so `req.entity` stays `undefined` and `entityId` is simply absent
|
|
14
|
+
* here — that is a valid, expected shape, not an error.
|
|
15
|
+
*
|
|
16
|
+
* @throws {AuthForbidden} when the request carries no authenticated user.
|
|
17
|
+
*/
|
|
18
|
+
export declare const subjectOf: (req: AbstractRequest) => MarketingConsentSubject;
|
|
19
|
+
/** One string key per subject — the state record's own `id`. */
|
|
20
|
+
export declare const subjectKey: (subject: MarketingConsentSubject) => string;
|
|
21
|
+
//# sourceMappingURL=subject.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subject.d.ts","sourceRoot":"","sources":["../src/subject.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAE3D,oEAAoE;AACpE,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS,QAAS,eAAe,KAAG,uBAOhD,CAAA;AAED,gEAAgE;AAChE,eAAO,MAAM,UAAU,YAAa,uBAAuB,KAAG,MACY,CAAA"}
|
package/build/subject.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AuthForbidden } from '@owlmeans/auth';
|
|
2
|
+
/**
|
|
3
|
+
* Read the subject off an authenticated request.
|
|
4
|
+
*
|
|
5
|
+
* `entityId` is read ONLY from `req.entity?.id` — never `requireEntityKey`, never a slug. A
|
|
6
|
+
* deployment with no organization concept at all (an end-user-facing generated target app, say)
|
|
7
|
+
* registers no entity resolver, so `req.entity` stays `undefined` and `entityId` is simply absent
|
|
8
|
+
* here — that is a valid, expected shape, not an error.
|
|
9
|
+
*
|
|
10
|
+
* @throws {AuthForbidden} when the request carries no authenticated user.
|
|
11
|
+
*/
|
|
12
|
+
export const subjectOf = (req) => {
|
|
13
|
+
const userId = req.auth?.userId;
|
|
14
|
+
if (userId == null || userId === '') {
|
|
15
|
+
throw new AuthForbidden('marketing-consent: no authenticated user');
|
|
16
|
+
}
|
|
17
|
+
return { userId, profileId: req.auth?.profileId, entityId: req.entity?.id };
|
|
18
|
+
};
|
|
19
|
+
/** One string key per subject — the state record's own `id`. */
|
|
20
|
+
export const subjectKey = (subject) => `${subject.entityId ?? ''}|${subject.userId}|${subject.profileId ?? ''}`;
|
|
21
|
+
//# sourceMappingURL=subject.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subject.js","sourceRoot":"","sources":["../src/subject.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAU9C;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,GAAoB,EAA2B,EAAE;IACzE,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,MAAM,CAAA;IAC/B,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;QACpC,MAAM,IAAI,aAAa,CAAC,0CAA0C,CAAC,CAAA;IACrE,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,EAAE,EAAE,CAAA;AAC7E,CAAC,CAAA;AAED,gEAAgE;AAChE,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,OAAgC,EAAU,EAAE,CACrE,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,SAAS,IAAI,EAAE,EAAE,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/server-marketing-consent",
|
|
3
|
+
"version": "0.1.18-rc.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "The server half of @owlmeans/marketing-consent — a database-agnostic MarketingConsentService (status/save/terms/purge over resources resolved by alias) plus the guarded status/save/terms entrypoint handlers.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -b",
|
|
9
|
+
"dev": "sleep 2 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
10
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty",
|
|
11
|
+
"test": "bun test ./tests"
|
|
12
|
+
},
|
|
13
|
+
"main": "build/index.js",
|
|
14
|
+
"module": "build/index.js",
|
|
15
|
+
"types": "build/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./build/index.js",
|
|
19
|
+
"require": "./build/index.js",
|
|
20
|
+
"default": "./build/index.js",
|
|
21
|
+
"module": "./build/index.js",
|
|
22
|
+
"types": "./build/index.d.ts"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@owlmeans/auth": "^0.1.18-rc.31",
|
|
27
|
+
"@owlmeans/auth-common": "^0.1.18-rc.33",
|
|
28
|
+
"@owlmeans/context": "^0.1.18-rc.29",
|
|
29
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.32",
|
|
30
|
+
"@owlmeans/error": "^0.1.18-rc.31",
|
|
31
|
+
"@owlmeans/marketing-consent": "^0.1.18-rc.0",
|
|
32
|
+
"@owlmeans/resource": "^0.1.18-rc.31",
|
|
33
|
+
"@owlmeans/server-api": "^0.1.18-rc.37",
|
|
34
|
+
"@owlmeans/server-auth-token": "^0.1.18-rc.18",
|
|
35
|
+
"@owlmeans/server-context": "^0.1.18-rc.32",
|
|
36
|
+
"@owlmeans/server-entrypoint": "^0.1.18-rc.31"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"ajv": "*"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@owlmeans/dep-config": "workspace:*",
|
|
43
|
+
"@owlmeans/static-resource": "^0.1.18-rc.29",
|
|
44
|
+
"@owlmeans/test": "^0.1.18-rc.29",
|
|
45
|
+
"@owlmeans/test-auth": "^0.1.18-rc.32",
|
|
46
|
+
"@types/bun": "^1.4.0",
|
|
47
|
+
"ajv": "^8.17.1",
|
|
48
|
+
"nodemon": "^3.1.14",
|
|
49
|
+
"typescript": "^7.0.2"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/consts.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two resource aliases a Mongo/Postgres extension package (`@owlmeans/marketing-consent-mongo`,
|
|
3
|
+
* `@owlmeans/marketing-consent-postgres` — later workstreams, not built yet) registers its concrete
|
|
4
|
+
* resources under.
|
|
5
|
+
*
|
|
6
|
+
* These string values matter beyond this package: a target project's generated resource FILE names
|
|
7
|
+
* are derived from an alias via a deterministic `resourceAlias()` helper elsewhere in the monorepo
|
|
8
|
+
* (`entity/type` -> `entity-type`). Keep them exactly as declared here.
|
|
9
|
+
*/
|
|
10
|
+
export const RES_MARKETING_CONSENT_STATE = 'marketing-consent-state'
|
|
11
|
+
export const RES_MARKETING_CONSENT_LOG = 'marketing-consent-log'
|
package/src/handlers.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { MARKETING_CONSENT_SERVICE } from '@owlmeans/marketing-consent'
|
|
2
|
+
import type { MarketingConsentEntrypoints } from '@owlmeans/marketing-consent'
|
|
3
|
+
import { handlers } from '@owlmeans/server-api'
|
|
4
|
+
import { refuseTokenAuth } from '@owlmeans/server-auth-token'
|
|
5
|
+
import type { MarketingConsentContext, MarketingConsentService } from './service.js'
|
|
6
|
+
import { subjectOf } from './subject.js'
|
|
7
|
+
|
|
8
|
+
export interface MarketingConsentHandlerOptions {
|
|
9
|
+
serviceAlias?: string
|
|
10
|
+
/** Let an OAuth-minted access token save decisions or record terms too. Defaults to `false` —
|
|
11
|
+
* `status` is never token-refused regardless of this option. */
|
|
12
|
+
allowAccessTokens?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const serviceOf = (context: MarketingConsentContext, opts?: MarketingConsentHandlerOptions): MarketingConsentService =>
|
|
16
|
+
context.service<MarketingConsentService>(opts?.serviceAlias ?? MARKETING_CONSENT_SERVICE)
|
|
17
|
+
|
|
18
|
+
/** GPC ("Sec-GPC: 1") read as a plain header — Fastify normalizes header names to lower case, and
|
|
19
|
+
* a repeated header arrives as an array, so only its first value is read. */
|
|
20
|
+
const gpcOf = (headers: Record<string, string[] | string | undefined>): boolean => {
|
|
21
|
+
const raw = headers['sec-gpc']
|
|
22
|
+
return (Array.isArray(raw) ? raw[0] : raw) === '1'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const marketingConsentStatus = (
|
|
26
|
+
protocol: MarketingConsentEntrypoints['status'], opts: MarketingConsentHandlerOptions = {},
|
|
27
|
+
) => handlers<MarketingConsentContext>().request(protocol, async (req, context) =>
|
|
28
|
+
serviceOf(context, opts).status(subjectOf(req), { gpc: gpcOf(req.headers ?? {}) }))
|
|
29
|
+
|
|
30
|
+
export const saveMarketingConsent = (
|
|
31
|
+
protocol: MarketingConsentEntrypoints['save'], opts: MarketingConsentHandlerOptions = {},
|
|
32
|
+
) => handlers<MarketingConsentContext>().body(protocol, async (payload, context, req) => {
|
|
33
|
+
if (opts.allowAccessTokens !== true) refuseTokenAuth(req, 'marketing-consent-save')
|
|
34
|
+
|
|
35
|
+
return serviceOf(context, opts).save(subjectOf(req), payload)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export const recordTermsAcceptance = (
|
|
39
|
+
protocol: MarketingConsentEntrypoints['terms'], opts: MarketingConsentHandlerOptions = {},
|
|
40
|
+
) => handlers<MarketingConsentContext>().body(protocol, async (payload, context, req) => {
|
|
41
|
+
if (opts.allowAccessTokens !== true) refuseTokenAuth(req, 'marketing-consent-terms')
|
|
42
|
+
|
|
43
|
+
return serviceOf(context, opts).recordTerms(subjectOf(req), payload)
|
|
44
|
+
})
|
package/src/helper.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { bind } from '@owlmeans/server-entrypoint'
|
|
2
|
+
import type { MarketingConsentEntrypoints } from '@owlmeans/marketing-consent'
|
|
3
|
+
import { marketingConsentStatus, recordTermsAcceptance, saveMarketingConsent } from './handlers.js'
|
|
4
|
+
import type { MarketingConsentHandlerOptions } from './handlers.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Bind this package's handlers to a tree declared by `makeMarketingConsentProtocols` — one binding
|
|
8
|
+
* per protocol that needs a server implementation. `base` and `screen` carry no handler of their
|
|
9
|
+
* own (a mounting anchor and a frontend address, the same shape as `@owlmeans/oauth`'s own consent
|
|
10
|
+
* screens) and are left for the application's route tree to materialize.
|
|
11
|
+
*/
|
|
12
|
+
export const serveMarketingConsentEntrypoints = (
|
|
13
|
+
protocols: MarketingConsentEntrypoints, opts: MarketingConsentHandlerOptions = {},
|
|
14
|
+
) => [
|
|
15
|
+
bind(protocols.status, marketingConsentStatus(protocols.status, opts)),
|
|
16
|
+
bind(protocols.save, saveMarketingConsent(protocols.save, opts)),
|
|
17
|
+
bind(protocols.terms, recordTermsAcceptance(protocols.terms, opts)),
|
|
18
|
+
]
|
package/src/index.ts
ADDED
package/src/model.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { JSONSchemaType } from 'ajv'
|
|
2
|
+
import type { ResourceRecord } from '@owlmeans/resource'
|
|
3
|
+
import type {
|
|
4
|
+
MarketingConsentDecision, MarketingConsentMode, MarketingConsentSource, TermsDocumentRef,
|
|
5
|
+
} from '@owlmeans/marketing-consent'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One subject's saved decisions, one record per subject (`id` = `subjectKey(subject)`).
|
|
9
|
+
*
|
|
10
|
+
* `decisions` is an ARRAY, never an object keyed by consent key. A dotted key such as
|
|
11
|
+
* `"marketing.email"` is read as a PATH by both Mongo dot-notation queries and Postgres jsonb path
|
|
12
|
+
* operators — an object-keyed shape breaks the moment a second consent key is added. This is the
|
|
13
|
+
* single most important shape decision in this package; the Mongo/Postgres extensions that will
|
|
14
|
+
* store this record must not "flatten" it into an object for convenience.
|
|
15
|
+
*/
|
|
16
|
+
export interface MarketingConsentStateRecord extends ResourceRecord {
|
|
17
|
+
id: string
|
|
18
|
+
/** `subjectKey(subjectOf(req))` — see `./subject.js`. Also this record's own `id`. */
|
|
19
|
+
subject: string
|
|
20
|
+
userId: string
|
|
21
|
+
profileId?: string
|
|
22
|
+
entityId?: string
|
|
23
|
+
decisions: MarketingConsentDecision[]
|
|
24
|
+
terms?: {
|
|
25
|
+
documents: TermsDocumentRef[]
|
|
26
|
+
notices?: TermsDocumentRef[]
|
|
27
|
+
version: string
|
|
28
|
+
locale?: string
|
|
29
|
+
acceptedAt: string
|
|
30
|
+
}
|
|
31
|
+
gpc?: boolean
|
|
32
|
+
createdAt: string
|
|
33
|
+
updatedAt: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Append-only evidence of every decision and terms acceptance ever recorded — GDPR Art. 7(1)
|
|
38
|
+
* "demonstrate consent" material. Never updated or deleted, including on `purge()`: only the
|
|
39
|
+
* current-state record is cleared there.
|
|
40
|
+
*/
|
|
41
|
+
export interface MarketingConsentLogRecord extends ResourceRecord {
|
|
42
|
+
id: string
|
|
43
|
+
subject: string
|
|
44
|
+
userId: string
|
|
45
|
+
profileId?: string
|
|
46
|
+
entityId?: string
|
|
47
|
+
kind: 'consent' | 'terms'
|
|
48
|
+
/** Present when `kind === 'consent'`. */
|
|
49
|
+
key?: string
|
|
50
|
+
granted?: boolean
|
|
51
|
+
revisedAt?: string
|
|
52
|
+
mode?: MarketingConsentMode
|
|
53
|
+
/** Present when `kind === 'terms'`. */
|
|
54
|
+
documents?: TermsDocumentRef[]
|
|
55
|
+
notices?: TermsDocumentRef[]
|
|
56
|
+
version?: string
|
|
57
|
+
decidedAt: string
|
|
58
|
+
source: MarketingConsentSource
|
|
59
|
+
locale?: string
|
|
60
|
+
gpc?: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// `@owlmeans/marketing-consent`'s own `TermsDocumentRefSchema` (in its `src/schemas.ts`) is not
|
|
64
|
+
// exported, so the shape is re-declared here rather than imported.
|
|
65
|
+
const TermsDocumentRefSchema: JSONSchemaType<TermsDocumentRef> = {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
key: { type: 'string', minLength: 1, maxLength: 128 },
|
|
69
|
+
href: { type: 'string', minLength: 1, maxLength: 2048 },
|
|
70
|
+
revisedAt: { type: 'string', nullable: true },
|
|
71
|
+
},
|
|
72
|
+
required: ['key', 'href'],
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const MarketingConsentDecisionSchema: JSONSchemaType<MarketingConsentDecision> = {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
key: { type: 'string', minLength: 1, maxLength: 64 },
|
|
80
|
+
granted: { type: 'boolean' },
|
|
81
|
+
revisedAt: { type: 'string', minLength: 1 },
|
|
82
|
+
mode: { type: 'string', enum: ['opt-in', 'opt-out'] },
|
|
83
|
+
decidedAt: { type: 'string', minLength: 1 },
|
|
84
|
+
source: { type: 'string', enum: ['sign-in', 'settings', 'cookie', 'api'] },
|
|
85
|
+
},
|
|
86
|
+
required: ['key', 'granted', 'revisedAt', 'mode', 'decidedAt', 'source'],
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Shared with the Mongo/Postgres extension packages this schema is built for — imported by them,
|
|
92
|
+
* never duplicated.
|
|
93
|
+
*/
|
|
94
|
+
export const MarketingConsentStateSchema: JSONSchemaType<MarketingConsentStateRecord> = {
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
id: { type: 'string', minLength: 1 },
|
|
98
|
+
subject: { type: 'string', minLength: 1 },
|
|
99
|
+
userId: { type: 'string', minLength: 1 },
|
|
100
|
+
profileId: { type: 'string', nullable: true },
|
|
101
|
+
entityId: { type: 'string', nullable: true },
|
|
102
|
+
decisions: { type: 'array', items: MarketingConsentDecisionSchema },
|
|
103
|
+
terms: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
nullable: true,
|
|
106
|
+
properties: {
|
|
107
|
+
documents: { type: 'array', items: TermsDocumentRefSchema },
|
|
108
|
+
notices: { type: 'array', items: TermsDocumentRefSchema, nullable: true },
|
|
109
|
+
version: { type: 'string', minLength: 1 },
|
|
110
|
+
locale: { type: 'string', nullable: true },
|
|
111
|
+
acceptedAt: { type: 'string', minLength: 1 },
|
|
112
|
+
},
|
|
113
|
+
required: ['documents', 'version', 'acceptedAt'],
|
|
114
|
+
additionalProperties: false,
|
|
115
|
+
},
|
|
116
|
+
gpc: { type: 'boolean', nullable: true },
|
|
117
|
+
createdAt: { type: 'string', minLength: 1 },
|
|
118
|
+
updatedAt: { type: 'string', minLength: 1 },
|
|
119
|
+
},
|
|
120
|
+
required: ['id', 'subject', 'userId', 'decisions', 'createdAt', 'updatedAt'],
|
|
121
|
+
additionalProperties: false,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const MarketingConsentLogSchema: JSONSchemaType<MarketingConsentLogRecord> = {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
id: { type: 'string', minLength: 1 },
|
|
128
|
+
subject: { type: 'string', minLength: 1 },
|
|
129
|
+
userId: { type: 'string', minLength: 1 },
|
|
130
|
+
profileId: { type: 'string', nullable: true },
|
|
131
|
+
entityId: { type: 'string', nullable: true },
|
|
132
|
+
kind: { type: 'string', enum: ['consent', 'terms'] },
|
|
133
|
+
key: { type: 'string', nullable: true },
|
|
134
|
+
granted: { type: 'boolean', nullable: true },
|
|
135
|
+
revisedAt: { type: 'string', nullable: true },
|
|
136
|
+
mode: { type: 'string', enum: ['opt-in', 'opt-out'], nullable: true },
|
|
137
|
+
documents: { type: 'array', items: TermsDocumentRefSchema, nullable: true },
|
|
138
|
+
notices: { type: 'array', items: TermsDocumentRefSchema, nullable: true },
|
|
139
|
+
version: { type: 'string', nullable: true },
|
|
140
|
+
decidedAt: { type: 'string', minLength: 1 },
|
|
141
|
+
source: { type: 'string', enum: ['sign-in', 'settings', 'cookie', 'api'] },
|
|
142
|
+
locale: { type: 'string', nullable: true },
|
|
143
|
+
gpc: { type: 'boolean', nullable: true },
|
|
144
|
+
},
|
|
145
|
+
required: ['id', 'subject', 'userId', 'kind', 'decidedAt', 'source'],
|
|
146
|
+
additionalProperties: false,
|
|
147
|
+
}
|