@c15t/backend 2.0.0-rc.5 → 2.0.0-rc.8
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/dist/302.js +473 -0
- package/dist/583.js +540 -0
- package/dist/915.js +1742 -0
- package/dist/cache.cjs +1 -1
- package/dist/cache.js +4 -415
- package/dist/core.cjs +484 -33
- package/dist/core.js +21 -2571
- package/dist/db/adapters/drizzle.cjs +1 -1
- package/dist/db/adapters/drizzle.js +1 -2
- package/dist/db/adapters/kysely.cjs +1 -1
- package/dist/db/adapters/kysely.js +1 -2
- package/dist/db/adapters/mongo.cjs +1 -1
- package/dist/db/adapters/mongo.js +1 -2
- package/dist/db/adapters/prisma.cjs +1 -1
- package/dist/db/adapters/prisma.js +1 -2
- package/dist/db/adapters/typeorm.cjs +1 -1
- package/dist/db/adapters/typeorm.js +1 -2
- package/dist/db/adapters.cjs +1 -1
- package/dist/db/migrator.cjs +1 -1
- package/dist/db/schema.cjs +5 -1
- package/dist/db/schema.js +3 -2
- package/dist/define-config.cjs +1 -1
- package/dist/edge.cjs +9 -9
- package/dist/edge.js +6 -885
- package/dist/router.cjs +239 -57
- package/dist/router.js +1 -2058
- package/dist/types/index.cjs +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/runtime-policy-decision.d.ts +1 -1
- package/dist-types/db/registry/types.d.ts +2 -1
- package/dist-types/db/schema/1.0.0/consent.d.ts +2 -2
- package/dist-types/db/schema/2.0.0/audit-log.d.ts +2 -2
- 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 +2 -2
- package/dist-types/db/schema/2.0.0/consent.d.ts +2 -2
- package/dist-types/db/schema/2.0.0/domain.d.ts +2 -2
- 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 +2 -2
- package/dist-types/db/schema/2.0.0/subject.d.ts +2 -2
- 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/policy/snapshot.d.ts +1 -1
- 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/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 +26 -5
- 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 +13 -2
- package/docs/guides/edge-deployment.md +18 -15
- package/docs/guides/policy-packs.md +1 -1
- package/package.json +19 -19
package/dist/302.js
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { SpanKind, SpanStatusCode as api_SpanStatusCode, context, metrics, trace } from "@opentelemetry/api";
|
|
2
|
+
const version = '2.0.0-rc.8';
|
|
3
|
+
function extractErrorMessage(error) {
|
|
4
|
+
if (error instanceof AggregateError && error.errors?.length > 0) {
|
|
5
|
+
const inner = error.errors.map((e)=>e instanceof Error ? e.message : String(e)).join('; ');
|
|
6
|
+
return `AggregateError: ${inner}`;
|
|
7
|
+
}
|
|
8
|
+
if (error instanceof Error) return error.message || error.name;
|
|
9
|
+
return String(error);
|
|
10
|
+
}
|
|
11
|
+
let cachedConfig = null;
|
|
12
|
+
let cachedDefaultAttributes = {};
|
|
13
|
+
function createTelemetryOptions(appName = 'c15t', telemetryConfig, tenantId) {
|
|
14
|
+
const defaultAttributes = {
|
|
15
|
+
...telemetryConfig?.defaultAttributes || {},
|
|
16
|
+
'service.name': String(appName),
|
|
17
|
+
'service.version': version
|
|
18
|
+
};
|
|
19
|
+
if (tenantId) defaultAttributes['tenant.id'] = tenantId;
|
|
20
|
+
const config = {
|
|
21
|
+
enabled: telemetryConfig?.enabled ?? false,
|
|
22
|
+
tracer: telemetryConfig?.tracer,
|
|
23
|
+
meter: telemetryConfig?.meter,
|
|
24
|
+
defaultAttributes
|
|
25
|
+
};
|
|
26
|
+
cachedConfig = config;
|
|
27
|
+
cachedDefaultAttributes = defaultAttributes;
|
|
28
|
+
return config;
|
|
29
|
+
}
|
|
30
|
+
function isTelemetryEnabled(options) {
|
|
31
|
+
if (options) return options.telemetry?.enabled === true;
|
|
32
|
+
return cachedConfig?.enabled === true;
|
|
33
|
+
}
|
|
34
|
+
const getTracer = (options)=>{
|
|
35
|
+
if (!isTelemetryEnabled(options)) return trace.getTracer('c15t-noop');
|
|
36
|
+
const tracer = options?.telemetry?.tracer ?? cachedConfig?.tracer;
|
|
37
|
+
if (tracer) return tracer;
|
|
38
|
+
return trace.getTracer(options?.appName ?? 'c15t');
|
|
39
|
+
};
|
|
40
|
+
const getMeter = (options)=>{
|
|
41
|
+
if (!isTelemetryEnabled(options)) return metrics.getMeter('c15t-noop');
|
|
42
|
+
const meter = options?.telemetry?.meter ?? cachedConfig?.meter;
|
|
43
|
+
if (meter) return meter;
|
|
44
|
+
return metrics.getMeter(options?.appName ?? 'c15t');
|
|
45
|
+
};
|
|
46
|
+
function getDefaultAttributes() {
|
|
47
|
+
return cachedDefaultAttributes;
|
|
48
|
+
}
|
|
49
|
+
const createRequestSpan = (method, path, options)=>{
|
|
50
|
+
if (!isTelemetryEnabled(options)) return null;
|
|
51
|
+
const tracer = getTracer(options);
|
|
52
|
+
const defaultAttrs = options?.telemetry?.defaultAttributes || getDefaultAttributes();
|
|
53
|
+
const span = tracer.startSpan(`${method} ${path}`, {
|
|
54
|
+
attributes: {
|
|
55
|
+
'http.method': method,
|
|
56
|
+
...defaultAttrs
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return span;
|
|
60
|
+
};
|
|
61
|
+
const handleSpanError = (span, error)=>{
|
|
62
|
+
span.setStatus({
|
|
63
|
+
code: api_SpanStatusCode.ERROR,
|
|
64
|
+
message: extractErrorMessage(error)
|
|
65
|
+
});
|
|
66
|
+
if (error instanceof Error) span.setAttribute('error.type', error.name);
|
|
67
|
+
};
|
|
68
|
+
function getTraceContext() {
|
|
69
|
+
const activeSpan = trace.getActiveSpan();
|
|
70
|
+
if (!activeSpan) return null;
|
|
71
|
+
const spanContext = activeSpan.spanContext();
|
|
72
|
+
if (!spanContext) return null;
|
|
73
|
+
return {
|
|
74
|
+
traceId: spanContext.traceId,
|
|
75
|
+
spanId: spanContext.spanId
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const withSpanContext = async (span, operation)=>context["with"](trace.setSpan(context.active(), span), operation);
|
|
79
|
+
async function executeWithSpan(span, operation) {
|
|
80
|
+
try {
|
|
81
|
+
const result = await withSpanContext(span, operation);
|
|
82
|
+
span.setStatus({
|
|
83
|
+
code: api_SpanStatusCode.OK
|
|
84
|
+
});
|
|
85
|
+
return result;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
handleSpanError(span, error);
|
|
88
|
+
throw error;
|
|
89
|
+
} finally{
|
|
90
|
+
span.end();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function resolveDefaultAttributes(options) {
|
|
94
|
+
return options?.telemetry?.defaultAttributes || getDefaultAttributes();
|
|
95
|
+
}
|
|
96
|
+
async function withDatabaseSpan(attributes, operation, options) {
|
|
97
|
+
if (!isTelemetryEnabled(options)) return operation();
|
|
98
|
+
const tracer = getTracer(options);
|
|
99
|
+
const spanName = `db.${attributes.entity}.${attributes.operation}`;
|
|
100
|
+
const span = tracer.startSpan(spanName, {
|
|
101
|
+
kind: SpanKind.CLIENT,
|
|
102
|
+
attributes: {
|
|
103
|
+
'db.system': 'c15t',
|
|
104
|
+
'db.operation': attributes.operation,
|
|
105
|
+
'db.entity': attributes.entity,
|
|
106
|
+
...resolveDefaultAttributes(options),
|
|
107
|
+
...Object.fromEntries(Object.entries(attributes).filter(([key])=>![
|
|
108
|
+
'operation',
|
|
109
|
+
'entity'
|
|
110
|
+
].includes(key)))
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return executeWithSpan(span, operation);
|
|
114
|
+
}
|
|
115
|
+
async function withExternalSpan(attributes, operation, options) {
|
|
116
|
+
if (!isTelemetryEnabled(options)) return operation();
|
|
117
|
+
const tracer = getTracer(options);
|
|
118
|
+
const url = new URL(attributes.url);
|
|
119
|
+
const spanName = `HTTP ${attributes.method} ${url.hostname}`;
|
|
120
|
+
const span = tracer.startSpan(spanName, {
|
|
121
|
+
kind: SpanKind.CLIENT,
|
|
122
|
+
attributes: {
|
|
123
|
+
'http.method': attributes.method,
|
|
124
|
+
'http.url': `${url.origin}${url.pathname}`,
|
|
125
|
+
'http.host': url.hostname,
|
|
126
|
+
...resolveDefaultAttributes(options),
|
|
127
|
+
...Object.fromEntries(Object.entries(attributes).filter(([key])=>![
|
|
128
|
+
'url',
|
|
129
|
+
'method'
|
|
130
|
+
].includes(key)))
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return executeWithSpan(span, operation);
|
|
134
|
+
}
|
|
135
|
+
async function withCacheSpan(operation, layer, fn, options) {
|
|
136
|
+
if (!isTelemetryEnabled(options)) return fn();
|
|
137
|
+
const tracer = getTracer(options);
|
|
138
|
+
const spanName = `cache.${layer}.${operation}`;
|
|
139
|
+
const span = tracer.startSpan(spanName, {
|
|
140
|
+
kind: SpanKind.CLIENT,
|
|
141
|
+
attributes: {
|
|
142
|
+
'cache.operation': operation,
|
|
143
|
+
'cache.layer': layer,
|
|
144
|
+
...resolveDefaultAttributes(options)
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return executeWithSpan(span, fn);
|
|
148
|
+
}
|
|
149
|
+
function sanitizeAttributes(attrs) {
|
|
150
|
+
return Object.fromEntries(Object.entries(attrs).filter(([_, v])=>null != v));
|
|
151
|
+
}
|
|
152
|
+
function createMetrics(meter) {
|
|
153
|
+
const consentCreated = meter.createCounter('c15t.consent.created', {
|
|
154
|
+
description: 'Number of consent submissions',
|
|
155
|
+
unit: '1'
|
|
156
|
+
});
|
|
157
|
+
const consentAccepted = meter.createCounter('c15t.consent.accepted', {
|
|
158
|
+
description: 'Number of consents accepted',
|
|
159
|
+
unit: '1'
|
|
160
|
+
});
|
|
161
|
+
const consentRejected = meter.createCounter('c15t.consent.rejected', {
|
|
162
|
+
description: 'Number of consents rejected',
|
|
163
|
+
unit: '1'
|
|
164
|
+
});
|
|
165
|
+
const subjectCreated = meter.createCounter('c15t.subject.created', {
|
|
166
|
+
description: 'Number of new subjects created',
|
|
167
|
+
unit: '1'
|
|
168
|
+
});
|
|
169
|
+
const subjectLinked = meter.createCounter('c15t.subject.linked', {
|
|
170
|
+
description: 'Number of subjects linked to external ID',
|
|
171
|
+
unit: '1'
|
|
172
|
+
});
|
|
173
|
+
const consentCheckCount = meter.createCounter('c15t.consent_check.count', {
|
|
174
|
+
description: 'Number of cross-device consent checks',
|
|
175
|
+
unit: '1'
|
|
176
|
+
});
|
|
177
|
+
const initCount = meter.createCounter('c15t.init.count', {
|
|
178
|
+
description: 'Number of init endpoint calls',
|
|
179
|
+
unit: '1'
|
|
180
|
+
});
|
|
181
|
+
const httpRequestDuration = meter.createHistogram('c15t.http.request.duration', {
|
|
182
|
+
description: 'HTTP request latency',
|
|
183
|
+
unit: 'ms'
|
|
184
|
+
});
|
|
185
|
+
const httpRequestCount = meter.createCounter('c15t.http.request.count', {
|
|
186
|
+
description: 'Number of HTTP requests',
|
|
187
|
+
unit: '1'
|
|
188
|
+
});
|
|
189
|
+
const httpErrorCount = meter.createCounter('c15t.http.error.count', {
|
|
190
|
+
description: 'Number of HTTP errors',
|
|
191
|
+
unit: '1'
|
|
192
|
+
});
|
|
193
|
+
const dbQueryDuration = meter.createHistogram('c15t.db.query.duration', {
|
|
194
|
+
description: 'Database query latency',
|
|
195
|
+
unit: 'ms'
|
|
196
|
+
});
|
|
197
|
+
const dbQueryCount = meter.createCounter('c15t.db.query.count', {
|
|
198
|
+
description: 'Number of database queries',
|
|
199
|
+
unit: '1'
|
|
200
|
+
});
|
|
201
|
+
const dbErrorCount = meter.createCounter('c15t.db.error.count', {
|
|
202
|
+
description: 'Number of database errors',
|
|
203
|
+
unit: '1'
|
|
204
|
+
});
|
|
205
|
+
const cacheHit = meter.createCounter('c15t.cache.hit', {
|
|
206
|
+
description: 'Number of cache hits',
|
|
207
|
+
unit: '1'
|
|
208
|
+
});
|
|
209
|
+
const cacheMiss = meter.createCounter('c15t.cache.miss', {
|
|
210
|
+
description: 'Number of cache misses',
|
|
211
|
+
unit: '1'
|
|
212
|
+
});
|
|
213
|
+
const cacheLatency = meter.createHistogram('c15t.cache.latency', {
|
|
214
|
+
description: 'Cache operation latency',
|
|
215
|
+
unit: 'ms'
|
|
216
|
+
});
|
|
217
|
+
const gvlFetchDuration = meter.createHistogram('c15t.gvl.fetch.duration', {
|
|
218
|
+
description: 'GVL fetch latency',
|
|
219
|
+
unit: 'ms'
|
|
220
|
+
});
|
|
221
|
+
const gvlFetchCount = meter.createCounter('c15t.gvl.fetch.count', {
|
|
222
|
+
description: 'Number of GVL fetches',
|
|
223
|
+
unit: '1'
|
|
224
|
+
});
|
|
225
|
+
const gvlFetchError = meter.createCounter('c15t.gvl.fetch.error', {
|
|
226
|
+
description: 'Number of GVL fetch errors',
|
|
227
|
+
unit: '1'
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
consentCreated,
|
|
231
|
+
consentAccepted,
|
|
232
|
+
consentRejected,
|
|
233
|
+
subjectCreated,
|
|
234
|
+
subjectLinked,
|
|
235
|
+
consentCheckCount,
|
|
236
|
+
initCount,
|
|
237
|
+
httpRequestDuration,
|
|
238
|
+
httpRequestCount,
|
|
239
|
+
httpErrorCount,
|
|
240
|
+
dbQueryDuration,
|
|
241
|
+
dbQueryCount,
|
|
242
|
+
dbErrorCount,
|
|
243
|
+
cacheHit,
|
|
244
|
+
cacheMiss,
|
|
245
|
+
cacheLatency,
|
|
246
|
+
gvlFetchDuration,
|
|
247
|
+
gvlFetchCount,
|
|
248
|
+
gvlFetchError,
|
|
249
|
+
recordConsentCreated (attributes) {
|
|
250
|
+
consentCreated.add(1, sanitizeAttributes(attributes));
|
|
251
|
+
},
|
|
252
|
+
recordConsentAccepted (attributes) {
|
|
253
|
+
consentAccepted.add(1, sanitizeAttributes(attributes));
|
|
254
|
+
},
|
|
255
|
+
recordConsentRejected (attributes) {
|
|
256
|
+
consentRejected.add(1, sanitizeAttributes(attributes));
|
|
257
|
+
},
|
|
258
|
+
recordSubjectCreated (attributes) {
|
|
259
|
+
subjectCreated.add(1, sanitizeAttributes(attributes));
|
|
260
|
+
},
|
|
261
|
+
recordSubjectLinked (identityProvider) {
|
|
262
|
+
subjectLinked.add(1, {
|
|
263
|
+
identityProvider: identityProvider || 'unknown'
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
recordConsentCheck (type, found) {
|
|
267
|
+
consentCheckCount.add(1, {
|
|
268
|
+
type,
|
|
269
|
+
found: String(found)
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
recordInit (attributes) {
|
|
273
|
+
initCount.add(1, sanitizeAttributes(attributes));
|
|
274
|
+
},
|
|
275
|
+
recordHttpRequest (attributes, durationMs) {
|
|
276
|
+
const attrs = sanitizeAttributes(attributes);
|
|
277
|
+
httpRequestCount.add(1, attrs);
|
|
278
|
+
httpRequestDuration.record(durationMs, attrs);
|
|
279
|
+
if (attributes.status >= 400) httpErrorCount.add(1, attrs);
|
|
280
|
+
},
|
|
281
|
+
recordDbQuery (attributes, durationMs) {
|
|
282
|
+
const attrs = sanitizeAttributes(attributes);
|
|
283
|
+
dbQueryCount.add(1, attrs);
|
|
284
|
+
dbQueryDuration.record(durationMs, attrs);
|
|
285
|
+
},
|
|
286
|
+
recordDbError (attributes) {
|
|
287
|
+
dbErrorCount.add(1, sanitizeAttributes(attributes));
|
|
288
|
+
},
|
|
289
|
+
recordCacheHit (layer) {
|
|
290
|
+
cacheHit.add(1, {
|
|
291
|
+
layer
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
recordCacheMiss (layer) {
|
|
295
|
+
cacheMiss.add(1, {
|
|
296
|
+
layer
|
|
297
|
+
});
|
|
298
|
+
},
|
|
299
|
+
recordCacheLatency (attributes, durationMs) {
|
|
300
|
+
cacheLatency.record(durationMs, sanitizeAttributes(attributes));
|
|
301
|
+
},
|
|
302
|
+
recordGvlFetch (attributes, durationMs) {
|
|
303
|
+
const attrs = sanitizeAttributes(attributes);
|
|
304
|
+
gvlFetchCount.add(1, attrs);
|
|
305
|
+
gvlFetchDuration.record(durationMs, attrs);
|
|
306
|
+
},
|
|
307
|
+
recordGvlError (attributes) {
|
|
308
|
+
gvlFetchError.add(1, sanitizeAttributes(attributes));
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
let metricsInstance = null;
|
|
313
|
+
function getMetrics(options) {
|
|
314
|
+
if (metricsInstance) return metricsInstance;
|
|
315
|
+
if (!isTelemetryEnabled(options)) return null;
|
|
316
|
+
metricsInstance = createMetrics(getMeter(options));
|
|
317
|
+
return metricsInstance;
|
|
318
|
+
}
|
|
319
|
+
const GVL_TTL_MS = 259200000;
|
|
320
|
+
const memory_memoryCache = new Map();
|
|
321
|
+
function createMemoryCacheAdapter() {
|
|
322
|
+
return {
|
|
323
|
+
async get (key) {
|
|
324
|
+
const entry = memory_memoryCache.get(key);
|
|
325
|
+
if (!entry) return null;
|
|
326
|
+
if (Date.now() > entry.expiresAt) {
|
|
327
|
+
memory_memoryCache.delete(key);
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return entry.value;
|
|
331
|
+
},
|
|
332
|
+
async set (key, value, ttlMs = 300000) {
|
|
333
|
+
memory_memoryCache.set(key, {
|
|
334
|
+
value,
|
|
335
|
+
expiresAt: Date.now() + ttlMs
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
async delete (key) {
|
|
339
|
+
memory_memoryCache.delete(key);
|
|
340
|
+
},
|
|
341
|
+
async has (key) {
|
|
342
|
+
const entry = memory_memoryCache.get(key);
|
|
343
|
+
if (!entry) return false;
|
|
344
|
+
if (Date.now() > entry.expiresAt) {
|
|
345
|
+
memory_memoryCache.delete(key);
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function clearMemoryCache() {
|
|
353
|
+
memory_memoryCache.clear();
|
|
354
|
+
}
|
|
355
|
+
function getMemoryCacheSize() {
|
|
356
|
+
return memory_memoryCache.size;
|
|
357
|
+
}
|
|
358
|
+
function createGVLCacheKey(appName, language, vendorIds) {
|
|
359
|
+
const sortedIds = vendorIds ? [
|
|
360
|
+
...vendorIds
|
|
361
|
+
].sort((a, b)=>a - b).join(',') : 'all';
|
|
362
|
+
return `${appName}:gvl:${language}:${sortedIds}`;
|
|
363
|
+
}
|
|
364
|
+
function createCacheKey(appName, namespace, ...parts) {
|
|
365
|
+
const allParts = [
|
|
366
|
+
appName,
|
|
367
|
+
namespace,
|
|
368
|
+
...parts
|
|
369
|
+
];
|
|
370
|
+
return allParts.join(':');
|
|
371
|
+
}
|
|
372
|
+
const GVL_ENDPOINT = 'https://gvl.consent.io';
|
|
373
|
+
const inflightRequests = new Map();
|
|
374
|
+
async function fetchGVLWithLanguage(language, vendorIds, endpoint = GVL_ENDPOINT) {
|
|
375
|
+
const sortedVendorIds = vendorIds ? [
|
|
376
|
+
...vendorIds
|
|
377
|
+
].sort((a, b)=>a - b) : [];
|
|
378
|
+
const dedupeKey = `${endpoint}|${language}|${sortedVendorIds.join(',')}`;
|
|
379
|
+
const existingRequest = inflightRequests.get(dedupeKey);
|
|
380
|
+
if (existingRequest) return existingRequest;
|
|
381
|
+
const url = new URL(endpoint);
|
|
382
|
+
if (sortedVendorIds.length > 0) url.searchParams.set('vendorIds', sortedVendorIds.join(','));
|
|
383
|
+
const promise = (async ()=>{
|
|
384
|
+
const fetchStart = Date.now();
|
|
385
|
+
try {
|
|
386
|
+
const gvl = await withExternalSpan({
|
|
387
|
+
url: url.toString(),
|
|
388
|
+
method: 'GET'
|
|
389
|
+
}, async ()=>{
|
|
390
|
+
const response = await fetch(url.toString(), {
|
|
391
|
+
headers: {
|
|
392
|
+
'Accept-Language': language
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
if (204 === response.status) return null;
|
|
396
|
+
if (!response.ok) throw new Error(`Failed to fetch GVL: ${response.status} ${response.statusText}`);
|
|
397
|
+
const text = await response.text();
|
|
398
|
+
const trimmed = text.trim().replace(/^\uFEFF/, '');
|
|
399
|
+
let parsed;
|
|
400
|
+
try {
|
|
401
|
+
parsed = JSON.parse(trimmed);
|
|
402
|
+
} catch {
|
|
403
|
+
let depth = 0;
|
|
404
|
+
let end = -1;
|
|
405
|
+
const start = trimmed.indexOf('{');
|
|
406
|
+
if (start >= 0) for(let i = start; i < trimmed.length; i++){
|
|
407
|
+
const c = trimmed[i];
|
|
408
|
+
if ('{' === c) depth++;
|
|
409
|
+
else if ('}' === c) {
|
|
410
|
+
depth--;
|
|
411
|
+
if (0 === depth) {
|
|
412
|
+
end = i + 1;
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (end > 0) parsed = JSON.parse(trimmed.slice(0, end));
|
|
418
|
+
else throw new SyntaxError('Invalid GVL response: not valid JSON');
|
|
419
|
+
}
|
|
420
|
+
if (!parsed.vendorListVersion || !parsed.purposes || !parsed.vendors) throw new Error('Invalid GVL response: missing required fields');
|
|
421
|
+
return parsed;
|
|
422
|
+
});
|
|
423
|
+
getMetrics()?.recordGvlFetch({
|
|
424
|
+
language,
|
|
425
|
+
source: 'fetch',
|
|
426
|
+
status: 200
|
|
427
|
+
}, Date.now() - fetchStart);
|
|
428
|
+
return gvl;
|
|
429
|
+
} catch (error) {
|
|
430
|
+
getMetrics()?.recordGvlError({
|
|
431
|
+
language,
|
|
432
|
+
errorType: error instanceof Error ? error.name : 'UnknownError'
|
|
433
|
+
});
|
|
434
|
+
throw error;
|
|
435
|
+
} finally{
|
|
436
|
+
inflightRequests.delete(dedupeKey);
|
|
437
|
+
}
|
|
438
|
+
})();
|
|
439
|
+
inflightRequests.set(dedupeKey, promise);
|
|
440
|
+
return promise;
|
|
441
|
+
}
|
|
442
|
+
function createGVLResolver(options) {
|
|
443
|
+
const { appName, bundled, cacheAdapter, vendorIds, endpoint } = options;
|
|
444
|
+
const memoryCache = createMemoryCacheAdapter();
|
|
445
|
+
return {
|
|
446
|
+
async get (language) {
|
|
447
|
+
const cacheKey = createGVLCacheKey(appName, language, vendorIds);
|
|
448
|
+
if (bundled?.[language]) return bundled[language];
|
|
449
|
+
const memoryHit = await withCacheSpan('get', 'memory', ()=>memoryCache.get(cacheKey));
|
|
450
|
+
if (memoryHit) {
|
|
451
|
+
getMetrics()?.recordCacheHit('memory');
|
|
452
|
+
return memoryHit;
|
|
453
|
+
}
|
|
454
|
+
getMetrics()?.recordCacheMiss('memory');
|
|
455
|
+
if (cacheAdapter) {
|
|
456
|
+
const externalHit = await withCacheSpan('get', 'external', ()=>cacheAdapter.get(cacheKey));
|
|
457
|
+
if (externalHit) {
|
|
458
|
+
getMetrics()?.recordCacheHit('external');
|
|
459
|
+
await withCacheSpan('set', 'memory', ()=>memoryCache.set(cacheKey, externalHit, 300000));
|
|
460
|
+
return externalHit;
|
|
461
|
+
}
|
|
462
|
+
getMetrics()?.recordCacheMiss('external');
|
|
463
|
+
}
|
|
464
|
+
const gvl = await fetchGVLWithLanguage(language, vendorIds, endpoint);
|
|
465
|
+
if (gvl) {
|
|
466
|
+
await withCacheSpan('set', 'memory', ()=>memoryCache.set(cacheKey, gvl, 300000));
|
|
467
|
+
if (cacheAdapter) await withCacheSpan('set', 'external', ()=>cacheAdapter.set(cacheKey, gvl, GVL_TTL_MS));
|
|
468
|
+
}
|
|
469
|
+
return gvl;
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
export { GVL_TTL_MS, clearMemoryCache, createCacheKey, createGVLCacheKey, createGVLResolver, createMemoryCacheAdapter, createRequestSpan, createTelemetryOptions, extractErrorMessage, getMemoryCacheSize, getMetrics, getTraceContext, handleSpanError, isTelemetryEnabled, version, withDatabaseSpan, withSpanContext };
|