fiftyone.pipeline.did 4.5.34 → 4.5.35
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/didClient.js +849 -0
- package/examples/creator-context-web/examples-main.min.css +1 -0
- package/examples/creator-context-web/page.html +453 -0
- package/examples/creator-context-web/server.js +277 -0
- package/examples/fodIdExample.js +31 -7
- package/fodId.js +454 -73
- package/fodIdParseError.js +45 -0
- package/index.js +26 -1
- package/node_modules/owid/.github/workflows/ci.yml +23 -0
- package/node_modules/owid/LICENSE +201 -0
- package/node_modules/owid/README.md +447 -0
- package/node_modules/owid/owid.crypto.test.js +359 -0
- package/node_modules/owid/owid.frame.test.js +565 -0
- package/node_modules/owid/owid.interop.test.js +244 -0
- package/node_modules/owid/owid.parse-contract.test.js +720 -0
- package/node_modules/owid/owid.payload-length.test.js +485 -0
- package/node_modules/owid/owid.status-coverage.test.js +158 -0
- package/node_modules/owid/owid.test.js +382 -0
- package/node_modules/owid/package.json +22 -0
- package/node_modules/owid/setupJest.js +1 -0
- package/node_modules/owid/v1.js +1487 -0
- package/package.json +9 -4
- package/readme.md +459 -31
- package/tests/creatorContextServer.test.js +191 -0
- package/tests/didClient.integration.test.js +95 -0
- package/tests/didClient.test.js +912 -0
- package/tests/envelope.js +175 -0
- package/tests/fodId.test.js +535 -134
- package/tsconfig.json +22 -0
- package/types/didClient.d.ts +428 -0
- package/types/fodId.d.ts +306 -0
- package/types/fodIdParseError.d.ts +19 -0
- package/types/idType.d.ts +29 -0
- package/types/index.d.ts +13 -0
package/didClient.js
ADDED
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
/* *********************************************************************
|
|
2
|
+
* This Original Work is copyright of 51 Degrees Mobile Experts Limited.
|
|
3
|
+
* Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House,
|
|
4
|
+
* Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU.
|
|
5
|
+
*
|
|
6
|
+
* This Original Work is licensed under the European Union Public Licence
|
|
7
|
+
* (EUPL) v.1.2 and is subject to its terms as set out below.
|
|
8
|
+
*
|
|
9
|
+
* If a copy of the EUPL was not distributed with this file, You can obtain
|
|
10
|
+
* one at https://opensource.org/licenses/EUPL-1.2.
|
|
11
|
+
*
|
|
12
|
+
* The 'Compatible Licences' set out in the Appendix to the EUPL (as may be
|
|
13
|
+
* amended by the European Commission) shall be deemed incompatible for
|
|
14
|
+
* the purposes of the Work and the provisions of the compatibility
|
|
15
|
+
* clause in Article 5 of the EUPL shall not apply.
|
|
16
|
+
*
|
|
17
|
+
* If using the Work as, or as part of, a network application, by
|
|
18
|
+
* including the attribution notice(s) required under Article 5 of the EUPL
|
|
19
|
+
* in the end user terms of the application under an appropriate heading,
|
|
20
|
+
* such notice(s) shall fulfill the requirements of that article.
|
|
21
|
+
* ********************************************************************* */
|
|
22
|
+
|
|
23
|
+
const FodId = require('./fodId');
|
|
24
|
+
const IdType = require('./idType');
|
|
25
|
+
const packageVersion = require('./package.json').version;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The public cloud API base, used when neither the endpoint option nor the
|
|
29
|
+
* FOD_CLOUD_API_URL environment variable is set.
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_ENDPOINT = 'https://cloud.51degrees.com/api/v4/';
|
|
32
|
+
|
|
33
|
+
/** Sent with every request so the cloud can tell which package called. */
|
|
34
|
+
const USER_AGENT = 'fiftyone.pipeline.did/' + packageVersion;
|
|
35
|
+
|
|
36
|
+
/** The OWID date field counts minutes from this moment. */
|
|
37
|
+
const OWID_EPOCH_MS = Date.UTC(2020, 0, 1);
|
|
38
|
+
const MINUTE_MS = 60 * 1000;
|
|
39
|
+
|
|
40
|
+
const BOUNDARY_TOLERANCE_MS = 15 * MINUTE_MS;
|
|
41
|
+
|
|
42
|
+
/** A cached key list older than this is fetched again before use. */
|
|
43
|
+
const KEY_LIST_MAX_AGE_MS = 24 * 60 * MINUTE_MS;
|
|
44
|
+
|
|
45
|
+
/** The only envelope version the cloud signs and verifies. */
|
|
46
|
+
const SUPPORTED_VERSION = 3;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The longest encoded identifier the client will look at. The figure is
|
|
50
|
+
* arbitrary and deliberately generous, far above anything the cloud issues,
|
|
51
|
+
* because its only job is to turn obviously malformed input away before the
|
|
52
|
+
* client decodes it, fetches a key or calls the cloud. It says nothing about
|
|
53
|
+
* how long a 51Did is, and a value under it is still left to the cloud to
|
|
54
|
+
* judge.
|
|
55
|
+
*/
|
|
56
|
+
const MAXIMUM_ENCODED_LENGTH = 4096;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The creator context outcome of a redemption, as the cloud reports it in
|
|
60
|
+
* the `context` field. The values are the cloud's own strings, so a result
|
|
61
|
+
* can be compared to a constant or printed as received.
|
|
62
|
+
*/
|
|
63
|
+
const ContextResult = Object.freeze({
|
|
64
|
+
/** Every factor matched the browser and connection that created it. */
|
|
65
|
+
VERIFIED: 'verified',
|
|
66
|
+
/** At least one factor differed. `factors` says which. */
|
|
67
|
+
MISMATCH: 'mismatch',
|
|
68
|
+
/** The identifier carries no creator context. */
|
|
69
|
+
NO_CONTEXT: 'nocontext',
|
|
70
|
+
/** The service holds no secret covering the identifier's date. */
|
|
71
|
+
NOT_CHECKABLE: 'notcheckable',
|
|
72
|
+
/** The sealed result was redeemed outside the freshness window. */
|
|
73
|
+
EXPIRED: 'expired',
|
|
74
|
+
/** The sealed result had already been redeemed on that instance. */
|
|
75
|
+
REPLAYED: 'replayed',
|
|
76
|
+
/**
|
|
77
|
+
* The sealed result could not be read. Every cryptographic failure, a
|
|
78
|
+
* missing licence key included, comes back as this one word by design,
|
|
79
|
+
* and a context string this package does not know is mapped here too.
|
|
80
|
+
*/
|
|
81
|
+
UNREADABLE: 'unreadable',
|
|
82
|
+
/** First use could not be confirmed (503). The caller may retry. */
|
|
83
|
+
UNCONFIRMED: 'unconfirmed'
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const KNOWN_CONTEXTS = new Set(Object.values(ContextResult));
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The signature outcome of a redemption, as the cloud reports it in the
|
|
90
|
+
* `signature` field of a redeemed result. Absent on every other outcome.
|
|
91
|
+
*/
|
|
92
|
+
const SignatureResult = Object.freeze({
|
|
93
|
+
VERIFIED: 'verified',
|
|
94
|
+
INVALID: 'invalid',
|
|
95
|
+
/** The cloud did not report the signature, as on an expired result. */
|
|
96
|
+
UNKNOWN: 'unknown'
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The outcome of one factor in a mismatch. The cloud reports `null` for a
|
|
101
|
+
* factor that was not compared, which is passed through unchanged.
|
|
102
|
+
*/
|
|
103
|
+
const FactorResult = Object.freeze({
|
|
104
|
+
VERIFIED: 'verified',
|
|
105
|
+
MISMATCH: 'mismatch'
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The reason a {@link DidClient#verifySignatureDetailed} answer was given.
|
|
110
|
+
*/
|
|
111
|
+
const SignatureReason = Object.freeze({
|
|
112
|
+
/** A candidate key verified the signature. */
|
|
113
|
+
VERIFIED: 'verified',
|
|
114
|
+
/** The envelope version is not the one the cloud signs. */
|
|
115
|
+
VERSION: 'version',
|
|
116
|
+
/** The payload is shorter than the base length for its type. */
|
|
117
|
+
LENGTH: 'length',
|
|
118
|
+
/** No published key covers the identifier's date. */
|
|
119
|
+
NO_KEY: 'nokey',
|
|
120
|
+
/** Every candidate key was tried and none verified the signature. */
|
|
121
|
+
SIGNATURE: 'signature'
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* An answer from the cloud that was not the one asked for. Carries the HTTP
|
|
126
|
+
* status and the response body so a caller can relay or log what the cloud
|
|
127
|
+
* said.
|
|
128
|
+
*/
|
|
129
|
+
class DidClientError extends Error {
|
|
130
|
+
/**
|
|
131
|
+
* Builds the error with the status and body the cloud answered.
|
|
132
|
+
* @param {string} message what went wrong
|
|
133
|
+
* @param {number} [statusCode] the HTTP status, where there was one
|
|
134
|
+
* @param {string} [body] the response body, where there was one
|
|
135
|
+
*/
|
|
136
|
+
constructor (message, statusCode, body) {
|
|
137
|
+
super(message);
|
|
138
|
+
this.name = 'DidClientError';
|
|
139
|
+
this.statusCode = statusCode;
|
|
140
|
+
this.body = body;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The 51Did argument is invalid, either when checked locally or refused by
|
|
146
|
+
* the cloud (HTTP 400 with an `errors` list).
|
|
147
|
+
*/
|
|
148
|
+
class DidArgumentError extends DidClientError {
|
|
149
|
+
/**
|
|
150
|
+
* Builds the error from the validation message.
|
|
151
|
+
* @param {string} message the validation message
|
|
152
|
+
* @param {number} [statusCode] the HTTP status
|
|
153
|
+
* @param {string} [body] the response body
|
|
154
|
+
*/
|
|
155
|
+
constructor (message, statusCode, body) {
|
|
156
|
+
super(message, statusCode, body);
|
|
157
|
+
this.name = 'DidArgumentError';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The host answering does not offer the creator context (HTTP 404 from the
|
|
163
|
+
* redeem endpoint). A caller can name this case rather than treat it as a
|
|
164
|
+
* failed check.
|
|
165
|
+
*/
|
|
166
|
+
class DidNotSupportedError extends DidClientError {
|
|
167
|
+
/**
|
|
168
|
+
* Builds the error from what the host said.
|
|
169
|
+
* @param {string} message what the host said
|
|
170
|
+
* @param {number} [statusCode] the HTTP status
|
|
171
|
+
* @param {string} [body] the response body
|
|
172
|
+
*/
|
|
173
|
+
constructor (message, statusCode, body) {
|
|
174
|
+
super(message, statusCode, body);
|
|
175
|
+
this.name = 'DidNotSupportedError';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The typed answer to a redemption. Built from the cloud's JSON body, with
|
|
181
|
+
* the raw status and body kept for logging.
|
|
182
|
+
*/
|
|
183
|
+
class RedeemResult {
|
|
184
|
+
/**
|
|
185
|
+
* Reads the typed fields out of the parsed body.
|
|
186
|
+
* @param {number} statusCode the HTTP status, 200 or 503
|
|
187
|
+
* @param {string} raw the response body as received
|
|
188
|
+
* @param {object} parsed the body parsed as JSON
|
|
189
|
+
*/
|
|
190
|
+
constructor (statusCode, raw, parsed) {
|
|
191
|
+
/** @type {number} the HTTP status the cloud answered with */
|
|
192
|
+
this.statusCode = statusCode;
|
|
193
|
+
/** @type {string} the response body exactly as received */
|
|
194
|
+
this.raw = raw;
|
|
195
|
+
const context = typeof parsed.context === 'string' ? parsed.context : '';
|
|
196
|
+
/**
|
|
197
|
+
* @type {string} the `context` string exactly as the cloud sent it,
|
|
198
|
+
* kept so an outcome this package does not know is still visible
|
|
199
|
+
*/
|
|
200
|
+
this.contextRaw = context;
|
|
201
|
+
/**
|
|
202
|
+
* @type {string} one of {@link ContextResult}. A string this package
|
|
203
|
+
* does not know maps to `unreadable`, so an unrecognised outcome is
|
|
204
|
+
* never mistaken for a good one.
|
|
205
|
+
*/
|
|
206
|
+
this.context = KNOWN_CONTEXTS.has(context)
|
|
207
|
+
? context
|
|
208
|
+
: ContextResult.UNREADABLE;
|
|
209
|
+
/** @type {string} one of {@link SignatureResult} */
|
|
210
|
+
this.signature = parsed.signature === SignatureResult.VERIFIED
|
|
211
|
+
? SignatureResult.VERIFIED
|
|
212
|
+
: parsed.signature === SignatureResult.INVALID
|
|
213
|
+
? SignatureResult.INVALID
|
|
214
|
+
: SignatureResult.UNKNOWN;
|
|
215
|
+
/**
|
|
216
|
+
* @type {object | undefined} factor name to {@link FactorResult} value
|
|
217
|
+
* (or null where nothing was compared), present only when the cloud
|
|
218
|
+
* sent `factors`, which is the mismatch outcome. The names are
|
|
219
|
+
* transport, device, browserip, connectionip, asn and browser.
|
|
220
|
+
*/
|
|
221
|
+
this.factors = parsed.factors && typeof parsed.factors === 'object'
|
|
222
|
+
? Object.freeze(Object.assign({}, parsed.factors))
|
|
223
|
+
: undefined;
|
|
224
|
+
const verifiedAt = typeof parsed.verifiedAt === 'string'
|
|
225
|
+
? new Date(parsed.verifiedAt)
|
|
226
|
+
: null;
|
|
227
|
+
/**
|
|
228
|
+
* @type {Date | undefined} when the verify endpoint checked the context
|
|
229
|
+
* and sealed the result, present on the redeemed and expired outcomes
|
|
230
|
+
*/
|
|
231
|
+
this.verifiedAt = verifiedAt && !isNaN(verifiedAt.getTime())
|
|
232
|
+
? verifiedAt
|
|
233
|
+
: undefined;
|
|
234
|
+
/**
|
|
235
|
+
* @type {number | undefined} whole seconds between the sealing and this
|
|
236
|
+
* redemption by the cloud's clock, present on the redeemed and expired
|
|
237
|
+
* outcomes
|
|
238
|
+
*/
|
|
239
|
+
this.secondsSinceVerified = Number.isFinite(parsed.secondsSinceVerified)
|
|
240
|
+
? parsed.secondsSinceVerified
|
|
241
|
+
: undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Builds a result from a redeem response body.
|
|
246
|
+
* @param {number} statusCode the HTTP status
|
|
247
|
+
* @param {string} raw the response body
|
|
248
|
+
* @returns {RedeemResult} the typed result
|
|
249
|
+
*/
|
|
250
|
+
static fromResponse (statusCode, raw) {
|
|
251
|
+
const parsed = tryParseJson(raw);
|
|
252
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
253
|
+
throw new DidClientError(
|
|
254
|
+
`Redeem answered HTTP ${statusCode} with a body that is not a ` +
|
|
255
|
+
`JSON object: ${raw}`, statusCode, raw);
|
|
256
|
+
}
|
|
257
|
+
return new RedeemResult(statusCode, raw, parsed);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The result in the cloud's own response shape (`signature`, `context`,
|
|
262
|
+
* `factors` when present, `verifiedAt`, `secondsSinceVerified`), so a
|
|
263
|
+
* server can answer a page with it directly. `signature` is left out when
|
|
264
|
+
* the cloud did not report it, as the cloud leaves it out.
|
|
265
|
+
* @returns {object} the plain object for JSON.stringify
|
|
266
|
+
*/
|
|
267
|
+
toJSON () {
|
|
268
|
+
const body = {};
|
|
269
|
+
if (this.signature !== SignatureResult.UNKNOWN) {
|
|
270
|
+
body.signature = this.signature;
|
|
271
|
+
}
|
|
272
|
+
body.context = this.context;
|
|
273
|
+
if (this.factors !== undefined) {
|
|
274
|
+
body.factors = this.factors;
|
|
275
|
+
}
|
|
276
|
+
if (this.verifiedAt !== undefined) {
|
|
277
|
+
// ISO 8601 UTC to the second, as the cloud writes it.
|
|
278
|
+
body.verifiedAt = this.verifiedAt.toISOString().replace(/\.\d+Z$/, 'Z');
|
|
279
|
+
}
|
|
280
|
+
if (this.secondsSinceVerified !== undefined) {
|
|
281
|
+
body.secondsSinceVerified = this.secondsSinceVerified;
|
|
282
|
+
}
|
|
283
|
+
return body;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* A function with the shape of the global `fetch`, taking a URL and an
|
|
289
|
+
* options object with `method`, `headers` and `body`, and resolving to a
|
|
290
|
+
* response with `status` and a `text()` method. Node 18 and later provide
|
|
291
|
+
* it globally, and tests inject one.
|
|
292
|
+
* @callback FetchFunction
|
|
293
|
+
* @param {string} url the absolute URL to request
|
|
294
|
+
* @param {object} init the request options
|
|
295
|
+
* @returns {Promise<{status: number, text: function(): Promise<string>}>}
|
|
296
|
+
* the response
|
|
297
|
+
*/
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* A published signing key and the moment it came into force. A key stays in
|
|
301
|
+
* force until the next key starts.
|
|
302
|
+
* @typedef {object} PublicKeyEntry
|
|
303
|
+
* @property {Date} startsAt when the key came, or comes, into force
|
|
304
|
+
* @property {string} publicKey the key in SPKI PEM form
|
|
305
|
+
*/
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The detailed answer to an offline signature check.
|
|
309
|
+
* @typedef {object} SignatureCheck
|
|
310
|
+
* @property {boolean} valid whether a candidate key verified the signature
|
|
311
|
+
* @property {string} reason one of {@link SignatureReason}
|
|
312
|
+
*/
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Options for {@link DidClient}.
|
|
316
|
+
* @typedef {object} DidClientOptions
|
|
317
|
+
* @property {string} resourceKey the page's resource key. Required. Public
|
|
318
|
+
* by nature, it travels in the route of the key and verify requests and in
|
|
319
|
+
* the form body of the redeem request.
|
|
320
|
+
* @property {string} [licenceKey] a licence key of the same account. Server
|
|
321
|
+
* side only. Needed to redeem where the account holds licence keys, and
|
|
322
|
+
* sent only in the body of the redeem request, never in a URL.
|
|
323
|
+
* @property {string} [endpoint] the API base including the `/api/v4/`
|
|
324
|
+
* segment. Defaults to the FOD_CLOUD_API_URL environment variable, then
|
|
325
|
+
* to the public cloud. A value without a trailing slash gains one.
|
|
326
|
+
* @property {FetchFunction} [fetch] the HTTP transport. Defaults to the
|
|
327
|
+
* global `fetch`.
|
|
328
|
+
* @property {function(): number} [now] the clock, as milliseconds since the
|
|
329
|
+
* Unix epoch. Defaults to `Date.now`. Tests inject one.
|
|
330
|
+
*/
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Everything a server does with a 51Did against the 51Degrees cloud: fetch
|
|
334
|
+
* the signing public keys and pick the one in force when an identifier was
|
|
335
|
+
* created, verify a signature offline against it, verify a signature
|
|
336
|
+
* through the cloud, and redeem a sealed creator context result with the
|
|
337
|
+
* licence key.
|
|
338
|
+
*
|
|
339
|
+
* Creating a 51Did is not part of this client. Creation is the cloud `json`
|
|
340
|
+
* endpoint through the cloud request engine and pipeline, and a page
|
|
341
|
+
* creates from the browser because the identifier describes the browser's
|
|
342
|
+
* own connection. The verify-context and verify-full endpoints are browser
|
|
343
|
+
* calls for the same reason and are not offered here.
|
|
344
|
+
*
|
|
345
|
+
* The public key list is cached per instance with the time it was fetched.
|
|
346
|
+
* One instance can serve a whole server.
|
|
347
|
+
*/
|
|
348
|
+
class DidClient {
|
|
349
|
+
/**
|
|
350
|
+
* @param {DidClientOptions} options the resource key, and optionally the
|
|
351
|
+
* licence key, endpoint, transport and clock
|
|
352
|
+
*/
|
|
353
|
+
constructor (options) {
|
|
354
|
+
if (!options || typeof options.resourceKey !== 'string' ||
|
|
355
|
+
options.resourceKey.length === 0) {
|
|
356
|
+
throw new TypeError('resourceKey is required');
|
|
357
|
+
}
|
|
358
|
+
this._resourceKey = options.resourceKey;
|
|
359
|
+
this._licenceKey = typeof options.licenceKey === 'string' &&
|
|
360
|
+
options.licenceKey.length > 0
|
|
361
|
+
? options.licenceKey
|
|
362
|
+
: null;
|
|
363
|
+
const endpoint = options.endpoint || process.env.FOD_CLOUD_API_URL ||
|
|
364
|
+
DEFAULT_ENDPOINT;
|
|
365
|
+
// Normalised to end in exactly one slash so every URL is the base plus
|
|
366
|
+
// a relative path, as the cloud request engine treats the same value.
|
|
367
|
+
this._endpoint = endpoint.replace(/\/*$/, '/');
|
|
368
|
+
const fetchFunction = options.fetch || globalThis.fetch;
|
|
369
|
+
if (typeof fetchFunction !== 'function') {
|
|
370
|
+
throw new TypeError('No fetch function is available. Run on Node 18 ' +
|
|
371
|
+
'or later, or pass one as options.fetch.');
|
|
372
|
+
}
|
|
373
|
+
/** @type {FetchFunction} */
|
|
374
|
+
this._fetch = fetchFunction;
|
|
375
|
+
this._now = typeof options.now === 'function'
|
|
376
|
+
? options.now
|
|
377
|
+
: () => Date.now();
|
|
378
|
+
/** @type {PublicKeyEntry[] | null} */
|
|
379
|
+
this._keys = null;
|
|
380
|
+
/** @type {number | null} */
|
|
381
|
+
this._fetchedAt = null;
|
|
382
|
+
/** @type {Promise<PublicKeyEntry[]> | null} */
|
|
383
|
+
this._pending = null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** @returns {string} the API base every request is built on */
|
|
387
|
+
get endpoint () {
|
|
388
|
+
return this._endpoint;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** @returns {string} the resource key the requests carry */
|
|
392
|
+
get resourceKey () {
|
|
393
|
+
return this._resourceKey;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The published signing keys, oldest first, fetched on first use and
|
|
398
|
+
* then served from the cache until the list is a day old. Keys are
|
|
399
|
+
* published up to three months ahead of their start, so the list holds
|
|
400
|
+
* entries that have not started yet.
|
|
401
|
+
* @returns {Promise<PublicKeyEntry[]>} the keys, oldest start first
|
|
402
|
+
*/
|
|
403
|
+
publicKeys () {
|
|
404
|
+
if (this._keys !== null && !this._stale()) {
|
|
405
|
+
return Promise.resolve(this._keys);
|
|
406
|
+
}
|
|
407
|
+
return this._refresh();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The key in force when the identifier was created, being the entry whose
|
|
412
|
+
* start is latest on or before the identifier's date. The list is fetched
|
|
413
|
+
* again, once, before answering when no entry covers the date, when the
|
|
414
|
+
* date is later than the newest start held, or when the list is more than
|
|
415
|
+
* a day old.
|
|
416
|
+
* @param {FodId | string} fodId the identifier, or its base64
|
|
417
|
+
* @returns {Promise<PublicKeyEntry | null>} the key, or null when the
|
|
418
|
+
* date precedes every published key
|
|
419
|
+
*/
|
|
420
|
+
async publicKeyFor (fodId) {
|
|
421
|
+
const id = asFodId(fodId);
|
|
422
|
+
const date = dateOf(id);
|
|
423
|
+
const keys = await this._keysFor(date);
|
|
424
|
+
return inForceAt(keys, date);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Verifies the identifier's signature offline against the published keys,
|
|
429
|
+
* as the cloud's own verify endpoint does. The envelope version must be
|
|
430
|
+
* the one the cloud signs, the payload must be at least the base length
|
|
431
|
+
* for its type (a longer payload carries a creator context and is
|
|
432
|
+
* accepted), and the signature must verify against the key in force at
|
|
433
|
+
* the identifier's date or, near a period boundary, the neighbouring key.
|
|
434
|
+
* No earlier key is tried.
|
|
435
|
+
* @param {FodId | string} fodId the identifier, or its base64
|
|
436
|
+
* @returns {Promise<boolean>} true when a candidate key verifies it
|
|
437
|
+
*/
|
|
438
|
+
async verifySignature (fodId) {
|
|
439
|
+
return (await this.verifySignatureDetailed(fodId)).valid;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* As {@link DidClient#verifySignature}, with the reason alongside the
|
|
444
|
+
* answer, so a caller can tell an identifier no key covers from one whose
|
|
445
|
+
* signature failed.
|
|
446
|
+
* @param {FodId | string} fodId the identifier, or its base64
|
|
447
|
+
* @returns {Promise<SignatureCheck>} the answer and its reason
|
|
448
|
+
*/
|
|
449
|
+
async verifySignatureDetailed (fodId) {
|
|
450
|
+
const id = asFodId(fodId);
|
|
451
|
+
if (id.version !== SUPPORTED_VERSION) {
|
|
452
|
+
return { valid: false, reason: SignatureReason.VERSION };
|
|
453
|
+
}
|
|
454
|
+
if (!payloadLengthValid(id)) {
|
|
455
|
+
return { valid: false, reason: SignatureReason.LENGTH };
|
|
456
|
+
}
|
|
457
|
+
const date = dateOf(id);
|
|
458
|
+
const keys = await this._keysFor(date);
|
|
459
|
+
const candidates = candidatesForDate(keys, date);
|
|
460
|
+
if (candidates.length === 0) {
|
|
461
|
+
return { valid: false, reason: SignatureReason.NO_KEY };
|
|
462
|
+
}
|
|
463
|
+
for (const key of candidates) {
|
|
464
|
+
if (await id.verify(key.publicKey)) {
|
|
465
|
+
return { valid: true, reason: SignatureReason.VERIFIED };
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return { valid: false, reason: SignatureReason.SIGNATURE };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Verifies the identifier's signature through the cloud's verify
|
|
473
|
+
* endpoint, the open endpoint that needs no licence key. One use against
|
|
474
|
+
* the resource key. The identifier is sent under both parameter names,
|
|
475
|
+
* `51did` and `owid`, so the request works with hosts that read either
|
|
476
|
+
* parameter. Hosts that recognise both prefer `51did` and keep `owid` as
|
|
477
|
+
* a compatibility alias.
|
|
478
|
+
* @param {FodId | string} fodId the identifier, or its base64 in either
|
|
479
|
+
* alphabet
|
|
480
|
+
* @returns {Promise<boolean>} whether the cloud found the signature valid
|
|
481
|
+
* @throws {DidArgumentError} when the value is not a 51Did, refused here
|
|
482
|
+
* with the reader's status before any request is made, or when the cloud
|
|
483
|
+
* refuses it (HTTP 400), with the cloud's message
|
|
484
|
+
* @throws {DidClientError} on any other answer than valid or invalid
|
|
485
|
+
*/
|
|
486
|
+
async verify (fodId) {
|
|
487
|
+
const id = identifierText(fodId);
|
|
488
|
+
const url = this._endpoint + 'id/verify/' +
|
|
489
|
+
encodeURIComponent(this._resourceKey) +
|
|
490
|
+
'?51did=' + encodeURIComponent(id) +
|
|
491
|
+
'&owid=' + encodeURIComponent(id);
|
|
492
|
+
const response = await this._fetch(url, {
|
|
493
|
+
method: 'GET',
|
|
494
|
+
headers: { 'User-Agent': USER_AGENT }
|
|
495
|
+
});
|
|
496
|
+
const body = await response.text();
|
|
497
|
+
const parsed = tryParseJson(body);
|
|
498
|
+
if (parsed && typeof parsed === 'object') {
|
|
499
|
+
if (typeof parsed.valid === 'boolean') {
|
|
500
|
+
return parsed.valid;
|
|
501
|
+
}
|
|
502
|
+
if (response.status === 400 && Array.isArray(parsed.errors)) {
|
|
503
|
+
throw new DidArgumentError(
|
|
504
|
+
parsed.errors.join(' '), response.status, body);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
throw new DidClientError(
|
|
508
|
+
`Verify answered HTTP ${response.status}: ${body}`,
|
|
509
|
+
response.status, body);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Redeems a sealed creator context result against the identifier, on the
|
|
514
|
+
* server, with the licence key. The resource key, the 51Did, the sealed
|
|
515
|
+
* result, the challenge and the licence key all travel in the body of a
|
|
516
|
+
* POST to id/redeem, so none of them reaches an access log. (The redeem
|
|
517
|
+
* endpoint takes the resource key in the form on a POST, where the key
|
|
518
|
+
* and verify endpoints take it in the route on a GET.) One use against
|
|
519
|
+
* the resource key, the second of the two a browser context check costs.
|
|
520
|
+
*
|
|
521
|
+
* A 200 and a 503 both produce a result, the 503 being the `unconfirmed`
|
|
522
|
+
* outcome the caller may retry. Every cryptographic failure comes back as
|
|
523
|
+
* the one word `unreadable` by design, so the client does not try to
|
|
524
|
+
* tell them apart either.
|
|
525
|
+
* @param {FodId | string} fodId the identifier the caller knows
|
|
526
|
+
* independently, or its base64 in either alphabet
|
|
527
|
+
* @param {string} result the sealed result exactly as the verify endpoint
|
|
528
|
+
* returned it to the page
|
|
529
|
+
* @param {string} [challenge] the single-use challenge given to the
|
|
530
|
+
* verify endpoint, where one was
|
|
531
|
+
* @returns {Promise<RedeemResult>} the typed outcome
|
|
532
|
+
* @throws {DidArgumentError} when the value is not a 51Did, refused here
|
|
533
|
+
* with the reader's status before any request is made, or when the cloud
|
|
534
|
+
* refuses it (HTTP 400), with the cloud's message
|
|
535
|
+
* @throws {DidNotSupportedError} when the host does not offer the creator
|
|
536
|
+
* context (HTTP 404)
|
|
537
|
+
* @throws {DidClientError} on any other status
|
|
538
|
+
*/
|
|
539
|
+
async redeem (fodId, result, challenge) {
|
|
540
|
+
const id = identifierText(fodId);
|
|
541
|
+
const form = new URLSearchParams();
|
|
542
|
+
form.set('resource', this._resourceKey);
|
|
543
|
+
form.set('51did', id);
|
|
544
|
+
form.set('result', typeof result === 'string' ? result : '');
|
|
545
|
+
form.set('challenge', typeof challenge === 'string' ? challenge : '');
|
|
546
|
+
if (this._licenceKey !== null) {
|
|
547
|
+
form.set('license', this._licenceKey);
|
|
548
|
+
}
|
|
549
|
+
const url = this._endpoint + 'id/redeem';
|
|
550
|
+
const response = await this._fetch(url, {
|
|
551
|
+
method: 'POST',
|
|
552
|
+
headers: {
|
|
553
|
+
'User-Agent': USER_AGENT,
|
|
554
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
555
|
+
},
|
|
556
|
+
body: form.toString()
|
|
557
|
+
});
|
|
558
|
+
const body = await response.text();
|
|
559
|
+
if (response.status === 200 || response.status === 503) {
|
|
560
|
+
return RedeemResult.fromResponse(response.status, body);
|
|
561
|
+
}
|
|
562
|
+
if (response.status === 400) {
|
|
563
|
+
const parsed = tryParseJson(body);
|
|
564
|
+
const message = parsed && Array.isArray(parsed.errors)
|
|
565
|
+
? parsed.errors.join(' ')
|
|
566
|
+
: body;
|
|
567
|
+
throw new DidArgumentError(message, response.status, body);
|
|
568
|
+
}
|
|
569
|
+
if (response.status === 404) {
|
|
570
|
+
throw new DidNotSupportedError(
|
|
571
|
+
'The host does not offer the creator context: ' + body,
|
|
572
|
+
response.status, body);
|
|
573
|
+
}
|
|
574
|
+
throw new DidClientError(
|
|
575
|
+
`Redeem answered HTTP ${response.status}: ${body}`,
|
|
576
|
+
response.status, body);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* The key list to select from for the given date, fetched again once
|
|
581
|
+
* where the rule in {@link DidClient#publicKeyFor} calls for it and the
|
|
582
|
+
* list was not just fetched.
|
|
583
|
+
* @param {Date} date the identifier's date
|
|
584
|
+
* @returns {Promise<PublicKeyEntry[]>} the keys to select from
|
|
585
|
+
* @private
|
|
586
|
+
*/
|
|
587
|
+
async _keysFor (date) {
|
|
588
|
+
const fetchedBefore = this._fetchedAt;
|
|
589
|
+
let keys = await this.publicKeys();
|
|
590
|
+
if (this._fetchedAt === fetchedBefore && this._needsRefetch(keys, date)) {
|
|
591
|
+
keys = await this._refresh();
|
|
592
|
+
}
|
|
593
|
+
return keys;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Whether the held list should be fetched again before selecting for the
|
|
598
|
+
* date.
|
|
599
|
+
* @param {PublicKeyEntry[]} keys the held list, oldest first
|
|
600
|
+
* @param {Date} date the identifier's date
|
|
601
|
+
* @returns {boolean} true to fetch again
|
|
602
|
+
* @private
|
|
603
|
+
*/
|
|
604
|
+
_needsRefetch (keys, date) {
|
|
605
|
+
if (inForceAt(keys, date) === null) {
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
const newestStart = keys[keys.length - 1].startsAt;
|
|
609
|
+
if (date.getTime() > newestStart.getTime()) {
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
return this._stale();
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* @returns {boolean} whether the held list is missing or over a day old
|
|
617
|
+
* @private
|
|
618
|
+
*/
|
|
619
|
+
_stale () {
|
|
620
|
+
return this._fetchedAt === null ||
|
|
621
|
+
this._now() - this._fetchedAt > KEY_LIST_MAX_AGE_MS;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Fetches the key list, sharing one request between concurrent callers.
|
|
626
|
+
* @returns {Promise<PublicKeyEntry[]>} the fresh list
|
|
627
|
+
* @private
|
|
628
|
+
*/
|
|
629
|
+
_refresh () {
|
|
630
|
+
if (this._pending === null) {
|
|
631
|
+
this._pending = this._fetchKeys()
|
|
632
|
+
.then((keys) => {
|
|
633
|
+
this._keys = keys;
|
|
634
|
+
this._fetchedAt = this._now();
|
|
635
|
+
return keys;
|
|
636
|
+
})
|
|
637
|
+
.finally(() => {
|
|
638
|
+
this._pending = null;
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
return this._pending;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* GET id/key/{resource} and read each entry's start and public key.
|
|
646
|
+
* `startsAt` is read where present and `created` otherwise. Both are
|
|
647
|
+
* supported start fields in key-list responses. `weekStart` is ignored.
|
|
648
|
+
* @returns {Promise<PublicKeyEntry[]>} the keys, oldest start first
|
|
649
|
+
* @private
|
|
650
|
+
*/
|
|
651
|
+
async _fetchKeys () {
|
|
652
|
+
const url = this._endpoint + 'id/key/' +
|
|
653
|
+
encodeURIComponent(this._resourceKey);
|
|
654
|
+
const response = await this._fetch(url, {
|
|
655
|
+
method: 'GET',
|
|
656
|
+
headers: { 'User-Agent': USER_AGENT }
|
|
657
|
+
});
|
|
658
|
+
const body = await response.text();
|
|
659
|
+
if (response.status !== 200) {
|
|
660
|
+
throw new DidClientError(
|
|
661
|
+
`Public keys answered HTTP ${response.status}: ${body}`,
|
|
662
|
+
response.status, body);
|
|
663
|
+
}
|
|
664
|
+
const parsed = tryParseJson(body);
|
|
665
|
+
if (!Array.isArray(parsed)) {
|
|
666
|
+
throw new DidClientError(
|
|
667
|
+
'Public keys answered with a body that is not a JSON array: ' +
|
|
668
|
+
body, response.status, body);
|
|
669
|
+
}
|
|
670
|
+
const keys = parsed.map((entry) => {
|
|
671
|
+
const start = entry && (entry.startsAt || entry.created);
|
|
672
|
+
const startsAt = typeof start === 'string' ? new Date(start) : null;
|
|
673
|
+
if (startsAt === null || isNaN(startsAt.getTime()) ||
|
|
674
|
+
typeof entry.publicKey !== 'string') {
|
|
675
|
+
throw new DidClientError(
|
|
676
|
+
'Public keys entry lacks a start or a publicKey: ' +
|
|
677
|
+
JSON.stringify(entry), response.status, body);
|
|
678
|
+
}
|
|
679
|
+
return Object.freeze({ startsAt, publicKey: entry.publicKey });
|
|
680
|
+
});
|
|
681
|
+
keys.sort((a, b) => a.startsAt.getTime() - b.startsAt.getTime());
|
|
682
|
+
return Object.freeze(keys);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* The identifier as a FodId, parsing a base64 string where one was given. A
|
|
688
|
+
* string the reader cannot parse is reported as a DidArgumentError, the same
|
|
689
|
+
* type the length guard and the cloud's own refusal use, so a caller
|
|
690
|
+
* matching on DidClientError catches every bad argument in one place.
|
|
691
|
+
* @param {FodId | string} value an identifier or its base64
|
|
692
|
+
* @returns {FodId} the identifier
|
|
693
|
+
*/
|
|
694
|
+
function asFodId (value) {
|
|
695
|
+
if (value instanceof FodId) {
|
|
696
|
+
return value;
|
|
697
|
+
}
|
|
698
|
+
if (typeof value === 'string') {
|
|
699
|
+
ensureEncodedLength(value);
|
|
700
|
+
return parseOrRefuse(value);
|
|
701
|
+
}
|
|
702
|
+
throw new TypeError('fodId must be a FodId or a base64 string');
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* The text sent to the cloud for an identifier. A parsed identifier goes in
|
|
707
|
+
* the URL-safe alphabet, which needs no further encoding. A string is read
|
|
708
|
+
* here first, so a value that is not a 51Did is refused before any request
|
|
709
|
+
* is made, and then goes as given so the cloud sees what the page sent.
|
|
710
|
+
* @param {FodId | string} value an identifier or its base64
|
|
711
|
+
* @returns {string} the text to send
|
|
712
|
+
*/
|
|
713
|
+
function identifierText (value) {
|
|
714
|
+
if (value instanceof FodId) {
|
|
715
|
+
return value.asBase64Url();
|
|
716
|
+
}
|
|
717
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
718
|
+
ensureEncodedLength(value);
|
|
719
|
+
parseOrRefuse(value);
|
|
720
|
+
return value;
|
|
721
|
+
}
|
|
722
|
+
throw new TypeError('fodId must be a FodId or a non-empty base64 string');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Reads a string as a 51Did, or refuses it as a DidArgumentError naming the
|
|
727
|
+
* reason. The reason is the reader's own status, so a caller sees why the
|
|
728
|
+
* value was refused without any key being fetched or any request made.
|
|
729
|
+
* @param {string} value the encoded identifier, already within the length
|
|
730
|
+
* guard
|
|
731
|
+
* @returns {FodId} the identifier
|
|
732
|
+
*/
|
|
733
|
+
function parseOrRefuse (value) {
|
|
734
|
+
const read = FodId.tryParse(value);
|
|
735
|
+
if (!read.ok) {
|
|
736
|
+
throw new DidArgumentError(
|
|
737
|
+
'The value could not be read as a 51Did (' + read.status + ').');
|
|
738
|
+
}
|
|
739
|
+
return read.value;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Turns away an encoded value too long to be worth decoding, before any
|
|
744
|
+
* work is done on it. Whitespace at either end is ignored, as the reader
|
|
745
|
+
* ignores it.
|
|
746
|
+
* @param {string} value the encoded identifier as the caller gave it
|
|
747
|
+
*/
|
|
748
|
+
function ensureEncodedLength (value) {
|
|
749
|
+
if (value.trim().length > MAXIMUM_ENCODED_LENGTH) {
|
|
750
|
+
throw new DidArgumentError(
|
|
751
|
+
'The value is longer than this client will read as a 51Did.');
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* The identifier's creation moment as a Date.
|
|
757
|
+
* @param {FodId} fodId the identifier
|
|
758
|
+
* @returns {Date} the moment the envelope says it was created
|
|
759
|
+
*/
|
|
760
|
+
function dateOf (fodId) {
|
|
761
|
+
return new Date(OWID_EPOCH_MS + fodId.dateMinutes * MINUTE_MS);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Whether the payload is at least the base length for its type, being five
|
|
766
|
+
* header bytes plus a 32 byte match key, or 16 for a Random identifier.
|
|
767
|
+
* Anything beyond the base is a creator context section, whose exact
|
|
768
|
+
* lengths belong to the cloud, so any longer payload is accepted here. The
|
|
769
|
+
* reader already refuses a Probabilistic, HashedEmail or Random payload
|
|
770
|
+
* shorter than its base, so in practice only a Reserved identifier, which
|
|
771
|
+
* the reader accepts at any length from the header up, reaches this check
|
|
772
|
+
* with too few bytes.
|
|
773
|
+
* @param {FodId} fodId the identifier
|
|
774
|
+
* @returns {boolean} whether the length is acceptable
|
|
775
|
+
*/
|
|
776
|
+
function payloadLengthValid (fodId) {
|
|
777
|
+
const matchKeyLength = fodId.type === IdType.RANDOM
|
|
778
|
+
? FodId.GUID_LENGTH
|
|
779
|
+
: FodId.HASH_LENGTH;
|
|
780
|
+
return fodId.payload.length >= FodId.HEADER_LENGTH + matchKeyLength;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* The entry in force at the moment, being the newest whose start has
|
|
785
|
+
* passed, or null when the moment precedes every entry.
|
|
786
|
+
* @param {PublicKeyEntry[]} keys the schedule, in any order
|
|
787
|
+
* @param {Date} at the moment
|
|
788
|
+
* @returns {PublicKeyEntry | null} the entry in force
|
|
789
|
+
*/
|
|
790
|
+
function inForceAt (keys, at) {
|
|
791
|
+
let best = null;
|
|
792
|
+
for (const key of keys) {
|
|
793
|
+
if (key.startsAt.getTime() > at.getTime()) {
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (best === null || key.startsAt.getTime() > best.startsAt.getTime()) {
|
|
797
|
+
best = key;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return best;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* The entries that may have signed something created at the moment, best
|
|
805
|
+
* first: the entry in force, then the entry in force a tolerance earlier
|
|
806
|
+
* and the entry in force a tolerance later where those differ. Not every
|
|
807
|
+
* earlier entry.
|
|
808
|
+
* @param {PublicKeyEntry[]} keys the schedule, in any order
|
|
809
|
+
* @param {Date} at the moment
|
|
810
|
+
* @returns {PublicKeyEntry[]} the entries to try, best first
|
|
811
|
+
*/
|
|
812
|
+
function candidatesForDate (keys, at) {
|
|
813
|
+
const candidates = [];
|
|
814
|
+
const add = (entry) => {
|
|
815
|
+
if (entry !== null && candidates.indexOf(entry) < 0) {
|
|
816
|
+
candidates.push(entry);
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
add(inForceAt(keys, at));
|
|
820
|
+
add(inForceAt(keys, new Date(at.getTime() - BOUNDARY_TOLERANCE_MS)));
|
|
821
|
+
add(inForceAt(keys, new Date(at.getTime() + BOUNDARY_TOLERANCE_MS)));
|
|
822
|
+
return candidates;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Parses JSON without throwing.
|
|
827
|
+
* @param {string} text a response body
|
|
828
|
+
* @returns {any} the parsed JSON, or null when the text is not JSON
|
|
829
|
+
*/
|
|
830
|
+
function tryParseJson (text) {
|
|
831
|
+
try {
|
|
832
|
+
return JSON.parse(text);
|
|
833
|
+
} catch (error) {
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
module.exports = {
|
|
839
|
+
DidClient,
|
|
840
|
+
RedeemResult,
|
|
841
|
+
ContextResult,
|
|
842
|
+
SignatureResult,
|
|
843
|
+
FactorResult,
|
|
844
|
+
SignatureReason,
|
|
845
|
+
DidClientError,
|
|
846
|
+
DidArgumentError,
|
|
847
|
+
DidNotSupportedError,
|
|
848
|
+
DEFAULT_ENDPOINT
|
|
849
|
+
};
|