@atproto/bsky 0.0.234 → 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 +26 -0
- package/dist/hydration/external.d.ts.map +1 -1
- package/dist/hydration/external.js +8 -0
- package/dist/hydration/external.js.map +1 -1
- 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/actor/getStatus.defs.d.ts +2 -0
- package/dist/lexicons/chat/bsky/actor/getStatus.defs.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/actor/getStatus.defs.js +1 -0
- package/dist/lexicons/chat/bsky/actor/getStatus.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/com/atproto/server/getServiceAuth.defs.d.ts +2 -2
- package/dist/lexicons/com/atproto/server/getServiceAuth.defs.js +1 -1
- package/dist/lexicons/com/atproto/server/getServiceAuth.defs.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 +17 -10
- package/dist/views/index.js.map +1 -1
- package/package.json +6 -6
- package/src/hydration/external.ts +17 -0
- package/src/hydration/hydrator.ts +24 -0
- package/src/util/standard-site.test.ts +152 -0
- package/src/util/standard-site.ts +67 -6
- package/src/views/index.ts +25 -13
- package/tests/views/profile.test.ts +3 -10
- 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,14 +48,14 @@
|
|
|
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
|
-
"@atproto/did": "^0.
|
|
56
|
+
"@atproto/did": "^0.5.0",
|
|
57
57
|
"@atproto/identity": "^0.5.0",
|
|
58
|
-
"@atproto/lex": "^0.1.
|
|
58
|
+
"@atproto/lex": "^0.1.3",
|
|
59
59
|
"@atproto/repo": "^0.10.0",
|
|
60
60
|
"@atproto/sync": "^0.3.1",
|
|
61
61
|
"@atproto/syntax": "^0.6.1",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"ts-node": "^10.8.2",
|
|
75
75
|
"typescript": "^6.0.3",
|
|
76
76
|
"vitest": "^4.0.16",
|
|
77
|
-
"@atproto/pds": "^0.5.
|
|
77
|
+
"@atproto/pds": "^0.5.1"
|
|
78
78
|
},
|
|
79
79
|
"type": "module",
|
|
80
80
|
"exports": {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AtUriString } from '@atproto/syntax'
|
|
2
2
|
import { DataPlaneClient } from '../data-plane/client/index.js'
|
|
3
3
|
import { site } from '../lexicons/index.js'
|
|
4
|
+
import { hydrationLogger } from '../logger.js'
|
|
4
5
|
import {
|
|
5
6
|
GetSiteStandardRecordsByRefResponse,
|
|
6
7
|
GetSiteStandardRecordsByURIResponse,
|
|
@@ -162,6 +163,14 @@ export const getSiteStandardRecordsFromHydrationMapsByRefs = (
|
|
|
162
163
|
// (or tampered with), so reject the whole pairing.
|
|
163
164
|
if (document && publication) {
|
|
164
165
|
if (document.info.record.site !== publication.ref.uri) {
|
|
166
|
+
hydrationLogger.warn(
|
|
167
|
+
{
|
|
168
|
+
documentUri: document.ref.uri,
|
|
169
|
+
documentSite: document.info.record.site,
|
|
170
|
+
publicationUri: publication.ref.uri,
|
|
171
|
+
},
|
|
172
|
+
'site.standard byRefs lookup failed: doc.site does not match hydrated publication.uri',
|
|
173
|
+
)
|
|
165
174
|
return { document: undefined, publication: undefined }
|
|
166
175
|
}
|
|
167
176
|
}
|
|
@@ -172,6 +181,10 @@ export const getSiteStandardRecordsFromHydrationMapsByRefs = (
|
|
|
172
181
|
if (document && !publication) {
|
|
173
182
|
const site = document.info.record.site
|
|
174
183
|
if (site && site.startsWith('at://')) {
|
|
184
|
+
hydrationLogger.warn(
|
|
185
|
+
{ documentUri: document.ref.uri, documentSite: site },
|
|
186
|
+
'site.standard byRefs lookup failed: document.site is AT URI but no matching publication was hydrated',
|
|
187
|
+
)
|
|
175
188
|
return { document: undefined, publication: undefined }
|
|
176
189
|
}
|
|
177
190
|
}
|
|
@@ -234,6 +247,10 @@ export const getSiteStandardRecordsFromHydrationMapsByDocumentUri = (
|
|
|
234
247
|
}
|
|
235
248
|
}
|
|
236
249
|
if (!publication) {
|
|
250
|
+
hydrationLogger.warn(
|
|
251
|
+
{ documentUri: document.ref.uri, documentSite: site },
|
|
252
|
+
'site.standard byDocumentUri lookup failed: document.site is AT URI but no matching publication was hydrated',
|
|
253
|
+
)
|
|
237
254
|
return { document: undefined, publication: undefined }
|
|
238
255
|
}
|
|
239
256
|
}
|
|
@@ -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
|
}
|
|
@@ -101,6 +101,13 @@ describe(validateStandardSiteForUrl, () => {
|
|
|
101
101
|
validateStandardSiteForUrl(undefined, pub, 'https://other.com'),
|
|
102
102
|
).toBe(false)
|
|
103
103
|
})
|
|
104
|
+
|
|
105
|
+
it('accepts when assumedUrl has a trailing slash and publication.url does not', () => {
|
|
106
|
+
const pub = makePub({ url: 'https://atproto.com/blog' })
|
|
107
|
+
expect(
|
|
108
|
+
validateStandardSiteForUrl(undefined, pub, 'https://atproto.com/blog/'),
|
|
109
|
+
).toBe(true)
|
|
110
|
+
})
|
|
104
111
|
})
|
|
105
112
|
|
|
106
113
|
describe('neither', () => {
|
|
@@ -268,4 +275,149 @@ describe(validateStandardSiteForUrl, () => {
|
|
|
268
275
|
})
|
|
269
276
|
}
|
|
270
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
|
+
})
|
|
271
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HOUR, MINUTE, mapDefined } from '@atproto/common'
|
|
1
|
+
import { HOUR, MINUTE, dedupeStrs, mapDefined } from '@atproto/common'
|
|
2
2
|
import {
|
|
3
3
|
$Typed,
|
|
4
4
|
Un$Typed,
|
|
@@ -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,
|
|
@@ -2141,14 +2149,6 @@ export class Views {
|
|
|
2141
2149
|
state,
|
|
2142
2150
|
assumedUrl: embed.external.uri,
|
|
2143
2151
|
})
|
|
2144
|
-
// Profiles of the owners of pinned `associatedRefs`. Hydrator covers
|
|
2145
|
-
// these DIDs alongside post-author profiles, so misses here only
|
|
2146
|
-
// happen when an actor is unavailable (suspended, deleted, etc.) —
|
|
2147
|
-
// drop those rather than emit `undefined` slots.
|
|
2148
|
-
const associatedProfiles = mapDefined(
|
|
2149
|
-
embed.external.associatedRefs ?? [],
|
|
2150
|
-
(ref) => this.profileBasic(uriToDid(ref.uri), state),
|
|
2151
|
-
) as ProfileViewBasic[]
|
|
2152
2152
|
return app.bsky.embed.external.view.$build({
|
|
2153
2153
|
external: {
|
|
2154
2154
|
uri: embed.external.uri,
|
|
@@ -2163,9 +2163,6 @@ export class Views {
|
|
|
2163
2163
|
: undefined,
|
|
2164
2164
|
...ssView,
|
|
2165
2165
|
associatedRefs: embed.external.associatedRefs,
|
|
2166
|
-
associatedProfiles: associatedProfiles.length
|
|
2167
|
-
? associatedProfiles
|
|
2168
|
-
: undefined,
|
|
2169
2166
|
},
|
|
2170
2167
|
})
|
|
2171
2168
|
}
|
|
@@ -2263,7 +2260,7 @@ export class Views {
|
|
|
2263
2260
|
documentUri: document?.ref.uri,
|
|
2264
2261
|
publicationUri: publication?.ref.uri,
|
|
2265
2262
|
},
|
|
2266
|
-
'
|
|
2263
|
+
'site.standard URL validation failed',
|
|
2267
2264
|
)
|
|
2268
2265
|
return undefined
|
|
2269
2266
|
}
|
|
@@ -2333,6 +2330,21 @@ export class Views {
|
|
|
2333
2330
|
overlay.source = this.externalEmbedSource(publication)
|
|
2334
2331
|
}
|
|
2335
2332
|
|
|
2333
|
+
// Profiles of the owners of the records backing this embed. Hydrator
|
|
2334
|
+
// covers these DIDs alongside post-author profiles, so misses here
|
|
2335
|
+
// only happen when an actor is unavailable (suspended, deleted, etc.)
|
|
2336
|
+
// — drop those rather than emit `undefined` slots.
|
|
2337
|
+
const uniqueDids = dedupeStrs(
|
|
2338
|
+
mapDefined([document?.ref.uri, publication?.ref.uri], (uri) =>
|
|
2339
|
+
uri ? uriToDid(uri) : undefined,
|
|
2340
|
+
) as DidString[],
|
|
2341
|
+
)
|
|
2342
|
+
const associatedProfiles = mapDefined(uniqueDids, (did) =>
|
|
2343
|
+
this.profileBasic(did, state),
|
|
2344
|
+
) as ProfileViewBasic[]
|
|
2345
|
+
if (associatedProfiles.length)
|
|
2346
|
+
overlay.associatedProfiles = associatedProfiles
|
|
2347
|
+
|
|
2336
2348
|
return overlay
|
|
2337
2349
|
}
|
|
2338
2350
|
|
|
@@ -724,18 +724,14 @@ describe('pds profile views', () => {
|
|
|
724
724
|
})
|
|
725
725
|
|
|
726
726
|
it('filters out Go zero-value dates from dataplane', async () => {
|
|
727
|
-
|
|
728
|
-
const getActorsSpy = vi.spyOn(network.bsky.ctx.dataplane, 'getActors')
|
|
727
|
+
using getActorsSpy = vi.spyOn(network.bsky.ctx.dataplane, 'getActors')
|
|
729
728
|
|
|
730
|
-
// Call the original implementation but modify the result
|
|
731
729
|
getActorsSpy.mockImplementationOnce(async (req) => {
|
|
732
|
-
// Call the real method
|
|
733
730
|
const result = await network.bsky.ctx.dataplane.getActors(req)
|
|
734
731
|
|
|
735
|
-
//
|
|
732
|
+
// Inject a Go zero-value date (0001-01-01 00:00:00 UTC)
|
|
736
733
|
if (result.actors.length > 0 && result.actors[0]) {
|
|
737
734
|
const actor = result.actors[0]
|
|
738
|
-
// Create a Timestamp with Go zero-value (0001-01-01 00:00:00 UTC)
|
|
739
735
|
const goZeroDate = new Date(-62135596800000)
|
|
740
736
|
actor.createdAt = Timestamp.fromDate(goZeroDate)
|
|
741
737
|
}
|
|
@@ -750,11 +746,8 @@ describe('pds profile views', () => {
|
|
|
750
746
|
},
|
|
751
747
|
)
|
|
752
748
|
|
|
753
|
-
// The
|
|
749
|
+
// The hydration layer filters Go zero-values out
|
|
754
750
|
expect(data.createdAt).toBeUndefined()
|
|
755
|
-
|
|
756
|
-
// Clean up
|
|
757
|
-
getActorsSpy.mockRestore()
|
|
758
751
|
})
|
|
759
752
|
|
|
760
753
|
async function updateProfile(did: string, record: Record<string, unknown>) {
|
|
@@ -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),
|