@atproto/bsky 0.0.235 → 0.0.236
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/CHANGELOG.md +13 -0
- package/dist/hydration/hydrator.d.ts.map +1 -1
- package/dist/hydration/hydrator.js +27 -0
- package/dist/hydration/hydrator.js.map +1 -1
- package/dist/lexicons/app/bsky/actor/defs.defs.d.ts +8 -0
- package/dist/lexicons/app/bsky/actor/defs.defs.d.ts.map +1 -1
- package/dist/lexicons/app/bsky/actor/defs.defs.js +3 -0
- package/dist/lexicons/app/bsky/actor/defs.defs.js.map +1 -1
- package/dist/lexicons/chat/bsky/authFullChatClient.defs.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/authFullChatClient.defs.js +1 -0
- package/dist/lexicons/chat/bsky/authFullChatClient.defs.js.map +1 -1
- package/dist/lexicons/chat/bsky/convo/defs.defs.d.ts +53 -14
- package/dist/lexicons/chat/bsky/convo/defs.defs.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/convo/defs.defs.js +33 -5
- package/dist/lexicons/chat/bsky/convo/defs.defs.js.map +1 -1
- package/dist/lexicons/chat/bsky/convo/getConvoForMembers.defs.d.ts +1 -1
- package/dist/lexicons/chat/bsky/convo/getConvoForMembers.defs.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/convo/getConvoForMembers.defs.js +1 -0
- package/dist/lexicons/chat/bsky/convo/getConvoForMembers.defs.js.map +1 -1
- package/dist/lexicons/chat/bsky/convo/getLog.defs.d.ts +2 -2
- package/dist/lexicons/chat/bsky/convo/getLog.defs.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/convo/getLog.defs.js +3 -0
- package/dist/lexicons/chat/bsky/convo/getLog.defs.js.map +1 -1
- package/dist/lexicons/chat/bsky/embed/joinLink.d.ts +3 -0
- package/dist/lexicons/chat/bsky/embed/joinLink.d.ts.map +1 -0
- package/dist/lexicons/chat/bsky/embed/joinLink.defs.d.ts +99 -0
- package/dist/lexicons/chat/bsky/embed/joinLink.defs.d.ts.map +1 -0
- package/dist/lexicons/chat/bsky/embed/joinLink.defs.js +28 -0
- package/dist/lexicons/chat/bsky/embed/joinLink.defs.js.map +1 -0
- package/dist/lexicons/chat/bsky/embed/joinLink.js +6 -0
- package/dist/lexicons/chat/bsky/embed/joinLink.js.map +1 -0
- package/dist/lexicons/chat/bsky/embed.d.ts +2 -0
- package/dist/lexicons/chat/bsky/embed.d.ts.map +1 -0
- package/dist/lexicons/chat/bsky/embed.js +5 -0
- package/dist/lexicons/chat/bsky/embed.js.map +1 -0
- package/dist/lexicons/chat/bsky/group/addMembers.defs.d.ts +1 -1
- package/dist/lexicons/chat/bsky/group/addMembers.defs.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/group/addMembers.defs.js +1 -0
- package/dist/lexicons/chat/bsky/group/addMembers.defs.js.map +1 -1
- package/dist/lexicons/chat/bsky/group/createGroup.defs.d.ts +1 -1
- package/dist/lexicons/chat/bsky/group/createGroup.defs.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/group/createGroup.defs.js +1 -0
- package/dist/lexicons/chat/bsky/group/createGroup.defs.js.map +1 -1
- package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.d.ts +1 -1
- package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.js +1 -1
- package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.js.map +1 -1
- package/dist/lexicons/chat/bsky/group/updateJoinRequestsRead.d.ts +3 -0
- package/dist/lexicons/chat/bsky/group/updateJoinRequestsRead.d.ts.map +1 -0
- package/dist/lexicons/chat/bsky/group/updateJoinRequestsRead.defs.d.ts +20 -0
- package/dist/lexicons/chat/bsky/group/updateJoinRequestsRead.defs.d.ts.map +1 -0
- package/dist/lexicons/chat/bsky/group/updateJoinRequestsRead.defs.js +19 -0
- package/dist/lexicons/chat/bsky/group/updateJoinRequestsRead.defs.js.map +1 -0
- package/dist/lexicons/chat/bsky/group/updateJoinRequestsRead.js +6 -0
- package/dist/lexicons/chat/bsky/group/updateJoinRequestsRead.js.map +1 -0
- package/dist/lexicons/chat/bsky/group/withdrawJoinRequest.d.ts +3 -0
- package/dist/lexicons/chat/bsky/group/withdrawJoinRequest.d.ts.map +1 -0
- package/dist/lexicons/chat/bsky/group/withdrawJoinRequest.defs.d.ts +20 -0
- package/dist/lexicons/chat/bsky/group/withdrawJoinRequest.defs.d.ts.map +1 -0
- package/dist/lexicons/chat/bsky/group/withdrawJoinRequest.defs.js +18 -0
- package/dist/lexicons/chat/bsky/group/withdrawJoinRequest.defs.js.map +1 -0
- package/dist/lexicons/chat/bsky/group/withdrawJoinRequest.js +6 -0
- package/dist/lexicons/chat/bsky/group/withdrawJoinRequest.js.map +1 -0
- package/dist/lexicons/chat/bsky/group.d.ts +2 -0
- package/dist/lexicons/chat/bsky/group.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/group.js +2 -0
- package/dist/lexicons/chat/bsky/group.js.map +1 -1
- package/dist/lexicons/chat/bsky/moderation/defs.d.ts +2 -0
- package/dist/lexicons/chat/bsky/moderation/defs.d.ts.map +1 -0
- package/dist/lexicons/chat/bsky/moderation/defs.defs.d.ts +58 -0
- package/dist/lexicons/chat/bsky/moderation/defs.defs.d.ts.map +1 -0
- package/dist/lexicons/chat/bsky/moderation/defs.defs.js +38 -0
- package/dist/lexicons/chat/bsky/moderation/defs.defs.js.map +1 -0
- package/dist/lexicons/chat/bsky/moderation/defs.js +5 -0
- package/dist/lexicons/chat/bsky/moderation/defs.js.map +1 -0
- package/dist/lexicons/chat/bsky/moderation/getConvo.d.ts +3 -0
- package/dist/lexicons/chat/bsky/moderation/getConvo.d.ts.map +1 -0
- package/dist/lexicons/chat/bsky/moderation/getConvo.defs.d.ts +22 -0
- package/dist/lexicons/chat/bsky/moderation/getConvo.defs.d.ts.map +1 -0
- package/dist/lexicons/chat/bsky/moderation/getConvo.defs.js +18 -0
- package/dist/lexicons/chat/bsky/moderation/getConvo.defs.js.map +1 -0
- package/dist/lexicons/chat/bsky/moderation/getConvo.js +6 -0
- package/dist/lexicons/chat/bsky/moderation/getConvo.js.map +1 -0
- package/dist/lexicons/chat/bsky/moderation/getConvoMembers.d.ts +3 -0
- package/dist/lexicons/chat/bsky/moderation/getConvoMembers.d.ts.map +1 -0
- package/dist/lexicons/chat/bsky/moderation/getConvoMembers.defs.d.ts +28 -0
- package/dist/lexicons/chat/bsky/moderation/getConvoMembers.defs.d.ts.map +1 -0
- package/dist/lexicons/chat/bsky/moderation/getConvoMembers.defs.js +24 -0
- package/dist/lexicons/chat/bsky/moderation/getConvoMembers.defs.js.map +1 -0
- package/dist/lexicons/chat/bsky/moderation/getConvoMembers.js +6 -0
- package/dist/lexicons/chat/bsky/moderation/getConvoMembers.js.map +1 -0
- package/dist/lexicons/chat/bsky/moderation/subscribeModEvents.defs.d.ts +20 -2
- package/dist/lexicons/chat/bsky/moderation/subscribeModEvents.defs.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/moderation/subscribeModEvents.defs.js +11 -0
- package/dist/lexicons/chat/bsky/moderation/subscribeModEvents.defs.js.map +1 -1
- package/dist/lexicons/chat/bsky/moderation.d.ts +3 -0
- package/dist/lexicons/chat/bsky/moderation.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/moderation.js +3 -0
- package/dist/lexicons/chat/bsky/moderation.js.map +1 -1
- package/dist/lexicons/chat/bsky.d.ts +1 -0
- package/dist/lexicons/chat/bsky.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky.js +1 -0
- package/dist/lexicons/chat/bsky.js.map +1 -1
- package/dist/lexicons/tools/ozone/moderation/defs.defs.d.ts +11 -3
- package/dist/lexicons/tools/ozone/moderation/defs.defs.d.ts.map +1 -1
- package/dist/lexicons/tools/ozone/moderation/defs.defs.js +9 -0
- package/dist/lexicons/tools/ozone/moderation/defs.defs.js.map +1 -1
- package/dist/lexicons/tools/ozone/moderation/queryEvents.defs.d.ts +2 -2
- package/dist/lexicons/tools/ozone/moderation/queryEvents.defs.d.ts.map +1 -1
- package/dist/lexicons/tools/ozone/moderation/queryEvents.defs.js.map +1 -1
- package/dist/lexicons/tools/ozone/moderation/queryStatuses.defs.d.ts +2 -2
- package/dist/lexicons/tools/ozone/moderation/queryStatuses.defs.d.ts.map +1 -1
- package/dist/lexicons/tools/ozone/moderation/queryStatuses.defs.js.map +1 -1
- package/dist/util/standard-site.d.ts +12 -4
- package/dist/util/standard-site.d.ts.map +1 -1
- package/dist/util/standard-site.js +58 -6
- package/dist/util/standard-site.js.map +1 -1
- package/dist/views/index.d.ts.map +1 -1
- package/dist/views/index.js +7 -0
- package/dist/views/index.js.map +1 -1
- package/package.json +3 -3
- package/src/hydration/hydrator.ts +24 -0
- package/src/util/standard-site.test.ts +145 -0
- package/src/util/standard-site.ts +67 -6
- package/src/views/index.ts +8 -0
- package/tests/views/verification.test.ts +16 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/bsky",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.236",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Reference implementation of app.bsky App View (Bluesky API)",
|
|
6
6
|
"keywords": [
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"uint8arrays": "^5.0.0",
|
|
49
49
|
"undici": "^6.19.8",
|
|
50
50
|
"zod": "3.23.8",
|
|
51
|
-
"@atproto-labs/fetch-node": "^0.3.0",
|
|
52
51
|
"@atproto-labs/xrpc-utils": "^0.1.0",
|
|
53
|
-
"@atproto/
|
|
52
|
+
"@atproto-labs/fetch-node": "^0.3.0",
|
|
53
|
+
"@atproto/api": "^0.20.7",
|
|
54
54
|
"@atproto/common": "^0.6.1",
|
|
55
55
|
"@atproto/crypto": "^0.5.0",
|
|
56
56
|
"@atproto/did": "^0.5.0",
|
|
@@ -277,6 +277,30 @@ export class Hydrator {
|
|
|
277
277
|
this.label.getLabelsForSubjects(labelSubjectsForDid(dids), ctx.labelers),
|
|
278
278
|
this.hydrateProfileViewers(dids, ctx),
|
|
279
279
|
])
|
|
280
|
+
// Hydrate verification issuer actors so the verification view can expose
|
|
281
|
+
// the issuer's current handle and displayName. Skipped when no hydrated
|
|
282
|
+
// actor has any verifications, which is the common case.
|
|
283
|
+
const issuerDids: DidString[] = []
|
|
284
|
+
const issuerDidSet = new Set<DidString>()
|
|
285
|
+
for (const actor of actors.values()) {
|
|
286
|
+
if (!actor) continue
|
|
287
|
+
for (const verification of actor.verifications) {
|
|
288
|
+
const issuer = verification.issuer
|
|
289
|
+
if (actors.has(issuer) || issuerDidSet.has(issuer)) continue
|
|
290
|
+
issuerDidSet.add(issuer)
|
|
291
|
+
issuerDids.push(issuer)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (issuerDids.length > 0) {
|
|
295
|
+
const issuerActors = await this.actor.getActors(issuerDids, {
|
|
296
|
+
includeTakedowns,
|
|
297
|
+
})
|
|
298
|
+
// Merge into actors without overwriting existing entries (the original
|
|
299
|
+
// dids may have been fetched with skipCacheForDids, etc.).
|
|
300
|
+
for (const [did, actor] of issuerActors) {
|
|
301
|
+
if (!actors.has(did)) actors.set(did, actor)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
280
304
|
if (!includeTakedowns) {
|
|
281
305
|
actionTakedownLabels(dids, actors, labels)
|
|
282
306
|
}
|
|
@@ -275,4 +275,149 @@ describe(validateStandardSiteForUrl, () => {
|
|
|
275
275
|
})
|
|
276
276
|
}
|
|
277
277
|
})
|
|
278
|
+
|
|
279
|
+
describe('subpath-friendly hosts', () => {
|
|
280
|
+
// Allowlist of hosts where each record's author owns the full subpath
|
|
281
|
+
// space under their canonical record URL. These platforms typically
|
|
282
|
+
// serve dynamic per-record subpaths (page numbers, revision ids,
|
|
283
|
+
// comment threads, etc.) under the same slug, so an `assumedUrl` with
|
|
284
|
+
// extra path segments past the record URL is still authentic content.
|
|
285
|
+
for (const { note, baseUrl, path, assumedUrl, expected } of [
|
|
286
|
+
// Allowlisted apex domain (subdomain) — extra path segments accepted.
|
|
287
|
+
{
|
|
288
|
+
note: 'pckt.blog subdomain accepts extra path segments past the record URL',
|
|
289
|
+
baseUrl: 'https://waow-tech.pckt.blog',
|
|
290
|
+
path: '/typeahead-more-like-typebehind-amirite-tzgmqge',
|
|
291
|
+
assumedUrl:
|
|
292
|
+
'https://waow-tech.pckt.blog/typeahead-more-like-typebehind-amirite-tzgmqge/589/621',
|
|
293
|
+
expected: true,
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
note: 'leaflet.pub subdomain with one extra segment',
|
|
297
|
+
baseUrl: 'https://author.leaflet.pub',
|
|
298
|
+
path: '/post-slug',
|
|
299
|
+
assumedUrl: 'https://author.leaflet.pub/post-slug/v2',
|
|
300
|
+
expected: true,
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
note: 'offprint.app at apex with multi-segment extension',
|
|
304
|
+
baseUrl: 'https://offprint.app',
|
|
305
|
+
path: '/story/abc',
|
|
306
|
+
assumedUrl: 'https://offprint.app/story/abc/chapter/3',
|
|
307
|
+
expected: true,
|
|
308
|
+
},
|
|
309
|
+
// Allowlisted host but the assumed URL diverges from the path —
|
|
310
|
+
// still rejected; subpath is "extends with extra segments after a
|
|
311
|
+
// path-segment boundary," not "any URL on the same host."
|
|
312
|
+
{
|
|
313
|
+
note: 'pckt.blog rejects assumed URLs that diverge before the boundary',
|
|
314
|
+
baseUrl: 'https://blog.pckt.blog',
|
|
315
|
+
path: '/post-slug',
|
|
316
|
+
assumedUrl: 'https://blog.pckt.blog/different-post',
|
|
317
|
+
expected: false,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
note: 'pckt.blog rejects partial-segment matches (no slash boundary)',
|
|
321
|
+
baseUrl: 'https://blog.pckt.blog',
|
|
322
|
+
path: '/foo',
|
|
323
|
+
assumedUrl: 'https://blog.pckt.blog/foobar',
|
|
324
|
+
expected: false,
|
|
325
|
+
},
|
|
326
|
+
// Non-allowlisted hosts — exact match still required.
|
|
327
|
+
{
|
|
328
|
+
note: 'arbitrary host rejects extra path segments',
|
|
329
|
+
baseUrl: 'https://example.com',
|
|
330
|
+
path: '/article',
|
|
331
|
+
assumedUrl: 'https://example.com/article/extra',
|
|
332
|
+
expected: false,
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
note: 'lookalike host (pckt.blog.evil.com) is NOT allowlisted',
|
|
336
|
+
baseUrl: 'https://pckt.blog.evil.com',
|
|
337
|
+
path: '/post',
|
|
338
|
+
assumedUrl: 'https://pckt.blog.evil.com/post/extra',
|
|
339
|
+
expected: false,
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
note: 'evilpckt.blog is NOT allowlisted (no subdomain dot before pckt.blog)',
|
|
343
|
+
baseUrl: 'https://evilpckt.blog',
|
|
344
|
+
path: '/post',
|
|
345
|
+
assumedUrl: 'https://evilpckt.blog/post/extra',
|
|
346
|
+
expected: false,
|
|
347
|
+
},
|
|
348
|
+
// Allowlist host with exact match still works (no regression).
|
|
349
|
+
{
|
|
350
|
+
note: 'pckt.blog still accepts exact match without subpath',
|
|
351
|
+
baseUrl: 'https://author.pckt.blog',
|
|
352
|
+
path: '/post-slug',
|
|
353
|
+
assumedUrl: 'https://author.pckt.blog/post-slug',
|
|
354
|
+
expected: true,
|
|
355
|
+
},
|
|
356
|
+
// Allowlist host with cross-host mismatch — still rejected.
|
|
357
|
+
{
|
|
358
|
+
note: 'allowlisted record host vs different host on assumed URL is rejected',
|
|
359
|
+
baseUrl: 'https://author.pckt.blog',
|
|
360
|
+
path: '/post',
|
|
361
|
+
assumedUrl: 'https://example.com/author.pckt.blog/post/extra',
|
|
362
|
+
expected: false,
|
|
363
|
+
},
|
|
364
|
+
]) {
|
|
365
|
+
it(`doc + pub: ${note}`, () => {
|
|
366
|
+
const doc = makeDoc(
|
|
367
|
+
path === undefined ? { site: pubUri } : { site: pubUri, path },
|
|
368
|
+
)
|
|
369
|
+
const pub = makePub({ url: baseUrl })
|
|
370
|
+
expect(validateStandardSiteForUrl(doc, pub, assumedUrl)).toBe(expected)
|
|
371
|
+
})
|
|
372
|
+
it(`loose doc: ${note}`, () => {
|
|
373
|
+
const doc = makeDoc(
|
|
374
|
+
path === undefined ? { site: baseUrl } : { site: baseUrl, path },
|
|
375
|
+
)
|
|
376
|
+
expect(validateStandardSiteForUrl(doc, undefined, assumedUrl)).toBe(
|
|
377
|
+
expected,
|
|
378
|
+
)
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Publication-only validation never accepts subpaths — `assumedUrl`
|
|
383
|
+
// for a publication is the home-page URL, not an article underneath
|
|
384
|
+
// it. Subpath relaxation belongs to documents.
|
|
385
|
+
it('publication-only: allowlisted host still requires exact match', () => {
|
|
386
|
+
const pub = makePub({ url: 'https://author.pckt.blog' })
|
|
387
|
+
expect(
|
|
388
|
+
validateStandardSiteForUrl(
|
|
389
|
+
undefined,
|
|
390
|
+
pub,
|
|
391
|
+
'https://author.pckt.blog/some/sub/path',
|
|
392
|
+
),
|
|
393
|
+
).toBe(false)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('publication-only: non-allowlisted host rejects subpath', () => {
|
|
397
|
+
const pub = makePub({ url: 'https://example.com' })
|
|
398
|
+
expect(
|
|
399
|
+
validateStandardSiteForUrl(
|
|
400
|
+
undefined,
|
|
401
|
+
pub,
|
|
402
|
+
'https://example.com/some/sub/path',
|
|
403
|
+
),
|
|
404
|
+
).toBe(false)
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
// Case-insensitivity on the host.
|
|
408
|
+
it('host comparison is case-insensitive', () => {
|
|
409
|
+
const doc = makeDoc({
|
|
410
|
+
site: pubUri,
|
|
411
|
+
path: '/post',
|
|
412
|
+
})
|
|
413
|
+
const pub = makePub({ url: 'https://Author.PCKT.blog' })
|
|
414
|
+
expect(
|
|
415
|
+
validateStandardSiteForUrl(
|
|
416
|
+
doc,
|
|
417
|
+
pub,
|
|
418
|
+
'https://author.pckt.BLOG/post/extra',
|
|
419
|
+
),
|
|
420
|
+
).toBe(true)
|
|
421
|
+
})
|
|
422
|
+
})
|
|
278
423
|
})
|
|
@@ -53,6 +53,52 @@ const joinPath = (base: string, path: string): string => {
|
|
|
53
53
|
return `${baseTrimmed}/${pathTrimmed}`
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Apex domains whose authors own the full subpath space under their
|
|
58
|
+
* record-claimed URL. Each entry matches itself and any subdomain; e.g.
|
|
59
|
+
* `pckt.blog` matches `pckt.blog` and `waow-tech.pckt.blog`.
|
|
60
|
+
*
|
|
61
|
+
* On these domains, an `assumedUrl` whose pathname extends the canonical
|
|
62
|
+
* record URL with extra segments is treated as valid. Adding to this list
|
|
63
|
+
* is a trust call — only platforms where each record's URL space is
|
|
64
|
+
* authoritatively owned by its author belong here.
|
|
65
|
+
*/
|
|
66
|
+
const SUBPATH_FRIENDLY_DOMAINS = ['pckt.blog', 'leaflet.pub', 'offprint.app']
|
|
67
|
+
|
|
68
|
+
const isSubpathFriendlyHost = (host: string): boolean => {
|
|
69
|
+
const lower = host.toLowerCase()
|
|
70
|
+
return SUBPATH_FRIENDLY_DOMAINS.some(
|
|
71
|
+
(domain) => lower === domain || lower.endsWith(`.${domain}`),
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Return whether `recordUrl` and `assumedUrl` should validate as the same
|
|
77
|
+
* canonical content. Strictly equal canonical forms always match. On
|
|
78
|
+
* subpath-friendly hosts (see `SUBPATH_FRIENDLY_DOMAINS`), an `assumedUrl`
|
|
79
|
+
* whose path extends `recordUrl`'s with extra segments is also accepted.
|
|
80
|
+
*
|
|
81
|
+
* Both inputs are pre-canonicalized strings (`canonicalizeHttpUrl` output)
|
|
82
|
+
* with no trailing slash and no query/fragment.
|
|
83
|
+
*/
|
|
84
|
+
const canonicalUrlMatchesAssumed = (
|
|
85
|
+
canonicalRecordUrl: string,
|
|
86
|
+
canonicalAssumedUrl: string,
|
|
87
|
+
): boolean => {
|
|
88
|
+
if (canonicalRecordUrl === canonicalAssumedUrl) return true
|
|
89
|
+
// Subpath fallback. Both strings are canonicalized, so a real
|
|
90
|
+
// path-segment boundary at `recordUrl + '/'` (e.g. `/foo` vs `/foo-bar`
|
|
91
|
+
// never matches; `/foo` vs `/foo/bar` does).
|
|
92
|
+
if (!canonicalAssumedUrl.startsWith(`${canonicalRecordUrl}/`)) return false
|
|
93
|
+
let host: string
|
|
94
|
+
try {
|
|
95
|
+
host = new URL(canonicalAssumedUrl).host
|
|
96
|
+
} catch {
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
return isSubpathFriendlyHost(host)
|
|
100
|
+
}
|
|
101
|
+
|
|
56
102
|
/**
|
|
57
103
|
* Confirm that the supplied SS records actually back `assumedUrl`. The
|
|
58
104
|
* record-side URL is built by concatenating the publication URL (or the
|
|
@@ -74,13 +120,21 @@ const joinPath = (base: string, path: string): string => {
|
|
|
74
120
|
* function runs the pair is already known to be structurally consistent,
|
|
75
121
|
* so we only check whether the records back the URL.
|
|
76
122
|
*
|
|
123
|
+
* For document validation, `SUBPATH_FRIENDLY_DOMAINS` (and their
|
|
124
|
+
* subdomains) accept an assumed URL whose path extends the canonical
|
|
125
|
+
* record URL with additional segments — these are platforms where each
|
|
126
|
+
* record's author owns the full subpath space under their claimed URL.
|
|
127
|
+
* Publication-only validation always requires exact match: there's no
|
|
128
|
+
* coherent "subpath" of a publication's home page.
|
|
129
|
+
*
|
|
77
130
|
* Cases:
|
|
78
131
|
* - Document + publication: `publication.url + document.path` must
|
|
79
|
-
* canonicalize to `assumedUrl
|
|
132
|
+
* canonicalize to `assumedUrl` (or be a subpath-friendly prefix of it).
|
|
80
133
|
* - Loose document (web-URL `site`): `document.site + document.path`
|
|
81
|
-
* must canonicalize to `assumedUrl
|
|
82
|
-
* publication can't reach this function
|
|
83
|
-
*
|
|
134
|
+
* must canonicalize to `assumedUrl` (or be a subpath-friendly prefix).
|
|
135
|
+
* (Doc with at-uri `site` but no publication can't reach this function
|
|
136
|
+
* — the lookups reject it.)
|
|
137
|
+
* - Publication only: `publication.url` must canonicalize exactly to
|
|
84
138
|
* `assumedUrl`.
|
|
85
139
|
* - Neither: vacuously valid; the caller short-circuits before we get
|
|
86
140
|
* here.
|
|
@@ -104,15 +158,22 @@ export const validateStandardSiteForUrl = (
|
|
|
104
158
|
const joined = canonicalizeHttpUrl(
|
|
105
159
|
joinPath(publication.info.record.url, document.info.record.path ?? ''),
|
|
106
160
|
)
|
|
107
|
-
return
|
|
161
|
+
return (
|
|
162
|
+
joined !== null && canonicalUrlMatchesAssumed(joined, canonicalAssumed)
|
|
163
|
+
)
|
|
108
164
|
}
|
|
109
165
|
if (document) {
|
|
110
166
|
const joined = canonicalizeHttpUrl(
|
|
111
167
|
joinPath(document.info.record.site, document.info.record.path ?? ''),
|
|
112
168
|
)
|
|
113
|
-
return
|
|
169
|
+
return (
|
|
170
|
+
joined !== null && canonicalUrlMatchesAssumed(joined, canonicalAssumed)
|
|
171
|
+
)
|
|
114
172
|
}
|
|
115
173
|
if (publication) {
|
|
174
|
+
// Publication-only matches are exact: `assumedUrl` represents the
|
|
175
|
+
// publication's home page, not an article underneath it. Subpath
|
|
176
|
+
// extensions belong to document validation.
|
|
116
177
|
return canonicalizeHttpUrl(publication.info.record.url) === canonicalAssumed
|
|
117
178
|
}
|
|
118
179
|
return true
|
package/src/views/index.ts
CHANGED
|
@@ -535,14 +535,22 @@ export class Views {
|
|
|
535
535
|
({ issuer, uri, displayName, handle, createdAt }): VerificationView => {
|
|
536
536
|
// @NOTE: We don't factor-in impersonation when evaluating the validity of each verification,
|
|
537
537
|
// only in the overall profile verification validity.
|
|
538
|
+
// The verification record's `displayName`/`handle` are the *subject's* snapshot at
|
|
539
|
+
// verification time; we compare them to the subject's current values to determine validity.
|
|
538
540
|
const isValid =
|
|
539
541
|
!!displayName &&
|
|
540
542
|
displayName === actor.profile?.displayName &&
|
|
541
543
|
!!handle &&
|
|
542
544
|
handle === actor.handle
|
|
543
545
|
|
|
546
|
+
// Expose the *issuer's* current handle/displayName, sourced from the
|
|
547
|
+
// issuer's hydrated actor record (see `Hydrator.hydrateProfiles`).
|
|
548
|
+
const issuerActor = state.actors?.get(issuer)
|
|
549
|
+
|
|
544
550
|
return {
|
|
545
551
|
issuer,
|
|
552
|
+
issuerDisplayName: issuerActor?.profile?.displayName,
|
|
553
|
+
issuerHandle: issuerActor?.handle,
|
|
546
554
|
uri,
|
|
547
555
|
isValid,
|
|
548
556
|
createdAt,
|
|
@@ -78,6 +78,8 @@ describe('verification views', () => {
|
|
|
78
78
|
verifications: [
|
|
79
79
|
{
|
|
80
80
|
createdAt: expect.any(String),
|
|
81
|
+
issuerDisplayName: 'display-verifier2',
|
|
82
|
+
issuerHandle: 'verifier2.test',
|
|
81
83
|
isValid: true,
|
|
82
84
|
issuer: verifier2,
|
|
83
85
|
uri: expect.any(String),
|
|
@@ -115,12 +117,16 @@ describe('verification views', () => {
|
|
|
115
117
|
verifications: [
|
|
116
118
|
{
|
|
117
119
|
createdAt: expect.any(String),
|
|
120
|
+
issuerDisplayName: 'display-verifier1',
|
|
121
|
+
issuerHandle: 'verifier1.test',
|
|
118
122
|
isValid: true,
|
|
119
123
|
issuer: verifier1,
|
|
120
124
|
uri: expect.any(String),
|
|
121
125
|
},
|
|
122
126
|
{
|
|
123
127
|
createdAt: expect.any(String),
|
|
128
|
+
issuerDisplayName: 'display-verifier2',
|
|
129
|
+
issuerHandle: 'verifier2.test',
|
|
124
130
|
isValid: true,
|
|
125
131
|
issuer: verifier2,
|
|
126
132
|
uri: expect.any(String),
|
|
@@ -141,12 +147,16 @@ describe('verification views', () => {
|
|
|
141
147
|
verifications: [
|
|
142
148
|
{
|
|
143
149
|
createdAt: expect.any(String),
|
|
150
|
+
issuerDisplayName: 'display-verifier1',
|
|
151
|
+
issuerHandle: 'verifier1.test',
|
|
144
152
|
isValid: true,
|
|
145
153
|
issuer: verifier1,
|
|
146
154
|
uri: expect.any(String),
|
|
147
155
|
},
|
|
148
156
|
{
|
|
149
157
|
createdAt: expect.any(String),
|
|
158
|
+
issuerDisplayName: 'display-verifier2',
|
|
159
|
+
issuerHandle: 'verifier2.test',
|
|
150
160
|
isValid: false,
|
|
151
161
|
issuer: verifier2,
|
|
152
162
|
uri: expect.any(String),
|
|
@@ -167,6 +177,8 @@ describe('verification views', () => {
|
|
|
167
177
|
verifications: [
|
|
168
178
|
{
|
|
169
179
|
createdAt: expect.any(String),
|
|
180
|
+
issuerDisplayName: 'display-verifier1',
|
|
181
|
+
issuerHandle: 'verifier1.test',
|
|
170
182
|
isValid: true,
|
|
171
183
|
issuer: verifier1,
|
|
172
184
|
uri: expect.any(String),
|
|
@@ -193,6 +205,8 @@ describe('verification views', () => {
|
|
|
193
205
|
verifications: [
|
|
194
206
|
{
|
|
195
207
|
createdAt: expect.any(String),
|
|
208
|
+
issuerDisplayName: 'display-verifier2',
|
|
209
|
+
issuerHandle: 'verifier2.test',
|
|
196
210
|
isValid: false,
|
|
197
211
|
issuer: verifier2,
|
|
198
212
|
uri: expect.any(String),
|
|
@@ -219,6 +233,8 @@ describe('verification views', () => {
|
|
|
219
233
|
verifications: [
|
|
220
234
|
{
|
|
221
235
|
createdAt: expect.any(String),
|
|
236
|
+
issuerDisplayName: 'display-verifier1',
|
|
237
|
+
issuerHandle: 'verifier1.test',
|
|
222
238
|
isValid: true,
|
|
223
239
|
issuer: verifier1,
|
|
224
240
|
uri: expect.any(String),
|