@c15t/backend 2.0.0-rc.6 → 2.0.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 +3 -3
- package/dist/302.js +2 -3
- package/dist/{364.js → 915.js} +656 -25
- package/dist/core.cjs +497 -15
- package/dist/core.js +8 -156
- package/dist/db/schema.cjs +4 -0
- package/dist/db/schema.js +3 -2
- package/dist/edge.cjs +8 -8
- package/dist/edge.js +3 -3
- package/dist/router.cjs +253 -42
- package/dist/router.js +1 -1
- package/dist-types/cache/gvl-resolver.d.ts +1 -1
- package/dist-types/db/registry/consent-policy.d.ts +57 -1
- package/dist-types/db/registry/index.d.ts +43 -1
- package/dist-types/db/registry/types.d.ts +2 -1
- package/dist-types/db/schema/1.0.0/consent.d.ts +1 -1
- package/dist-types/db/schema/2.0.0/audit-log.d.ts +1 -1
- package/dist-types/db/schema/2.0.0/consent-policy.d.ts +3 -2
- package/dist-types/db/schema/2.0.0/consent-purpose.d.ts +1 -1
- package/dist-types/db/schema/2.0.0/consent.d.ts +1 -1
- package/dist-types/db/schema/2.0.0/domain.d.ts +1 -1
- package/dist-types/db/schema/2.0.0/index.d.ts +7 -0
- package/dist-types/db/schema/2.0.0/runtime-policy-decision.d.ts +1 -1
- package/dist-types/db/schema/2.0.0/subject.d.ts +1 -1
- package/dist-types/db/schema/index.d.ts +14 -0
- package/dist-types/edge/index.d.ts +2 -2
- package/dist-types/edge/init-handler.d.ts +5 -3
- package/dist-types/edge/resolve-consent.d.ts +6 -6
- package/dist-types/edge/types.d.ts +1 -1
- package/dist-types/handlers/init/index.d.ts +4 -4
- package/dist-types/handlers/init/policy.d.ts +1 -1
- package/dist-types/handlers/init/resolve-init.d.ts +2 -2
- package/dist-types/handlers/init/translations.d.ts +1 -1
- package/dist-types/handlers/legal-document/current.handler.d.ts +11 -0
- package/dist-types/handlers/legal-document/snapshot.d.ts +39 -0
- package/dist-types/handlers/subject/get.handler.d.ts +3 -0
- package/dist-types/handlers/subject/list.handler.d.ts +3 -0
- package/dist-types/handlers/utils/consent-enrichment.d.ts +3 -0
- package/dist-types/middleware/cors/is-origin-trusted.d.ts +1 -1
- package/dist-types/policies/builder.d.ts +7 -7
- package/dist-types/policies/defaults.d.ts +2 -2
- package/dist-types/policies/matchers.d.ts +2 -2
- package/dist-types/routes/index.d.ts +1 -0
- package/dist-types/routes/legal-document.d.ts +7 -0
- package/dist-types/types/index.d.ts +39 -18
- package/dist-types/utils/instrumentation.d.ts +2 -2
- package/dist-types/utils/logger.d.ts +1 -1
- package/dist-types/version.d.ts +1 -1
- package/docs/api/configuration.md +24 -13
- package/docs/guides/database-setup.md +4 -4
- package/docs/guides/edge-deployment.md +18 -15
- package/docs/guides/iab-tcf.md +4 -4
- package/docs/quickstart.md +9 -9
- package/package.json +8 -8
package/dist/{364.js → 915.js}
RENAMED
|
@@ -1,11 +1,349 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { hashSha256Hex } from "@c15t/schema/types";
|
|
2
|
+
import base_x from "base-x";
|
|
3
|
+
import { checkConsentOutputSchema, checkConsentQuerySchema, getSubjectInputSchema, getSubjectOutputSchema, initOutputSchema, legalDocumentCurrentInputSchema, legalDocumentCurrentOutputSchema, legalDocumentCurrentParamsSchema, listSubjectsOutputSchema, listSubjectsQuerySchema, patchSubjectOutputSchema, postSubjectInputSchema, postSubjectOutputSchema, statusOutputSchema, subjectIdSchema } from "@c15t/schema";
|
|
2
4
|
import { Hono } from "hono";
|
|
3
5
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
4
6
|
import { HTTPException } from "hono/http-exception";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
+
import { errors, jwtVerify } from "jose";
|
|
8
|
+
import { extractErrorMessage, getMetrics, withDatabaseSpan } from "./302.js";
|
|
7
9
|
import { getLocation, resolveInitPayload, policy_resolvePolicyDecision, verifyPolicySnapshotToken, getJurisdiction } from "./583.js";
|
|
8
10
|
import * as __rspack_external_valibot from "valibot";
|
|
11
|
+
const prefixes = {
|
|
12
|
+
auditLog: 'log',
|
|
13
|
+
consent: 'cns',
|
|
14
|
+
consentPolicy: 'pol',
|
|
15
|
+
consentPurpose: 'pur',
|
|
16
|
+
domain: 'dom',
|
|
17
|
+
runtimePolicyDecision: 'rpd',
|
|
18
|
+
subject: 'sub'
|
|
19
|
+
};
|
|
20
|
+
const b58 = base_x('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
|
|
21
|
+
function generateId(model) {
|
|
22
|
+
const buf = crypto.getRandomValues(new Uint8Array(20));
|
|
23
|
+
const prefix = prefixes[model];
|
|
24
|
+
const EPOCH_TIMESTAMP = 1700000000000;
|
|
25
|
+
const t = Date.now() - EPOCH_TIMESTAMP;
|
|
26
|
+
const high = Math.floor(t / 0x100000000);
|
|
27
|
+
const low = t >>> 0;
|
|
28
|
+
buf[0] = high >>> 24 & 255;
|
|
29
|
+
buf[1] = high >>> 16 & 255;
|
|
30
|
+
buf[2] = high >>> 8 & 255;
|
|
31
|
+
buf[3] = 255 & high;
|
|
32
|
+
buf[4] = low >>> 24 & 255;
|
|
33
|
+
buf[5] = low >>> 16 & 255;
|
|
34
|
+
buf[6] = low >>> 8 & 255;
|
|
35
|
+
buf[7] = 255 & low;
|
|
36
|
+
return `${prefix}_${b58.encode(buf)}`;
|
|
37
|
+
}
|
|
38
|
+
async function generateUniqueId(db, model, ctx, options = {}) {
|
|
39
|
+
const { maxRetries = 10, attempt = 0, baseDelay = 5 } = options;
|
|
40
|
+
if (attempt >= maxRetries) {
|
|
41
|
+
const error = new Error(`Failed to generate unique ID for ${model} after ${maxRetries} attempts`);
|
|
42
|
+
ctx?.logger?.error?.('ID generation failed', {
|
|
43
|
+
model,
|
|
44
|
+
maxRetries
|
|
45
|
+
});
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
const id = generateId(model);
|
|
49
|
+
try {
|
|
50
|
+
const existing = await db.findFirst(model, {
|
|
51
|
+
where: (b)=>b('id', '=', id)
|
|
52
|
+
});
|
|
53
|
+
if (existing) {
|
|
54
|
+
ctx?.logger?.debug?.('ID conflict detected', {
|
|
55
|
+
id,
|
|
56
|
+
model,
|
|
57
|
+
attempt: attempt + 1,
|
|
58
|
+
maxRetries
|
|
59
|
+
});
|
|
60
|
+
const delay = Math.min(baseDelay * 2 ** attempt, 1000);
|
|
61
|
+
await new Promise((resolve)=>setTimeout(resolve, delay));
|
|
62
|
+
return generateUniqueId(db, model, ctx, {
|
|
63
|
+
maxRetries,
|
|
64
|
+
attempt: attempt + 1,
|
|
65
|
+
baseDelay
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return id;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
ctx?.logger?.error?.('Error checking ID uniqueness', {
|
|
71
|
+
error: error.message,
|
|
72
|
+
model,
|
|
73
|
+
attempt
|
|
74
|
+
});
|
|
75
|
+
if (attempt < maxRetries - 1) {
|
|
76
|
+
const delay = Math.min(baseDelay * 2 ** attempt, 2000);
|
|
77
|
+
await new Promise((resolve)=>setTimeout(resolve, delay));
|
|
78
|
+
return generateUniqueId(db, model, ctx, {
|
|
79
|
+
maxRetries,
|
|
80
|
+
attempt: attempt + 1,
|
|
81
|
+
baseDelay
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
class LegalDocumentPolicyConflictError extends Error {
|
|
88
|
+
constructor(message){
|
|
89
|
+
super(message);
|
|
90
|
+
this.name = 'LegalDocumentPolicyConflictError';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function buildLegalDocumentPolicyId(input) {
|
|
94
|
+
const digest = await hashSha256Hex([
|
|
95
|
+
input.tenantId ?? 'default',
|
|
96
|
+
input.type,
|
|
97
|
+
input.hash
|
|
98
|
+
].join('|'));
|
|
99
|
+
return `pol_${digest}`;
|
|
100
|
+
}
|
|
101
|
+
function hasLegalDocumentPolicyConflict(policy, input) {
|
|
102
|
+
return policy.version !== input.version || policy.hash !== input.hash || policy.effectiveDate.getTime() !== input.effectiveDate.getTime();
|
|
103
|
+
}
|
|
104
|
+
function policyRegistry({ db, ctx }) {
|
|
105
|
+
const { logger } = ctx;
|
|
106
|
+
return {
|
|
107
|
+
findConsentPolicyById: async (policyId)=>{
|
|
108
|
+
const start = Date.now();
|
|
109
|
+
try {
|
|
110
|
+
const result = await withDatabaseSpan({
|
|
111
|
+
operation: 'find',
|
|
112
|
+
entity: 'consentPolicy'
|
|
113
|
+
}, async ()=>{
|
|
114
|
+
const policy = await db.findFirst('consentPolicy', {
|
|
115
|
+
where: (b)=>b('id', '=', policyId)
|
|
116
|
+
});
|
|
117
|
+
return policy;
|
|
118
|
+
});
|
|
119
|
+
getMetrics()?.recordDbQuery({
|
|
120
|
+
operation: 'find',
|
|
121
|
+
entity: 'consentPolicy'
|
|
122
|
+
}, Date.now() - start);
|
|
123
|
+
return result;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
getMetrics()?.recordDbError({
|
|
126
|
+
operation: 'find',
|
|
127
|
+
entity: 'consentPolicy'
|
|
128
|
+
});
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
findLatestPolicyByType: async (type)=>{
|
|
133
|
+
const start = Date.now();
|
|
134
|
+
try {
|
|
135
|
+
const result = await withDatabaseSpan({
|
|
136
|
+
operation: 'findLatest',
|
|
137
|
+
entity: 'consentPolicy'
|
|
138
|
+
}, async ()=>db.findFirst('consentPolicy', {
|
|
139
|
+
where: (b)=>b.and(b('isActive', '=', true), b('type', '=', type)),
|
|
140
|
+
orderBy: [
|
|
141
|
+
'effectiveDate',
|
|
142
|
+
'desc'
|
|
143
|
+
]
|
|
144
|
+
}));
|
|
145
|
+
getMetrics()?.recordDbQuery({
|
|
146
|
+
operation: 'findLatest',
|
|
147
|
+
entity: 'consentPolicy'
|
|
148
|
+
}, Date.now() - start);
|
|
149
|
+
return result;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
getMetrics()?.recordDbError({
|
|
152
|
+
operation: 'findLatest',
|
|
153
|
+
entity: 'consentPolicy'
|
|
154
|
+
});
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
findLegalDocumentPolicyByHash: async (type, hash)=>{
|
|
159
|
+
const start = Date.now();
|
|
160
|
+
try {
|
|
161
|
+
const policyId = await buildLegalDocumentPolicyId({
|
|
162
|
+
tenantId: ctx.tenantId,
|
|
163
|
+
type,
|
|
164
|
+
hash
|
|
165
|
+
});
|
|
166
|
+
const result = await withDatabaseSpan({
|
|
167
|
+
operation: 'findByHash',
|
|
168
|
+
entity: 'consentPolicy'
|
|
169
|
+
}, async ()=>db.findFirst('consentPolicy', {
|
|
170
|
+
where: (b)=>b('id', '=', policyId)
|
|
171
|
+
}));
|
|
172
|
+
getMetrics()?.recordDbQuery({
|
|
173
|
+
operation: 'findByHash',
|
|
174
|
+
entity: 'consentPolicy'
|
|
175
|
+
}, Date.now() - start);
|
|
176
|
+
return result;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
getMetrics()?.recordDbError({
|
|
179
|
+
operation: 'findByHash',
|
|
180
|
+
entity: 'consentPolicy'
|
|
181
|
+
});
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
syncCurrentLegalDocumentPolicy: async (input)=>{
|
|
186
|
+
const start = Date.now();
|
|
187
|
+
try {
|
|
188
|
+
const result = await withDatabaseSpan({
|
|
189
|
+
operation: 'syncCurrent',
|
|
190
|
+
entity: 'consentPolicy'
|
|
191
|
+
}, async ()=>{
|
|
192
|
+
const policyId = await buildLegalDocumentPolicyId({
|
|
193
|
+
tenantId: ctx.tenantId,
|
|
194
|
+
type: input.type,
|
|
195
|
+
hash: input.hash
|
|
196
|
+
});
|
|
197
|
+
return db.transaction(async (tx)=>{
|
|
198
|
+
const existing = await tx.findFirst('consentPolicy', {
|
|
199
|
+
where: (b)=>b('id', '=', policyId)
|
|
200
|
+
});
|
|
201
|
+
if (existing) {
|
|
202
|
+
if (hasLegalDocumentPolicyConflict(existing, input)) throw new LegalDocumentPolicyConflictError('Release metadata conflicts with existing consent policy');
|
|
203
|
+
await tx.updateMany('consentPolicy', {
|
|
204
|
+
where: (b)=>b.and(b('type', '=', input.type), b('isActive', '=', true), b('id', '!=', existing.id)),
|
|
205
|
+
set: {
|
|
206
|
+
isActive: false
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
if (!existing.isActive) {
|
|
210
|
+
await tx.updateMany('consentPolicy', {
|
|
211
|
+
where: (b)=>b('id', '=', existing.id),
|
|
212
|
+
set: {
|
|
213
|
+
isActive: true
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
...existing,
|
|
218
|
+
isActive: true
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return existing;
|
|
222
|
+
}
|
|
223
|
+
await tx.updateMany('consentPolicy', {
|
|
224
|
+
where: (b)=>b.and(b('type', '=', input.type), b('isActive', '=', true)),
|
|
225
|
+
set: {
|
|
226
|
+
isActive: false
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
const policy = await tx.create('consentPolicy', {
|
|
230
|
+
id: policyId,
|
|
231
|
+
version: input.version,
|
|
232
|
+
type: input.type,
|
|
233
|
+
hash: input.hash,
|
|
234
|
+
effectiveDate: input.effectiveDate,
|
|
235
|
+
isActive: true
|
|
236
|
+
});
|
|
237
|
+
return policy;
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
getMetrics()?.recordDbQuery({
|
|
241
|
+
operation: 'syncCurrent',
|
|
242
|
+
entity: 'consentPolicy'
|
|
243
|
+
}, Date.now() - start);
|
|
244
|
+
return result;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
getMetrics()?.recordDbError({
|
|
247
|
+
operation: 'syncCurrent',
|
|
248
|
+
entity: 'consentPolicy'
|
|
249
|
+
});
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
findOrCreateLegalDocumentPolicy: async (input)=>{
|
|
254
|
+
const start = Date.now();
|
|
255
|
+
try {
|
|
256
|
+
const result = await withDatabaseSpan({
|
|
257
|
+
operation: 'findOrCreateLegalDocument',
|
|
258
|
+
entity: 'consentPolicy'
|
|
259
|
+
}, async ()=>{
|
|
260
|
+
const policyId = await buildLegalDocumentPolicyId({
|
|
261
|
+
tenantId: ctx.tenantId,
|
|
262
|
+
type: input.type,
|
|
263
|
+
hash: input.hash
|
|
264
|
+
});
|
|
265
|
+
const existing = await db.findFirst('consentPolicy', {
|
|
266
|
+
where: (b)=>b('id', '=', policyId)
|
|
267
|
+
});
|
|
268
|
+
if (existing) {
|
|
269
|
+
if (hasLegalDocumentPolicyConflict(existing, input)) throw new LegalDocumentPolicyConflictError('Release metadata conflicts with existing consent policy');
|
|
270
|
+
return existing;
|
|
271
|
+
}
|
|
272
|
+
const policy = await db.create('consentPolicy', {
|
|
273
|
+
id: policyId,
|
|
274
|
+
version: input.version,
|
|
275
|
+
type: input.type,
|
|
276
|
+
hash: input.hash,
|
|
277
|
+
effectiveDate: input.effectiveDate,
|
|
278
|
+
isActive: false
|
|
279
|
+
}).catch(async ()=>{
|
|
280
|
+
const concurrent = await db.findFirst('consentPolicy', {
|
|
281
|
+
where: (b)=>b('id', '=', policyId)
|
|
282
|
+
});
|
|
283
|
+
if (!concurrent) throw new LegalDocumentPolicyConflictError('Failed to create legal document consent policy');
|
|
284
|
+
if (hasLegalDocumentPolicyConflict(concurrent, input)) throw new LegalDocumentPolicyConflictError('Release metadata conflicts with existing consent policy');
|
|
285
|
+
return concurrent;
|
|
286
|
+
});
|
|
287
|
+
return policy;
|
|
288
|
+
});
|
|
289
|
+
getMetrics()?.recordDbQuery({
|
|
290
|
+
operation: 'findOrCreateLegalDocument',
|
|
291
|
+
entity: 'consentPolicy'
|
|
292
|
+
}, Date.now() - start);
|
|
293
|
+
return result;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
getMetrics()?.recordDbError({
|
|
296
|
+
operation: 'findOrCreateLegalDocument',
|
|
297
|
+
entity: 'consentPolicy'
|
|
298
|
+
});
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
findOrCreatePolicy: async (type)=>{
|
|
303
|
+
const start = Date.now();
|
|
304
|
+
try {
|
|
305
|
+
const result = await withDatabaseSpan({
|
|
306
|
+
operation: 'findOrCreate',
|
|
307
|
+
entity: 'consentPolicy'
|
|
308
|
+
}, async ()=>{
|
|
309
|
+
const existingPolicy = await db.findFirst('consentPolicy', {
|
|
310
|
+
where: (b)=>b.and(b('isActive', '=', true), b('type', '=', type)),
|
|
311
|
+
orderBy: [
|
|
312
|
+
'effectiveDate',
|
|
313
|
+
'desc'
|
|
314
|
+
]
|
|
315
|
+
});
|
|
316
|
+
if (existingPolicy) {
|
|
317
|
+
logger.debug('Found existing policy', {
|
|
318
|
+
type,
|
|
319
|
+
policyId: existingPolicy.id
|
|
320
|
+
});
|
|
321
|
+
return existingPolicy;
|
|
322
|
+
}
|
|
323
|
+
const policy = await db.create('consentPolicy', {
|
|
324
|
+
id: await generateUniqueId(db, 'consentPolicy', ctx),
|
|
325
|
+
version: '1.0.0',
|
|
326
|
+
type,
|
|
327
|
+
effectiveDate: new Date(),
|
|
328
|
+
isActive: true
|
|
329
|
+
});
|
|
330
|
+
return policy;
|
|
331
|
+
});
|
|
332
|
+
getMetrics()?.recordDbQuery({
|
|
333
|
+
operation: 'findOrCreate',
|
|
334
|
+
entity: 'consentPolicy'
|
|
335
|
+
}, Date.now() - start);
|
|
336
|
+
return result;
|
|
337
|
+
} catch (error) {
|
|
338
|
+
getMetrics()?.recordDbError({
|
|
339
|
+
operation: 'findOrCreate',
|
|
340
|
+
entity: 'consentPolicy'
|
|
341
|
+
});
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
9
347
|
function parsePurposeIds(purposeIds) {
|
|
10
348
|
if (null == purposeIds) return [];
|
|
11
349
|
const ids = 'object' == typeof purposeIds && 'json' in purposeIds ? purposeIds.json : purposeIds;
|
|
@@ -26,7 +364,7 @@ async function batchLoadPolicies(policyIds, ctx) {
|
|
|
26
364
|
for (const p of policyMap.values())uniqueTypes.add(p.type);
|
|
27
365
|
const latestPolicyByType = new Map();
|
|
28
366
|
for (const type of uniqueTypes){
|
|
29
|
-
const latest = await registry.
|
|
367
|
+
const latest = await registry.findLatestPolicyByType(type);
|
|
30
368
|
if (latest) latestPolicyByType.set(type, latest.id);
|
|
31
369
|
}
|
|
32
370
|
return {
|
|
@@ -52,11 +390,17 @@ async function enrichConsents(consents, ctx) {
|
|
|
52
390
|
}
|
|
53
391
|
return consents.map((consent)=>{
|
|
54
392
|
let policyType = 'unknown';
|
|
393
|
+
let policyVersion;
|
|
394
|
+
let policyHash;
|
|
395
|
+
let policyEffectiveDate;
|
|
55
396
|
let isLatestPolicy = false;
|
|
56
397
|
if (consent.policyId) {
|
|
57
398
|
const policy = policyMap.get(consent.policyId);
|
|
58
399
|
if (policy) {
|
|
59
400
|
policyType = policy.type;
|
|
401
|
+
policyVersion = policy.version;
|
|
402
|
+
policyHash = policy.hash ?? void 0;
|
|
403
|
+
policyEffectiveDate = policy.effectiveDate;
|
|
60
404
|
isLatestPolicy = latestPolicyByType.get(policyType) === consent.policyId;
|
|
61
405
|
}
|
|
62
406
|
}
|
|
@@ -73,6 +417,9 @@ async function enrichConsents(consents, ctx) {
|
|
|
73
417
|
id: consent.id,
|
|
74
418
|
type: policyType,
|
|
75
419
|
policyId: consent.policyId ?? void 0,
|
|
420
|
+
policyVersion,
|
|
421
|
+
policyHash,
|
|
422
|
+
policyEffectiveDate,
|
|
76
423
|
isLatestPolicy,
|
|
77
424
|
preferences,
|
|
78
425
|
givenAt: consent.givenAt
|
|
@@ -240,6 +587,94 @@ Use for geo-targeted consent banners and regional compliance.`,
|
|
|
240
587
|
});
|
|
241
588
|
return app;
|
|
242
589
|
};
|
|
590
|
+
const syncCurrentLegalDocumentHandler = async (c)=>{
|
|
591
|
+
const ctx = c.get('c15tContext');
|
|
592
|
+
const logger = ctx.logger;
|
|
593
|
+
logger.info('Handling PUT /legal-documents/:type/current request');
|
|
594
|
+
if (!ctx.apiKeyAuthenticated) throw new HTTPException(401, {
|
|
595
|
+
message: 'API key required. Use Authorization: Bearer <api_key>',
|
|
596
|
+
cause: {
|
|
597
|
+
code: 'UNAUTHORIZED'
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
const type = c.req.param('type');
|
|
601
|
+
const body = await c.req.json();
|
|
602
|
+
const effectiveDate = new Date(body.effectiveDate);
|
|
603
|
+
if (Number.isNaN(effectiveDate.getTime())) throw new HTTPException(422, {
|
|
604
|
+
message: 'effectiveDate must be a valid ISO-8601 string',
|
|
605
|
+
cause: {
|
|
606
|
+
code: 'INPUT_VALIDATION_FAILED'
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
try {
|
|
610
|
+
const policy = await ctx.registry.syncCurrentLegalDocumentPolicy({
|
|
611
|
+
type,
|
|
612
|
+
version: body.version,
|
|
613
|
+
hash: body.hash,
|
|
614
|
+
effectiveDate
|
|
615
|
+
});
|
|
616
|
+
return c.json({
|
|
617
|
+
policy: {
|
|
618
|
+
id: policy.id,
|
|
619
|
+
type: policy.type,
|
|
620
|
+
version: policy.version,
|
|
621
|
+
hash: policy.hash,
|
|
622
|
+
effectiveDate: policy.effectiveDate,
|
|
623
|
+
isActive: policy.isActive
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
} catch (error) {
|
|
627
|
+
logger.error('Error in PUT /legal-documents/:type/current handler', {
|
|
628
|
+
error: extractErrorMessage(error),
|
|
629
|
+
errorType: error instanceof Error ? error.constructor.name : typeof error
|
|
630
|
+
});
|
|
631
|
+
if (error instanceof LegalDocumentPolicyConflictError) throw new HTTPException(409, {
|
|
632
|
+
message: error.message,
|
|
633
|
+
cause: {
|
|
634
|
+
code: 'LEGAL_DOCUMENT_RELEASE_CONFLICT'
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
if (error instanceof HTTPException) throw error;
|
|
638
|
+
throw new HTTPException(500, {
|
|
639
|
+
message: 'Internal server error',
|
|
640
|
+
cause: {
|
|
641
|
+
code: 'INTERNAL_SERVER_ERROR'
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
const createLegalDocumentRoutes = ()=>{
|
|
647
|
+
const app = new Hono();
|
|
648
|
+
app.put('/:type/current', describeRoute({
|
|
649
|
+
summary: 'Sync the current legal document release (API key required)',
|
|
650
|
+
description: 'Marks a legal document release as the latest known version for its type. Requires a Bearer API key.',
|
|
651
|
+
tags: [
|
|
652
|
+
'LegalDocument'
|
|
653
|
+
],
|
|
654
|
+
security: [
|
|
655
|
+
{
|
|
656
|
+
bearerAuth: []
|
|
657
|
+
}
|
|
658
|
+
],
|
|
659
|
+
responses: {
|
|
660
|
+
200: {
|
|
661
|
+
description: 'Current legal document release synced successfully',
|
|
662
|
+
content: {
|
|
663
|
+
'application/json': {
|
|
664
|
+
schema: resolver(legalDocumentCurrentOutputSchema)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
401: {
|
|
669
|
+
description: 'Missing or invalid API key'
|
|
670
|
+
},
|
|
671
|
+
409: {
|
|
672
|
+
description: 'Release metadata conflicts with an existing release'
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}), validator('param', legalDocumentCurrentParamsSchema), validator('json', legalDocumentCurrentInputSchema), syncCurrentLegalDocumentHandler);
|
|
676
|
+
return app;
|
|
677
|
+
};
|
|
243
678
|
function getHeaders(headers) {
|
|
244
679
|
if (!headers) return {
|
|
245
680
|
countryCode: null,
|
|
@@ -274,7 +709,7 @@ const statusHandler = async (c)=>{
|
|
|
274
709
|
try {
|
|
275
710
|
await ctx.db.findFirst('subject', {});
|
|
276
711
|
return c.json({
|
|
277
|
-
version:
|
|
712
|
+
version: "2.0.0",
|
|
278
713
|
timestamp: new Date(),
|
|
279
714
|
client: clientInfo
|
|
280
715
|
});
|
|
@@ -439,7 +874,7 @@ const listSubjectsHandler = async (c)=>{
|
|
|
439
874
|
});
|
|
440
875
|
}
|
|
441
876
|
};
|
|
442
|
-
const
|
|
877
|
+
const utils_prefixes = {
|
|
443
878
|
auditLog: 'log',
|
|
444
879
|
consent: 'cns',
|
|
445
880
|
consentPolicy: 'pol',
|
|
@@ -447,10 +882,10 @@ const prefixes = {
|
|
|
447
882
|
domain: 'dom',
|
|
448
883
|
subject: 'sub'
|
|
449
884
|
};
|
|
450
|
-
const
|
|
451
|
-
function
|
|
885
|
+
const utils_b58 = base_x('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
|
|
886
|
+
function utils_generateId(model) {
|
|
452
887
|
const buf = crypto.getRandomValues(new Uint8Array(20));
|
|
453
|
-
const prefix =
|
|
888
|
+
const prefix = utils_prefixes[model];
|
|
454
889
|
const EPOCH_TIMESTAMP = 1700000000000;
|
|
455
890
|
const t = Date.now() - EPOCH_TIMESTAMP;
|
|
456
891
|
const high = Math.floor(t / 0x100000000);
|
|
@@ -463,9 +898,9 @@ function generateId(model) {
|
|
|
463
898
|
buf[5] = low >>> 16 & 255;
|
|
464
899
|
buf[6] = low >>> 8 & 255;
|
|
465
900
|
buf[7] = 255 & low;
|
|
466
|
-
return `${prefix}_${
|
|
901
|
+
return `${prefix}_${utils_b58.encode(buf)}`;
|
|
467
902
|
}
|
|
468
|
-
async function
|
|
903
|
+
async function utils_generateUniqueId(db, model, ctx, options = {}) {
|
|
469
904
|
const { maxRetries = 10, attempt = 0, baseDelay = 5 } = options;
|
|
470
905
|
if (attempt >= maxRetries) {
|
|
471
906
|
const error = new Error(`Failed to generate unique ID for ${model} after ${maxRetries} attempts`);
|
|
@@ -475,7 +910,7 @@ async function generateUniqueId(db, model, ctx, options = {}) {
|
|
|
475
910
|
});
|
|
476
911
|
throw error;
|
|
477
912
|
}
|
|
478
|
-
const id =
|
|
913
|
+
const id = utils_generateId(model);
|
|
479
914
|
try {
|
|
480
915
|
const existing = await db.findFirst(model, {
|
|
481
916
|
where: (b)=>b('id', '=', id)
|
|
@@ -489,7 +924,7 @@ async function generateUniqueId(db, model, ctx, options = {}) {
|
|
|
489
924
|
});
|
|
490
925
|
const delay = Math.min(baseDelay * 2 ** attempt, 1000);
|
|
491
926
|
await new Promise((resolve)=>setTimeout(resolve, delay));
|
|
492
|
-
return
|
|
927
|
+
return utils_generateUniqueId(db, model, ctx, {
|
|
493
928
|
maxRetries,
|
|
494
929
|
attempt: attempt + 1,
|
|
495
930
|
baseDelay
|
|
@@ -505,7 +940,7 @@ async function generateUniqueId(db, model, ctx, options = {}) {
|
|
|
505
940
|
if (attempt < maxRetries - 1) {
|
|
506
941
|
const delay = Math.min(baseDelay * 2 ** attempt, 2000);
|
|
507
942
|
await new Promise((resolve)=>setTimeout(resolve, delay));
|
|
508
|
-
return
|
|
943
|
+
return utils_generateUniqueId(db, model, ctx, {
|
|
509
944
|
maxRetries,
|
|
510
945
|
attempt: attempt + 1,
|
|
511
946
|
baseDelay
|
|
@@ -554,7 +989,7 @@ const patchSubjectHandler = async (c)=>{
|
|
|
554
989
|
}
|
|
555
990
|
});
|
|
556
991
|
await tx.create('auditLog', {
|
|
557
|
-
id: await
|
|
992
|
+
id: await utils_generateUniqueId(tx, 'auditLog', ctx),
|
|
558
993
|
subjectId,
|
|
559
994
|
entityType: 'subject',
|
|
560
995
|
entityId: subjectId,
|
|
@@ -604,6 +1039,79 @@ const patchSubjectHandler = async (c)=>{
|
|
|
604
1039
|
});
|
|
605
1040
|
}
|
|
606
1041
|
};
|
|
1042
|
+
const DEFAULT_ISSUER = 'c15t';
|
|
1043
|
+
const DEFAULT_AUDIENCE = 'c15t-legal-document-snapshot';
|
|
1044
|
+
function isLegalDocumentPolicyType(type) {
|
|
1045
|
+
return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
|
|
1046
|
+
}
|
|
1047
|
+
function resolveSnapshotIssuer(options) {
|
|
1048
|
+
return options?.issuer?.trim() || DEFAULT_ISSUER;
|
|
1049
|
+
}
|
|
1050
|
+
function resolveSnapshotAudience(params) {
|
|
1051
|
+
const configuredAudience = params.options?.audience?.trim();
|
|
1052
|
+
if (configuredAudience) return configuredAudience;
|
|
1053
|
+
return params.tenantId ? `${DEFAULT_AUDIENCE}:${params.tenantId}` : DEFAULT_AUDIENCE;
|
|
1054
|
+
}
|
|
1055
|
+
function getSigningKey(secret) {
|
|
1056
|
+
return new TextEncoder().encode(secret);
|
|
1057
|
+
}
|
|
1058
|
+
function isLegalDocumentSnapshotPayload(payload) {
|
|
1059
|
+
return 'string' == typeof payload.iss && 'string' == typeof payload.aud && 'string' == typeof payload.sub && isLegalDocumentPolicyType(payload.type) && 'string' == typeof payload.version && 'string' == typeof payload.hash && 'string' == typeof payload.effectiveDate && 'number' == typeof payload.iat && 'number' == typeof payload.exp;
|
|
1060
|
+
}
|
|
1061
|
+
async function verifyLegalDocumentSnapshotToken(params) {
|
|
1062
|
+
const { token, options, tenantId } = params;
|
|
1063
|
+
if (!options?.signingKey) return {
|
|
1064
|
+
valid: false,
|
|
1065
|
+
reason: 'missing'
|
|
1066
|
+
};
|
|
1067
|
+
if (!token) return {
|
|
1068
|
+
valid: false,
|
|
1069
|
+
reason: 'missing'
|
|
1070
|
+
};
|
|
1071
|
+
if (3 !== token.split('.').length) return {
|
|
1072
|
+
valid: false,
|
|
1073
|
+
reason: 'malformed'
|
|
1074
|
+
};
|
|
1075
|
+
try {
|
|
1076
|
+
const { payload, protectedHeader } = await jwtVerify(token, getSigningKey(options.signingKey), {
|
|
1077
|
+
issuer: resolveSnapshotIssuer(options),
|
|
1078
|
+
audience: resolveSnapshotAudience({
|
|
1079
|
+
options,
|
|
1080
|
+
tenantId
|
|
1081
|
+
})
|
|
1082
|
+
});
|
|
1083
|
+
const header = protectedHeader;
|
|
1084
|
+
if ('HS256' !== header.alg || 'JWT' !== header.typ) return {
|
|
1085
|
+
valid: false,
|
|
1086
|
+
reason: 'invalid'
|
|
1087
|
+
};
|
|
1088
|
+
if (!isLegalDocumentSnapshotPayload(payload)) return {
|
|
1089
|
+
valid: false,
|
|
1090
|
+
reason: 'invalid'
|
|
1091
|
+
};
|
|
1092
|
+
if (payload.sub !== payload.hash) return {
|
|
1093
|
+
valid: false,
|
|
1094
|
+
reason: 'invalid'
|
|
1095
|
+
};
|
|
1096
|
+
if ((tenantId ?? void 0) !== (payload.tenantId ?? void 0)) return {
|
|
1097
|
+
valid: false,
|
|
1098
|
+
reason: 'invalid'
|
|
1099
|
+
};
|
|
1100
|
+
return {
|
|
1101
|
+
valid: true,
|
|
1102
|
+
payload
|
|
1103
|
+
};
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
if (error instanceof errors.JWTExpired) return {
|
|
1106
|
+
valid: false,
|
|
1107
|
+
reason: 'expired'
|
|
1108
|
+
};
|
|
1109
|
+
return {
|
|
1110
|
+
valid: false,
|
|
1111
|
+
reason: 'invalid'
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
607
1115
|
function buildRuntimeDecisionDedupeKey(input) {
|
|
608
1116
|
return [
|
|
609
1117
|
input.tenantId ?? 'default',
|
|
@@ -683,6 +1191,9 @@ function parseLanguageFromHeader(header) {
|
|
|
683
1191
|
if (!firstLanguage) return;
|
|
684
1192
|
return firstLanguage.split('-')[0]?.toLowerCase();
|
|
685
1193
|
}
|
|
1194
|
+
function isLegalDocumentType(type) {
|
|
1195
|
+
return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
|
|
1196
|
+
}
|
|
686
1197
|
function resolveSnapshotFailureMode(ctx) {
|
|
687
1198
|
return ctx.policySnapshot?.onValidationFailure ?? 'reject';
|
|
688
1199
|
}
|
|
@@ -717,6 +1228,45 @@ function buildSnapshotHttpException(reason) {
|
|
|
717
1228
|
}
|
|
718
1229
|
}
|
|
719
1230
|
}
|
|
1231
|
+
function buildLegalDocumentSnapshotHttpException(reason) {
|
|
1232
|
+
switch(reason){
|
|
1233
|
+
case 'missing':
|
|
1234
|
+
return new HTTPException(409, {
|
|
1235
|
+
message: 'Legal document snapshot token is required',
|
|
1236
|
+
cause: {
|
|
1237
|
+
code: 'LEGAL_DOCUMENT_SNAPSHOT_REQUIRED'
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
case 'expired':
|
|
1241
|
+
return new HTTPException(409, {
|
|
1242
|
+
message: 'Legal document snapshot token has expired',
|
|
1243
|
+
cause: {
|
|
1244
|
+
code: 'LEGAL_DOCUMENT_SNAPSHOT_EXPIRED'
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
case 'malformed':
|
|
1248
|
+
case 'invalid':
|
|
1249
|
+
return new HTTPException(409, {
|
|
1250
|
+
message: 'Legal document snapshot token is invalid',
|
|
1251
|
+
cause: {
|
|
1252
|
+
code: 'LEGAL_DOCUMENT_SNAPSHOT_INVALID'
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
default:
|
|
1256
|
+
{
|
|
1257
|
+
const _exhaustive = reason;
|
|
1258
|
+
throw new Error(`Unhandled legal document snapshot verification failure reason: ${_exhaustive}`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
function buildLegalDocumentProofHttpException(message) {
|
|
1263
|
+
return new HTTPException(409, {
|
|
1264
|
+
message,
|
|
1265
|
+
cause: {
|
|
1266
|
+
code: 'LEGAL_DOCUMENT_PROOF_REQUIRED'
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
720
1270
|
const postSubjectHandler = async (c)=>{
|
|
721
1271
|
const ctx = c.get('c15tContext');
|
|
722
1272
|
const logger = ctx.logger;
|
|
@@ -742,16 +1292,30 @@ const postSubjectHandler = async (c)=>{
|
|
|
742
1292
|
const requestLanguage = parseLanguageFromHeader(acceptLanguage);
|
|
743
1293
|
const location = await getLocation(request, ctx);
|
|
744
1294
|
const resolvedJurisdiction = getJurisdiction(location, ctx);
|
|
745
|
-
const
|
|
1295
|
+
const legalDocumentConsent = isLegalDocumentType(type);
|
|
1296
|
+
const runtimeSnapshotVerification = legalDocumentConsent ? {
|
|
1297
|
+
valid: false,
|
|
1298
|
+
reason: 'missing'
|
|
1299
|
+
} : await verifyPolicySnapshotToken({
|
|
746
1300
|
token: input.policySnapshotToken,
|
|
747
1301
|
options: ctx.policySnapshot,
|
|
748
1302
|
tenantId: ctx.tenantId
|
|
749
1303
|
});
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1304
|
+
const legalDocumentSnapshotVerification = legalDocumentConsent ? await verifyLegalDocumentSnapshotToken({
|
|
1305
|
+
token: input.documentSnapshotToken,
|
|
1306
|
+
options: ctx.legalDocumentSnapshot,
|
|
1307
|
+
tenantId: ctx.tenantId
|
|
1308
|
+
}) : {
|
|
1309
|
+
valid: false,
|
|
1310
|
+
reason: 'missing'
|
|
1311
|
+
};
|
|
1312
|
+
const hasValidSnapshot = runtimeSnapshotVerification.valid;
|
|
1313
|
+
const snapshotPayload = runtimeSnapshotVerification.valid ? runtimeSnapshotVerification.payload : null;
|
|
1314
|
+
const shouldRequireSnapshot = !legalDocumentConsent && !!ctx.policySnapshot?.signingKey && 'reject' === resolveSnapshotFailureMode(ctx);
|
|
1315
|
+
if (!hasValidSnapshot && shouldRequireSnapshot) throw buildSnapshotHttpException(runtimeSnapshotVerification.reason);
|
|
1316
|
+
const shouldRequireLegalDocumentSnapshot = legalDocumentConsent && !!ctx.legalDocumentSnapshot?.signingKey;
|
|
1317
|
+
if (shouldRequireLegalDocumentSnapshot && !legalDocumentSnapshotVerification.valid) throw buildLegalDocumentSnapshotHttpException(legalDocumentSnapshotVerification.reason);
|
|
1318
|
+
const resolvedPolicyDecision = hasValidSnapshot ? void 0 : legalDocumentConsent ? void 0 : await policy_resolvePolicyDecision({
|
|
755
1319
|
policies: ctx.policyPacks,
|
|
756
1320
|
countryCode: location.countryCode,
|
|
757
1321
|
regionCode: location.regionCode,
|
|
@@ -808,7 +1372,61 @@ const postSubjectHandler = async (c)=>{
|
|
|
808
1372
|
let purposeIds = [];
|
|
809
1373
|
let appliedPreferences;
|
|
810
1374
|
const inputPolicyId = 'policyId' in input ? input.policyId : void 0;
|
|
811
|
-
|
|
1375
|
+
const inputPolicyHash = 'policyHash' in input ? input.policyHash : void 0;
|
|
1376
|
+
if (legalDocumentConsent && legalDocumentSnapshotVerification.valid) {
|
|
1377
|
+
if (legalDocumentSnapshotVerification.payload.type !== type) throw buildLegalDocumentSnapshotHttpException('invalid');
|
|
1378
|
+
const effectiveDate = new Date(legalDocumentSnapshotVerification.payload.effectiveDate);
|
|
1379
|
+
if (Number.isNaN(effectiveDate.getTime())) throw buildLegalDocumentSnapshotHttpException('invalid');
|
|
1380
|
+
const documentPolicy = await registry.findOrCreateLegalDocumentPolicy({
|
|
1381
|
+
type,
|
|
1382
|
+
version: legalDocumentSnapshotVerification.payload.version,
|
|
1383
|
+
hash: legalDocumentSnapshotVerification.payload.hash,
|
|
1384
|
+
effectiveDate
|
|
1385
|
+
});
|
|
1386
|
+
policyId = documentPolicy.id;
|
|
1387
|
+
} else if (legalDocumentConsent) {
|
|
1388
|
+
if (!ctx.legalDocumentSnapshot?.signingKey && !inputPolicyId && !inputPolicyHash) throw buildLegalDocumentProofHttpException('Legal document consent requires policyId or policyHash when snapshot verification is disabled');
|
|
1389
|
+
if (inputPolicyId) {
|
|
1390
|
+
policyId = inputPolicyId;
|
|
1391
|
+
const policy = await registry.findConsentPolicyById(inputPolicyId);
|
|
1392
|
+
if (!policy) throw new HTTPException(404, {
|
|
1393
|
+
message: 'Policy not found',
|
|
1394
|
+
cause: {
|
|
1395
|
+
code: 'POLICY_NOT_FOUND',
|
|
1396
|
+
policyId,
|
|
1397
|
+
type
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
if (!policy.isActive) throw new HTTPException(400, {
|
|
1401
|
+
message: 'Policy is inactive',
|
|
1402
|
+
cause: {
|
|
1403
|
+
code: 'POLICY_INACTIVE',
|
|
1404
|
+
policyId,
|
|
1405
|
+
type
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
} else if (inputPolicyHash) {
|
|
1409
|
+
const policy = await registry.findLegalDocumentPolicyByHash(type, inputPolicyHash);
|
|
1410
|
+
if (!policy) throw new HTTPException(404, {
|
|
1411
|
+
message: 'Policy not found',
|
|
1412
|
+
cause: {
|
|
1413
|
+
code: 'POLICY_NOT_FOUND',
|
|
1414
|
+
type,
|
|
1415
|
+
policyHash: inputPolicyHash
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
if (!policy.isActive) throw new HTTPException(400, {
|
|
1419
|
+
message: 'Policy is inactive',
|
|
1420
|
+
cause: {
|
|
1421
|
+
code: 'POLICY_INACTIVE',
|
|
1422
|
+
policyId: policy.id,
|
|
1423
|
+
type,
|
|
1424
|
+
policyHash: inputPolicyHash
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
policyId = policy.id;
|
|
1428
|
+
}
|
|
1429
|
+
} else if (inputPolicyId) {
|
|
812
1430
|
policyId = inputPolicyId;
|
|
813
1431
|
const policy = await registry.findConsentPolicyById(inputPolicyId);
|
|
814
1432
|
if (!policy) throw new HTTPException(404, {
|
|
@@ -870,6 +1488,13 @@ const postSubjectHandler = async (c)=>{
|
|
|
870
1488
|
});
|
|
871
1489
|
purposeIds = purposes;
|
|
872
1490
|
}
|
|
1491
|
+
if (!policyId) throw new HTTPException(500, {
|
|
1492
|
+
message: 'Failed to resolve policy',
|
|
1493
|
+
cause: {
|
|
1494
|
+
code: 'POLICY_RESOLUTION_FAILED',
|
|
1495
|
+
type
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
873
1498
|
const expiryDays = effectivePolicy?.consent?.expiryDays;
|
|
874
1499
|
const validUntil = 'number' == typeof expiryDays && Number.isFinite(expiryDays) ? new Date(givenAt.getTime() + 86400000 * Math.max(0, expiryDays)) : void 0;
|
|
875
1500
|
const proofConfig = effectivePolicy?.proof;
|
|
@@ -962,7 +1587,7 @@ const postSubjectHandler = async (c)=>{
|
|
|
962
1587
|
where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
|
|
963
1588
|
})) : void 0;
|
|
964
1589
|
const consentRecord = await tx.create('consent', {
|
|
965
|
-
id: await
|
|
1590
|
+
id: await utils_generateUniqueId(tx, 'consent', ctx),
|
|
966
1591
|
subjectId: subject.id,
|
|
967
1592
|
domainId: domainRecord.id,
|
|
968
1593
|
policyId,
|
|
@@ -1033,6 +1658,12 @@ const postSubjectHandler = async (c)=>{
|
|
|
1033
1658
|
errorType: error instanceof Error ? error.constructor.name : typeof error
|
|
1034
1659
|
});
|
|
1035
1660
|
if (error instanceof HTTPException) throw error;
|
|
1661
|
+
if (error instanceof LegalDocumentPolicyConflictError) throw new HTTPException(409, {
|
|
1662
|
+
message: error.message,
|
|
1663
|
+
cause: {
|
|
1664
|
+
code: 'LEGAL_DOCUMENT_RELEASE_CONFLICT'
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1036
1667
|
throw new HTTPException(500, {
|
|
1037
1668
|
message: 'Internal server error',
|
|
1038
1669
|
cause: {
|
|
@@ -1066,7 +1697,7 @@ const createSubjectRoutes = ()=>{
|
|
|
1066
1697
|
}), validator('param', getSubjectInputSchema), getSubjectHandler);
|
|
1067
1698
|
app.post('/', describeRoute({
|
|
1068
1699
|
summary: 'Record consent for a subject',
|
|
1069
|
-
description: "Creates a new consent record (append-only). Creates the subject if it does not exist.\n\n**Request body by `type`:**\n- `cookie_banner` – Requires `preferences` object\n- `privacy_policy`, `dpa`, `terms_and_conditions` –
|
|
1700
|
+
description: "Creates a new consent record (append-only). Creates the subject if it does not exist.\n\n**Request body by `type`:**\n- `cookie_banner` – Requires `preferences` object\n- `privacy_policy`, `dpa`, `terms_and_conditions` – Prefer a signed `documentSnapshotToken`; otherwise use a release `policyHash`, with `policyId` kept only for compatibility\n- `marketing_communications`, `age_verification`, `other` – Optional `preferences`",
|
|
1070
1701
|
tags: [
|
|
1071
1702
|
'Subject',
|
|
1072
1703
|
'Consent'
|
|
@@ -1137,4 +1768,4 @@ const createSubjectRoutes = ()=>{
|
|
|
1137
1768
|
}), validator('query', listSubjectsQuerySchema), listSubjectsHandler);
|
|
1138
1769
|
return app;
|
|
1139
1770
|
};
|
|
1140
|
-
export { createConsentRoutes, createInitRoute, createStatusRoute, createSubjectRoutes };
|
|
1771
|
+
export { createConsentRoutes, createInitRoute, createLegalDocumentRoutes, createStatusRoute, createSubjectRoutes, generateUniqueId, policyRegistry };
|