fiftyone.pipeline.did 4.5.37 → 4.5.38

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.
@@ -22,6 +22,12 @@ public key from their well known end point and verifies the ECDSA signature
22
22
  locally. When `crypto.subtle` is not available it falls back to the creator's
23
23
  remote verify end point.
24
24
 
25
+ Both end points are versioned, `/owid/api/v<version>/creator` and
26
+ `/owid/api/v<version>/verify`, and the version in the path is the version
27
+ byte of the OWID being verified rather than a fixed number, because a
28
+ creator serves each version of the format at its own path and returns 404
29
+ for the others.
30
+
25
31
  The public key is requested for the OWID's own creation date
26
32
  (`?date=<minutes>`), so OWIDs signed before a signing-key rotation still
27
33
  verify. A creator that does not support the `date` parameter ignores it and
@@ -38,6 +44,12 @@ The headers are sent to every creator domain that a verification touches,
38
44
  so only set a credential that all the creators in the tree are meant to
39
45
  see.
40
46
 
47
+ A custom header makes the key request a non simple cross origin request, so
48
+ the browser sends a `OPTIONS` preflight first and the creator has to answer
49
+ it with `Access-Control-Allow-Headers` naming that header. A creator that
50
+ does not answer the preflight cannot be given a credential this way, and
51
+ the request never leaves the browser.
52
+
41
53
  ## Reading answers instead of throwing
42
54
 
43
55
  An OWID is read from whatever a caller was handed, which on a public end
