@c15t/backend 2.1.0 → 2.2.0-canary-20260728085441
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/AGENTS.md +20 -0
- package/README.md +25 -12
- package/dist/{583.js → 261.js} +1 -1
- package/dist/{915.js → 49.js} +234 -133
- package/dist/{302.js → 744.js} +4 -3
- package/dist/cache.cjs +17 -13
- package/dist/cache.js +2 -2
- package/dist/core.cjs +255 -148
- package/dist/core.js +9 -8
- package/dist/db/adapters/drizzle.cjs +9 -5
- package/dist/db/adapters/kysely.cjs +9 -5
- package/dist/db/adapters/mongo.cjs +9 -5
- package/dist/db/adapters/prisma.cjs +9 -5
- package/dist/db/adapters/typeorm.cjs +9 -5
- package/dist/db/adapters.js +1 -0
- package/dist/db/migrator.cjs +12 -8
- package/dist/db/schema.cjs +24 -17
- package/dist/db/schema.js +2 -2
- package/dist/define-config.cjs +12 -8
- package/dist/edge.cjs +59 -18
- package/dist/edge.js +49 -12
- package/dist/router.cjs +261 -134
- package/dist/router.js +1 -1
- package/dist/types/index.cjs +9 -5
- package/dist-types/db/registry/index.d.ts +35 -35
- package/dist-types/db/registry/utils/generate-id.d.ts +32 -0
- package/dist-types/db/schema/1.0.0/index.d.ts +36 -1096
- package/dist-types/db/schema/2.0.0/consent-policy.d.ts +2 -2
- package/dist-types/db/schema/2.0.0/index.d.ts +16 -599
- package/dist-types/db/schema/index.d.ts +253 -2479
- package/dist-types/handlers/init/index.d.ts +1 -1
- package/dist-types/handlers/subject/consent-idempotency.d.ts +87 -0
- package/dist-types/handlers/subject/consent-time.d.ts +12 -0
- package/dist-types/handlers/subject/get.handler.d.ts +1 -3
- package/dist-types/handlers/subject/list.handler.d.ts +1 -3
- package/dist-types/handlers/subject/post.handler.d.ts +5 -1
- package/dist-types/middleware/cors/is-origin-trusted.d.ts +0 -11
- package/dist-types/middleware/openapi/config.d.ts +6 -6
- package/dist-types/version.d.ts +1 -1
- package/docs/README.md +15 -44
- package/docs/self-host/api/configuration.md +98 -0
- package/docs/{api → self-host/api}/endpoints.md +3 -2
- package/docs/{guides → self-host/guides}/caching.md +3 -1
- package/docs/{guides → self-host/guides}/database-setup.md +2 -6
- package/docs/{guides → self-host/guides}/edge-deployment.md +3 -2
- package/docs/{guides → self-host/guides}/framework-integration.md +1 -0
- package/docs/{guides → self-host/guides}/iab-tcf.md +6 -4
- package/docs/{guides → self-host/guides}/observability.md +1 -0
- package/docs/{guides → self-host/guides}/policy-packs.md +3 -1
- package/docs/{quickstart.md → self-host/quickstart.md} +10 -21
- package/package.json +24 -19
- package/readme.json +13 -7
- package/docs/api/configuration.md +0 -208
- /package/dist/{942.js → 292.js} +0 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @c15t/backend
|
|
2
|
+
|
|
3
|
+
> Self-hosted c15t backend docs for configuration, APIs, storage, policy packs, and operations.
|
|
4
|
+
|
|
5
|
+
These docs ship inside the package so coding agents can read them offline. Open the topic file you need from the list below — paths are relative to this file.
|
|
6
|
+
|
|
7
|
+
## Self Host
|
|
8
|
+
|
|
9
|
+
Run the c15t backend, configure storage, and operate consent infrastructure.
|
|
10
|
+
|
|
11
|
+
- [Configuration Reference](./docs/self-host/api/configuration.md): Complete reference for all c15t backend configuration options.
|
|
12
|
+
- [API Endpoints](./docs/self-host/api/endpoints.md): Full reference for every c15t consent backend endpoint.
|
|
13
|
+
- [Caching](./docs/self-host/guides/caching.md): Add a caching layer to your self-hosted c15t backend for production performance.
|
|
14
|
+
- [Database Setup](./docs/self-host/guides/database-setup.md): Configure a database adapter for your self-hosted c15t backend.
|
|
15
|
+
- [Edge Deployment](./docs/self-host/guides/edge-deployment.md): Run consent policy resolution at the edge for faster initial banner loads.
|
|
16
|
+
- [Framework Integration](./docs/self-host/guides/framework-integration.md): Mount the c15t consent backend in any JavaScript framework or runtime.
|
|
17
|
+
- [IAB TCF Support](./docs/self-host/guides/iab-tcf.md): Enable IAB Transparency and Consent Framework (TCF) support in your self-hosted backend.
|
|
18
|
+
- [Observability](./docs/self-host/guides/observability.md): Add logging, metrics, and tracing to your self-hosted c15t backend.
|
|
19
|
+
- [Policy Packs](./docs/self-host/guides/policy-packs.md): Configure regional policy packs on the backend and return deterministic runtime policy decisions from /init.
|
|
20
|
+
- [Quickstart](./docs/self-host/quickstart.md): Self-host the c15t consent management backend in your own infrastructure.
|
package/README.md
CHANGED
|
@@ -9,25 +9,27 @@
|
|
|
9
9
|
|
|
10
10
|
# @c15t/backend: Consent Management Backend
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
<p>
|
|
13
|
+
<a href="https://www.npmjs.com/package/@c15t/backend"><picture><source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/npm/%40c15t%2Fbackend.svg?variant=outline&mode=dark"><img src="https://shieldcn.dev/npm/%40c15t%2Fbackend.svg?variant=outline&mode=light" alt="Latest NPM Version"></picture></a>
|
|
14
|
+
<a href="https://github.com/c15t/c15t"><picture><source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/github/c15t/c15t/stars.svg?variant=outline&mode=dark"><img src="https://shieldcn.dev/github/c15t/c15t/stars.svg?variant=outline&mode=light" alt="Stars"></picture></a>
|
|
15
|
+
<a href="https://github.com/c15t/c15t/blob/main/LICENSE.md"><picture><source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/github/c15t/c15t/license.svg?variant=outline&mode=dark"><img src="https://shieldcn.dev/github/c15t/c15t/license.svg?variant=outline&mode=light" alt="License"></picture></a>
|
|
16
|
+
<a href="https://c15t.link/discord"><picture><source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/discord/1312171102268690493.svg?variant=outline&mode=dark"><img src="https://shieldcn.dev/discord/1312171102268690493.svg?variant=outline&mode=light" alt="Discord"></picture></a>
|
|
17
|
+
<a href="https://skills.sh/c15t/skills/c15t"><picture><source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/skills/c15t/skills/c15t.svg?variant=outline&mode=dark"><img src="https://shieldcn.dev/skills/c15t/skills/c15t.svg?variant=outline&mode=light" alt="Skills"></picture></a>
|
|
18
|
+
<a href="https://inth.com?utm_source=npm&utm_medium=readme&utm_campaign=oss_readme&utm_content=%40c15t%2Fbackend"><picture><source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/badge/Made%20By-Inth-ffc803.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAzOTMgNDAwIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTgyLjY2MiAwdjM2Ljg5NWgtNTkuMDMxdjgyLjczM2g1OS4wMzF2MzYuODkzSDI3LjQ4MnYtMzYuODkzaDU5LjAzVjM2Ljg5NWgtNTkuMDNWMHpNMzIxLjk0MSA4OS44NVYwaDM1LjM1NXYxNTYuNTIxaC0yNS43MTNsLTg2LjEzNy05MC4zNjR2OTAuMzY0aC0zNS4zNTVWMGgyNi4zNTV6Ii8%2BPHBhdGggZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzE4LjU3MSAxODUuNzE0aDc0LjI4NlY0MDBIMFYxODUuNzE0aDI3Mi44NTd2LTQ3LjE0M3ptLTI5MS4wOSAyOC45Njl2MzcuMTE4aDU4LjEzN3YxMTkuNjI4aDM2Ljg5NVYyNTEuODAxaDU4LjU4NHYtMzcuMTE4em0xODIuNjEuMjI0djE1Ni41MjJoMzYuODk0VjMxMy41OWg3My4zNDF2NTcuODM5aDM3LjExOFYyMTQuOTA3aC0zNy4xMTh2NjEuNzg4aC03My4zNDF2LTYxLjc4OHoiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg%3D%3D&color=ffc803&labelTextColor=000000&valueColor=000000&mode=dark"><img src="https://shieldcn.dev/badge/Made%20By-Inth-ffc803.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAzOTMgNDAwIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTgyLjY2MiAwdjM2Ljg5NWgtNTkuMDMxdjgyLjczM2g1OS4wMzF2MzYuODkzSDI3LjQ4MnYtMzYuODkzaDU5LjAzVjM2Ljg5NWgtNTkuMDNWMHpNMzIxLjk0MSA4OS44NVYwaDM1LjM1NXYxNTYuNTIxaC0yNS43MTNsLTg2LjEzNy05MC4zNjR2OTAuMzY0aC0zNS4zNTVWMGgyNi4zNTV6Ii8%2BPHBhdGggZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzE4LjU3MSAxODUuNzE0aDc0LjI4NlY0MDBIMFYxODUuNzE0aDI3Mi44NTd2LTQ3LjE0M3ptLTI5MS4wOSAyOC45Njl2MzcuMTE4aDU4LjEzN3YxMTkuNjI4aDM2Ljg5NVYyNTEuODAxaDU4LjU4NHYtMzcuMTE4em0xODIuNjEuMjI0djE1Ni41MjJoMzYuODk0VjMxMy41OWg3My4zNDF2NTcuODM5aDM3LjExOFYyMTQuOTA3aC0zNy4xMTh2NjEuNzg4aC03My4zNDF2LTYxLjc4OHoiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg%3D%3D&color=ffc803&labelTextColor=000000&valueColor=000000&mode=light" alt="Made by Inth"></picture></a>
|
|
19
|
+
</p>
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Self-hostable consent management backend for c15t. Powers cookie banners, consent managers, preference centers, policy resolution, audit logs, and consent records.
|
|
22
22
|
|
|
23
23
|
## Key Features
|
|
24
24
|
|
|
25
|
-
-
|
|
26
|
-
-
|
|
25
|
+
- Self-hosted CMP backend: Run c15t on your own infrastructure with your own database
|
|
26
|
+
- Consent Management: Track and manage user consent preferences and consent records
|
|
27
|
+
- Geo-Location: Identify a user's location to show relevant consent preferences
|
|
27
28
|
- Server-Side Translation: Reduce banner bundle size by translating consent preferences on the server
|
|
28
29
|
- Audit Logging: Comprehensive logging of all consent-related actions
|
|
29
30
|
- Domain Management: Handle multiple domains and subdomains
|
|
30
31
|
- Policy Management: Version and manage consent policies
|
|
32
|
+
- IAB TCF 2.3 support: Configure GVL, CMP registration, and custom vendors
|
|
31
33
|
|
|
32
34
|
## Prerequisites
|
|
33
35
|
|
|
@@ -48,7 +50,18 @@ Configure your database adapter (supports Drizzle, Kysely, Prisma, TypeORM, and
|
|
|
48
50
|
|
|
49
51
|
## Documentation
|
|
50
52
|
|
|
51
|
-
For further information, guides, and examples visit the [reference documentation](https://c15t.com/docs/self-host/
|
|
53
|
+
For further information, guides, and examples visit the [reference documentation](https://c15t.com/docs/self-host/quickstart).
|
|
54
|
+
|
|
55
|
+
## Hosted or Self-Hosted
|
|
56
|
+
|
|
57
|
+
Use [inth.com](https://inth.com) when you want a hosted c15t backend with no infrastructure to maintain. Use @c15t/backend when you need full control over hosting, database, CORS origins, policy configuration, audit logs, and deployment topology.
|
|
58
|
+
|
|
59
|
+
## Related Packages
|
|
60
|
+
|
|
61
|
+
- [c15t](https://www.npmjs.com/package/c15t): Headless JavaScript consent engine
|
|
62
|
+
- [@c15t/react](https://www.npmjs.com/package/@c15t/react): React consent UI and headless hooks
|
|
63
|
+
- [@c15t/nextjs](https://www.npmjs.com/package/@c15t/nextjs): Next.js App Router and Pages Router integration
|
|
64
|
+
- [@c15t/node-sdk](https://www.npmjs.com/package/@c15t/node-sdk): Type-safe Node.js API client
|
|
52
65
|
|
|
53
66
|
## Support
|
|
54
67
|
|
package/dist/{583.js → 261.js}
RENAMED
|
@@ -2,7 +2,7 @@ import { inspectPolicies, resolvePolicyDecision, resolvePolicySync, validatePoli
|
|
|
2
2
|
import { deepMergeTranslations, selectLanguage } from "@c15t/translations";
|
|
3
3
|
import { baseTranslations } from "@c15t/translations/all";
|
|
4
4
|
import { SignJWT, errors, jwtVerify } from "jose";
|
|
5
|
-
import { createGVLResolver, getMetrics } from "./
|
|
5
|
+
import { createGVLResolver, getMetrics } from "./744.js";
|
|
6
6
|
const DEFAULT_PROFILE = 'default';
|
|
7
7
|
const warnedKeys = new Set();
|
|
8
8
|
function isSupportedBaseLanguage(lang) {
|
package/dist/{915.js → 49.js}
RENAMED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { hashSha256Hex } from "@c15t/schema/types";
|
|
2
2
|
import base_x from "base-x";
|
|
3
|
-
import { checkConsentOutputSchema, checkConsentQuerySchema,
|
|
3
|
+
import { checkConsentOutputSchema, checkConsentQuerySchema, getSubjectOutputSchema, getSubjectParamsSchema, getSubjectQuerySchema, initOutputSchema, isLegalDocumentType, legalDocumentCurrentInputSchema, legalDocumentCurrentOutputSchema, legalDocumentCurrentParamsSchema, listSubjectsOutputSchema, listSubjectsQuerySchema, patchSubjectInputSchema, patchSubjectOutputSchema, patchSubjectParamsSchema, postSubjectInputSchema, postSubjectOutputSchema, statusOutputSchema } from "@c15t/schema";
|
|
4
4
|
import { Hono } from "hono";
|
|
5
5
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
6
6
|
import { HTTPException } from "hono/http-exception";
|
|
7
7
|
import { errors, jwtVerify } from "jose";
|
|
8
|
-
import {
|
|
9
|
-
import { getLocation, resolveInitPayload, policy_resolvePolicyDecision, verifyPolicySnapshotToken, getJurisdiction } from "./
|
|
10
|
-
import * as __rspack_external_valibot from "valibot";
|
|
8
|
+
import { version, getMetrics, withDatabaseSpan, extractErrorMessage } from "./744.js";
|
|
9
|
+
import { getLocation, resolveInitPayload, policy_resolvePolicyDecision, verifyPolicySnapshotToken, getJurisdiction } from "./261.js";
|
|
11
10
|
const prefixes = {
|
|
12
11
|
auditLog: 'log',
|
|
13
12
|
consent: 'cns',
|
|
@@ -18,11 +17,12 @@ const prefixes = {
|
|
|
18
17
|
subject: 'sub'
|
|
19
18
|
};
|
|
20
19
|
const b58 = base_x('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
20
|
+
const generate_id_EPOCH_TIMESTAMP = 1700000000000;
|
|
21
|
+
const ID_BYTE_LENGTH = 20;
|
|
22
|
+
const TIMESTAMP_BYTE_LENGTH = 8;
|
|
23
|
+
function writeTimestamp(buf, timestamp) {
|
|
24
|
+
const offset = timestamp - generate_id_EPOCH_TIMESTAMP;
|
|
25
|
+
const t = Number.isFinite(offset) ? Math.max(0, offset) : 0;
|
|
26
26
|
const high = Math.floor(t / 0x100000000);
|
|
27
27
|
const low = t >>> 0;
|
|
28
28
|
buf[0] = high >>> 24 & 255;
|
|
@@ -33,7 +33,21 @@ function generateId(model) {
|
|
|
33
33
|
buf[5] = low >>> 16 & 255;
|
|
34
34
|
buf[6] = low >>> 8 & 255;
|
|
35
35
|
buf[7] = 255 & low;
|
|
36
|
-
|
|
36
|
+
}
|
|
37
|
+
function generateId(model) {
|
|
38
|
+
const buf = crypto.getRandomValues(new Uint8Array(ID_BYTE_LENGTH));
|
|
39
|
+
writeTimestamp(buf, Date.now());
|
|
40
|
+
return `${prefixes[model]}_${b58.encode(buf)}`;
|
|
41
|
+
}
|
|
42
|
+
async function generateDeterministicId(model, timestamp, identity) {
|
|
43
|
+
const digest = await hashSha256Hex(JSON.stringify(identity));
|
|
44
|
+
const buf = new Uint8Array(ID_BYTE_LENGTH);
|
|
45
|
+
writeTimestamp(buf, timestamp);
|
|
46
|
+
for(let i = TIMESTAMP_BYTE_LENGTH; i < ID_BYTE_LENGTH; i++){
|
|
47
|
+
const offset = (i - TIMESTAMP_BYTE_LENGTH) * 2;
|
|
48
|
+
buf[i] = Number.parseInt(digest.slice(offset, offset + 2), 16);
|
|
49
|
+
}
|
|
50
|
+
return `${prefixes[model]}_${b58.encode(buf)}`;
|
|
37
51
|
}
|
|
38
52
|
async function generateUniqueId(db, model, ctx, options = {}) {
|
|
39
53
|
const { maxRetries = 10, attempt = 0, baseDelay = 5 } = options;
|
|
@@ -709,7 +723,7 @@ const statusHandler = async (c)=>{
|
|
|
709
723
|
try {
|
|
710
724
|
await ctx.db.findFirst('subject', {});
|
|
711
725
|
return c.json({
|
|
712
|
-
version:
|
|
726
|
+
version: version,
|
|
713
727
|
timestamp: new Date(),
|
|
714
728
|
client: clientInfo
|
|
715
729
|
});
|
|
@@ -813,6 +827,7 @@ const getSubjectHandler = async (c)=>{
|
|
|
813
827
|
});
|
|
814
828
|
}
|
|
815
829
|
};
|
|
830
|
+
const SUBJECT_ID_BATCH_SIZE = 500;
|
|
816
831
|
const listSubjectsHandler = async (c)=>{
|
|
817
832
|
const ctx = c.get('c15tContext');
|
|
818
833
|
const logger = ctx.logger;
|
|
@@ -838,21 +853,42 @@ const listSubjectsHandler = async (c)=>{
|
|
|
838
853
|
const subjects = await db.findMany('subject', {
|
|
839
854
|
where: (b)=>b('externalId', '=', externalId)
|
|
840
855
|
});
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
856
|
+
if (0 === subjects.length) {
|
|
857
|
+
logger.info('Found subjects for externalId', {
|
|
858
|
+
externalId,
|
|
859
|
+
count: 0
|
|
844
860
|
});
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
registry
|
|
861
|
+
return c.json({
|
|
862
|
+
subjects: []
|
|
848
863
|
});
|
|
849
|
-
|
|
864
|
+
}
|
|
865
|
+
const subjectIds = subjects.map((subject)=>subject.id);
|
|
866
|
+
const findConsents = (batch)=>db.findMany('consent', {
|
|
867
|
+
where: (b)=>b('subjectId', 'in', batch)
|
|
868
|
+
});
|
|
869
|
+
const consents = await findConsents(subjectIds.slice(0, SUBJECT_ID_BATCH_SIZE));
|
|
870
|
+
for(let index = SUBJECT_ID_BATCH_SIZE; index < subjectIds.length; index += SUBJECT_ID_BATCH_SIZE){
|
|
871
|
+
const batch = await findConsents(subjectIds.slice(index, index + SUBJECT_ID_BATCH_SIZE));
|
|
872
|
+
consents.push(...batch);
|
|
873
|
+
}
|
|
874
|
+
const consentItems = await enrichConsents(consents, {
|
|
875
|
+
db,
|
|
876
|
+
registry
|
|
877
|
+
});
|
|
878
|
+
const consentsBySubjectId = new Map();
|
|
879
|
+
for (const [index, consent] of consents.entries()){
|
|
880
|
+
const consentItem = consentItems[index];
|
|
881
|
+
if (!consentItem) throw new Error('Consent enrichment returned an incomplete result');
|
|
882
|
+
const subjectConsents = consentsBySubjectId.get(consent.subjectId) ?? [];
|
|
883
|
+
subjectConsents.push(consentItem);
|
|
884
|
+
consentsBySubjectId.set(consent.subjectId, subjectConsents);
|
|
885
|
+
}
|
|
886
|
+
const subjectItems = subjects.map((subject)=>({
|
|
850
887
|
id: subject.id,
|
|
851
888
|
externalId: subject.externalId ?? externalId,
|
|
852
889
|
createdAt: subject.createdAt,
|
|
853
|
-
consents:
|
|
854
|
-
};
|
|
855
|
-
}));
|
|
890
|
+
consents: consentsBySubjectId.get(subject.id) ?? []
|
|
891
|
+
}));
|
|
856
892
|
logger.info('Found subjects for externalId', {
|
|
857
893
|
externalId,
|
|
858
894
|
count: subjectItems.length
|
|
@@ -1041,9 +1077,6 @@ const patchSubjectHandler = async (c)=>{
|
|
|
1041
1077
|
};
|
|
1042
1078
|
const DEFAULT_ISSUER = 'c15t';
|
|
1043
1079
|
const DEFAULT_AUDIENCE = 'c15t-legal-document-snapshot';
|
|
1044
|
-
function isLegalDocumentPolicyType(type) {
|
|
1045
|
-
return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
|
|
1046
|
-
}
|
|
1047
1080
|
function resolveSnapshotIssuer(options) {
|
|
1048
1081
|
return options?.issuer?.trim() || DEFAULT_ISSUER;
|
|
1049
1082
|
}
|
|
@@ -1056,7 +1089,7 @@ function getSigningKey(secret) {
|
|
|
1056
1089
|
return new TextEncoder().encode(secret);
|
|
1057
1090
|
}
|
|
1058
1091
|
function isLegalDocumentSnapshotPayload(payload) {
|
|
1059
|
-
return 'string' == typeof payload.iss && 'string' == typeof payload.aud && 'string' == typeof payload.sub &&
|
|
1092
|
+
return 'string' == typeof payload.iss && 'string' == typeof payload.aud && 'string' == typeof payload.sub && isLegalDocumentType(payload.type) && 'string' == typeof payload.version && 'string' == typeof payload.hash && 'string' == typeof payload.effectiveDate && 'number' == typeof payload.iat && 'number' == typeof payload.exp;
|
|
1060
1093
|
}
|
|
1061
1094
|
async function verifyLegalDocumentSnapshotToken(params) {
|
|
1062
1095
|
const { token, options, tenantId } = params;
|
|
@@ -1112,6 +1145,38 @@ async function verifyLegalDocumentSnapshotToken(params) {
|
|
|
1112
1145
|
};
|
|
1113
1146
|
}
|
|
1114
1147
|
}
|
|
1148
|
+
function buildConsentId(input) {
|
|
1149
|
+
return generateDeterministicId('consent', input.givenAt.getTime(), [
|
|
1150
|
+
input.tenantId ?? null,
|
|
1151
|
+
input.subjectId,
|
|
1152
|
+
input.domainId,
|
|
1153
|
+
input.policyId ?? null,
|
|
1154
|
+
input.givenAt.toISOString()
|
|
1155
|
+
]);
|
|
1156
|
+
}
|
|
1157
|
+
function findConsentById(database, consentId) {
|
|
1158
|
+
return database.findFirst('consent', {
|
|
1159
|
+
where: (builder)=>builder('id', '=', consentId)
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
async function findExistingConsentSubmission(database, consentId, identity) {
|
|
1163
|
+
const deterministicConsent = await findConsentById(database, consentId);
|
|
1164
|
+
if (deterministicConsent) return deterministicConsent;
|
|
1165
|
+
return database.findFirst('consent', {
|
|
1166
|
+
where: (builder)=>builder.and(builder('subjectId', '=', identity.subjectId), builder('domainId', '=', identity.domainId), builder('policyId', '=', identity.policyId ?? null), builder('givenAt', '=', identity.givenAt), void 0 === identity.tenantId ? builder.isNull('tenantId') : builder('tenantId', '=', identity.tenantId))
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
function isUniqueConstraintViolationError(error) {
|
|
1170
|
+
const code = 'object' == typeof error && null !== error && 'code' in error ? String(error.code) : void 0;
|
|
1171
|
+
if ('23505' === code || 'ER_DUP_ENTRY' === code || '1062' === code || 'P2002' === code || code?.startsWith('SQLITE_CONSTRAINT')) return true;
|
|
1172
|
+
const message = extractErrorMessage(error).toLowerCase();
|
|
1173
|
+
return message.includes('unique constraint') || message.includes('unique violation') || message.includes('duplicate key') || message.includes('duplicate entry') || message.includes('unique conflict');
|
|
1174
|
+
}
|
|
1175
|
+
const MAX_FUTURE_CONSENT_TIME_DRIFT_MS = 300000;
|
|
1176
|
+
function clampConsentGivenAt(givenAt, now = Date.now()) {
|
|
1177
|
+
if (givenAt.getTime() > now + MAX_FUTURE_CONSENT_TIME_DRIFT_MS) return new Date(now);
|
|
1178
|
+
return givenAt;
|
|
1179
|
+
}
|
|
1115
1180
|
function buildRuntimeDecisionDedupeKey(input) {
|
|
1116
1181
|
return [
|
|
1117
1182
|
input.tenantId ?? 'default',
|
|
@@ -1191,9 +1256,6 @@ function parseLanguageFromHeader(header) {
|
|
|
1191
1256
|
if (!firstLanguage) return;
|
|
1192
1257
|
return firstLanguage.split('-')[0]?.toLowerCase();
|
|
1193
1258
|
}
|
|
1194
|
-
function isLegalDocumentType(type) {
|
|
1195
|
-
return 'privacy_policy' === type || 'terms_and_conditions' === type || 'dpa' === type;
|
|
1196
|
-
}
|
|
1197
1259
|
function resolveSnapshotFailureMode(ctx) {
|
|
1198
1260
|
return ctx.policySnapshot?.onValidationFailure ?? 'reject';
|
|
1199
1261
|
}
|
|
@@ -1275,7 +1337,13 @@ const postSubjectHandler = async (c)=>{
|
|
|
1275
1337
|
const input = await c.req.json();
|
|
1276
1338
|
const { type, subjectId, identityProvider, externalSubjectId, domain, metadata, givenAt: givenAtEpoch } = input;
|
|
1277
1339
|
const preferences = 'preferences' in input ? input.preferences : void 0;
|
|
1278
|
-
const
|
|
1340
|
+
const requestedGivenAt = new Date(givenAtEpoch);
|
|
1341
|
+
const givenAt = clampConsentGivenAt(requestedGivenAt);
|
|
1342
|
+
const wasGivenAtClamped = givenAt.getTime() !== requestedGivenAt.getTime();
|
|
1343
|
+
if (wasGivenAtClamped) logger.warn('Consent givenAt was too far in the future and was clamped to server time', {
|
|
1344
|
+
requestedGivenAt: requestedGivenAt.toISOString(),
|
|
1345
|
+
clampedGivenAt: givenAt.toISOString()
|
|
1346
|
+
});
|
|
1279
1347
|
const rawConsentAction = 'consentAction' in input ? input.consentAction : void 0;
|
|
1280
1348
|
let derivedConsentAction;
|
|
1281
1349
|
logger.debug('Request parameters', {
|
|
@@ -1510,6 +1578,9 @@ const postSubjectHandler = async (c)=>{
|
|
|
1510
1578
|
...metadata ?? {},
|
|
1511
1579
|
...shouldStoreLanguage && effectiveLanguage ? {
|
|
1512
1580
|
policyLanguage: effectiveLanguage
|
|
1581
|
+
} : {},
|
|
1582
|
+
...wasGivenAtClamped ? {
|
|
1583
|
+
clientGivenAt: requestedGivenAt.toISOString()
|
|
1513
1584
|
} : {}
|
|
1514
1585
|
};
|
|
1515
1586
|
const effectiveJurisdiction = hasValidSnapshot && snapshotPayload ? snapshotPayload.jurisdiction : resolvedJurisdiction;
|
|
@@ -1528,9 +1599,15 @@ const postSubjectHandler = async (c)=>{
|
|
|
1528
1599
|
language: effectiveLanguage,
|
|
1529
1600
|
proofConfig
|
|
1530
1601
|
});
|
|
1531
|
-
const
|
|
1532
|
-
|
|
1533
|
-
|
|
1602
|
+
const consentIdentity = {
|
|
1603
|
+
tenantId: ctx.tenantId,
|
|
1604
|
+
subjectId: subject.id,
|
|
1605
|
+
domainId: domainRecord.id,
|
|
1606
|
+
policyId,
|
|
1607
|
+
givenAt: requestedGivenAt
|
|
1608
|
+
};
|
|
1609
|
+
const consentId = await buildConsentId(consentIdentity);
|
|
1610
|
+
const existingConsent = await findExistingConsentSubmission(db, consentId, consentIdentity);
|
|
1534
1611
|
if (existingConsent) {
|
|
1535
1612
|
logger.debug('Duplicate consent detected, returning existing record', {
|
|
1536
1613
|
consentId: existingConsent.id
|
|
@@ -1547,103 +1624,132 @@ const postSubjectHandler = async (c)=>{
|
|
|
1547
1624
|
givenAt: existingConsent.givenAt
|
|
1548
1625
|
});
|
|
1549
1626
|
}
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
|
|
1559
|
-
}) ?? await tx.create('runtimePolicyDecision', {
|
|
1560
|
-
id: `rpd_${crypto.randomUUID().replaceAll('-', '')}`,
|
|
1561
|
-
tenantId: decisionPayload.tenantId,
|
|
1562
|
-
policyId: decisionPayload.policyId,
|
|
1563
|
-
fingerprint: decisionPayload.fingerprint,
|
|
1564
|
-
matchedBy: decisionPayload.matchedBy,
|
|
1565
|
-
countryCode: decisionPayload.countryCode,
|
|
1566
|
-
regionCode: decisionPayload.regionCode,
|
|
1567
|
-
jurisdiction: decisionPayload.jurisdiction,
|
|
1568
|
-
language: decisionPayload.language,
|
|
1569
|
-
model: decisionPayload.model,
|
|
1570
|
-
policyI18n: decisionPayload.policyI18n ? {
|
|
1571
|
-
json: decisionPayload.policyI18n
|
|
1572
|
-
} : void 0,
|
|
1573
|
-
uiMode: decisionPayload.uiMode,
|
|
1574
|
-
bannerUi: decisionPayload.bannerUi ? {
|
|
1575
|
-
json: decisionPayload.bannerUi
|
|
1576
|
-
} : void 0,
|
|
1577
|
-
dialogUi: decisionPayload.dialogUi ? {
|
|
1578
|
-
json: decisionPayload.dialogUi
|
|
1579
|
-
} : void 0,
|
|
1580
|
-
categories: decisionPayload.categories ? {
|
|
1581
|
-
json: decisionPayload.categories
|
|
1582
|
-
} : void 0,
|
|
1583
|
-
preselectedCategories: decisionPayload.preselectedCategories ? {
|
|
1584
|
-
json: decisionPayload.preselectedCategories
|
|
1585
|
-
} : void 0,
|
|
1586
|
-
proofConfig: decisionPayload.proofConfig ? {
|
|
1587
|
-
json: decisionPayload.proofConfig
|
|
1588
|
-
} : void 0,
|
|
1589
|
-
dedupeKey: decisionPayload.dedupeKey
|
|
1590
|
-
}).catch(async ()=>tx.findFirst('runtimePolicyDecision', {
|
|
1627
|
+
const runConsentTransaction = ()=>db.transaction(async (tx)=>{
|
|
1628
|
+
logger.debug('Creating consent record', {
|
|
1629
|
+
subjectId: subject.id,
|
|
1630
|
+
domainId: domainRecord.id,
|
|
1631
|
+
policyId,
|
|
1632
|
+
purposeIds
|
|
1633
|
+
});
|
|
1634
|
+
const runtimePolicyDecision = decisionPayload ? await tx.findFirst('runtimePolicyDecision', {
|
|
1591
1635
|
where: (b)=>b('dedupeKey', '=', decisionPayload.dedupeKey)
|
|
1592
|
-
})
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1636
|
+
}) ?? await tx.create('runtimePolicyDecision', {
|
|
1637
|
+
id: `rpd_${crypto.randomUUID().replaceAll('-', '')}`,
|
|
1638
|
+
tenantId: decisionPayload.tenantId,
|
|
1639
|
+
policyId: decisionPayload.policyId,
|
|
1640
|
+
fingerprint: decisionPayload.fingerprint,
|
|
1641
|
+
matchedBy: decisionPayload.matchedBy,
|
|
1642
|
+
countryCode: decisionPayload.countryCode,
|
|
1643
|
+
regionCode: decisionPayload.regionCode,
|
|
1644
|
+
jurisdiction: decisionPayload.jurisdiction,
|
|
1645
|
+
language: decisionPayload.language,
|
|
1646
|
+
model: decisionPayload.model,
|
|
1647
|
+
policyI18n: decisionPayload.policyI18n ? {
|
|
1648
|
+
json: decisionPayload.policyI18n
|
|
1649
|
+
} : void 0,
|
|
1650
|
+
uiMode: decisionPayload.uiMode,
|
|
1651
|
+
bannerUi: decisionPayload.bannerUi ? {
|
|
1652
|
+
json: decisionPayload.bannerUi
|
|
1653
|
+
} : void 0,
|
|
1654
|
+
dialogUi: decisionPayload.dialogUi ? {
|
|
1655
|
+
json: decisionPayload.dialogUi
|
|
1656
|
+
} : void 0,
|
|
1657
|
+
categories: decisionPayload.categories ? {
|
|
1658
|
+
json: decisionPayload.categories
|
|
1659
|
+
} : void 0,
|
|
1660
|
+
preselectedCategories: decisionPayload.preselectedCategories ? {
|
|
1661
|
+
json: decisionPayload.preselectedCategories
|
|
1662
|
+
} : void 0,
|
|
1663
|
+
proofConfig: decisionPayload.proofConfig ? {
|
|
1664
|
+
json: decisionPayload.proofConfig
|
|
1665
|
+
} : void 0,
|
|
1666
|
+
dedupeKey: decisionPayload.dedupeKey
|
|
1667
|
+
}) : void 0;
|
|
1668
|
+
const consentRecord = await tx.create('consent', {
|
|
1669
|
+
id: consentId,
|
|
1623
1670
|
subjectId: subject.id,
|
|
1624
|
-
|
|
1625
|
-
|
|
1671
|
+
domainId: domainRecord.id,
|
|
1672
|
+
policyId,
|
|
1673
|
+
purposeIds: {
|
|
1674
|
+
json: purposeIds
|
|
1675
|
+
},
|
|
1676
|
+
metadata: Object.keys(metadataWithPolicy).length > 0 ? {
|
|
1677
|
+
json: metadataWithPolicy
|
|
1678
|
+
} : void 0,
|
|
1679
|
+
ipAddress: shouldStoreIp ? ctx.ipAddress : null,
|
|
1680
|
+
userAgent: shouldStoreUserAgent ? ctx.userAgent : null,
|
|
1681
|
+
jurisdiction: effectiveJurisdiction,
|
|
1682
|
+
jurisdictionModel: effectiveModel,
|
|
1683
|
+
tcString: input.tcString,
|
|
1684
|
+
uiSource: input.uiSource,
|
|
1685
|
+
consentAction: derivedConsentAction,
|
|
1686
|
+
givenAt,
|
|
1687
|
+
validUntil,
|
|
1688
|
+
runtimePolicyDecisionId: runtimePolicyDecision?.id,
|
|
1689
|
+
runtimePolicySource: decisionPayload?.source
|
|
1690
|
+
});
|
|
1691
|
+
if (!consentRecord) throw new HTTPException(500, {
|
|
1692
|
+
message: 'Failed to create consent',
|
|
1693
|
+
cause: {
|
|
1694
|
+
code: 'CONSENT_CREATION_FAILED',
|
|
1695
|
+
subjectId: subject.id,
|
|
1696
|
+
domain
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
logger.debug('Created consent', {
|
|
1700
|
+
consentRecord: consentRecord.id
|
|
1701
|
+
});
|
|
1702
|
+
return {
|
|
1703
|
+
consent: consentRecord,
|
|
1704
|
+
created: true
|
|
1705
|
+
};
|
|
1626
1706
|
});
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1707
|
+
const maxTransactionAttempts = 3;
|
|
1708
|
+
let result;
|
|
1709
|
+
for(let attempt = 1; attempt <= maxTransactionAttempts; attempt++)try {
|
|
1710
|
+
result = await runConsentTransaction();
|
|
1711
|
+
break;
|
|
1712
|
+
} catch (error) {
|
|
1713
|
+
if (!isUniqueConstraintViolationError(error)) throw error;
|
|
1714
|
+
const concurrentConsent = await findConsentById(db, consentId);
|
|
1715
|
+
if (concurrentConsent) {
|
|
1716
|
+
logger.debug('Consent insert conflicted, returning existing record', {
|
|
1717
|
+
consentId: concurrentConsent.id
|
|
1718
|
+
});
|
|
1719
|
+
result = {
|
|
1720
|
+
consent: concurrentConsent,
|
|
1721
|
+
created: false
|
|
1722
|
+
};
|
|
1723
|
+
break;
|
|
1724
|
+
}
|
|
1725
|
+
if (attempt === maxTransactionAttempts) throw error;
|
|
1726
|
+
}
|
|
1727
|
+
if (!result) throw new HTTPException(500, {
|
|
1728
|
+
message: 'Failed to create consent',
|
|
1729
|
+
cause: {
|
|
1730
|
+
code: 'CONSENT_CREATION_FAILED',
|
|
1731
|
+
subjectId: subject.id,
|
|
1732
|
+
domain
|
|
1733
|
+
}
|
|
1630
1734
|
});
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1735
|
+
if (result.created) {
|
|
1736
|
+
const metrics = getMetrics();
|
|
1737
|
+
if (metrics) {
|
|
1738
|
+
const jurisdiction = effectiveJurisdiction;
|
|
1739
|
+
metrics.recordConsentCreated({
|
|
1740
|
+
type,
|
|
1741
|
+
jurisdiction
|
|
1742
|
+
});
|
|
1743
|
+
const hasAccepted = preferences && Object.values(preferences).some(Boolean);
|
|
1744
|
+
if (hasAccepted) metrics.recordConsentAccepted({
|
|
1745
|
+
type,
|
|
1746
|
+
jurisdiction
|
|
1747
|
+
});
|
|
1748
|
+
else metrics.recordConsentRejected({
|
|
1749
|
+
type,
|
|
1750
|
+
jurisdiction
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1647
1753
|
}
|
|
1648
1754
|
return c.json({
|
|
1649
1755
|
subjectId: subject.id,
|
|
@@ -1698,10 +1804,10 @@ const createSubjectRoutes = ()=>{
|
|
|
1698
1804
|
description: 'Subject not found for the given ID'
|
|
1699
1805
|
}
|
|
1700
1806
|
}
|
|
1701
|
-
}), validator('param',
|
|
1807
|
+
}), validator('param', getSubjectParamsSchema), validator('query', getSubjectQuerySchema), getSubjectHandler);
|
|
1702
1808
|
app.post('/', describeRoute({
|
|
1703
1809
|
summary: 'Record consent for a subject',
|
|
1704
|
-
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`",
|
|
1810
|
+
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`, and suffixed legal-document variants such as `terms_and_conditions_b2b` – Prefer a signed `documentSnapshotToken`; otherwise use a release `policyHash`, with `policyId` kept only for compatibility\n- `marketing_communications`, `age_verification`, `other` – Optional `preferences`",
|
|
1705
1811
|
tags: [
|
|
1706
1812
|
'Subject',
|
|
1707
1813
|
'Consent'
|
|
@@ -1739,12 +1845,7 @@ const createSubjectRoutes = ()=>{
|
|
|
1739
1845
|
description: 'Subject not found for the given ID'
|
|
1740
1846
|
}
|
|
1741
1847
|
}
|
|
1742
|
-
}), validator('param',
|
|
1743
|
-
id: subjectIdSchema
|
|
1744
|
-
})), validator('json', __rspack_external_valibot.object({
|
|
1745
|
-
externalId: __rspack_external_valibot.string(),
|
|
1746
|
-
identityProvider: __rspack_external_valibot.optional(__rspack_external_valibot.string())
|
|
1747
|
-
})), patchSubjectHandler);
|
|
1848
|
+
}), validator('param', patchSubjectParamsSchema), validator('json', patchSubjectInputSchema), patchSubjectHandler);
|
|
1748
1849
|
app.get('/', describeRoute({
|
|
1749
1850
|
summary: 'List subjects by external ID (API key required)',
|
|
1750
1851
|
description: 'Returns all subjects linked to the given external ID. Requires Bearer token (API key). Use for server-side consent lookups.',
|
package/dist/{302.js → 744.js}
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SpanKind, SpanStatusCode as api_SpanStatusCode, context, metrics, trace } from "@opentelemetry/api";
|
|
2
|
+
const version = '2.2.0-canary-20260728085441';
|
|
2
3
|
function extractErrorMessage(error) {
|
|
3
4
|
if (error instanceof AggregateError && error.errors?.length > 0) {
|
|
4
5
|
const inner = error.errors.map((e)=>e instanceof Error ? e.message : String(e)).join('; ');
|
|
@@ -13,7 +14,7 @@ function createTelemetryOptions(appName = 'c15t', telemetryConfig, tenantId) {
|
|
|
13
14
|
const defaultAttributes = {
|
|
14
15
|
...telemetryConfig?.defaultAttributes || {},
|
|
15
16
|
'service.name': String(appName),
|
|
16
|
-
'service.version':
|
|
17
|
+
'service.version': version
|
|
17
18
|
};
|
|
18
19
|
if (tenantId) defaultAttributes['tenant.id'] = tenantId;
|
|
19
20
|
const config = {
|
|
@@ -315,7 +316,6 @@ function getMetrics(options) {
|
|
|
315
316
|
metricsInstance = createMetrics(getMeter(options));
|
|
316
317
|
return metricsInstance;
|
|
317
318
|
}
|
|
318
|
-
const GVL_TTL_MS = 259200000;
|
|
319
319
|
const memory_memoryCache = new Map();
|
|
320
320
|
function createMemoryCacheAdapter() {
|
|
321
321
|
return {
|
|
@@ -368,6 +368,7 @@ function createCacheKey(appName, namespace, ...parts) {
|
|
|
368
368
|
];
|
|
369
369
|
return allParts.join(':');
|
|
370
370
|
}
|
|
371
|
+
const GVL_TTL_MS = 259200000;
|
|
371
372
|
const GVL_ENDPOINT = 'https://gvl.inth.app';
|
|
372
373
|
const inflightRequests = new Map();
|
|
373
374
|
async function fetchGVLWithLanguage(language, vendorIds, endpoint = GVL_ENDPOINT) {
|
|
@@ -469,4 +470,4 @@ function createGVLResolver(options) {
|
|
|
469
470
|
}
|
|
470
471
|
};
|
|
471
472
|
}
|
|
472
|
-
export { GVL_TTL_MS, clearMemoryCache, createCacheKey, createGVLCacheKey, createGVLResolver, createMemoryCacheAdapter, createRequestSpan, createTelemetryOptions, extractErrorMessage, getMemoryCacheSize, getMetrics, getTraceContext, handleSpanError, isTelemetryEnabled, withDatabaseSpan, withSpanContext };
|
|
473
|
+
export { GVL_TTL_MS, clearMemoryCache, createCacheKey, createGVLCacheKey, createGVLResolver, createMemoryCacheAdapter, createRequestSpan, createTelemetryOptions, extractErrorMessage, getMemoryCacheSize, getMetrics, getTraceContext, handleSpanError, isTelemetryEnabled, version, withDatabaseSpan, withSpanContext };
|