@c15t/node-sdk 3.0.0-alpha.3 → 3.0.0-alpha.4
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 +53 -45
- package/dist/160.js +68 -0
- package/dist/index.js +1016 -349
- package/dist/testing.js +42 -87
- package/dist-types/client.d.ts +112 -705
- package/dist-types/configuration-error.d.ts +17 -0
- package/dist-types/contract.d.ts +930 -0
- package/dist-types/errors.d.ts +143 -0
- package/dist-types/index.d.ts +13 -40
- package/dist-types/options.d.ts +152 -0
- package/dist-types/path.d.ts +24 -0
- package/dist-types/result.d.ts +77 -0
- package/dist-types/testing.d.ts +81 -90
- package/dist-types/transport.d.ts +19 -0
- package/dist-types/version.d.ts +1 -1
- package/package.json +6 -12
- package/dist-types/endpoints/consent.d.ts +0 -16
- package/dist-types/endpoints/index.d.ts +0 -4
- package/dist-types/endpoints/init.d.ts +0 -15
- package/dist-types/endpoints/status.d.ts +0 -15
- package/dist-types/endpoints/subjects.d.ts +0 -45
- package/dist-types/error.d.ts +0 -85
- package/dist-types/fetcher.d.ts +0 -37
- package/dist-types/headers.d.ts +0 -3
- package/dist-types/types.d.ts +0 -234
package/dist/index.js
CHANGED
|
@@ -1,397 +1,1064 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
import { checkConsentQuerySchema, experimentSummaryQuerySchema, legalDocumentCurrentInputSchema, legalDocumentCurrentParamsSchema, listSubjectsQuerySchema, patchSubjectInputSchema, postSubjectInputSchema } from "@c15t/schema";
|
|
2
|
+
import { POLICY_CONTRACT_HEADER, POLICY_CONTRACT_VERSION, brandingValues } from "@c15t/schema/types";
|
|
3
|
+
import { C15tError, isStalePolicyReason } from "./160.js";
|
|
4
|
+
const defineEndpoint = (endpoint)=>endpoint;
|
|
5
|
+
const checkSchema = async (schema, value, prefix = [])=>{
|
|
6
|
+
const result = await schema['~standard'].validate(value);
|
|
7
|
+
return (result.issues ?? []).map((issue)=>({
|
|
8
|
+
message: issue.message,
|
|
9
|
+
path: [
|
|
10
|
+
...prefix,
|
|
11
|
+
...(issue.path ?? []).map((segment)=>'object' == typeof segment ? segment.key : segment)
|
|
12
|
+
]
|
|
13
|
+
}));
|
|
14
|
+
};
|
|
15
|
+
const requireText = (value, path)=>'string' == typeof value && '' !== value.trim() ? [] : [
|
|
16
|
+
{
|
|
17
|
+
message: 'Expected a non-empty string.',
|
|
18
|
+
path
|
|
19
|
+
}
|
|
20
|
+
];
|
|
21
|
+
const requirePathSegment = (value, path)=>{
|
|
22
|
+
const issues = requireText(value, path);
|
|
23
|
+
if (issues.length > 0) return issues;
|
|
24
|
+
return '.' === value || '..' === value ? [
|
|
25
|
+
{
|
|
26
|
+
message: 'Expected an id, not a dot path segment.',
|
|
27
|
+
path
|
|
28
|
+
}
|
|
29
|
+
] : [];
|
|
30
|
+
};
|
|
31
|
+
const optionalText = (value, path)=>void 0 === value ? [] : requireText(value, path);
|
|
32
|
+
const checkTypeList = (value, path, { required })=>{
|
|
33
|
+
if (void 0 === value && !required) return [];
|
|
34
|
+
if (!Array.isArray(value) || required && 0 === value.length) return [
|
|
35
|
+
{
|
|
36
|
+
message: required ? 'Expected a non-empty array of policy types.' : 'Expected an array of policy types.',
|
|
37
|
+
path
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
return value.flatMap((entry, index)=>'string' != typeof entry || '' === entry || entry.includes(',') ? [
|
|
41
|
+
{
|
|
42
|
+
message: 'Expected a policy type without commas.',
|
|
43
|
+
path: [
|
|
44
|
+
...path,
|
|
45
|
+
index
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
] : []);
|
|
49
|
+
};
|
|
50
|
+
const ISO_DATE_OR_TIMESTAMP = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2}))?$/u;
|
|
51
|
+
const toIsoString = (value)=>value instanceof Date ? value.toISOString() : value;
|
|
52
|
+
const checkDateLike = (value, path, { required })=>{
|
|
53
|
+
if (void 0 === value && !required) return [];
|
|
54
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? [
|
|
55
|
+
{
|
|
56
|
+
message: 'Expected a valid Date.',
|
|
57
|
+
path
|
|
58
|
+
}
|
|
59
|
+
] : [];
|
|
60
|
+
return 'string' == typeof value && ISO_DATE_OR_TIMESTAMP.test(value) && !Number.isNaN(Date.parse(value)) && new Date(value.slice(0, 10)).toISOString().slice(0, 10) === value.slice(0, 10) ? [] : [
|
|
61
|
+
{
|
|
62
|
+
message: 'Expected a Date or an ISO 8601 string.',
|
|
63
|
+
path
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
};
|
|
67
|
+
const isObject = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
|
|
68
|
+
const requireObject = (value, path)=>isObject(value) ? [] : [
|
|
69
|
+
{
|
|
70
|
+
message: 'Expected an object.',
|
|
71
|
+
path
|
|
72
|
+
}
|
|
73
|
+
];
|
|
74
|
+
class DecodeError extends Error {
|
|
75
|
+
name = 'DecodeError';
|
|
76
|
+
}
|
|
77
|
+
const asObject = (value, field)=>{
|
|
78
|
+
if (!isObject(value)) throw new DecodeError(`Expected ${field} to be an object.`);
|
|
79
|
+
return value;
|
|
80
|
+
};
|
|
81
|
+
const asArray = (value, field)=>{
|
|
82
|
+
if (!Array.isArray(value)) throw new DecodeError(`Expected ${field} to be an array.`);
|
|
83
|
+
return value;
|
|
84
|
+
};
|
|
85
|
+
const toDate = (value, field)=>{
|
|
86
|
+
if ('string' == typeof value || 'number' == typeof value) {
|
|
87
|
+
const date = new Date(value);
|
|
88
|
+
if (!Number.isNaN(date.getTime())) return date;
|
|
20
89
|
}
|
|
21
|
-
|
|
22
|
-
|
|
90
|
+
throw new DecodeError(`Expected ${field} to be a date.`);
|
|
91
|
+
};
|
|
92
|
+
const decodeConsentItem = (value, field)=>{
|
|
93
|
+
const item = asObject(value, field);
|
|
94
|
+
const decoded = {
|
|
95
|
+
...item,
|
|
96
|
+
givenAt: toDate(item.givenAt, `${field}.givenAt`)
|
|
97
|
+
};
|
|
98
|
+
if (null === item.policyEffectiveDate) delete decoded.policyEffectiveDate;
|
|
99
|
+
else if (void 0 !== item.policyEffectiveDate) decoded.policyEffectiveDate = toDate(item.policyEffectiveDate, `${field}.policyEffectiveDate`);
|
|
100
|
+
return decoded;
|
|
101
|
+
};
|
|
102
|
+
const decodeConsents = (value, field)=>asArray(value, field).map((item, index)=>decodeConsentItem(item, `${field}[${index}]`));
|
|
103
|
+
const NO_ISSUES = [];
|
|
104
|
+
const noValidation = ()=>Promise.resolve(NO_ISSUES);
|
|
105
|
+
const contract_status = defineEndpoint({
|
|
106
|
+
auth: 'none',
|
|
107
|
+
decode: (body)=>{
|
|
108
|
+
const value = asObject(body, 'body');
|
|
109
|
+
return {
|
|
110
|
+
...value,
|
|
111
|
+
timestamp: toDate(value.timestamp, 'timestamp')
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
errorCodes: [
|
|
115
|
+
'SERVICE_UNAVAILABLE'
|
|
116
|
+
],
|
|
117
|
+
idempotent: true,
|
|
118
|
+
method: 'GET',
|
|
119
|
+
name: 'status',
|
|
120
|
+
path: '/status',
|
|
121
|
+
prepare: (_input)=>({
|
|
122
|
+
params: {}
|
|
123
|
+
}),
|
|
124
|
+
validate: noValidation
|
|
125
|
+
});
|
|
126
|
+
const contract_init = defineEndpoint({
|
|
127
|
+
auth: 'none',
|
|
128
|
+
decode: (body)=>asObject(body, 'body'),
|
|
129
|
+
errorCodes: [],
|
|
130
|
+
idempotent: true,
|
|
131
|
+
method: 'GET',
|
|
132
|
+
name: 'init',
|
|
133
|
+
path: '/init',
|
|
134
|
+
prepare: (input)=>{
|
|
135
|
+
const headers = {};
|
|
136
|
+
if (input?.language !== void 0) headers['accept-language'] = input.language;
|
|
137
|
+
if (input?.country !== void 0) headers['x-c15t-country'] = input.country;
|
|
138
|
+
if (input?.region !== void 0) headers['x-c15t-region'] = input.region;
|
|
139
|
+
if (input?.gpc !== void 0) headers['x-c15t-gpc'] = input.gpc ? '1' : '0';
|
|
140
|
+
return {
|
|
141
|
+
headers,
|
|
142
|
+
params: {}
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
validate: (input)=>{
|
|
146
|
+
if (void 0 === input) return Promise.resolve(NO_ISSUES);
|
|
147
|
+
if (!isObject(input)) return Promise.resolve(requireObject(input, []));
|
|
148
|
+
const issues = [
|
|
149
|
+
...optionalText(input.language, [
|
|
150
|
+
'language'
|
|
151
|
+
]),
|
|
152
|
+
...optionalText(input.country, [
|
|
153
|
+
'country'
|
|
154
|
+
]),
|
|
155
|
+
...optionalText(input.region, [
|
|
156
|
+
'region'
|
|
157
|
+
]),
|
|
158
|
+
...void 0 === input.gpc || 'boolean' == typeof input.gpc ? [] : [
|
|
159
|
+
{
|
|
160
|
+
message: 'Expected a boolean.',
|
|
161
|
+
path: [
|
|
162
|
+
'gpc'
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
];
|
|
167
|
+
return Promise.resolve(issues);
|
|
23
168
|
}
|
|
24
|
-
|
|
25
|
-
|
|
169
|
+
});
|
|
170
|
+
const manifest = defineEndpoint({
|
|
171
|
+
accepts: (code)=>code >= 200 && code < 300 || 304 === code,
|
|
172
|
+
auth: 'none',
|
|
173
|
+
decode: (body, response)=>{
|
|
174
|
+
const etag = response.headers.get('etag') ?? void 0;
|
|
175
|
+
if (304 === response.status) return {
|
|
176
|
+
etag,
|
|
177
|
+
status: 'not-modified'
|
|
178
|
+
};
|
|
179
|
+
const value = asObject(body, 'body');
|
|
180
|
+
if (2 !== value.schemaVersion || 'string' != typeof value.revision || !brandingValues.some((branding)=>branding === value.branding)) throw new DecodeError('Expected manifest schemaVersion 2, a string revision and a supported branding value.');
|
|
181
|
+
return {
|
|
182
|
+
etag,
|
|
183
|
+
manifest: value,
|
|
184
|
+
status: 'modified'
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
errorCodes: [],
|
|
188
|
+
idempotent: true,
|
|
189
|
+
method: 'GET',
|
|
190
|
+
name: 'manifest',
|
|
191
|
+
path: '/manifest',
|
|
192
|
+
prepare: (input)=>{
|
|
193
|
+
const headers = {};
|
|
194
|
+
if (input?.ifNoneMatch !== void 0) headers['if-none-match'] = input.ifNoneMatch;
|
|
195
|
+
return {
|
|
196
|
+
headers,
|
|
197
|
+
params: {},
|
|
198
|
+
query: {
|
|
199
|
+
language: input?.language
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
validate: (input)=>{
|
|
204
|
+
if (void 0 === input) return Promise.resolve(NO_ISSUES);
|
|
205
|
+
if (!isObject(input)) return Promise.resolve(requireObject(input, []));
|
|
206
|
+
return Promise.resolve([
|
|
207
|
+
...optionalText(input.language, [
|
|
208
|
+
'language'
|
|
209
|
+
]),
|
|
210
|
+
...optionalText(input.ifNoneMatch, [
|
|
211
|
+
'ifNoneMatch'
|
|
212
|
+
])
|
|
213
|
+
]);
|
|
26
214
|
}
|
|
27
|
-
|
|
28
|
-
|
|
215
|
+
});
|
|
216
|
+
const getSubject = defineEndpoint({
|
|
217
|
+
auth: 'none',
|
|
218
|
+
decode: (body)=>{
|
|
219
|
+
const value = asObject(body, 'body');
|
|
220
|
+
const subject = {
|
|
221
|
+
...asObject(value.subject, 'subject')
|
|
222
|
+
};
|
|
223
|
+
if (null === subject.createdAt) delete subject.createdAt;
|
|
224
|
+
else if (void 0 !== subject.createdAt) subject.createdAt = toDate(subject.createdAt, 'subject.createdAt');
|
|
225
|
+
return {
|
|
226
|
+
...value,
|
|
227
|
+
consents: decodeConsents(value.consents, 'consents'),
|
|
228
|
+
subject
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
errorCodes: [
|
|
232
|
+
'DATABASE_ERROR',
|
|
233
|
+
'NOT_FOUND'
|
|
234
|
+
],
|
|
235
|
+
idempotent: true,
|
|
236
|
+
method: 'GET',
|
|
237
|
+
name: 'subjects.get',
|
|
238
|
+
path: '/subjects/:id',
|
|
239
|
+
prepare: (input)=>({
|
|
240
|
+
params: {
|
|
241
|
+
id: input.id
|
|
242
|
+
},
|
|
243
|
+
query: {
|
|
244
|
+
type: input.request?.types?.join(',')
|
|
245
|
+
}
|
|
246
|
+
}),
|
|
247
|
+
validate: (input)=>{
|
|
248
|
+
if (void 0 !== input.request && !isObject(input.request)) return Promise.resolve(requireObject(input.request, [
|
|
249
|
+
'options'
|
|
250
|
+
]));
|
|
251
|
+
return Promise.resolve([
|
|
252
|
+
...requirePathSegment(input.id, [
|
|
253
|
+
'id'
|
|
254
|
+
]),
|
|
255
|
+
...checkTypeList(input.request?.types, [
|
|
256
|
+
'types'
|
|
257
|
+
], {
|
|
258
|
+
required: false
|
|
259
|
+
})
|
|
260
|
+
]);
|
|
29
261
|
}
|
|
30
|
-
|
|
31
|
-
|
|
262
|
+
});
|
|
263
|
+
const listSubjects = defineEndpoint({
|
|
264
|
+
auth: 'api-key',
|
|
265
|
+
decode: (body)=>{
|
|
266
|
+
const value = asObject(body, 'body');
|
|
267
|
+
return {
|
|
268
|
+
...value,
|
|
269
|
+
subjects: asArray(value.subjects, 'subjects').map((entry, index)=>{
|
|
270
|
+
const subject = asObject(entry, `subjects[${index}]`);
|
|
271
|
+
return {
|
|
272
|
+
...subject,
|
|
273
|
+
consents: decodeConsents(subject.consents, `subjects[${index}].consents`),
|
|
274
|
+
createdAt: toDate(subject.createdAt, `subjects[${index}].createdAt`)
|
|
275
|
+
};
|
|
276
|
+
})
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
errorCodes: [
|
|
280
|
+
'DATABASE_ERROR',
|
|
281
|
+
'EXTERNAL_ID_REQUIRED',
|
|
282
|
+
'UNAUTHORIZED'
|
|
283
|
+
],
|
|
284
|
+
idempotent: true,
|
|
285
|
+
method: 'GET',
|
|
286
|
+
name: 'subjects.list',
|
|
287
|
+
path: '/subjects',
|
|
288
|
+
prepare: (input)=>({
|
|
289
|
+
params: {},
|
|
290
|
+
query: {
|
|
291
|
+
externalId: input.externalId
|
|
292
|
+
}
|
|
293
|
+
}),
|
|
294
|
+
validate: async (input)=>isObject(input) ? [
|
|
295
|
+
...requireText(input.externalId, [
|
|
296
|
+
'externalId'
|
|
297
|
+
]),
|
|
298
|
+
...await checkSchema(listSubjectsQuerySchema, input)
|
|
299
|
+
] : requireObject(input, [])
|
|
300
|
+
});
|
|
301
|
+
const toWireSubject = (input)=>({
|
|
302
|
+
...input,
|
|
303
|
+
givenAt: input.givenAt instanceof Date ? input.givenAt.getTime() : input.givenAt
|
|
304
|
+
});
|
|
305
|
+
const createSubject = defineEndpoint({
|
|
306
|
+
auth: 'none',
|
|
307
|
+
decode: (body)=>{
|
|
308
|
+
const { ok: _ok, ...value } = asObject(body, 'body');
|
|
309
|
+
return {
|
|
310
|
+
...value,
|
|
311
|
+
givenAt: toDate(value.givenAt, 'givenAt')
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
errorCodes: [
|
|
315
|
+
'CHOICE_OUT_OF_SCOPE',
|
|
316
|
+
'CHOICE_PREFERENCE_MISMATCH',
|
|
317
|
+
'CONFLICT',
|
|
318
|
+
'DATABASE_ERROR',
|
|
319
|
+
'INPUT_VALIDATION_FAILED',
|
|
320
|
+
'POLICY_SNAPSHOT_EXPIRED',
|
|
321
|
+
'POLICY_SNAPSHOT_INVALID',
|
|
322
|
+
'POLICY_SNAPSHOT_REQUIRED',
|
|
323
|
+
'PURPOSE_NOT_ALLOWED',
|
|
324
|
+
'STALE_POLICY',
|
|
325
|
+
'SUBJECT_CONFLICT'
|
|
326
|
+
],
|
|
327
|
+
idempotent: true,
|
|
328
|
+
method: 'POST',
|
|
329
|
+
name: 'subjects.create',
|
|
330
|
+
path: '/subjects',
|
|
331
|
+
prepare: (input)=>({
|
|
332
|
+
body: toWireSubject(input),
|
|
333
|
+
params: {}
|
|
334
|
+
}),
|
|
335
|
+
validate: (input)=>isObject(input) ? checkSchema(postSubjectInputSchema, toWireSubject(input)) : Promise.resolve(requireObject(input, []))
|
|
336
|
+
});
|
|
337
|
+
const identifySubject = defineEndpoint({
|
|
338
|
+
auth: 'none',
|
|
339
|
+
decode: (body)=>{
|
|
340
|
+
const value = asObject(body, 'body');
|
|
341
|
+
asObject(value.subject, 'subject');
|
|
342
|
+
return value;
|
|
343
|
+
},
|
|
344
|
+
errorCodes: [
|
|
345
|
+
'DATABASE_ERROR',
|
|
346
|
+
'EXTERNAL_ID_REQUIRED',
|
|
347
|
+
'NOT_FOUND'
|
|
348
|
+
],
|
|
349
|
+
idempotent: true,
|
|
350
|
+
method: 'PATCH',
|
|
351
|
+
name: 'subjects.identify',
|
|
352
|
+
path: '/subjects/:id',
|
|
353
|
+
prepare: (input)=>({
|
|
354
|
+
body: input.input,
|
|
355
|
+
params: {
|
|
356
|
+
id: input.id
|
|
357
|
+
}
|
|
358
|
+
}),
|
|
359
|
+
validate: async (input)=>[
|
|
360
|
+
...requirePathSegment(input.id, [
|
|
361
|
+
'id'
|
|
362
|
+
]),
|
|
363
|
+
...isObject(input.input) ? [
|
|
364
|
+
...requireText(input.input.externalId, [
|
|
365
|
+
'externalId'
|
|
366
|
+
]),
|
|
367
|
+
...await checkSchema(patchSubjectInputSchema, input.input)
|
|
368
|
+
] : requireObject(input.input, [])
|
|
369
|
+
]
|
|
370
|
+
});
|
|
371
|
+
const checkConsent = defineEndpoint({
|
|
372
|
+
auth: 'none',
|
|
373
|
+
decode: (body, _response, input)=>{
|
|
374
|
+
const value = asObject(body, 'body');
|
|
375
|
+
const results = asObject(value.results, 'results');
|
|
376
|
+
for (const type of new Set([
|
|
377
|
+
...Object.keys(results),
|
|
378
|
+
...input.types
|
|
379
|
+
])){
|
|
380
|
+
const result = asObject(results[type], `results.${type}`);
|
|
381
|
+
if ('boolean' != typeof result.hasConsent || 'boolean' != typeof result.isLatestPolicy) throw new DecodeError(`Expected results.${type}.hasConsent and isLatestPolicy to be booleans.`);
|
|
382
|
+
}
|
|
383
|
+
return value;
|
|
384
|
+
},
|
|
385
|
+
errorCodes: [
|
|
386
|
+
'DATABASE_ERROR',
|
|
387
|
+
'EXTERNAL_ID_REQUIRED',
|
|
388
|
+
'TYPE_REQUIRED'
|
|
389
|
+
],
|
|
390
|
+
idempotent: true,
|
|
391
|
+
method: 'GET',
|
|
392
|
+
name: 'consents.check',
|
|
393
|
+
path: '/consents/check',
|
|
394
|
+
prepare: (input)=>({
|
|
395
|
+
params: {},
|
|
396
|
+
query: {
|
|
397
|
+
externalId: input.externalId,
|
|
398
|
+
type: input.types.join(',')
|
|
399
|
+
}
|
|
400
|
+
}),
|
|
401
|
+
validate: (input)=>{
|
|
402
|
+
if (!isObject(input)) return Promise.resolve(requireObject(input, []));
|
|
403
|
+
const issues = [
|
|
404
|
+
...requireText(input.externalId, [
|
|
405
|
+
'externalId'
|
|
406
|
+
]),
|
|
407
|
+
...checkTypeList(input.types, [
|
|
408
|
+
'types'
|
|
409
|
+
], {
|
|
410
|
+
required: true
|
|
411
|
+
})
|
|
412
|
+
];
|
|
413
|
+
if (issues.length > 0) return Promise.resolve(issues);
|
|
414
|
+
return checkSchema(checkConsentQuerySchema, {
|
|
415
|
+
externalId: input.externalId,
|
|
416
|
+
type: input.types.join(',')
|
|
417
|
+
});
|
|
32
418
|
}
|
|
33
|
-
|
|
34
|
-
|
|
419
|
+
});
|
|
420
|
+
const toSummaryQuery = (request)=>({
|
|
421
|
+
domain: request?.domain,
|
|
422
|
+
from: request?.from === void 0 ? void 0 : toIsoString(request.from),
|
|
423
|
+
to: request?.to === void 0 ? void 0 : toIsoString(request.to)
|
|
424
|
+
});
|
|
425
|
+
const experimentSummary = defineEndpoint({
|
|
426
|
+
auth: 'api-key',
|
|
427
|
+
decode: (body)=>{
|
|
428
|
+
const value = asObject(body, 'body');
|
|
429
|
+
asArray(value.arms, 'arms');
|
|
430
|
+
return value;
|
|
431
|
+
},
|
|
432
|
+
errorCodes: [
|
|
433
|
+
'DATABASE_ERROR',
|
|
434
|
+
'INPUT_VALIDATION_FAILED',
|
|
435
|
+
'UNAUTHORIZED'
|
|
436
|
+
],
|
|
437
|
+
idempotent: true,
|
|
438
|
+
method: 'GET',
|
|
439
|
+
name: 'experiments.summary',
|
|
440
|
+
path: '/experiments/:id/summary',
|
|
441
|
+
prepare: (input)=>({
|
|
442
|
+
params: {
|
|
443
|
+
id: input.id
|
|
444
|
+
},
|
|
445
|
+
query: toSummaryQuery(input.request)
|
|
446
|
+
}),
|
|
447
|
+
validate: (input)=>{
|
|
448
|
+
if (void 0 !== input.request && !isObject(input.request)) return Promise.resolve(requireObject(input.request, [
|
|
449
|
+
'options'
|
|
450
|
+
]));
|
|
451
|
+
const issues = [
|
|
452
|
+
...requirePathSegment(input.id, [
|
|
453
|
+
'id'
|
|
454
|
+
]),
|
|
455
|
+
...checkDateLike(input.request?.from, [
|
|
456
|
+
'from'
|
|
457
|
+
], {
|
|
458
|
+
required: false
|
|
459
|
+
}),
|
|
460
|
+
...checkDateLike(input.request?.to, [
|
|
461
|
+
'to'
|
|
462
|
+
], {
|
|
463
|
+
required: false
|
|
464
|
+
})
|
|
465
|
+
];
|
|
466
|
+
if (issues.length > 0) return Promise.resolve(issues);
|
|
467
|
+
const query = Object.fromEntries(Object.entries(toSummaryQuery(input.request)).filter(([, value])=>void 0 !== value));
|
|
468
|
+
return checkSchema(experimentSummaryQuerySchema, query);
|
|
35
469
|
}
|
|
36
|
-
|
|
470
|
+
});
|
|
471
|
+
const publishLegalDocument = defineEndpoint({
|
|
472
|
+
auth: 'api-key',
|
|
473
|
+
decode: (body)=>{
|
|
474
|
+
const value = asObject(body, 'body');
|
|
475
|
+
const policy = asObject(value.policy, 'policy');
|
|
37
476
|
return {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
477
|
+
...value,
|
|
478
|
+
policy: {
|
|
479
|
+
...policy,
|
|
480
|
+
effectiveDate: toDate(policy.effectiveDate, 'policy.effectiveDate')
|
|
481
|
+
}
|
|
43
482
|
};
|
|
483
|
+
},
|
|
484
|
+
errorCodes: [
|
|
485
|
+
'CONFLICT',
|
|
486
|
+
'DATABASE_ERROR',
|
|
487
|
+
'INPUT_VALIDATION_FAILED',
|
|
488
|
+
'UNAUTHORIZED'
|
|
489
|
+
],
|
|
490
|
+
idempotent: true,
|
|
491
|
+
method: 'PUT',
|
|
492
|
+
name: 'legalDocuments.publish',
|
|
493
|
+
path: '/legal-documents/:type/current',
|
|
494
|
+
prepare: (input)=>({
|
|
495
|
+
body: {
|
|
496
|
+
effectiveDate: toIsoString(input.input.effectiveDate),
|
|
497
|
+
hash: input.input.hash,
|
|
498
|
+
version: input.input.version
|
|
499
|
+
},
|
|
500
|
+
params: {
|
|
501
|
+
type: input.type
|
|
502
|
+
}
|
|
503
|
+
}),
|
|
504
|
+
validate: async (input)=>{
|
|
505
|
+
if (!isObject(input.input)) return requireObject(input.input, []);
|
|
506
|
+
const dateIssues = checkDateLike(input.input.effectiveDate, [
|
|
507
|
+
'effectiveDate'
|
|
508
|
+
], {
|
|
509
|
+
required: true
|
|
510
|
+
});
|
|
511
|
+
return [
|
|
512
|
+
...await checkSchema(legalDocumentCurrentParamsSchema, {
|
|
513
|
+
type: input.type
|
|
514
|
+
}, []),
|
|
515
|
+
...requireText(input.input.version, [
|
|
516
|
+
'version'
|
|
517
|
+
]),
|
|
518
|
+
...requireText(input.input.hash, [
|
|
519
|
+
'hash'
|
|
520
|
+
]),
|
|
521
|
+
...dateIssues,
|
|
522
|
+
...dateIssues.length > 0 ? [] : await checkSchema(legalDocumentCurrentInputSchema, {
|
|
523
|
+
effectiveDate: toIsoString(input.input.effectiveDate),
|
|
524
|
+
hash: input.input.hash,
|
|
525
|
+
version: input.input.version
|
|
526
|
+
})
|
|
527
|
+
];
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
const endpoints = {
|
|
531
|
+
checkConsent: checkConsent,
|
|
532
|
+
createSubject: createSubject,
|
|
533
|
+
experimentSummary: experimentSummary,
|
|
534
|
+
getSubject: getSubject,
|
|
535
|
+
identifySubject: identifySubject,
|
|
536
|
+
init: contract_init,
|
|
537
|
+
listSubjects: listSubjects,
|
|
538
|
+
manifest: manifest,
|
|
539
|
+
publishLegalDocument: publishLegalDocument,
|
|
540
|
+
status: contract_status
|
|
541
|
+
};
|
|
542
|
+
class C15tConfigurationError extends Error {
|
|
543
|
+
name = 'C15tConfigurationError';
|
|
544
|
+
issues;
|
|
545
|
+
constructor(issues){
|
|
546
|
+
const count = issues.length;
|
|
547
|
+
super([
|
|
548
|
+
`Invalid c15t client options: ${count} ${1 === count ? 'problem' : 'problems'}.`,
|
|
549
|
+
...issues.map((issue)=>`- ${issue.option}: ${issue.message}`)
|
|
550
|
+
].join('\n'));
|
|
551
|
+
this.issues = issues;
|
|
44
552
|
}
|
|
45
553
|
}
|
|
46
|
-
const
|
|
47
|
-
|
|
554
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
555
|
+
const MAX_RETRIES_LIMIT = 10;
|
|
556
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
557
|
+
const NO_RETRY = {
|
|
558
|
+
initialDelayMs: 0,
|
|
559
|
+
maxDelayMs: 0,
|
|
560
|
+
maxRetries: 0
|
|
48
561
|
};
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
562
|
+
const DEFAULT_RETRY = {
|
|
563
|
+
initialDelayMs: 200,
|
|
564
|
+
maxDelayMs: 5000,
|
|
565
|
+
maxRetries: 2
|
|
52
566
|
};
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
567
|
+
const LOOPBACK_HOSTS = new Set([
|
|
568
|
+
'localhost',
|
|
569
|
+
'127.0.0.1',
|
|
570
|
+
'[::1]'
|
|
571
|
+
]);
|
|
572
|
+
const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
|
|
573
|
+
const isNonNegativeInteger = (value)=>'number' == typeof value && Number.isInteger(value) && value >= 0;
|
|
574
|
+
const isTimerDelay = (value)=>isNonNegativeInteger(value) && value <= MAX_TIMER_DELAY_MS;
|
|
575
|
+
const isTimeout = (value)=>isTimerDelay(value) && value > 0;
|
|
576
|
+
const decodeBaseUrl = (value, hasApiKey, issues)=>{
|
|
577
|
+
if ('string' != typeof value && !(value instanceof URL)) return void issues.push({
|
|
578
|
+
message: 'Pass the backend URL as a string or URL.',
|
|
579
|
+
option: 'baseUrl'
|
|
580
|
+
});
|
|
581
|
+
let url;
|
|
582
|
+
try {
|
|
583
|
+
url = new URL(value);
|
|
584
|
+
} catch {
|
|
585
|
+
issues.push({
|
|
586
|
+
message: `"${String(value)}" is not an absolute URL. Include the scheme, such as https://example.com/api/c15t.`,
|
|
587
|
+
option: 'baseUrl'
|
|
588
|
+
});
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if ('https:' !== url.protocol && 'http:' !== url.protocol) return void issues.push({
|
|
592
|
+
message: `Use an http: or https: URL, not ${url.protocol}.`,
|
|
593
|
+
option: 'baseUrl'
|
|
594
|
+
});
|
|
595
|
+
if ('' !== url.search || '' !== url.hash) return void issues.push({
|
|
596
|
+
message: 'Remove the query string and fragment from the base URL.',
|
|
597
|
+
option: 'baseUrl'
|
|
598
|
+
});
|
|
599
|
+
if (hasApiKey && 'http:' === url.protocol && !LOOPBACK_HOSTS.has(url.hostname)) return void issues.push({
|
|
600
|
+
message: 'Use https: when sending an API key. Plain http: is accepted only for localhost, 127.0.0.1 and [::1], because anywhere else the key travels in the clear.',
|
|
601
|
+
option: 'baseUrl'
|
|
602
|
+
});
|
|
603
|
+
if (!url.pathname.endsWith('/')) url.pathname = `${url.pathname}/`;
|
|
604
|
+
return url;
|
|
70
605
|
};
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
606
|
+
const decodeHeaders = (value, option, issues)=>{
|
|
607
|
+
if (void 0 === value) return {};
|
|
608
|
+
if (!isRecord(value) || !Object.values(value).every((entry)=>'string' == typeof entry)) {
|
|
609
|
+
issues.push({
|
|
610
|
+
message: 'Pass headers as an object of string values.',
|
|
611
|
+
option
|
|
612
|
+
});
|
|
613
|
+
return {};
|
|
614
|
+
}
|
|
615
|
+
return value;
|
|
616
|
+
};
|
|
617
|
+
const options_decodeRetry = function(value, option, issues, fallback = DEFAULT_RETRY) {
|
|
618
|
+
if (void 0 === value) return fallback;
|
|
619
|
+
if (false === value) return NO_RETRY;
|
|
620
|
+
if (!isRecord(value)) {
|
|
621
|
+
issues.push({
|
|
622
|
+
message: 'Pass retry options as an object, or false to disable retries.',
|
|
623
|
+
option
|
|
624
|
+
});
|
|
625
|
+
return fallback;
|
|
626
|
+
}
|
|
627
|
+
const base = fallback === NO_RETRY ? DEFAULT_RETRY : fallback;
|
|
628
|
+
const maxRetries = value.maxRetries ?? base.maxRetries;
|
|
629
|
+
const initialDelayMs = value.initialDelayMs ?? base.initialDelayMs;
|
|
630
|
+
const maxDelayMs = value.maxDelayMs ?? base.maxDelayMs;
|
|
631
|
+
let valid = true;
|
|
632
|
+
if (!isNonNegativeInteger(maxRetries) || maxRetries > MAX_RETRIES_LIMIT) {
|
|
633
|
+
issues.push({
|
|
634
|
+
message: `Use an integer from 0 to ${MAX_RETRIES_LIMIT}.`,
|
|
635
|
+
option: `${option}.maxRetries`
|
|
636
|
+
});
|
|
637
|
+
valid = false;
|
|
638
|
+
}
|
|
639
|
+
if (!isTimerDelay(initialDelayMs)) {
|
|
640
|
+
issues.push({
|
|
641
|
+
message: `Use an integer from 0 to ${MAX_TIMER_DELAY_MS} milliseconds.`,
|
|
642
|
+
option: `${option}.initialDelayMs`
|
|
643
|
+
});
|
|
644
|
+
valid = false;
|
|
645
|
+
}
|
|
646
|
+
if (!isTimerDelay(maxDelayMs)) {
|
|
647
|
+
issues.push({
|
|
648
|
+
message: `Use an integer from 0 to ${MAX_TIMER_DELAY_MS} milliseconds.`,
|
|
649
|
+
option: `${option}.maxDelayMs`
|
|
650
|
+
});
|
|
651
|
+
valid = false;
|
|
652
|
+
}
|
|
653
|
+
if (!valid) return fallback;
|
|
654
|
+
return {
|
|
655
|
+
initialDelayMs: initialDelayMs,
|
|
656
|
+
maxDelayMs: maxDelayMs,
|
|
657
|
+
maxRetries: maxRetries
|
|
658
|
+
};
|
|
75
659
|
};
|
|
76
|
-
const
|
|
77
|
-
|
|
660
|
+
const options_decodeOptions = function(options) {
|
|
661
|
+
const issues = [];
|
|
662
|
+
if (!isRecord(options)) throw new C15tConfigurationError([
|
|
663
|
+
{
|
|
664
|
+
message: 'Pass an options object with at least baseUrl.',
|
|
665
|
+
option: 'options'
|
|
666
|
+
}
|
|
667
|
+
]);
|
|
668
|
+
let apiKey;
|
|
669
|
+
if (void 0 !== options.apiKey) if ('string' == typeof options.apiKey && '' !== options.apiKey.trim()) ({ apiKey } = options);
|
|
670
|
+
else issues.push({
|
|
671
|
+
message: 'Pass the API key as a non-empty string, or leave it out for a client without key-only methods.',
|
|
672
|
+
option: 'apiKey'
|
|
673
|
+
});
|
|
674
|
+
const baseUrl = decodeBaseUrl(options.baseUrl, void 0 !== options.apiKey, issues);
|
|
675
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
676
|
+
if ('function' != typeof fetchImpl) issues.push({
|
|
677
|
+
message: 'Pass a fetch function. This runtime has no global fetch, so it must be provided.',
|
|
678
|
+
option: 'fetch'
|
|
78
679
|
});
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
680
|
+
const headers = decodeHeaders(options.headers, 'headers', issues);
|
|
681
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
682
|
+
if (!isTimeout(timeoutMs)) issues.push({
|
|
683
|
+
message: `Use an integer from 1 to ${MAX_TIMER_DELAY_MS} milliseconds.`,
|
|
684
|
+
option: 'timeoutMs'
|
|
84
685
|
});
|
|
686
|
+
const retry = options_decodeRetry(options.retry, 'retry', issues);
|
|
687
|
+
if (void 0 !== options.onEvent && 'function' != typeof options.onEvent) issues.push({
|
|
688
|
+
message: 'Pass a function.',
|
|
689
|
+
option: 'onEvent'
|
|
690
|
+
});
|
|
691
|
+
if (issues.length > 0 || void 0 === baseUrl) throw new C15tConfigurationError(issues);
|
|
692
|
+
return {
|
|
693
|
+
apiKey,
|
|
694
|
+
baseUrl,
|
|
695
|
+
fetch: (input, init)=>fetchImpl(input, init),
|
|
696
|
+
headers,
|
|
697
|
+
onEvent: options.onEvent,
|
|
698
|
+
retry,
|
|
699
|
+
timeoutMs: timeoutMs
|
|
700
|
+
};
|
|
85
701
|
};
|
|
86
|
-
const
|
|
702
|
+
const options_decodeCallOptions = function(value, client, issues) {
|
|
703
|
+
const options = isRecord(value) ? value : {};
|
|
704
|
+
if (void 0 !== value && !isRecord(value)) issues.push({
|
|
705
|
+
message: 'Pass call options as an object.',
|
|
706
|
+
option: 'options'
|
|
707
|
+
});
|
|
708
|
+
const { signal } = options;
|
|
709
|
+
if (void 0 !== signal && !(signal instanceof AbortSignal)) issues.push({
|
|
710
|
+
message: 'Pass an AbortSignal.',
|
|
711
|
+
option: 'options.signal'
|
|
712
|
+
});
|
|
713
|
+
const timeoutMs = options.timeoutMs ?? client.timeoutMs;
|
|
714
|
+
if (!isTimeout(timeoutMs)) issues.push({
|
|
715
|
+
message: `Use an integer from 1 to ${MAX_TIMER_DELAY_MS} milliseconds.`,
|
|
716
|
+
option: 'options.timeoutMs'
|
|
717
|
+
});
|
|
718
|
+
const requestId = options.requestId ?? crypto.randomUUID();
|
|
719
|
+
if ('string' != typeof requestId || '' === requestId) issues.push({
|
|
720
|
+
message: 'Use a non-empty string.',
|
|
721
|
+
option: 'options.requestId'
|
|
722
|
+
});
|
|
87
723
|
return {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (!isSuccess || null === data) throw new C15TError({
|
|
92
|
-
cause: error?.cause,
|
|
93
|
-
code: error?.code,
|
|
94
|
-
details: error?.details,
|
|
95
|
-
message,
|
|
96
|
-
status: error?.status || 0
|
|
97
|
-
});
|
|
98
|
-
return data;
|
|
99
|
-
},
|
|
100
|
-
map (fn) {
|
|
101
|
-
if (!isSuccess || null === data) return createResponseContext(false, null, error, response);
|
|
102
|
-
return createResponseContext(true, fn(data), null, response);
|
|
103
|
-
},
|
|
104
|
-
ok: isSuccess,
|
|
105
|
-
response,
|
|
106
|
-
unwrap () {
|
|
107
|
-
if (!isSuccess || null === data) throw new C15TError({
|
|
108
|
-
cause: error?.cause,
|
|
109
|
-
code: error?.code,
|
|
110
|
-
details: error?.details,
|
|
111
|
-
message: error?.message || 'Request failed',
|
|
112
|
-
status: error?.status || 0
|
|
113
|
-
});
|
|
114
|
-
return data;
|
|
724
|
+
headers: {
|
|
725
|
+
...client.headers,
|
|
726
|
+
...decodeHeaders(options.headers, 'options.headers', issues)
|
|
115
727
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
728
|
+
requestId: 'string' == typeof requestId ? requestId : '',
|
|
729
|
+
retry: options_decodeRetry(options.retry, 'options.retry', issues, client.retry),
|
|
730
|
+
signal: signal instanceof AbortSignal ? signal : void 0,
|
|
731
|
+
timeoutMs: isTimeout(timeoutMs) ? timeoutMs : client.timeoutMs
|
|
120
732
|
};
|
|
121
733
|
};
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
734
|
+
const path_buildPath = function(template, params) {
|
|
735
|
+
const values = params;
|
|
736
|
+
return template.replace(/:(?<name>[A-Za-z]+)/gu, (_match, name)=>encodeURIComponent(values[name] ?? ''));
|
|
737
|
+
};
|
|
738
|
+
const version = '3.0.0-alpha.4';
|
|
739
|
+
const RETRYABLE_STATUSES = new Set([
|
|
740
|
+
408,
|
|
741
|
+
429,
|
|
742
|
+
500,
|
|
743
|
+
502,
|
|
744
|
+
503,
|
|
745
|
+
504
|
|
746
|
+
]);
|
|
747
|
+
const ownedHeaders = (options, requestId, hasBody)=>{
|
|
748
|
+
const headers = {
|
|
749
|
+
accept: 'application/json',
|
|
750
|
+
[POLICY_CONTRACT_HEADER]: String(POLICY_CONTRACT_VERSION),
|
|
751
|
+
'x-c15t-version': version,
|
|
752
|
+
'x-request-id': requestId
|
|
753
|
+
};
|
|
754
|
+
if (hasBody) headers['content-type'] = 'application/json';
|
|
755
|
+
if (void 0 !== options.apiKey) headers.authorization = `Bearer ${options.apiKey}`;
|
|
756
|
+
return headers;
|
|
126
757
|
};
|
|
127
|
-
const
|
|
758
|
+
const normalizeHeaders = (headers)=>Object.fromEntries(Object.entries(headers).map(([name, value])=>[
|
|
759
|
+
name.toLowerCase(),
|
|
760
|
+
value
|
|
761
|
+
]));
|
|
762
|
+
const buildUrl = (baseUrl, path, query)=>{
|
|
763
|
+
const url = new URL(path.replace(/^\//u, ''), baseUrl);
|
|
764
|
+
for (const [name, value] of Object.entries(query ?? {}))if (void 0 !== value) url.searchParams.set(name, value);
|
|
765
|
+
return url;
|
|
766
|
+
};
|
|
767
|
+
const emit = (options, event)=>{
|
|
128
768
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
769
|
+
options.onEvent?.(event);
|
|
770
|
+
} catch {}
|
|
771
|
+
};
|
|
772
|
+
const parseRetryAfter = (value)=>{
|
|
773
|
+
if (null === value) return;
|
|
774
|
+
const trimmed = value.trim();
|
|
775
|
+
if (/^\d+$/u.test(trimmed)) return 1000 * Number(trimmed);
|
|
776
|
+
const date = Date.parse(trimmed);
|
|
777
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
|
|
778
|
+
};
|
|
779
|
+
const backoff = (retry, attempt)=>Math.floor(Math.random() * Math.min(retry.maxDelayMs, retry.initialDelayMs * 2 ** attempt));
|
|
780
|
+
const wait = (ms, signal)=>new Promise((resolve)=>{
|
|
781
|
+
if (signal?.aborted) return void resolve(false);
|
|
782
|
+
const cleanup = new AbortController();
|
|
783
|
+
const timer = setTimeout(()=>{
|
|
784
|
+
cleanup.abort();
|
|
785
|
+
resolve(true);
|
|
786
|
+
}, ms);
|
|
787
|
+
signal?.addEventListener('abort', ()=>{
|
|
788
|
+
clearTimeout(timer);
|
|
789
|
+
resolve(false);
|
|
790
|
+
}, {
|
|
791
|
+
once: true,
|
|
792
|
+
signal: cleanup.signal
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
const readErrorBody = (text)=>{
|
|
796
|
+
let body;
|
|
797
|
+
try {
|
|
798
|
+
body = JSON.parse(text);
|
|
799
|
+
} catch {
|
|
800
|
+
return {};
|
|
143
801
|
}
|
|
802
|
+
if ('object' != typeof body || null === body) return {};
|
|
803
|
+
const { message, cause } = body;
|
|
804
|
+
const parsed = {};
|
|
805
|
+
if ('string' == typeof message) parsed.message = message;
|
|
806
|
+
if ('string' == typeof cause?.code) parsed.code = cause.code;
|
|
807
|
+
if (isStalePolicyReason(cause?.reason)) parsed.reason = cause.reason;
|
|
808
|
+
return parsed;
|
|
144
809
|
};
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
response: errorResponse,
|
|
162
|
-
retry: false
|
|
163
|
-
};
|
|
810
|
+
const failure = (error)=>({
|
|
811
|
+
error,
|
|
812
|
+
ok: false
|
|
813
|
+
});
|
|
814
|
+
const invalidInput = (endpoint, cause)=>{
|
|
815
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
816
|
+
return failure(new C15tError({
|
|
817
|
+
cause,
|
|
818
|
+
code: 'INVALID_INPUT',
|
|
819
|
+
issues: [
|
|
820
|
+
{
|
|
821
|
+
message
|
|
822
|
+
}
|
|
823
|
+
],
|
|
824
|
+
message: `Invalid input for ${endpoint.name}: ${message}`
|
|
825
|
+
}));
|
|
164
826
|
};
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
827
|
+
const aborted = (endpoint, requestId, cause)=>failure(new C15tError({
|
|
828
|
+
cause,
|
|
829
|
+
code: 'ABORTED',
|
|
830
|
+
message: `${endpoint.name} was aborted.`,
|
|
831
|
+
requestId
|
|
832
|
+
}));
|
|
833
|
+
const describeIssues = (issues)=>issues.slice(0, 5).map((issue)=>issue.path && issue.path.length > 0 ? `${issue.path.map(String).join('.')}: ${issue.message}` : issue.message).join('; ');
|
|
834
|
+
const decodeSuccess = (endpoint, response, text, requestId, input)=>{
|
|
835
|
+
try {
|
|
836
|
+
const body = '' === text ? null : JSON.parse(text);
|
|
837
|
+
const success = {
|
|
838
|
+
data: endpoint.decode(body, response, input),
|
|
839
|
+
headers: response.headers,
|
|
840
|
+
ok: true,
|
|
841
|
+
requestId,
|
|
172
842
|
status: response.status
|
|
173
|
-
}, response);
|
|
174
|
-
options?.onError?.(errorResponse, path);
|
|
175
|
-
if (options?.throw) throw new Error('Failed to parse response');
|
|
176
|
-
return {
|
|
177
|
-
response: errorResponse,
|
|
178
|
-
retry: false
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
if (response.ok) {
|
|
182
|
-
fetcher_debugLog(context.debug, requestMethod, path, durationMs, response.status);
|
|
183
|
-
const successResponse = fetcher_createResponseContext(true, data, null, response);
|
|
184
|
-
options?.onSuccess?.(successResponse);
|
|
185
|
-
return {
|
|
186
|
-
response: successResponse,
|
|
187
|
-
retry: false
|
|
188
843
|
};
|
|
844
|
+
return success;
|
|
845
|
+
} catch (error) {
|
|
846
|
+
return failure(new C15tError({
|
|
847
|
+
cause: error,
|
|
848
|
+
code: 'UNEXPECTED_RESPONSE',
|
|
849
|
+
message: error instanceof DecodeError ? `${endpoint.name} returned a body without the documented shape: ${error.message}` : `${endpoint.name} returned a body that is not JSON.`,
|
|
850
|
+
requestId,
|
|
851
|
+
status: response.status
|
|
852
|
+
}));
|
|
189
853
|
}
|
|
190
|
-
return processApiError(response, data, requestMethod, path, durationMs, context, options, retryableStatusCodes, nonRetryableStatusCodes, attemptsMade, maxRetries);
|
|
191
854
|
};
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
855
|
+
const classifyResponse = async (endpoint, response, requestId, input)=>{
|
|
856
|
+
const text = await response.text();
|
|
857
|
+
const accepts = endpoint.accepts ?? ((code)=>code >= 200 && code < 300);
|
|
858
|
+
if (accepts(response.status)) return {
|
|
859
|
+
kind: 'done',
|
|
860
|
+
result: decodeSuccess(endpoint, response, text, requestId, input)
|
|
861
|
+
};
|
|
862
|
+
const body = readErrorBody(text);
|
|
863
|
+
const retryable = RETRYABLE_STATUSES.has(response.status);
|
|
864
|
+
const declared = void 0 !== body.code && endpoint.errorCodes.includes(body.code);
|
|
865
|
+
const error = declared ? new C15tError({
|
|
866
|
+
code: body.code,
|
|
867
|
+
message: body.message ?? `Request failed with status ${response.status}.`,
|
|
868
|
+
reason: body.reason,
|
|
869
|
+
requestId,
|
|
870
|
+
retryable,
|
|
871
|
+
status: response.status
|
|
872
|
+
}) : new C15tError({
|
|
873
|
+
code: 'UNEXPECTED_RESPONSE',
|
|
874
|
+
message: body.message ?? `${endpoint.name} failed with status ${response.status}${void 0 === body.code ? '' : ` and undocumented code ${body.code}`}.`,
|
|
875
|
+
requestId,
|
|
876
|
+
retryable,
|
|
877
|
+
serverCode: body.code,
|
|
878
|
+
status: response.status
|
|
879
|
+
});
|
|
880
|
+
if (retryable && endpoint.idempotent) return {
|
|
881
|
+
error,
|
|
882
|
+
kind: 'retry',
|
|
883
|
+
retryAfterMs: parseRetryAfter(response.headers.get('retry-after'))
|
|
207
884
|
};
|
|
208
|
-
fetcher_debugLog(context.debug, requestMethod, path, Date.now() - startTime, 'ERROR');
|
|
209
|
-
options?.onError?.(errorResponse, path);
|
|
210
|
-
if (options?.throw) throw fetchError;
|
|
211
885
|
return {
|
|
212
|
-
|
|
213
|
-
|
|
886
|
+
kind: 'done',
|
|
887
|
+
result: failure(error)
|
|
214
888
|
};
|
|
215
889
|
};
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
890
|
+
const attemptOnce = async (plan, attempt)=>{
|
|
891
|
+
const { call, endpoint, options } = plan;
|
|
892
|
+
let timeout;
|
|
893
|
+
const eventBase = {
|
|
894
|
+
attempt,
|
|
895
|
+
method: endpoint.method,
|
|
896
|
+
path: endpoint.path,
|
|
897
|
+
requestId: call.requestId
|
|
221
898
|
};
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const requestId = fetcher_generateUUID();
|
|
237
|
-
const resolvedUrl = fetcher_resolveUrl(context.baseUrl, path);
|
|
238
|
-
const url = new URL(resolvedUrl);
|
|
239
|
-
if (options?.query) Object.entries(options.query).forEach(([key, value])=>{
|
|
240
|
-
if (null != value) url.searchParams.append(key, String(value));
|
|
899
|
+
const startedAt = Date.now();
|
|
900
|
+
emit(options, {
|
|
901
|
+
...eventBase,
|
|
902
|
+
type: 'request'
|
|
903
|
+
});
|
|
904
|
+
try {
|
|
905
|
+
timeout = AbortSignal.timeout(call.timeoutMs);
|
|
906
|
+
const signal = void 0 === call.signal ? timeout : AbortSignal.any([
|
|
907
|
+
call.signal,
|
|
908
|
+
timeout
|
|
909
|
+
]);
|
|
910
|
+
const response = await options.fetch(plan.url, {
|
|
911
|
+
...plan.init,
|
|
912
|
+
signal
|
|
241
913
|
});
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
method: options?.method || 'GET',
|
|
255
|
-
signal: controller.signal
|
|
914
|
+
const outcome = await classifyResponse(endpoint, response, call.requestId, plan.input);
|
|
915
|
+
emit(options, {
|
|
916
|
+
...eventBase,
|
|
917
|
+
durationMs: Date.now() - startedAt,
|
|
918
|
+
status: response.status,
|
|
919
|
+
type: 'response'
|
|
920
|
+
});
|
|
921
|
+
return outcome;
|
|
922
|
+
} catch (error) {
|
|
923
|
+
if (call.signal?.aborted) return {
|
|
924
|
+
kind: 'done',
|
|
925
|
+
result: aborted(endpoint, call.requestId, error)
|
|
256
926
|
};
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
const consent_checkConsent = function(context, query, options) {
|
|
279
|
-
return fetcher_fetcher(context, CONSENT_CHECK_PATH, {
|
|
280
|
-
method: 'GET',
|
|
281
|
-
query,
|
|
282
|
-
...options
|
|
283
|
-
});
|
|
284
|
-
};
|
|
285
|
-
const INIT_PATH = '/init';
|
|
286
|
-
const init_init = function(context, options) {
|
|
287
|
-
return fetcher_fetcher(context, INIT_PATH, {
|
|
288
|
-
method: 'GET',
|
|
289
|
-
...options
|
|
290
|
-
});
|
|
291
|
-
};
|
|
292
|
-
const STATUS_PATH = '/status';
|
|
293
|
-
const status_status = function(context, options) {
|
|
294
|
-
return fetcher_fetcher(context, STATUS_PATH, {
|
|
295
|
-
method: 'GET',
|
|
296
|
-
...options
|
|
297
|
-
});
|
|
298
|
-
};
|
|
299
|
-
const SUBJECTS_PATH = '/subjects';
|
|
300
|
-
const subjects_createSubject = function(context, input, options) {
|
|
301
|
-
return fetcher_fetcher(context, SUBJECTS_PATH, {
|
|
302
|
-
body: input,
|
|
303
|
-
method: 'POST',
|
|
304
|
-
...options
|
|
305
|
-
});
|
|
306
|
-
};
|
|
307
|
-
const subjects_getSubject = function(context, id, query, options) {
|
|
308
|
-
return fetcher_fetcher(context, `${SUBJECTS_PATH}/${id}`, {
|
|
309
|
-
method: 'GET',
|
|
310
|
-
query,
|
|
311
|
-
...options
|
|
312
|
-
});
|
|
927
|
+
if (void 0 === timeout) return {
|
|
928
|
+
kind: 'done',
|
|
929
|
+
result: invalidInput(endpoint, error)
|
|
930
|
+
};
|
|
931
|
+
const timedOut = timeout.aborted;
|
|
932
|
+
const transportError = new C15tError({
|
|
933
|
+
cause: error,
|
|
934
|
+
code: timedOut ? 'TIMEOUT' : 'NETWORK_ERROR',
|
|
935
|
+
message: timedOut ? `${endpoint.name} timed out after ${call.timeoutMs}ms.` : `${endpoint.name} failed before a response arrived: ${error instanceof Error ? error.message : String(error)}`,
|
|
936
|
+
requestId: call.requestId,
|
|
937
|
+
retryable: true
|
|
938
|
+
});
|
|
939
|
+
return endpoint.idempotent ? {
|
|
940
|
+
error: transportError,
|
|
941
|
+
kind: 'retry',
|
|
942
|
+
retryAfterMs: void 0
|
|
943
|
+
} : {
|
|
944
|
+
kind: 'done',
|
|
945
|
+
result: failure(transportError)
|
|
946
|
+
};
|
|
947
|
+
}
|
|
313
948
|
};
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
949
|
+
const runAttempts = async (plan, attempt)=>{
|
|
950
|
+
const { call, endpoint, options } = plan;
|
|
951
|
+
if (call.signal?.aborted) return aborted(endpoint, call.requestId, call.signal.reason);
|
|
952
|
+
const outcome = await attemptOnce(plan, attempt);
|
|
953
|
+
if ('done' === outcome.kind) return outcome.result;
|
|
954
|
+
const delayMs = outcome.retryAfterMs ?? backoff(call.retry, attempt);
|
|
955
|
+
if (attempt >= call.retry.maxRetries || delayMs > call.retry.maxDelayMs) return failure(outcome.error);
|
|
956
|
+
emit(options, {
|
|
957
|
+
attempt,
|
|
958
|
+
delayMs,
|
|
959
|
+
error: outcome.error,
|
|
960
|
+
method: endpoint.method,
|
|
961
|
+
path: endpoint.path,
|
|
962
|
+
requestId: call.requestId,
|
|
963
|
+
type: 'retry'
|
|
319
964
|
});
|
|
965
|
+
if (!await wait(delayMs, call.signal)) return aborted(endpoint, call.requestId, call.signal?.reason);
|
|
966
|
+
return runAttempts(plan, attempt + 1);
|
|
320
967
|
};
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
968
|
+
const collectIssues = async (endpoint, input, optionIssues)=>{
|
|
969
|
+
const fromOptions = optionIssues.map((issue)=>({
|
|
970
|
+
message: issue.message,
|
|
971
|
+
path: issue.option.split('.')
|
|
972
|
+
}));
|
|
973
|
+
try {
|
|
974
|
+
return [
|
|
975
|
+
...fromOptions,
|
|
976
|
+
...await endpoint.validate(input)
|
|
977
|
+
];
|
|
978
|
+
} catch (error) {
|
|
979
|
+
return [
|
|
980
|
+
...fromOptions,
|
|
981
|
+
{
|
|
982
|
+
message: error instanceof Error ? error.message : String(error)
|
|
983
|
+
}
|
|
984
|
+
];
|
|
985
|
+
}
|
|
327
986
|
};
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
this.context = {
|
|
346
|
-
baseUrl: baseUrl.toString(),
|
|
347
|
-
debug,
|
|
987
|
+
const transport_send = async function(options, endpoint, input, rawCallOptions) {
|
|
988
|
+
try {
|
|
989
|
+
const optionIssues = [];
|
|
990
|
+
const call = options_decodeCallOptions(rawCallOptions, options, optionIssues);
|
|
991
|
+
const issues = await collectIssues(endpoint, input, optionIssues);
|
|
992
|
+
if (issues.length > 0) return failure(new C15tError({
|
|
993
|
+
code: 'INVALID_INPUT',
|
|
994
|
+
issues,
|
|
995
|
+
message: `Invalid input for ${endpoint.name}: ${describeIssues(issues)}`
|
|
996
|
+
}));
|
|
997
|
+
if ('api-key' === endpoint.auth && void 0 === options.apiKey) return failure(new C15tError({
|
|
998
|
+
code: 'MISSING_API_KEY',
|
|
999
|
+
message: `${endpoint.name} needs an API key. Pass apiKey to createC15tClient.`
|
|
1000
|
+
}));
|
|
1001
|
+
const prepared = endpoint.prepare(input);
|
|
1002
|
+
const hasBody = void 0 !== prepared.body;
|
|
1003
|
+
const init = {
|
|
348
1004
|
headers: {
|
|
349
|
-
...
|
|
350
|
-
...
|
|
1005
|
+
...normalizeHeaders(call.headers),
|
|
1006
|
+
...normalizeHeaders(prepared.headers ?? {}),
|
|
1007
|
+
...ownedHeaders(options, call.requestId, hasBody)
|
|
351
1008
|
},
|
|
352
|
-
|
|
353
|
-
timeout
|
|
1009
|
+
method: endpoint.method
|
|
354
1010
|
};
|
|
1011
|
+
if (hasBody) init.body = JSON.stringify(prepared.body);
|
|
1012
|
+
return runAttempts({
|
|
1013
|
+
call,
|
|
1014
|
+
endpoint: endpoint,
|
|
1015
|
+
init,
|
|
1016
|
+
input,
|
|
1017
|
+
options,
|
|
1018
|
+
url: buildUrl(options.baseUrl, path_buildPath(endpoint.path, prepared.params), prepared.query)
|
|
1019
|
+
}, 0);
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
return invalidInput(endpoint, error);
|
|
355
1022
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
init: (options)=>this.init(options),
|
|
391
|
-
status: (options)=>this.status(options)
|
|
1023
|
+
};
|
|
1024
|
+
function createC15tClient(options) {
|
|
1025
|
+
const resolved = options_decodeOptions(options);
|
|
1026
|
+
return {
|
|
1027
|
+
consents: {
|
|
1028
|
+
check: (input, callOptions)=>transport_send(resolved, endpoints.checkConsent, input, callOptions)
|
|
1029
|
+
},
|
|
1030
|
+
experiments: {
|
|
1031
|
+
summary: (id, request, callOptions)=>transport_send(resolved, endpoints.experimentSummary, {
|
|
1032
|
+
id,
|
|
1033
|
+
request
|
|
1034
|
+
}, callOptions)
|
|
1035
|
+
},
|
|
1036
|
+
init: (request, callOptions)=>transport_send(resolved, endpoints.init, request, callOptions),
|
|
1037
|
+
legalDocuments: {
|
|
1038
|
+
publish: (type, input, callOptions)=>transport_send(resolved, endpoints.publishLegalDocument, {
|
|
1039
|
+
input,
|
|
1040
|
+
type
|
|
1041
|
+
}, callOptions)
|
|
1042
|
+
},
|
|
1043
|
+
manifest: (request, callOptions)=>transport_send(resolved, endpoints.manifest, request, callOptions),
|
|
1044
|
+
status: (callOptions)=>transport_send(resolved, endpoints.status, void 0, callOptions),
|
|
1045
|
+
subjects: {
|
|
1046
|
+
create: (input, callOptions)=>transport_send(resolved, endpoints.createSubject, input, callOptions),
|
|
1047
|
+
get: (id, request, callOptions)=>transport_send(resolved, endpoints.getSubject, {
|
|
1048
|
+
id,
|
|
1049
|
+
request
|
|
1050
|
+
}, callOptions),
|
|
1051
|
+
identify: (id, input, callOptions)=>transport_send(resolved, endpoints.identifySubject, {
|
|
1052
|
+
id,
|
|
1053
|
+
input
|
|
1054
|
+
}, callOptions),
|
|
1055
|
+
list: (query, callOptions)=>transport_send(resolved, endpoints.listSubjects, query, callOptions)
|
|
1056
|
+
}
|
|
392
1057
|
};
|
|
393
1058
|
}
|
|
394
|
-
const
|
|
395
|
-
|
|
1059
|
+
const result_unwrap = function(result) {
|
|
1060
|
+
if (result.ok) return result.data;
|
|
1061
|
+
throw result.error;
|
|
396
1062
|
};
|
|
397
|
-
export {
|
|
1063
|
+
export { C15T_API_ERROR_CODES, C15T_CLIENT_ERROR_CODES, C15tError, isC15tError } from "./160.js";
|
|
1064
|
+
export { C15tConfigurationError, createC15tClient, result_unwrap as unwrap };
|