@@ -45,6 +45,25 @@ const otherKeyPair = nodeCrypto.generateKeyPairSync('ec', {
45
45
  // 2021-04-06 12:59 UTC expressed as minutes since 2020-01-01 00:00 UTC.
46
46
  const testDateInMinutes = 664619;
47
47
 
48
+ // The version byte every OWID built here is written with. The mocked
49
+ // creator only answers the path for this version, and the expected URL is
50
+ // built from the version the library read back out of the OWID, so the test
51
+ // and the library take the version from the same place and a wrong version
52
+ // in either one fails rather than passing quietly.
53
+ const testVersion = 3;
54
+
55
+ /**
56
+ * The creator key URL the library must request for an OWID. The version
57
+ * segment comes from the OWID's own version byte, which is the value the
58
+ * library uses, so this expectation cannot drift away from the code.
59
+ * @param {Object} o - the OWID that was read.
60
+ * @returns {string} the expected URL.
61
+ */
62
+ function expectedCreatorUrl(o) {
63
+ return "//" + o.domain + "/owid/api/v" + o.version +
64
+ "/creator?date=" + o.date;
65
+ }
66
+
48
67
  /**
49
68
  * Reads an OWID that the test expects to be valid, asserting the three facts
50
69
  * a read always reports before handing back the OWID itself.
@@ -64,8 +83,9 @@ const wrongKeyDomain = "wrong-key.swan-demo.uk";
64
83
  const emptyKeyDomain = "empty-key.swan-demo.uk";
65
84
 
66
85
  /**
67
- * Builds the unsigned portion of a version 3 OWID as a byte array in the
68
- * same form the library serializes for verification.
86
+ * Builds the unsigned portion of an OWID, at the version named by
87
+ * testVersion, as a byte array in the same form the library serializes for
88
+ * verification.
69
89
  * @param {string} domain - the creator domain.
70
90
  * @param {number} dateInMinutes - minutes since the OWID base date.
71
91
  * @param {Buffer} payload - the payload bytes.
@@ -77,7 +97,7 @@ function buildUnsignedOWID(domain, dateInMinutes, payload) {
77
97
  var length = Buffer.alloc(4);
78
98
  length.writeUInt32LE(payload.length);
79
99
  return Buffer.concat([
80
- Buffer.from([3]),
100
+ Buffer.from([testVersion]),
81
101
  Buffer.from(domain, 'ascii'),
82
102
  Buffer.from([0]),
83
103
  date,
@@ -132,7 +152,10 @@ beforeEach(() => {
132
152
  }
133
153
  var url = new URL(urlString);
134
154
 
135
- if (url.pathname.endsWith("/creator")) {
155
+ // A real creator serves each version of the format at its own
156
+ // path and returns 404 for the others, so answering any version
157
+ // here would hide a request sent to the wrong one.
158
+ if (url.pathname === "/owid/api/v" + testVersion + "/creator") {
136
159
  // This domain returns a header only PEM with no key body, which
137
160
  // exercises the empty public key guard in the library.
138
161
  if (url.hostname == emptyKeyDomain) {
@@ -169,8 +192,38 @@ test('crypto verify valid OWID passes', () => {
169
192
  // The library must have used the public key path, so the only
170
193
  // request is to the creator end point.
171
194
  expect(fetch.mock.calls.length).toBe(1);
195
+ expect(fetch.mock.calls[0][0]).toBe(expectedCreatorUrl(o));
196
+ });
197
+ });
198
+
199
+ test('crypto verify asks the end point for the version the OWID carries', () => {
200
+ // Every other OWID in this suite is version 3, the same value a constant
201
+ // in the path would give, so this is the test that catches a constant
202
+ // put back. A version 2 OWID must ask the version 2 end point, which
203
+ // the mocked creator does not serve, so the key is unavailable and the
204
+ // one request that was made names the right path.
205
+ var date = Buffer.alloc(4);
206
+ date.writeUInt32LE(testDateInMinutes);
207
+ var payload = Buffer.from("example");
208
+ var length = Buffer.alloc(4);
209
+ length.writeUInt32LE(payload.length);
210
+ var unsigned = Buffer.concat([
211
+ Buffer.from([2]),
212
+ Buffer.from(creatorDomain, 'ascii'),
213
+ Buffer.from([0]),
214
+ date,
215
+ length,
216
+ payload
217
+ ]);
218
+ var o = read(signOWID(unsigned, creatorKeyPair.privateKey));
219
+ expect(o.version).toBe(2);
220
+
221
+ return o.checkSignature().then(r => {
222
+ expect(r.status).toBe(owid.SignatureStatus.KEY_UNAVAILABLE);
223
+ expect(fetch.mock.calls.length).toBe(1);
172
224
  expect(fetch.mock.calls[0][0]).toBe(
173
- "//" + creatorDomain + "/owid/api/v1/creator?date=" + testDateInMinutes);
225
+ "//" + creatorDomain + "/owid/api/v2/creator?date=" +
226
+ testDateInMinutes);
174
227
  });
175
228
  });
176
229
 
@@ -139,6 +139,18 @@ fixtures.forEach(f => {
139
139
  publicKeys[f.domain] = f.publicKeySPKI;
140
140
  });
141
141
 
142
+ /**
143
+ * The creator key URL the library must request for an OWID. The version
144
+ * segment comes from the OWID's own version byte, which is the value the
145
+ * library uses, so this expectation cannot drift away from the code.
146
+ * @param {Object} o - the OWID that was read.
147
+ * @returns {string} the expected URL.
148
+ */
149
+ function expectedCreatorUrl(o) {
150
+ return "//" + o.domain + "/owid/api/v" + o.version +
151
+ "/creator?date=" + o.date;
152
+ }
153
+
142
154
  /**
143
155
  * Returns the base 64 string with a single byte changed at the given offset
144
156
  * of the decoded byte array.
@@ -164,7 +176,12 @@ beforeEach(() => {
164
176
  }
165
177
  var url = new URL(urlString);
166
178
 
167
- if (url.pathname.endsWith("/creator") && publicKeys[url.hostname]) {
179
+ // Every fixture is a version 3 OWID and a real creator serves each
180
+ // version of the format at its own path, returning 404 for the
181
+ // others, so the key is only handed back on the path that names the
182
+ // version the OWID was written in.
183
+ if (url.pathname === "/owid/api/v3/creator" &&
184
+ publicKeys[url.hostname]) {
168
185
  return Promise.resolve(JSON.stringify({
169
186
  publicKeySPKI: publicKeys[url.hostname]
170
187
  }));
@@ -186,8 +203,8 @@ fixtures.forEach(f => {
186
203
  // The library must have used the public key path, so the only
187
204
  // request is to the creator end point.
188
205
  expect(fetch.mock.calls.length).toBe(1);
189
- expect(fetch.mock.calls[0][0]).toBe(
190
- "//" + f.domain + "/owid/api/v1/creator?date=" + o.date);
206
+ expect(fetch.mock.calls[0][0]).toBe(expectedCreatorUrl(o));
207
+ expect(o.version).toBe(3);
191
208
  });
192
209
  });
193
210
 
@@ -0,0 +1,253 @@
1
+ /* ****************************************************************************
2
+ * Copyright 2021 51 Degrees Mobile Experts Limited (51degrees.com)
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5
+ * use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+ * License for the specific language governing permissions and limitations
14
+ * under the License.
15
+ * ***************************************************************************/
16
+
17
+ // These tests verify a real identifier issued by a live creator against that
18
+ // creator's real published signing key schedule, rather than against a key
19
+ // pair the test made up for itself. The other suites build their own OWIDs,
20
+ // so they prove the library is self consistent but they cannot prove it can
21
+ // talk to a creator that actually exists.
22
+ //
23
+ // The test data in testdata was taken on 4 September 2026 from
24
+ // cloud.51degrees.com, and the identifier names 51d.es as its creator. Both
25
+ // files are public. An identifier is meant to be handed to anyone who asks,
26
+ // and the key schedule is the set of public keys the creator publishes so
27
+ // that anyone can check a signature, so neither carries a secret and neither
28
+ // can be used to sign anything.
29
+ //
30
+ // The creator is stood in for here rather than called over the network, so
31
+ // the suite stays offline and repeatable, but the stand in behaves the way
32
+ // the real service does. It serves each version of the format at its own path
33
+ // and returns 404 for the others, and it picks the signing key by the
34
+ // creation date the request asks for. Both of those matter. Measured against
35
+ // the live service on 4 September 2026, GET /owid/api/v1/creator and
36
+ // /owid/api/v2/creator both answered 404 whilst /owid/api/v3/creator answered
37
+ // 401, meaning it exists and wants a credential.
38
+
39
+ const owid = require('./v1');
40
+ const nodeCrypto = require('crypto');
41
+ const fs = require('fs');
42
+ const path = require('path');
43
+
44
+ Object.defineProperty(global.self, 'crypto', {
45
+ value: {
46
+ subtle: nodeCrypto.webcrypto.subtle
47
+ }
48
+ });
49
+
50
+ // The identifier as it was handed out, together with the facts recorded
51
+ // alongside it at the time it was taken.
52
+ const fixture = JSON.parse(fs.readFileSync(
53
+ path.join(__dirname, 'testdata', '51did-identifier.json'), 'utf8'));
54
+
55
+ // The creator's published signing key schedule, oldest first. A key runs from
56
+ // its own start until the next key starts.
57
+ const schedule = JSON.parse(fs.readFileSync(
58
+ path.join(__dirname, 'testdata', '51did-public-keys.json'), 'utf8'))
59
+ .slice()
60
+ .sort((a, b) => Date.parse(a.startsAt) - Date.parse(b.startsAt));
61
+
62
+ // The facts the fixture was recorded with. The identifier is a version 3
63
+ // OWID created on 4 September 2026 by 51d.es.
64
+ const expectedVersion = 3;
65
+ const expectedDomain = "51d.es";
66
+ const expectedDate = 3510720;
67
+
68
+ // The moment the stand in treats as now, in minutes since the OWID base
69
+ // date, being 14 September 2026, ten days after the identifier was created
70
+ // and in the week that followed. Every port's stand in uses this moment. A
71
+ // real creator answers an undated request with the key in force at the
72
+ // moment of the request, so by then that is no longer the key that signed
73
+ // this identifier.
74
+ const requestMoment = expectedDate + 10 * 24 * 60;
75
+
76
+ // The OWID base date as a millisecond timestamp, so a date in minutes can be
77
+ // compared with the start of a key's window.
78
+ const owidBase = Date.UTC(2020, 0, 1);
79
+
80
+ /**
81
+ * Returns the key from the published schedule that was in force at the
82
+ * creation date a creator request asks for, which is the way a real creator
83
+ * answers the date parameter.
84
+ * @param {number} dateInMinutes - minutes since the OWID base date.
85
+ * @returns {Object|null} the key, or null when the date falls before the
86
+ * schedule starts.
87
+ */
88
+ function keyInForceAt(dateInMinutes) {
89
+ var at = owidBase + dateInMinutes * 60000;
90
+ var found = null;
91
+ schedule.forEach(k => {
92
+ if (Date.parse(k.startsAt) <= at) {
93
+ found = k;
94
+ }
95
+ });
96
+ return found;
97
+ }
98
+
99
+ /**
100
+ * Reads an OWID that the test expects to be valid, asserting the three facts
101
+ * a read always reports before handing back the OWID itself.
102
+ * @param {string} data - the OWID as base 64.
103
+ * @returns {Object} the OWID.
104
+ */
105
+ function read(data) {
106
+ var r = owid.parse(data);
107
+ expect(r.ok).toBe(true);
108
+ expect(r.status).toBe(owid.ParseStatus.PARSED);
109
+ expect(r.owid).not.toBeNull();
110
+ return r.owid;
111
+ }
112
+
113
+ /**
114
+ * Stands in for the creator. Only the path naming the version given answers,
115
+ * every other path returns the 404 the live service returns, and the key
116
+ * handed back is the one in force at the date on the request unless the
117
+ * stand in is asked to ignore the date. As the live service does, a request
118
+ * naming no date is answered with the key in force at the moment of the
119
+ * request, a date later than that moment is read as that moment, and a date
120
+ * that is not a number is refused with a 400.
121
+ * @param {Object} [options] - servedVersion is the only version served, and
122
+ * ignoreDate set to true returns the key in force at requestMoment whatever
123
+ * date is asked for, which is what a creator that ignores the date does.
124
+ */
125
+ function mockCreator(options) {
126
+ var servedVersion = options && options.servedVersion !== undefined
127
+ ? options.servedVersion
128
+ : expectedVersion;
129
+ var ignoreDate = options && options.ignoreDate === true;
130
+ fetchMock.mockResponse(req => {
131
+ var urlString = req.url;
132
+ if (urlString.startsWith("//")) {
133
+ urlString = "https:" + urlString;
134
+ }
135
+ var url = new URL(urlString);
136
+ if (url.hostname !== expectedDomain ||
137
+ url.pathname !== "/owid/api/v" + servedVersion + "/creator") {
138
+ return Promise.resolve({
139
+ status: 404,
140
+ body: "Status code: 404, method not found"
141
+ });
142
+ }
143
+ var asked = url.searchParams.get("date");
144
+ var at;
145
+ if (ignoreDate || asked === null) {
146
+ at = requestMoment;
147
+ } else if (/^\d+$/.test(asked) === false) {
148
+ return Promise.resolve({
149
+ status: 400,
150
+ body: "date must be the number of minutes since 2020-01-01"
151
+ });
152
+ } else {
153
+ at = Math.min(Number(asked), requestMoment);
154
+ }
155
+ var key = keyInForceAt(at);
156
+ if (key === null) {
157
+ return Promise.resolve({
158
+ status: 404,
159
+ body: "no published key covers that date"
160
+ });
161
+ }
162
+ return Promise.resolve(JSON.stringify({
163
+ publicKeySPKI: key.publicKey
164
+ }));
165
+ });
166
+ }
167
+
168
+ beforeEach(() => {
169
+ fetchMock.resetMocks();
170
+ });
171
+
172
+ test('the published identifier reads as the version 3 OWID it was ' +
173
+ 'recorded as', () => {
174
+ var o = read(fixture.identifier);
175
+
176
+ expect(o.version).toBe(expectedVersion);
177
+ expect(o.domain).toBe(expectedDomain);
178
+ expect(o.date).toBe(expectedDate);
179
+ expect(o.signature.length).toBe(64);
180
+ });
181
+
182
+ test('the published identifier verifies against the published key schedule',
183
+ () => {
184
+ mockCreator();
185
+ var o = read(fixture.identifier);
186
+
187
+ return o.verify().then(valid => {
188
+ expect(valid).toBe(true);
189
+ // One request, to the end point that serves the version this
190
+ // identifier was written in, asking for the key that was in force
191
+ // when it was created.
192
+ expect(fetch.mock.calls.length).toBe(1);
193
+ expect(fetch.mock.calls[0][0]).toBe(
194
+ "//" + o.domain + "/owid/api/v" + o.version +
195
+ "/creator?date=" + o.date);
196
+ });
197
+ });
198
+
199
+ test('the published identifier reports a valid signature', () => {
200
+ mockCreator();
201
+ var o = read(fixture.identifier);
202
+
203
+ return o.checkSignature().then(r => {
204
+ expect(r.ok).toBe(true);
205
+ expect(r.status).toBe(owid.SignatureStatus.SIGNATURE_VALID);
206
+ });
207
+ });
208
+
209
+ test('a creator that only serves an older version leaves the signature ' +
210
+ 'unjudged', () => {
211
+ // This is the outcome the library produced against the live service for
212
+ // as long as it asked for version 1 whatever version the OWID named.
213
+ // Nothing was forged and nothing was wrong with the identifier, the key
214
+ // simply could never be fetched.
215
+ mockCreator({ servedVersion: 1 });
216
+ var o = read(fixture.identifier);
217
+
218
+ return o.checkSignature().then(r => {
219
+ expect(r.ok).toBe(false);
220
+ expect(r.status).toBe(owid.SignatureStatus.KEY_UNAVAILABLE);
221
+ });
222
+ });
223
+
224
+ test('the key is chosen by the creation date, so the key in force in the ' +
225
+ 'following week does not verify an older identifier', () => {
226
+ // The key that signed this identifier is not the key in force in the
227
+ // following week, so a creator that ignored the date and handed back the
228
+ // key in force at the moment of the request would report a genuine
229
+ // identifier as a forgery. That is what the date parameter is for.
230
+ var signing = keyInForceAt(expectedDate);
231
+ expect(signing).not.toBeNull();
232
+ expect(signing.startsAt).not.toBe(keyInForceAt(requestMoment).startsAt);
233
+
234
+ mockCreator({ ignoreDate: true });
235
+ var o = read(fixture.identifier);
236
+
237
+ return o.verify().then(valid => {
238
+ expect(valid).toBe(false);
239
+ });
240
+ });
241
+
242
+ test('the identifier verifies offline against the published key on its own',
243
+ () => {
244
+ // No network at all, which proves the fixture and the published schedule
245
+ // agree independently of how the creator URL is built.
246
+ var o = read(fixture.identifier);
247
+
248
+ return o.verifyWithPublicKey(keyInForceAt(o.date).publicKey)
249
+ .then(valid => {
250
+ expect(valid).toBe(true);
251
+ expect(fetch.mock.calls.length).toBe(0);
252
+ });
253
+ });
@@ -66,7 +66,12 @@ beforeEach(() => {
66
66
  }
67
67
  var url = new URL(urlString);
68
68
 
69
- if (url.pathname.endsWith("/verify")) {
69
+ // Every OWID these tests read is version 2, and a real creator
70
+ // serves each version of the format at its own path, so only the
71
+ // version 2 verify path answers. A request to any other version
72
+ // path is the defect this guards against, and it falls through to
73
+ // the 404 below.
74
+ if (url.pathname === "/owid/api/v2/verify") {
70
75
  // The library sends the OWID being verified as form parameters
71
76
  // in the POST body, so read them from there.
72
77
  var body = new URLSearchParams(await req.text());
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "4.4.32-context.3",
3
+ "createdAt": "2026-09-04T20:37:00.4032665Z",
4
+ "identifier": "AzUxZC5lcwDAkTUAOAAAAAFkcBNhYL0rPISwcMhuxl0ezIe0Nywzr/lhMtzX5QjydfVQA40Aw6iylkIfGYQWecrhzDfF8g11B1KtuOBxa98MEyJZOXw3PB/EBuyOfGxZ/pu6rlt8zpCyIn1Prbu7V300bQBqrqyYfU0nNeZf/PCwLtXJRak76A==",
5
+ "source": "cloud.51degrees.com"
6
+ }
@@ -0,0 +1,182 @@
1
+ [
2
+ {
3
+ "startsAt": "2026-05-11T00:00:00Z",
4
+ "weekStart": "2026-05-11T00:00:00Z",
5
+ "created": "2026-05-13T07:53:00Z",
6
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1S5u0ke8IvXO9tKBBjkNgUTLkMRR\nHcdbVUCYIDZbhOvvU2Sa7OTVfb/jDwcHwd/KVrl33uHBFHuH2E/UdBXBSw==\n-----END PUBLIC KEY-----"
7
+ },
8
+ {
9
+ "startsAt": "2026-05-18T00:00:00Z",
10
+ "weekStart": "2026-05-18T00:00:00Z",
11
+ "created": "2026-05-18T05:31:00Z",
12
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMaXEnA/hG6e9ZBIBxRnqaOspCS2f\nMDZ7MRaGMVo2CL0hcqdI4rP+KJll83xUxQq0nwwo11j/C2BwWfCbfcjFbw==\n-----END PUBLIC KEY-----"
13
+ },
14
+ {
15
+ "startsAt": "2026-05-25T00:00:00Z",
16
+ "weekStart": "2026-05-25T00:00:00Z",
17
+ "created": "2026-05-25T05:41:00Z",
18
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7GW67FrL7KfFZpxWt94xM4mgqtXB\n3jLL1gE/bcLaXVO7Myj5eo38oGTggos5iWAxlpY0bFUuqHSTkffQ+2jxog==\n-----END PUBLIC KEY-----"
19
+ },
20
+ {
21
+ "startsAt": "2026-06-01T00:00:00Z",
22
+ "weekStart": "2026-06-01T00:00:00Z",
23
+ "created": "2026-06-01T05:46:00Z",
24
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOUaE86mCaq/kUcSbfdp6lIckGvPd\nnbDr++RVrj6WmJbOXpvdKk/nFfPb27TYz1P/mT0/YgGLsNRii/DK41Ag2g==\n-----END PUBLIC KEY-----"
25
+ },
26
+ {
27
+ "startsAt": "2026-06-08T00:00:00Z",
28
+ "weekStart": "2026-06-08T00:00:00Z",
29
+ "created": "2026-06-08T05:44:00Z",
30
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6FK+7t+HkffkPe9Yhqtuy+lt8bgL\nPGUYty3nFid3mGub6QnIJFkyj9aAPe3VRcPY77Oaux78Xy+2tcdSBb91LA==\n-----END PUBLIC KEY-----"
31
+ },
32
+ {
33
+ "startsAt": "2026-06-15T00:00:00Z",
34
+ "weekStart": "2026-06-15T00:00:00Z",
35
+ "created": "2026-06-15T05:57:00Z",
36
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYU6uwIX2RJ6YtC516iCEITSs0W++\nRLM/Yo3ZmM2iCYe/HfSgqJwfy7ioVce3784+/YPgkmX8LBNwJTarVy/SmA==\n-----END PUBLIC KEY-----"
37
+ },
38
+ {
39
+ "startsAt": "2026-06-22T00:00:00Z",
40
+ "weekStart": "2026-06-22T00:00:00Z",
41
+ "created": "2026-06-22T05:59:00Z",
42
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJTOx47gMykTRMwsZq6xMrwO0+2ny\npeIxKfa6gvPWpBvDFFJMbNR9cUbHzrb0iFs1WzlKfEAjSrCtxaSfsNXkoA==\n-----END PUBLIC KEY-----"
43
+ },
44
+ {
45
+ "startsAt": "2026-06-29T00:00:00Z",
46
+ "weekStart": "2026-06-29T00:00:00Z",
47
+ "created": "2026-06-29T05:44:00Z",
48
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoKU47SHVpEjqD9txbXA4/StmOz2O\n1taTquaTes9lE+0jHB9/A8EUcodt8+oAG/YRRA6pjpe01/FlkgKIKWrhlw==\n-----END PUBLIC KEY-----"
49
+ },
50
+ {
51
+ "startsAt": "2026-07-06T00:00:00Z",
52
+ "weekStart": "2026-07-06T00:00:00Z",
53
+ "created": "2026-07-06T05:33:00Z",
54
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEx2jFmRxOqUH74IRCEPgNG0/e7FyN\neWstmBx0GVRNORYou5LtA++OlygptcuPRy/XqSy+/etiz6o8SzUiZ2F4qQ==\n-----END PUBLIC KEY-----"
55
+ },
56
+ {
57
+ "startsAt": "2026-07-13T00:00:00Z",
58
+ "weekStart": "2026-07-13T00:00:00Z",
59
+ "created": "2026-07-13T05:15:00Z",
60
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhBOts6uqkfMIjfkNtoyYxNNAFtA1\nRvkx7ZdvbvlQDDgze7YadBUmvJwMNzBNeOeyAIshl8FklLB8R2VpyAkIRg==\n-----END PUBLIC KEY-----"
61
+ },
62
+ {
63
+ "startsAt": "2026-07-20T00:00:00Z",
64
+ "weekStart": "2026-07-20T00:00:00Z",
65
+ "created": "2026-07-20T05:19:00Z",
66
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElZGKUk21yUH5DaJj41X3NiqDUoGa\niGgq/mZFQcTQkHQ2mr/Y6W6JbcSRiRFfTT21bpYd7BkatRotGtpHxXPuvQ==\n-----END PUBLIC KEY-----"
67
+ },
68
+ {
69
+ "startsAt": "2026-07-27T00:00:00Z",
70
+ "weekStart": "2026-07-27T00:00:00Z",
71
+ "created": "2026-07-27T05:22:00Z",
72
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEayA4kh6boraOurRdy4epwDcW5Gay\ntuKdpqdLtcz5yEtH4bpmJxx74ZGbStWuilAWvLutBOWp0mAjpUu5JgPE3A==\n-----END PUBLIC KEY-----"
73
+ },
74
+ {
75
+ "startsAt": "2026-08-03T00:00:00Z",
76
+ "weekStart": "2026-08-03T00:00:00Z",
77
+ "created": "2026-08-03T05:18:00Z",
78
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZMR1ut4GRhbc1dOxwXree/efQTtg\nDqzK2dicaCRO5Pz4E5ms3rDksBrv382MI0W7BgrSPR5VAtPEA93S/xcmFg==\n-----END PUBLIC KEY-----"
79
+ },
80
+ {
81
+ "startsAt": "2026-08-10T00:00:00Z",
82
+ "weekStart": "2026-08-10T00:00:00Z",
83
+ "created": "2026-08-10T04:35:00Z",
84
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2UwwNdW+L68uziCcJUegj49Xi07P\nY4EGiGgMlESa2xoJfwVHoUc6oqRNqrPBnsrqQYsU2uYlR9QOWIomui5qEw==\n-----END PUBLIC KEY-----"
85
+ },
86
+ {
87
+ "startsAt": "2026-08-17T00:00:00Z",
88
+ "weekStart": "2026-08-17T00:00:00Z",
89
+ "created": "2026-08-17T04:18:00Z",
90
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpcTDCZlVsO1glWnDqfBiTOZysOIz\ndx2eFdPbrndBTWz7fu588pjZACM2CCs03dslwehiCuOHqJtdmHQv41V6Yw==\n-----END PUBLIC KEY-----"
91
+ },
92
+ {
93
+ "startsAt": "2026-08-24T00:00:00Z",
94
+ "weekStart": "2026-08-24T00:00:00Z",
95
+ "created": "2026-08-24T04:19:00Z",
96
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJ7YEHH6liCiSn6jmcZXXydRRiCAR\nHaZHkyazSq1bsUipbYOmz3kCrk1r3DmqrqiKBWZZS64yrGckLf5UkNsteg==\n-----END PUBLIC KEY-----"
97
+ },
98
+ {
99
+ "startsAt": "2026-08-31T00:00:00Z",
100
+ "weekStart": "2026-08-31T00:00:00Z",
101
+ "created": "2026-08-31T04:21:00Z",
102
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEilbjpzqx8zx3oShIDgDoGTcBqivw\n08FKqFvRtX8rCS4Ftguvu2RwXDFLEwho0IhD/boxM1IwZ6u34hwkhf5AiQ==\n-----END PUBLIC KEY-----"
103
+ },
104
+ {
105
+ "startsAt": "2026-09-07T00:00:00Z",
106
+ "weekStart": "2026-09-07T00:00:00Z",
107
+ "created": "2026-09-01T10:54:00Z",
108
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0KhXTjSD4xyW7Zc9d+kHWPzhx1yZ\n7c3gAMCF0iFHJokWejq9FrWSVEc9bMBJPv1Br/q/oIiPTfPuhiOJ1xMkFQ==\n-----END PUBLIC KEY-----"
109
+ },
110
+ {
111
+ "startsAt": "2026-09-14T00:00:00Z",
112
+ "weekStart": "2026-09-14T00:00:00Z",
113
+ "created": "2026-09-01T10:54:00Z",
114
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEXKsoKyAZRkK9o1k4heQjAzUjHEP\nPWFnE0KKA5RI9wZHQffQcrK6rb2hcnObeZsYz7OQ/FneOMWpeiqK03OKHA==\n-----END PUBLIC KEY-----"
115
+ },
116
+ {
117
+ "startsAt": "2026-09-21T00:00:00Z",
118
+ "weekStart": "2026-09-21T00:00:00Z",
119
+ "created": "2026-09-01T10:54:00Z",
120
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFGOV57ItWDb76wzvzx+m2GWsJ+dX\n5OvtQFdPkAFV9OL4w0ntiVG0iUzZEGDRyCE5Xla82jhvA8YILz0gTdMFHQ==\n-----END PUBLIC KEY-----"
121
+ },
122
+ {
123
+ "startsAt": "2026-09-28T00:00:00Z",
124
+ "weekStart": "2026-09-28T00:00:00Z",
125
+ "created": "2026-09-01T10:54:00Z",
126
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEi85ASgFPo4LerVK/QU3J5mdaW4o\nCYRzjqNqLzRjXWoiAQTyB7LB6MwDXSv4svsh7SNpjNmOFXcjVJ3CwwoNjA==\n-----END PUBLIC KEY-----"
127
+ },
128
+ {
129
+ "startsAt": "2026-10-05T00:00:00Z",
130
+ "weekStart": "2026-10-05T00:00:00Z",
131
+ "created": "2026-09-01T10:54:00Z",
132
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoRogg/dXpCiV3psJPu9ffND7JvBm\nFg9gOgRlXrCXFBIIoEWvGb/7q1MsquKoTF1uKkHy7HEJTkr9CUvSd83CcQ==\n-----END PUBLIC KEY-----"
133
+ },
134
+ {
135
+ "startsAt": "2026-10-12T00:00:00Z",
136
+ "weekStart": "2026-10-12T00:00:00Z",
137
+ "created": "2026-09-01T10:54:00Z",
138
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBkAwOOX+Oykv6xHewe0CJuXzSGts\npAFEc82QhwzlanYJlY1lHLuP2D1xsEGOqsKXf9BOb/SesRjNHSTmi6ZGpw==\n-----END PUBLIC KEY-----"
139
+ },
140
+ {
141
+ "startsAt": "2026-10-19T00:00:00Z",
142
+ "weekStart": "2026-10-19T00:00:00Z",
143
+ "created": "2026-09-01T10:54:00Z",
144
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkkUuirYrxuFQmWjYZRWGSyvJuYcA\nOWz4zIP6c33VOKqEEtkvGJDoCV0Qm6EdgGJspDRQ2d7dtbKV2kMPnKW7UA==\n-----END PUBLIC KEY-----"
145
+ },
146
+ {
147
+ "startsAt": "2026-10-26T00:00:00Z",
148
+ "weekStart": "2026-10-26T00:00:00Z",
149
+ "created": "2026-09-01T10:54:00Z",
150
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnqgWDgDUXTqY2ZMOePx7qBFGgD/w\nepMX7HmpyRWr0XyZSUzeq2duV+VMJzNiZmQ0x34F1auNd+Dcf/3JkvXa/w==\n-----END PUBLIC KEY-----"
151
+ },
152
+ {
153
+ "startsAt": "2026-11-02T00:00:00Z",
154
+ "weekStart": "2026-11-02T00:00:00Z",
155
+ "created": "2026-09-01T10:54:00Z",
156
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENlPq/5aO/kU6VbLMipaZMukRQFre\nMsE7Tp+8CqIZA7HPi/ikrgCOOeimi5QURTjXin8JoUiVNae3SFxBRwTsYQ==\n-----END PUBLIC KEY-----"
157
+ },
158
+ {
159
+ "startsAt": "2026-11-09T00:00:00Z",
160
+ "weekStart": "2026-11-09T00:00:00Z",
161
+ "created": "2026-09-01T10:54:00Z",
162
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAET3UEAs41VZiXt+SmzEzL/4pyJJxV\nwoY+sD/haSnvyRUxSurp7ju8ACZm/bDKOPtM9xmZtRbmQaU2y221EB+Okw==\n-----END PUBLIC KEY-----"
163
+ },
164
+ {
165
+ "startsAt": "2026-11-16T00:00:00Z",
166
+ "weekStart": "2026-11-16T00:00:00Z",
167
+ "created": "2026-09-01T10:54:00Z",
168
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgEbUOGc6d+rJNVrW6FSozCr3MXYr\njDf1ap19zD2pJZaRI0ORxs5A19mAEf11Nl64NT0gPI2ls9RVnMDY1tq8sg==\n-----END PUBLIC KEY-----"
169
+ },
170
+ {
171
+ "startsAt": "2026-11-23T00:00:00Z",
172
+ "weekStart": "2026-11-23T00:00:00Z",
173
+ "created": "2026-09-01T10:54:00Z",
174
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKAazotnWdw0hbDJMRJjdCwa5Eaem\nMfwA2+Aks6g7NelJ4GsU4yLhITmrmcFWexSz3JzCa+wN255rZMqiIFSHIA==\n-----END PUBLIC KEY-----"
175
+ },
176
+ {
177
+ "startsAt": "2026-11-30T00:00:00Z",
178
+ "weekStart": "2026-11-30T00:00:00Z",
179
+ "created": "2026-09-01T10:54:00Z",
180
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhDrkhpx28KTSNqXvs8vEXoUJEmLD\nqEyaVONFOZxgsnJehJqBtHVOevjptFfY6G1E6T4Opi4hH6m+/elwh8R2aA==\n-----END PUBLIC KEY-----"
181
+ }
182
+ ]
@@ -1016,6 +1016,23 @@ var owid = (function () {
1016
1016
  });
1017
1017
  }
1018
1018
 
1019
+ /**
1020
+ * Builds the URL of one of the creator's end points for the OWID being
1021
+ * verified. The version in the path is the OWID's own version byte,
1022
+ * because a creator serves each version of the format at its own path
1023
+ * and only the end point that understands the version an OWID was
1024
+ * written in can answer for it. Both the key request and the fall back
1025
+ * verify request are built here so the two cannot name different
1026
+ * versions of the same creator.
1027
+ * @param {Object} instance - the OWID being verified.
1028
+ * @param {string} method - the end point, "creator" or "verify".
1029
+ * @returns {string} the URL, with no query string.
1030
+ */
1031
+ function creatorApiUrl(instance, method) {
1032
+ return "//" + instance.domain +
1033
+ "/owid/api/v" + instance.version + "/" + method;
1034
+ }
1035
+
1019
1036
  /**
1020
1037
  * Fetches the creator's public key and checks the signature here. The key
1021
1038
  * is requested for the OWID's own creation date, so OWIDs signed before a
@@ -1027,7 +1044,7 @@ var owid = (function () {
1027
1044
  */
1028
1045
  function verifyWithCreatorKey(instance, unsigned, extra) {
1029
1046
  var subtle = getSubtle();
1030
- var url = "//" + instance.domain + "/owid/api/v1/creator";
1047
+ var url = creatorApiUrl(instance, "creator");
1031
1048
  if (instance.date != null) {
1032
1049
  url += "?date=" + instance.date;
1033
1050
  }
@@ -1096,8 +1113,7 @@ var owid = (function () {
1096
1113
  var body = new URLSearchParams();
1097
1114
  body.append("parent", encodeBase64(joined));
1098
1115
  body.append("owid", instance.data);
1099
- var url = "//" + instance.domain +
1100
- "/owid/api/v" + instance.version + "/verify";
1116
+ var url = creatorApiUrl(instance, "verify");
1101
1117
  return fetch(url, {
1102
1118
  method: "POST",
1103
1119
  mode: "cors",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fiftyone.pipeline.did",
3
- "version": "4.5.37",
3
+ "version": "4.5.38",
4
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.",
5
5
  "keywords": [
6
6
  "51degrees",
@@ -17,7 +17,7 @@
17
17
  "author": "51Degrees Engineering <engineering@51degrees.com>",
18
18
  "license": "EUPL-1.2",
19
19
  "dependencies": {
20
- "owid": "github:51Degrees/owid-js#8363541e"
20
+ "owid": "github:51Degrees/owid-js#9aeb0d24"
21
21
  },
22
22
  "bundleDependencies": [
23
23
  "owid"
package/readme.md CHANGED
@@ -158,7 +158,7 @@ and `FodId.checkSignature()` run without contacting a network endpoint.
158
158
 
159
159
  The library is not on the npm registry, so `package.json` names it as a
160
160
  GitHub reference, which npm resolves by cloning the repository. The reference
161
- is `github:51Degrees/owid-js#8363541e`, the merged hardening commit on `main`
161
+ is `github:51Degrees/owid-js#9aeb0d24`, the commit on `main` that carries the key selection fix
162
162
  of the [51Degrees/owid-js](https://github.com/51Degrees/owid-js) fork. That
163
163
  applies only when working in this repository, because the published
164
164
  `fiftyone.pipeline.did` package carries the OWID source inside its own