fiftyone.pipeline.did 4.5.38 → 4.5.39

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 CHANGED
@@ -21,6 +21,7 @@
21
21
  * ********************************************************************* */
22
22
 
23
23
  const FodId = require('./fodId');
24
+ const layout = require('./internal/layout');
24
25
  const IdType = require('./idType');
25
26
  const packageVersion = require('./package.json').version;
26
27
 
@@ -777,7 +778,7 @@ function ensureEncodedLength (value) {
777
778
  * @returns {Date} the moment the envelope says it was created
778
779
  */
779
780
  function dateOf (fodId) {
780
- return new Date(OWID_EPOCH_MS + fodId.dateMinutes * MINUTE_MS);
781
+ return new Date(OWID_EPOCH_MS + fodId.date * MINUTE_MS);
781
782
  }
782
783
 
783
784
  /**
@@ -794,9 +795,9 @@ function dateOf (fodId) {
794
795
  */
795
796
  function payloadLengthValid (fodId) {
796
797
  const matchKeyLength = fodId.type === IdType.RANDOM
797
- ? FodId.GUID_LENGTH
798
- : FodId.MATCH_KEY_LENGTH;
799
- return fodId.payload.length >= FodId.HEADER_LENGTH + matchKeyLength;
798
+ ? layout.GUID_LENGTH
799
+ : layout.MATCH_KEY_LENGTH;
800
+ return fodId.payload.length >= layout.HEADER_LENGTH + matchKeyLength;
800
801
  }
801
802
 
802
803
  /**
@@ -33,7 +33,12 @@
33
33
  */
34
34
 
35
35
  const { webcrypto } = require('crypto');
36
- const { FodId, IdType } = require('../index');
36
+ const { FodId, IdType, Usage } = require('../index');
37
+ // The payload byte layout is internal to the package, so a consumer
38
+ // never reaches for it. This example builds a sample 51Did to work on,
39
+ // which the cloud would otherwise issue, so it reads the layout from
40
+ // inside the package.
41
+ const layout = require('../internal/layout');
37
42
 
38
43
  const subtle = webcrypto.subtle;
39
44
  const VERSION = 2;
@@ -45,13 +50,17 @@ function uint32LE (v) {
45
50
  }
46
51
 
47
52
  function samplePayload () {
48
- const p = new Uint8Array(FodId.PAYLOAD_LENGTH); // Probabilistic (flags 0x00)
49
- p[FodId.LICENSE_ID_OFFSET] = 0x78;
50
- p[FodId.LICENSE_ID_OFFSET + 1] = 0x56;
51
- p[FodId.LICENSE_ID_OFFSET + 2] = 0x34;
52
- p[FodId.LICENSE_ID_OFFSET + 3] = 0x12;
53
- for (let i = 0; i < FodId.MATCH_KEY_LENGTH; i++) {
54
- p[FodId.MATCH_KEY_OFFSET + i] = 0x20 + i;
53
+ const p = new Uint8Array(layout.PAYLOAD_LENGTH);
54
+ // Bits 6 and 7 are zero, so the type is Probabilistic. Bits 0 to 2 are
55
+ // the usage and they are cumulative, so 0b011 grants standard
56
+ // marketing and, with it, non-marketing use.
57
+ p[layout.FLAGS_OFFSET] = 0b0000_0011;
58
+ p[layout.LICENSE_ID_OFFSET] = 0x78;
59
+ p[layout.LICENSE_ID_OFFSET + 1] = 0x56;
60
+ p[layout.LICENSE_ID_OFFSET + 2] = 0x34;
61
+ p[layout.LICENSE_ID_OFFSET + 3] = 0x12;
62
+ for (let i = 0; i < layout.MATCH_KEY_LENGTH; i++) {
63
+ p[layout.MATCH_KEY_OFFSET + i] = 0x20 + i;
55
64
  }
56
65
  return p;
57
66
  }
@@ -94,7 +103,8 @@ async function run () {
94
103
  console.log('51Did parsed from base64:');
95
104
  console.log(' Domain :', fodId.domain);
96
105
  console.log(' Type :', IdType.name(fodId.type));
97
- console.log(' Flags : 0x' + fodId.flags.toString(16));
106
+ console.log(' Usage :', Usage.name(fodId.usage));
107
+ console.log(' From cons.:', fodId.usageFromConsent);
98
108
  console.log(' LicenseId :', fodId.licenseId);
99
109
  console.log(' Match key :', Buffer.from(fodId.matchKey).toString('hex'));
100
110
  console.log(' Verifies :', await fodId.verify(publicPem));
@@ -129,7 +139,7 @@ async function run () {
129
139
  ', status ' + result.status);
130
140
  }
131
141
  const shortPayload = await issue(
132
- keyPair.privateKey, payload.slice(0, FodId.PAYLOAD_LENGTH - 1), DATE);
142
+ keyPair.privateKey, payload.slice(0, layout.PAYLOAD_LENGTH - 1), DATE);
133
143
  const short = FodId.tryParse(shortPayload);
134
144
  console.log(' a payload one byte short -> ok ' + short.ok +
135
145
  ', status ' + short.status);
package/fodId.js CHANGED
@@ -21,7 +21,9 @@
21
21
  * ********************************************************************* */
22
22
 
23
23
  const owid = require('owid');
24
+ const layout = require('./internal/layout');
24
25
  const IdType = require('./idType');
26
+ const Usage = require('./usage');
25
27
  const FodIdParseError = require('./fodIdParseError');
26
28
 
27
29
  /**
@@ -82,42 +84,17 @@ const ParseStatus = Object.freeze(Object.assign({}, owid.ParseStatus, {
82
84
  * A FodId composes the OWID the OWID library read (holds it and delegates
83
85
  * the envelope fields to it). That OWID is frozen and hands out its byte
84
86
  * arrays as copies, so nothing a caller holds can change the identifier.
87
+ *
88
+ * Every field has a named accessor here, so nothing needs the payload
89
+ * bytes or their offsets. The byte layout itself is specified once for all
90
+ * languages, and that specification is the authority rather than this
91
+ * comment:
92
+ * https://github.com/51Degrees/specifications/blob/main/did-specification/identifier-layout.md
93
+ *
94
+ * What each 51Did package offers on top of that layout is specified at:
95
+ * https://github.com/51Degrees/specifications/blob/main/did-specification/package-surface.md
85
96
  */
86
97
  class FodId {
87
- static FLAGS_OFFSET = 0;
88
- static LICENSE_ID_OFFSET = 1;
89
- static LICENSE_ID_LENGTH = 4;
90
- /** Byte offset of the match key field within the payload. */
91
- static MATCH_KEY_OFFSET = 5;
92
- /**
93
- * Byte length of the match key field for Probabilistic and HashedEmail
94
- * identifiers, being a SHA-256.
95
- */
96
- static MATCH_KEY_LENGTH = 32;
97
- /**
98
- * Deprecated alias for {@link FodId.MATCH_KEY_OFFSET}. The stable,
99
- * comparable part of a 51Did is now called the match key, mirroring the
100
- * Model Terms for Marketing vocabulary. This alias will be removed in a
101
- * future release.
102
- * @deprecated Renamed to MATCH_KEY_OFFSET. This alias will be removed in
103
- * a future release.
104
- */
105
- static HASH_OFFSET = FodId.MATCH_KEY_OFFSET;
106
- /**
107
- * Deprecated alias for {@link FodId.MATCH_KEY_LENGTH}. The stable,
108
- * comparable part of a 51Did is now called the match key, mirroring the
109
- * Model Terms for Marketing vocabulary. This alias will be removed in a
110
- * future release.
111
- * @deprecated Renamed to MATCH_KEY_LENGTH. This alias will be removed in
112
- * a future release.
113
- */
114
- static HASH_LENGTH = FodId.MATCH_KEY_LENGTH;
115
- static HEADER_LENGTH = FodId.MATCH_KEY_OFFSET;
116
- /** Byte length of the GUID match key carried by Random identifiers. */
117
- static GUID_LENGTH = 16;
118
- static RANDOM_PAYLOAD_LENGTH = 21;
119
- static PAYLOAD_LENGTH = FodId.MATCH_KEY_OFFSET + FodId.MATCH_KEY_LENGTH;
120
-
121
98
  /**
122
99
  * Why a read succeeded or failed, being the OWID library's statuses plus
123
100
  * `PAYLOAD_TOO_SHORT` and `INVALID_TYPE_PAYLOAD_LENGTH`. Frozen.
@@ -303,16 +280,31 @@ class FodId {
303
280
  return new FodId(owidInstance);
304
281
  }
305
282
 
306
- /** @returns {number} the 1-byte usage flags bit-mask (0-255). */
307
- get flags () {
308
- return this._flags;
309
- }
310
-
311
283
  /** @returns {number} the IdType carried in bits 6-7 of the flags. */
312
284
  get type () {
313
285
  return IdType.fromFlags(this._flags);
314
286
  }
315
287
 
288
+ /**
289
+ * The Usage carried in bits 0-2 of the flags, as the highest usage
290
+ * granted. See Usage for why it is read that way.
291
+ * @returns {number} a Usage value
292
+ */
293
+ get usage () {
294
+ return Usage.fromFlags(this._flags);
295
+ }
296
+
297
+ /**
298
+ * Whether the usage was derived from an IAB consent string the caller
299
+ * sent, rather than stated by the caller directly. Bit 3 of the flags.
300
+ * Both are legitimate ways to arrive at a usage, and this says nothing
301
+ * about which usage it is.
302
+ * @returns {boolean}
303
+ */
304
+ get usageFromConsent () {
305
+ return (this._flags & 0b1000) !== 0;
306
+ }
307
+
316
308
  /**
317
309
  * The 4-byte little-endian field at offset 1 of the payload, as an
318
310
  * unsigned integer (0-4294967295).
@@ -341,19 +333,6 @@ class FodId {
341
333
  return this._matchKey.slice();
342
334
  }
343
335
 
344
- /**
345
- * Deprecated alias for {@link FodId#matchKey}. The stable, comparable
346
- * part of a 51Did is now called the match key, mirroring the Model Terms
347
- * for Marketing vocabulary. This alias will be removed in a future
348
- * release.
349
- * @deprecated Renamed to matchKey. This alias will be removed in a future
350
- * release.
351
- * @returns {Uint8Array} the same bytes as {@link FodId#matchKey}
352
- */
353
- get hash () {
354
- return this.matchKey;
355
- }
356
-
357
336
  /** @returns {number} the OWID version. */
358
337
  get version () {
359
338
  return this._owid.version;
@@ -365,24 +344,13 @@ class FodId {
365
344
  }
366
345
 
367
346
  /**
368
- * @returns {number} the OWID date as minutes since 2020-01-01 UTC, as an
369
- * unsigned 32-bit number, the same value as {@link FodId#dateMinutes}.
370
- */
371
- get date () {
372
- return this._owid.date;
373
- }
374
-
375
- /**
376
- * The envelope's own date as the unsigned 32-bit count of minutes since
377
- * 2020-01-01T00:00:00Z, exactly as the wire carries it. This is the value
378
- * the OWID `public-key?date=` parameter takes, and the integer to use when
379
- * comparing creation times. The OWID library now reads the field unsigned
380
- * too, so {@link FodId#date} agrees with this getter. The getter is kept
381
- * because callers were told to use it, and it still forces the unsigned
382
- * reading should the field ever arrive signed.
347
+ * The envelope's own date, being the count of minutes since
348
+ * 2020-01-01T00:00:00Z exactly as the wire carries it, read as an
349
+ * unsigned 32-bit number. This is the integer to compare when asking
350
+ * which of two identifiers was issued first.
383
351
  * @returns {number} minutes since 2020-01-01T00:00:00Z
384
352
  */
385
- get dateMinutes () {
353
+ get date () {
386
354
  return this._owid.date >>> 0;
387
355
  }
388
356
 
@@ -464,34 +432,34 @@ class FodId {
464
432
  */
465
433
  function unpack (payload) {
466
434
  const length = payload.length;
467
- if (length < FodId.HEADER_LENGTH) {
435
+ if (length < layout.HEADER_LENGTH) {
468
436
  return {
469
437
  status: ParseStatus.PAYLOAD_TOO_SHORT,
470
438
  length,
471
- required: FodId.HEADER_LENGTH
439
+ required: layout.HEADER_LENGTH
472
440
  };
473
441
  }
474
- const flags = payload[FodId.FLAGS_OFFSET];
442
+ const flags = payload[layout.FLAGS_OFFSET];
475
443
  // Little-endian unsigned 32-bit. `>>> 0` forces unsigned so the high bit
476
444
  // does not produce a negative number.
477
445
  const licenseId = (
478
- payload[FodId.LICENSE_ID_OFFSET] |
479
- (payload[FodId.LICENSE_ID_OFFSET + 1] << 8) |
480
- (payload[FodId.LICENSE_ID_OFFSET + 2] << 16) |
481
- (payload[FodId.LICENSE_ID_OFFSET + 3] << 24)
446
+ payload[layout.LICENSE_ID_OFFSET] |
447
+ (payload[layout.LICENSE_ID_OFFSET + 1] << 8) |
448
+ (payload[layout.LICENSE_ID_OFFSET + 2] << 16) |
449
+ (payload[layout.LICENSE_ID_OFFSET + 3] << 24)
482
450
  ) >>> 0;
483
451
  const type = IdType.fromFlags(flags);
484
452
  let matchKeyLength;
485
453
  if (type === IdType.RANDOM) {
486
- matchKeyLength = FodId.GUID_LENGTH;
454
+ matchKeyLength = layout.GUID_LENGTH;
487
455
  } else if (type === IdType.RESERVED) {
488
456
  // Not yet assigned, so read best-effort, whatever follows the header
489
457
  // is the match key.
490
- matchKeyLength = length - FodId.HEADER_LENGTH;
458
+ matchKeyLength = length - layout.HEADER_LENGTH;
491
459
  } else {
492
- matchKeyLength = FodId.MATCH_KEY_LENGTH;
460
+ matchKeyLength = layout.MATCH_KEY_LENGTH;
493
461
  }
494
- const required = FodId.HEADER_LENGTH + matchKeyLength;
462
+ const required = layout.HEADER_LENGTH + matchKeyLength;
495
463
  if (length < required) {
496
464
  return {
497
465
  status: ParseStatus.INVALID_TYPE_PAYLOAD_LENGTH,
@@ -506,7 +474,7 @@ function unpack (payload) {
506
474
  licenseId,
507
475
  // slice() copies, so the stored match key is this identifier's own.
508
476
  matchKey: payload.slice(
509
- FodId.MATCH_KEY_OFFSET, FodId.MATCH_KEY_OFFSET + matchKeyLength),
477
+ layout.MATCH_KEY_OFFSET, layout.MATCH_KEY_OFFSET + matchKeyLength),
510
478
  length,
511
479
  required
512
480
  };
package/index.js CHANGED
@@ -23,6 +23,7 @@
23
23
  const FodId = require('./fodId');
24
24
  const FodIdParseError = require('./fodIdParseError');
25
25
  const IdType = require('./idType');
26
+ const Usage = require('./usage');
26
27
  const {
27
28
  DidClient,
28
29
  RedeemResult,
@@ -39,6 +40,7 @@ module.exports = {
39
40
  FodId,
40
41
  FodIdParseError,
41
42
  IdType,
43
+ Usage,
42
44
  DidClient,
43
45
  RedeemResult,
44
46
  ContextResult,
@@ -0,0 +1,62 @@
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
+ /**
24
+ * The byte layout of a 51Did payload. Internal to this package, because the
25
+ * only reason to read the payload by hand is to read a field that already
26
+ * has a name on FodId, and reading the flags byte by hand is how the usage
27
+ * comes out backwards. The usage bits are cumulative, so a caller masking
28
+ * for the non-marketing bit alone reads every marketing identifier as
29
+ * non-marketing. Use the FodId accessors instead.
30
+ *
31
+ * The layout is described once for every language in the 51Did
32
+ * specification, which is the authority for these numbers:
33
+ * https://github.com/51Degrees/specifications/blob/main/did-specification/identifier-layout.md
34
+ *
35
+ * What each package offers on top of that layout is described in:
36
+ * https://github.com/51Degrees/specifications/blob/main/did-specification/package-surface.md
37
+ */
38
+ module.exports = Object.freeze({
39
+ /** Byte offset of the flags byte within the payload. */
40
+ FLAGS_OFFSET: 0,
41
+ /** Byte offset of the licence id field within the payload. */
42
+ LICENSE_ID_OFFSET: 1,
43
+ /** Byte length of the licence id field. */
44
+ LICENSE_ID_LENGTH: 4,
45
+ /** Byte offset of the match key field within the payload. */
46
+ MATCH_KEY_OFFSET: 5,
47
+ /**
48
+ * Byte length of the match key field for Probabilistic and HashedEmail
49
+ * identifiers, being a SHA-256.
50
+ */
51
+ MATCH_KEY_LENGTH: 32,
52
+ /** Byte length of the flags and licence id fields together. */
53
+ HEADER_LENGTH: 5,
54
+ /** Byte length of the GUID match key carried by Random identifiers. */
55
+ GUID_LENGTH: 16,
56
+ /** Least payload length for a Random identifier. */
57
+ RANDOM_PAYLOAD_LENGTH: 21,
58
+ /**
59
+ * Least payload length for a Probabilistic or HashedEmail identifier.
60
+ */
61
+ PAYLOAD_LENGTH: 37
62
+ });
@@ -227,6 +227,33 @@ test('crypto verify asks the end point for the version the OWID carries', () =>
227
227
  });
228
228
  });
229
229
 
230
+ // A creator whose domain answers with a redirect does not get the key at
231
+ // the other end trusted as its own. The library asks fetch not to follow,
232
+ // so the answer is the redirect itself, which is not ok and reads as the
233
+ // key being unavailable, and no second request is made. Without this a
234
+ // network attacker able to bend a creator's DNS, or a misconfigured
235
+ // creator, could substitute the key, and the publisher's fetchHeaders
236
+ // credential would travel to wherever the redirect pointed.
237
+ test('crypto verify does not follow a redirect from the creator', () => {
238
+ var redirectDomain = "redirecting.example";
239
+ fetchMock.mockResponse(req => {
240
+ return Promise.resolve({
241
+ status: 302,
242
+ headers: { Location: "https://elsewhere.example/key.pem" },
243
+ body: ""
244
+ });
245
+ });
246
+ var unsigned = buildUnsignedOWID(
247
+ redirectDomain, testDateInMinutes, Buffer.from("example"));
248
+ var o = read(signOWID(unsigned, creatorKeyPair.privateKey));
249
+
250
+ return o.checkSignature().then(r => {
251
+ expect(r.status).toBe(owid.SignatureStatus.KEY_UNAVAILABLE);
252
+ expect(fetch.mock.calls.length).toBe(1);
253
+ expect(fetch.mock.calls[0][1].redirect).toBe("manual");
254
+ });
255
+ });
256
+
230
257
  test('crypto verify sends configured fetch headers', () => {
231
258
  var unsigned = buildUnsignedOWID(
232
259
  creatorDomain, testDateInMinutes, Buffer.from("example"));
@@ -1048,9 +1048,20 @@ var owid = (function () {
1048
1048
  if (instance.date != null) {
1049
1049
  url += "?date=" + instance.date;
1050
1050
  }
1051
+ // A redirect is never followed. fetch follows one by default, to
1052
+ // any other origin, so a creator whose domain answered 302 to
1053
+ // some other place would have that other place's key trusted
1054
+ // as its own, and a network attacker able to bend the creator's
1055
+ // DNS, or a creator that was simply misconfigured, could put a
1056
+ // key there and have forgeries verify. It would also carry
1057
+ // owid.fetchHeaders, a publisher's credential, to wherever the
1058
+ // redirect pointed. With manual the browser hands back an opaque
1059
+ // redirect that is not ok, which reads below as the key being
1060
+ // unavailable, which it is.
1051
1061
  return fetch(url, {
1052
1062
  mode: "cors",
1053
1063
  cache: "default",
1064
+ redirect: "manual",
1054
1065
  headers: owid.fetchHeaders
1055
1066
  }).then(function (r) {
1056
1067
  if (r.ok) {
@@ -1114,10 +1125,14 @@ var owid = (function () {
1114
1125
  body.append("parent", encodeBase64(joined));
1115
1126
  body.append("owid", instance.data);
1116
1127
  var url = creatorApiUrl(instance, "verify");
1128
+ // Not followed either, for the same reasons as the creator
1129
+ // request, and because a redirected POST would resend the
1130
+ // identifier and the credential to wherever it pointed.
1117
1131
  return fetch(url, {
1118
1132
  method: "POST",
1119
1133
  mode: "cors",
1120
1134
  cache: "no-cache",
1135
+ redirect: "manual",
1121
1136
  headers: owid.fetchHeaders,
1122
1137
  body: body
1123
1138
  }).then(function (r) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fiftyone.pipeline.did",
3
- "version": "4.5.38",
4
- "description": "Strongly typed reader for the 51Did (51Degrees Identifier) value returned by the 51Degrees Cloud service. Parses the OWID envelope and exposes the Flags, License Id and match key plus the identifier type. Compare match keys, never envelopes.",
3
+ "version": "4.5.39",
4
+ "description": "Strongly typed reader for the 51Did (51Degrees Identifier) value returned by the 51Degrees Cloud service. Parses the OWID envelope and exposes the usage, licence id and match key plus the identifier type. Compare match keys, never envelopes.",
5
5
  "keywords": [
6
6
  "51degrees",
7
7
  "51did",
@@ -11,13 +11,20 @@
11
11
  ],
12
12
  "main": "index.js",
13
13
  "types": "types/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./types/index.d.ts",
17
+ "default": "./index.js"
18
+ },
19
+ "./package.json": "./package.json"
20
+ },
14
21
  "scripts": {
15
22
  "test": "jest"
16
23
  },
17
24
  "author": "51Degrees Engineering <engineering@51degrees.com>",
18
25
  "license": "EUPL-1.2",
19
26
  "dependencies": {
20
- "owid": "github:51Degrees/owid-js#9aeb0d24"
27
+ "owid": "github:51Degrees/owid-js#148763cb"
21
28
  },
22
29
  "bundleDependencies": [
23
30
  "owid"
package/readme.md CHANGED
@@ -20,11 +20,22 @@ envelopes.**
20
20
 
21
21
  ## Payload layout
22
22
 
23
- | Offset | Length | Field | Type |
24
- |-------:|-------:|------------|-------------------------------------------------|
25
- | 0 | 1 | Flags | uint8: bits 0-2 usage, bits 6-7 identifier type |
26
- | 1 | 4 | LicenseId | uint32 (little-endian), see below |
27
- | 5 | 16/32 | Match key | SHA-256 (Probabilistic, HashedEmail) or GUID (Random) |
23
+ The byte layout is specified once for every language in the 51Did
24
+ specification, and that specification is the authority for it:
25
+
26
+ - [Identifier layout](https://github.com/51Degrees/specifications/blob/main/did-specification/identifier-layout.md)
27
+ - [Package surface](https://github.com/51Degrees/specifications/blob/main/did-specification/package-surface.md)
28
+
29
+ Your code never needs the offsets, because every field has a named accessor,
30
+ so the offsets and the lengths are internal to this package. Reading the flags
31
+ byte by hand is the mistake they are kept internal to prevent, because the
32
+ usage bits are cumulative and a mask for one of them answers the wrong
33
+ question. The summary here explains what the accessors report, and the
34
+ specification governs wherever the two differ.
35
+
36
+ The payload carries a one byte Flags field, a four byte little-endian
37
+ LicenseId and then the match key. Bits 6 and 7 of the flags name the
38
+ identifier type, which decides how long the match key is.
28
39
 
29
40
  | Bits 7-6 | `IdType` | Match key length | Least payload accepted |
30
41
  |---------:|-----------------|-----------------:|-----------------------:|
@@ -47,6 +58,36 @@ the lower bound for the identifier type, holds no upper bound of its own, and
47
58
  leaves anything longer for the cloud to judge. A reader built before a longer
48
59
  context section existed therefore still reads the identifier.
49
60
 
61
+ ## The usage a 51Did was created for
62
+
63
+ Every 51Did says what it was created for, and `fodId.usage` reports it as one
64
+ of the `Usage` values. The usage decides where the identifier may go, so it is
65
+ read before the identifier is passed anywhere.
66
+
67
+ | `Usage` | The cloud's `id.usage` | Meaning |
68
+ | --- | --- | --- |
69
+ | `NONE` | none | No usage bit is set. The cloud never issues such an identifier, so treat it as one that may not be passed on |
70
+ | `NON_MARKETING` | `non-marketing` | Created for use that is not marketing. Must never be passed to a demand source |
71
+ | `STANDARD` | `standard` | Created for standard marketing, being targeting unrelated to browsing history |
72
+ | `PERSONALIZED` | `personalized` | Created for personalized marketing, being targeting related to browsing history |
73
+
74
+ The three usages are cumulative in the flags byte rather than exclusive.
75
+ Non-marketing sets one bit, standard sets two and personalized sets three, so
76
+ every marketing identifier also carries the non-marketing bit. Code masking
77
+ the byte for that bit alone would read every marketing identifier as
78
+ non-marketing, which is the wrong way round for a rule that turns on it.
79
+ `fodId.usage` answers with the highest usage granted, so that mistake cannot
80
+ be made, and it is the only supported way to read the usage.
81
+
82
+ `fodId.usageFromConsent` says whether the usage was worked out from an IAB
83
+ consent string the caller sent rather than stated by the caller directly. Both
84
+ are legitimate ways to arrive at a usage, and it says nothing about which
85
+ usage it is.
86
+
87
+ `Usage.name(usage)` gives the cross language name, for example
88
+ `"NonMarketing"`, and `Usage.idUsage(usage)` gives the cloud's own `id.usage`
89
+ value, for example `"non-marketing"`, or `null` for `NONE`.
90
+
50
91
  ## Reading a 51Did
51
92
 
52
93
  A 51Did arrives from outside, from a page, a link or a log line, so a value
@@ -134,9 +175,26 @@ the OWID library through this package changes as follows.
134
175
  | `FodId.fromOwid(new owid(s))` | `FodId.fromBase64(s)`, or `FodId.tryParse(s)` for a result |
135
176
  | `try { FodId.fromBase64(s) } catch (e) { /* e was a string */ }` | `const r = FodId.tryParse(s); if (!r.ok) { /* r.status */ }` |
136
177
  | `catch (e)` on `fromBase64` reading `e.message` | `catch (e)` reading `e.status`, one of `FodId.ParseStatus` |
137
- | `fodId.date` read as a signed number | `fodId.date` is now unsigned, the same value as `fodId.dateMinutes` |
178
+ | `fodId.date` read as a signed number | `fodId.date` is now unsigned |
138
179
  | `fodId.verify(pem)` resolving `false` for a key that could not be imported | `verify` rejects when the question could not be answered, and `checkSignature(pem)` reports `INVALID_KEY` |
139
180
 
181
+ ### Migrating from the removed raw surface
182
+
183
+ The raw byte and the offsets were the way to read by hand a field that already
184
+ has a name, so they have gone. `fodId.flags`, `fodId.dateMinutes`, the
185
+ deprecated `fodId.hash`, and the layout constants `FLAGS_OFFSET`,
186
+ `LICENSE_ID_OFFSET`, `LICENSE_ID_LENGTH`, `MATCH_KEY_OFFSET`,
187
+ `MATCH_KEY_LENGTH`, `HEADER_LENGTH`, `GUID_LENGTH`, `RANDOM_PAYLOAD_LENGTH`
188
+ and `PAYLOAD_LENGTH` are no longer part of the package.
189
+
190
+ | Before | After |
191
+ | --- | --- |
192
+ | `fodId.flags` masked for a usage bit | `fodId.usage`, and `fodId.usageFromConsent` for bit 3 |
193
+ | `fodId.flags` masked for the type bits | `fodId.type` |
194
+ | `fodId.hash` | `fodId.matchKey`, the same bytes under the name the Model Terms for Marketing use |
195
+ | `fodId.dateMinutes` | `fodId.date`, which reports the same unsigned value |
196
+ | `FodId.PAYLOAD_LENGTH` and the other layout constants | Nothing. Every field has a named accessor, and the layout is in the specification linked above |
197
+
140
198
  ## OWID dependency
141
199
 
142
200
  `FodId` builds on the OWID envelope library
@@ -158,7 +216,7 @@ and `FodId.checkSignature()` run without contacting a network endpoint.
158
216
 
159
217
  The library is not on the npm registry, so `package.json` names it as a
160
218
  GitHub reference, which npm resolves by cloning the repository. The reference
161
- is `github:51Degrees/owid-js#9aeb0d24`, the commit on `main` that carries the key selection fix
219
+ is `github:51Degrees/owid-js#148763cb`, the commit on `main` that carries the key selection fix and refuses redirects when fetching a key
162
220
  of the [51Degrees/owid-js](https://github.com/51Degrees/owid-js) fork. That
163
221
  applies only when working in this repository, because the published
164
222
  `fiftyone.pipeline.did` package carries the OWID source inside its own
@@ -178,32 +236,28 @@ npm test
178
236
  ## Usage
179
237
 
180
238
  ```js
181
- const { FodId, IdType } = require('fiftyone.pipeline.did');
239
+ const { FodId, IdType, Usage } = require('fiftyone.pipeline.did');
182
240
 
183
241
  // Either base64 alphabet is accepted, the standard one the cloud issues and
184
242
  // the URL-safe one a page puts in a link, with or without padding.
185
243
  const fodId = FodId.fromBase64(base64FromCloudService);
186
244
 
187
- const flags = fodId.flags;
245
+ const usage = fodId.usage; // Usage.NON_MARKETING / STANDARD / PERSONALIZED
246
+ const fromConsent = fodId.usageFromConsent;
188
247
  const type = fodId.type; // IdType.PROBABILISTIC / RANDOM / HASHED_EMAIL
189
248
  const licenseId = fodId.licenseId;
190
249
  const matchKey = fodId.matchKey; // Uint8Array: SHA-256 or GUID bytes, see type
191
250
 
192
251
  const domain = fodId.domain;
193
- const minutes = fodId.dateMinutes; // minutes since 2020-01-01T00:00:00Z
252
+ const minutes = fodId.date; // minutes since 2020-01-01T00:00:00Z
194
253
  const verified = await fodId.verify(publicKeyPem); // async (Web Crypto)
195
254
  const base64 = fodId.asBase64(); // standard alphabet with padding
196
255
  const inLink = fodId.asBase64Url(); // URL-safe alphabet, no padding
197
256
  ```
198
257
 
199
- `fodId.hash` remains as a deprecated alias of `matchKey` returning the same
200
- bytes, so existing callers keep working, and will be removed in a future
201
- release.
202
-
203
- `dateMinutes` is the envelope's own date as the unsigned 32-bit count of
204
- minutes since 2020-01-01T00:00:00Z, the value the OWID `public-key?date=`
205
- parameter takes, for callers comparing creation times. `date` now reports
206
- the same unsigned value.
258
+ `date` is the envelope's own date as the unsigned 32-bit count of minutes
259
+ since 2020-01-01T00:00:00Z, exactly as the wire carries it, and it is the
260
+ integer to compare when asking which of two identifiers was issued first.
207
261
 
208
262
  Where the value may not be a 51Did at all, read it without throwing.
209
263
 
@@ -66,7 +66,7 @@ live('DidClient against the cloud', () => {
66
66
 
67
67
  test('parses, verifies offline and verifies through the cloud', async () => {
68
68
  expect(fodId.version).toBe(3);
69
- expect(fodId.dateMinutes).toBeGreaterThan(0);
69
+ expect(fodId.date).toBeGreaterThan(0);
70
70
  const key = await client.publicKeyFor(fodId);
71
71
  expect(key).not.toBeNull();
72
72
  expect(key.publicKey).toMatch(/BEGIN PUBLIC KEY/);
@@ -40,6 +40,7 @@ const {
40
40
  signedWith,
41
41
  minutesOf
42
42
  } = require('./envelope');
43
+ const layout = require('../internal/layout');
43
44
 
44
45
  const RESOURCE = 'AQTestResourceKey';
45
46
  const LICENCE = 'TEST-LICENCE-KEY';
@@ -429,7 +430,7 @@ describe('DidClient verifySignature', () => {
429
430
  // A Reserved type parses at any length from the header up, so it is
430
431
  // the way to present a payload the cloud's length rule refuses.
431
432
  const short = new Uint8Array(20);
432
- short[FodId.FLAGS_OFFSET] = 0b11000000;
433
+ short[layout.FLAGS_OFFSET] = 0b11000000;
433
434
  const fod = await signedAt(pairs[1], new Date(START_2.getTime() + DAY), { payload: short });
434
435
  await expect(client.verifySignatureDetailed(fod)).resolves.toEqual({
435
436
  valid: false, reason: SignatureReason.LENGTH
@@ -449,9 +450,9 @@ describe('DidClient verifySignature', () => {
449
450
  test('true for a payload longer than the base (a context section)', async () => {
450
451
  const { pairs, json } = await schedule();
451
452
  const { client } = keyClient(json);
452
- const withContext = new Uint8Array(FodId.PAYLOAD_LENGTH + 40);
453
+ const withContext = new Uint8Array(layout.PAYLOAD_LENGTH + 40);
453
454
  withContext.set(canonicalPayload());
454
- withContext.fill(0x5A, FodId.PAYLOAD_LENGTH);
455
+ withContext.fill(0x5A, layout.PAYLOAD_LENGTH);
455
456
  const fod = await signedAt(pairs[1], new Date(START_2.getTime() + DAY), {
456
457
  payload: withContext
457
458
  });
@@ -461,9 +462,9 @@ describe('DidClient verifySignature', () => {
461
462
  test('true for a long context section and a long creator domain', async () => {
462
463
  const { pairs, json } = await schedule();
463
464
  const { client } = keyClient(json);
464
- const withContext = new Uint8Array(FodId.PAYLOAD_LENGTH + 200);
465
+ const withContext = new Uint8Array(layout.PAYLOAD_LENGTH + 200);
465
466
  withContext.set(canonicalPayload());
466
- withContext.fill(0x5A, FodId.PAYLOAD_LENGTH);
467
+ withContext.fill(0x5A, layout.PAYLOAD_LENGTH);
467
468
  const fod = await signedAt(pairs[1], new Date(START_2.getTime() + DAY), {
468
469
  payload: withContext,
469
470
  domain: 'a-self-hosted-container.example.internal.51degrees.com'