@atproto/bsky 0.0.201 → 0.0.203

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.
Files changed (126) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/api/age-assurance/const.d.ts.map +1 -1
  3. package/dist/api/age-assurance/const.js +20 -0
  4. package/dist/api/age-assurance/const.js.map +1 -1
  5. package/dist/api/app/bsky/ageassurance/begin.d.ts.map +1 -1
  6. package/dist/api/app/bsky/ageassurance/begin.js +15 -8
  7. package/dist/api/app/bsky/ageassurance/begin.js.map +1 -1
  8. package/dist/api/app/bsky/contact/dismissMatch.d.ts.map +1 -1
  9. package/dist/api/app/bsky/contact/dismissMatch.js +2 -3
  10. package/dist/api/app/bsky/contact/dismissMatch.js.map +1 -1
  11. package/dist/api/app/bsky/contact/getMatches.js +2 -3
  12. package/dist/api/app/bsky/contact/getMatches.js.map +1 -1
  13. package/dist/api/app/bsky/contact/getSyncStatus.d.ts.map +1 -1
  14. package/dist/api/app/bsky/contact/getSyncStatus.js +2 -3
  15. package/dist/api/app/bsky/contact/getSyncStatus.js.map +1 -1
  16. package/dist/api/app/bsky/contact/importContacts.js +2 -3
  17. package/dist/api/app/bsky/contact/importContacts.js.map +1 -1
  18. package/dist/api/app/bsky/contact/removeData.d.ts.map +1 -1
  19. package/dist/api/app/bsky/contact/removeData.js +2 -3
  20. package/dist/api/app/bsky/contact/removeData.js.map +1 -1
  21. package/dist/api/app/bsky/contact/sendNotification.d.ts.map +1 -1
  22. package/dist/api/app/bsky/contact/sendNotification.js.map +1 -1
  23. package/dist/api/app/bsky/contact/startPhoneVerification.d.ts.map +1 -1
  24. package/dist/api/app/bsky/contact/startPhoneVerification.js +2 -3
  25. package/dist/api/app/bsky/contact/startPhoneVerification.js.map +1 -1
  26. package/dist/api/app/bsky/contact/util.d.ts +7 -0
  27. package/dist/api/app/bsky/contact/util.d.ts.map +1 -1
  28. package/dist/api/app/bsky/contact/util.js +68 -0
  29. package/dist/api/app/bsky/contact/util.js.map +1 -1
  30. package/dist/api/app/bsky/contact/verifyPhone.d.ts.map +1 -1
  31. package/dist/api/app/bsky/contact/verifyPhone.js +2 -3
  32. package/dist/api/app/bsky/contact/verifyPhone.js.map +1 -1
  33. package/dist/api/index.d.ts +1 -0
  34. package/dist/api/index.d.ts.map +1 -1
  35. package/dist/api/index.js +4 -1
  36. package/dist/api/index.js.map +1 -1
  37. package/dist/api/sitemap.d.ts +4 -0
  38. package/dist/api/sitemap.d.ts.map +1 -0
  39. package/dist/api/sitemap.js +67 -0
  40. package/dist/api/sitemap.js.map +1 -0
  41. package/dist/data-plane/server/routes/index.d.ts.map +1 -1
  42. package/dist/data-plane/server/routes/index.js +2 -0
  43. package/dist/data-plane/server/routes/index.js.map +1 -1
  44. package/dist/data-plane/server/routes/profile.d.ts.map +1 -1
  45. package/dist/data-plane/server/routes/profile.js +10 -8
  46. package/dist/data-plane/server/routes/profile.js.map +1 -1
  47. package/dist/data-plane/server/routes/sitemap.d.ts +5 -0
  48. package/dist/data-plane/server/routes/sitemap.d.ts.map +1 -0
  49. package/dist/data-plane/server/routes/sitemap.js +38 -0
  50. package/dist/data-plane/server/routes/sitemap.js.map +1 -0
  51. package/dist/hydration/actor.js +1 -1
  52. package/dist/hydration/actor.js.map +1 -1
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +3 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/lexicon/lexicons.d.ts +100 -46
  57. package/dist/lexicon/lexicons.d.ts.map +1 -1
  58. package/dist/lexicon/lexicons.js +67 -22
  59. package/dist/lexicon/lexicons.js.map +1 -1
  60. package/dist/lexicon/types/app/bsky/contact/dismissMatch.d.ts +1 -1
  61. package/dist/lexicon/types/app/bsky/contact/dismissMatch.d.ts.map +1 -1
  62. package/dist/lexicon/types/app/bsky/contact/dismissMatch.js.map +1 -1
  63. package/dist/lexicon/types/app/bsky/contact/getMatches.d.ts +1 -1
  64. package/dist/lexicon/types/app/bsky/contact/getMatches.d.ts.map +1 -1
  65. package/dist/lexicon/types/app/bsky/contact/getMatches.js.map +1 -1
  66. package/dist/lexicon/types/app/bsky/contact/getSyncStatus.d.ts +1 -1
  67. package/dist/lexicon/types/app/bsky/contact/getSyncStatus.d.ts.map +1 -1
  68. package/dist/lexicon/types/app/bsky/contact/getSyncStatus.js.map +1 -1
  69. package/dist/lexicon/types/app/bsky/contact/importContacts.d.ts +1 -1
  70. package/dist/lexicon/types/app/bsky/contact/importContacts.d.ts.map +1 -1
  71. package/dist/lexicon/types/app/bsky/contact/importContacts.js.map +1 -1
  72. package/dist/lexicon/types/app/bsky/contact/removeData.d.ts +1 -1
  73. package/dist/lexicon/types/app/bsky/contact/removeData.d.ts.map +1 -1
  74. package/dist/lexicon/types/app/bsky/contact/removeData.js.map +1 -1
  75. package/dist/lexicon/types/app/bsky/contact/startPhoneVerification.d.ts +1 -1
  76. package/dist/lexicon/types/app/bsky/contact/startPhoneVerification.d.ts.map +1 -1
  77. package/dist/lexicon/types/app/bsky/contact/startPhoneVerification.js.map +1 -1
  78. package/dist/lexicon/types/app/bsky/contact/verifyPhone.d.ts +1 -1
  79. package/dist/lexicon/types/app/bsky/contact/verifyPhone.d.ts.map +1 -1
  80. package/dist/lexicon/types/app/bsky/contact/verifyPhone.js.map +1 -1
  81. package/dist/lexicon/types/app/bsky/notification/listNotifications.d.ts +1 -1
  82. package/dist/lexicon/types/app/bsky/notification/listNotifications.d.ts.map +1 -1
  83. package/dist/lexicon/types/app/bsky/notification/listNotifications.js.map +1 -1
  84. package/dist/proto/bsky_connect.d.ts +21 -1
  85. package/dist/proto/bsky_connect.d.ts.map +1 -1
  86. package/dist/proto/bsky_connect.js +20 -0
  87. package/dist/proto/bsky_connect.js.map +1 -1
  88. package/dist/proto/bsky_pb.d.ts +97 -0
  89. package/dist/proto/bsky_pb.d.ts.map +1 -1
  90. package/dist/proto/bsky_pb.js +256 -5
  91. package/dist/proto/bsky_pb.js.map +1 -1
  92. package/package.json +7 -7
  93. package/proto/bsky.proto +31 -0
  94. package/src/api/age-assurance/const.ts +20 -0
  95. package/src/api/app/bsky/ageassurance/begin.ts +21 -11
  96. package/src/api/app/bsky/contact/dismissMatch.ts +7 -6
  97. package/src/api/app/bsky/contact/getMatches.ts +8 -7
  98. package/src/api/app/bsky/contact/getSyncStatus.ts +6 -5
  99. package/src/api/app/bsky/contact/importContacts.ts +8 -7
  100. package/src/api/app/bsky/contact/removeData.ts +6 -5
  101. package/src/api/app/bsky/contact/sendNotification.ts +2 -1
  102. package/src/api/app/bsky/contact/startPhoneVerification.ts +7 -6
  103. package/src/api/app/bsky/contact/util.ts +80 -1
  104. package/src/api/app/bsky/contact/verifyPhone.ts +8 -7
  105. package/src/api/index.ts +4 -0
  106. package/src/api/sitemap.ts +76 -0
  107. package/src/data-plane/server/routes/index.ts +2 -0
  108. package/src/data-plane/server/routes/profile.ts +8 -6
  109. package/src/data-plane/server/routes/sitemap.ts +43 -0
  110. package/src/hydration/actor.ts +1 -1
  111. package/src/index.ts +6 -1
  112. package/src/lexicon/lexicons.ts +67 -22
  113. package/src/lexicon/types/app/bsky/contact/dismissMatch.ts +1 -1
  114. package/src/lexicon/types/app/bsky/contact/getMatches.ts +1 -1
  115. package/src/lexicon/types/app/bsky/contact/getSyncStatus.ts +1 -1
  116. package/src/lexicon/types/app/bsky/contact/importContacts.ts +6 -1
  117. package/src/lexicon/types/app/bsky/contact/removeData.ts +1 -1
  118. package/src/lexicon/types/app/bsky/contact/startPhoneVerification.ts +1 -1
  119. package/src/lexicon/types/app/bsky/contact/verifyPhone.ts +6 -1
  120. package/src/lexicon/types/app/bsky/notification/listNotifications.ts +1 -0
  121. package/src/proto/bsky_connect.ts +21 -1
  122. package/src/proto/bsky_pb.ts +188 -0
  123. package/tests/sitemap.test.ts +75 -0
  124. package/tests/views/age-assurance-v2.test.ts +51 -0
  125. package/tsconfig.build.tsbuildinfo +1 -1
  126. package/tsconfig.tests.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/bsky",
3
- "version": "0.0.201",
3
+ "version": "0.0.203",
4
4
  "license": "MIT",
5
5
  "description": "Reference implementation of app.bsky App View (Bluesky API)",
6
6
  "keywords": [
@@ -52,9 +52,9 @@
52
52
  "undici": "^6.19.8",
53
53
  "zod": "3.23.8",
54
54
  "@atproto-labs/fetch-node": "0.2.0",
55
- "@atproto-labs/xrpc-utils": "0.0.24",
56
- "@atproto/api": "^0.18.6",
57
55
  "@atproto/common": "^0.5.3",
56
+ "@atproto-labs/xrpc-utils": "0.0.24",
57
+ "@atproto/api": "^0.18.8",
58
58
  "@atproto/crypto": "^0.4.5",
59
59
  "@atproto/did": "^0.2.3",
60
60
  "@atproto/identity": "^0.4.10",
@@ -62,7 +62,7 @@
62
62
  "@atproto/repo": "^0.8.12",
63
63
  "@atproto/syntax": "^0.4.2",
64
64
  "@atproto/sync": "^0.1.39",
65
- "@atproto/xrpc-server": "^0.10.3"
65
+ "@atproto/xrpc-server": "^0.10.4"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@bufbuild/buf": "^1.28.1",
@@ -78,10 +78,10 @@
78
78
  "jest": "^28.1.2",
79
79
  "ts-node": "^10.8.2",
80
80
  "typescript": "^5.6.3",
81
+ "@atproto/api": "^0.18.8",
81
82
  "@atproto/lex-cli": "^0.9.8",
82
- "@atproto/api": "^0.18.6",
83
- "@atproto/pds": "^0.4.199",
84
- "@atproto/xrpc": "^0.7.7"
83
+ "@atproto/xrpc": "^0.7.7",
84
+ "@atproto/pds": "^0.4.199"
85
85
  },
86
86
  "scripts": {
87
87
  "codegen": "lex gen-server --yes ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* ../../lexicons/chat/bsky/*/*",
package/proto/bsky.proto CHANGED
@@ -1310,6 +1310,33 @@ message GetFollowsFollowingResponse {
1310
1310
  repeated FollowsFollowing results = 1;
1311
1311
  }
1312
1312
 
1313
+ message GetSitemapIndexRequest {
1314
+ SitemapPageType type = 1;
1315
+ }
1316
+
1317
+ message GetSitemapIndexResponse {
1318
+ // GZIP compressed XML sitemap
1319
+ bytes sitemap = 1;
1320
+ }
1321
+
1322
+ // Sitemap HTTP paths are typically of the form `/type/yyyy-mm-dd/N.xml.gz`, i.e. `/users/2025-01-01/1.xml.gz`
1323
+ message GetSitemapPageRequest {
1324
+ SitemapPageType type = 1;
1325
+ google.protobuf.Timestamp date = 2;
1326
+ // One-indexed
1327
+ int32 bucket = 3;
1328
+ }
1329
+
1330
+ enum SitemapPageType {
1331
+ SITEMAP_PAGE_TYPE_UNSPECIFIED = 0;
1332
+ SITEMAP_PAGE_TYPE_USER = 1;
1333
+ }
1334
+
1335
+ message GetSitemapPageResponse {
1336
+ // GZIP compressed XML sitemap
1337
+ bytes sitemap = 1;
1338
+ }
1339
+
1313
1340
  // Ping
1314
1341
  message PingRequest {}
1315
1342
  message PingResponse {}
@@ -1470,6 +1497,10 @@ service Service {
1470
1497
  // Graph
1471
1498
  rpc GetFollowsFollowing(GetFollowsFollowingRequest) returns (GetFollowsFollowingResponse);
1472
1499
 
1500
+ // Sitemaps
1501
+ rpc GetSitemapIndex(GetSitemapIndexRequest) returns (GetSitemapIndexResponse);
1502
+ rpc GetSitemapPage(GetSitemapPageRequest) returns (GetSitemapPageResponse);
1503
+
1473
1504
  // Ping
1474
1505
  rpc Ping(PingRequest) returns (PingResponse);
1475
1506
 
@@ -138,5 +138,25 @@ export const AGE_ASSURANCE_CONFIG: AppBskyAgeassuranceDefs.Config = {
138
138
  },
139
139
  ],
140
140
  },
141
+ {
142
+ countryCode: 'US',
143
+ regionCode: 'TN',
144
+ rules: [
145
+ {
146
+ $type: ids.IfAssuredOverAge,
147
+ age: 18,
148
+ access: 'full',
149
+ },
150
+ {
151
+ $type: ids.IfDeclaredOverAge,
152
+ age: 18,
153
+ access: 'full',
154
+ },
155
+ {
156
+ $type: ids.Default,
157
+ access: 'none',
158
+ },
159
+ ],
160
+ },
141
161
  ],
142
162
  }
@@ -37,14 +37,14 @@ export default function (server: Server, ctx: AppContext) {
37
37
 
38
38
  const actorDid = auth.credentials.iss
39
39
  const actorInfo = await getAgeVerificationState(ctx, actorDid)
40
+ const existingStatus = actorInfo?.ageAssuranceStatus?.status
41
+ const existingAccess = actorInfo?.ageAssuranceStatus?.access
40
42
 
41
- if (actorInfo?.ageAssuranceStatus) {
42
- if (actorInfo.ageAssuranceStatus.status === 'blocked') {
43
- throw new InvalidRequestError(
44
- `Cannot initiate age assurance flow from current state: ${actorInfo.ageAssuranceStatus.status}`,
45
- 'InvalidInitiation',
46
- )
47
- }
43
+ if (existingStatus === 'blocked') {
44
+ throw new InvalidRequestError(
45
+ `Cannot initiate age assurance flow from current state: ${existingStatus}`,
46
+ 'InvalidInitiation',
47
+ )
48
48
  }
49
49
 
50
50
  const attemptId = crypto.randomUUID()
@@ -107,14 +107,24 @@ export default function (server: Server, ctx: AppContext) {
107
107
  })
108
108
  }
109
109
 
110
+ // If we have existing status/access for this region, retain it.
111
+ const nextStatus =
112
+ existingStatus && existingStatus !== 'unknown'
113
+ ? existingStatus
114
+ : 'pending'
115
+ const nextAccess =
116
+ existingAccess && existingAccess !== 'unknown'
117
+ ? existingAccess
118
+ : 'unknown'
119
+
110
120
  const event = await createEvent(ctx, actorDid, {
111
121
  attemptId,
112
122
  email,
113
123
  // Assumes `app.set('trust proxy', ...)` configured with `true` or specific values.
114
124
  initIp: req.ip,
115
125
  initUa: getClientUa(req),
116
- status: 'pending',
117
- access: 'unknown',
126
+ status: nextStatus,
127
+ access: nextAccess,
118
128
  countryCode,
119
129
  regionCode,
120
130
  })
@@ -123,8 +133,8 @@ export default function (server: Server, ctx: AppContext) {
123
133
  encoding: 'application/json',
124
134
  body: {
125
135
  lastInitiatedAt: event.createdAt,
126
- status: 'pending',
127
- access: 'unknown',
136
+ status: nextStatus,
137
+ access: nextAccess,
128
138
  },
129
139
  }
130
140
  },
@@ -1,6 +1,6 @@
1
1
  import { AppContext } from '../../../../context'
2
2
  import { Server } from '../../../../lexicon'
3
- import { assertRolodexOrThrowUnimplemented } from './util'
3
+ import { assertRolodexOrThrowUnimplemented, callRolodexClient } from './util'
4
4
 
5
5
  export default function (server: Server, ctx: AppContext) {
6
6
  server.app.bsky.contact.dismissMatch({
@@ -9,11 +9,12 @@ export default function (server: Server, ctx: AppContext) {
9
9
  assertRolodexOrThrowUnimplemented(ctx)
10
10
 
11
11
  const actor = auth.credentials.iss
12
- // TODO: Error handling.
13
- await ctx.rolodexClient.dismissMatch({
14
- actor,
15
- subject: input.body.subject,
16
- })
12
+ await callRolodexClient(
13
+ ctx.rolodexClient.dismissMatch({
14
+ actor,
15
+ subject: input.body.subject,
16
+ }),
17
+ )
17
18
 
18
19
  return {
19
20
  encoding: 'application/json',
@@ -14,7 +14,7 @@ import {
14
14
  } from '../../../../pipeline'
15
15
  import { RolodexClient } from '../../../../rolodex'
16
16
  import { Views } from '../../../../views'
17
- import { assertRolodexOrThrowUnimplemented } from './util'
17
+ import { assertRolodexOrThrowUnimplemented, callRolodexClient } from './util'
18
18
 
19
19
  export default function (server: Server, ctx: AppContext) {
20
20
  const getMatches = createPipeline(skeleton, hydration, noBlocks, presentation)
@@ -48,12 +48,13 @@ const skeleton = async (
48
48
  ): Promise<SkeletonState> => {
49
49
  const { params, ctx } = input
50
50
  const actor = params.hydrateCtx.viewer
51
- // TODO: Error handling.
52
- const { cursor, subjects } = await ctx.rolodexClient.getMatches({
53
- actor: params.hydrateCtx.viewer,
54
- limit: params.limit,
55
- cursor: params.cursor,
56
- })
51
+ const { cursor, subjects } = await callRolodexClient(
52
+ ctx.rolodexClient.getMatches({
53
+ actor: params.hydrateCtx.viewer,
54
+ limit: params.limit,
55
+ cursor: params.cursor,
56
+ }),
57
+ )
57
58
  return {
58
59
  actor,
59
60
  subjects,
@@ -1,7 +1,7 @@
1
1
  import { AppContext } from '../../../../context'
2
2
  import { Server } from '../../../../lexicon'
3
3
  import { SyncStatus } from '../../../../lexicon/types/app/bsky/contact/defs'
4
- import { assertRolodexOrThrowUnimplemented } from './util'
4
+ import { assertRolodexOrThrowUnimplemented, callRolodexClient } from './util'
5
5
 
6
6
  export default function (server: Server, ctx: AppContext) {
7
7
  server.app.bsky.contact.getSyncStatus({
@@ -10,10 +10,11 @@ export default function (server: Server, ctx: AppContext) {
10
10
  assertRolodexOrThrowUnimplemented(ctx)
11
11
 
12
12
  const actor = auth.credentials.iss
13
- // TODO: Error handling.
14
- const res = await ctx.rolodexClient.getSyncStatus({
15
- actor,
16
- })
13
+ const res = await callRolodexClient(
14
+ ctx.rolodexClient.getSyncStatus({
15
+ actor,
16
+ }),
17
+ )
17
18
 
18
19
  let syncStatus: SyncStatus | undefined
19
20
  if (res.status && res.status.syncedAt) {
@@ -17,7 +17,7 @@ import {
17
17
  import { ImportContactsMatch } from '../../../../proto/rolodex_pb'
18
18
  import { RolodexClient } from '../../../../rolodex'
19
19
  import { Views } from '../../../../views'
20
- import { assertRolodexOrThrowUnimplemented } from './util'
20
+ import { assertRolodexOrThrowUnimplemented, callRolodexClient } from './util'
21
21
 
22
22
  export default function (server: Server, ctx: AppContext) {
23
23
  const importContacts = createPipeline(
@@ -56,12 +56,13 @@ const skeleton = async (
56
56
  ): Promise<SkeletonState> => {
57
57
  const { params, ctx } = input
58
58
  const actor = params.hydrateCtx.viewer
59
- // TODO: Error handling.
60
- const { matches } = await ctx.rolodexClient.importContacts({
61
- actor: params.hydrateCtx.viewer,
62
- contacts: params.contacts,
63
- token: params.token,
64
- })
59
+ const { matches } = await callRolodexClient(
60
+ ctx.rolodexClient.importContacts({
61
+ actor: params.hydrateCtx.viewer,
62
+ contacts: params.contacts,
63
+ token: params.token,
64
+ }),
65
+ )
65
66
  return {
66
67
  actor,
67
68
  matches,
@@ -1,6 +1,6 @@
1
1
  import { AppContext } from '../../../../context'
2
2
  import { Server } from '../../../../lexicon'
3
- import { assertRolodexOrThrowUnimplemented } from './util'
3
+ import { assertRolodexOrThrowUnimplemented, callRolodexClient } from './util'
4
4
 
5
5
  export default function (server: Server, ctx: AppContext) {
6
6
  server.app.bsky.contact.removeData({
@@ -9,10 +9,11 @@ export default function (server: Server, ctx: AppContext) {
9
9
  assertRolodexOrThrowUnimplemented(ctx)
10
10
 
11
11
  const actor = auth.credentials.iss
12
- // TODO: Error handling.
13
- await ctx.rolodexClient.removeData({
14
- actor,
15
- })
12
+ await callRolodexClient(
13
+ ctx.rolodexClient.removeData({
14
+ actor,
15
+ }),
16
+ )
16
17
 
17
18
  return {
18
19
  encoding: 'application/json',
@@ -1,6 +1,7 @@
1
1
  import { TID } from '@atproto/common'
2
2
  import { AppContext } from '../../../../context'
3
3
  import { Server } from '../../../../lexicon'
4
+ import { Notification } from '../../../../lexicon/types/app/bsky/contact/defs'
4
5
  import { Namespaces } from '../../../../stash'
5
6
  import { assertRolodexOrThrowUnimplemented } from './util'
6
7
 
@@ -19,7 +20,7 @@ export default function (server: Server, ctx: AppContext) {
19
20
  payload: {
20
21
  from,
21
22
  to,
22
- },
23
+ } satisfies Notification,
23
24
  key: TID.nextStr(),
24
25
  })
25
26
 
@@ -1,6 +1,6 @@
1
1
  import { AppContext } from '../../../../context'
2
2
  import { Server } from '../../../../lexicon'
3
- import { assertRolodexOrThrowUnimplemented } from './util'
3
+ import { assertRolodexOrThrowUnimplemented, callRolodexClient } from './util'
4
4
 
5
5
  export default function (server: Server, ctx: AppContext) {
6
6
  server.app.bsky.contact.startPhoneVerification({
@@ -9,11 +9,12 @@ export default function (server: Server, ctx: AppContext) {
9
9
  assertRolodexOrThrowUnimplemented(ctx)
10
10
 
11
11
  const actor = auth.credentials.iss
12
- // TODO: Error handling.
13
- await ctx.rolodexClient.startPhoneVerification({
14
- actor,
15
- phone: input.body.phone,
16
- })
12
+ await callRolodexClient(
13
+ ctx.rolodexClient.startPhoneVerification({
14
+ actor,
15
+ phone: input.body.phone,
16
+ }),
17
+ )
17
18
 
18
19
  return {
19
20
  encoding: 'application/json',
@@ -1,4 +1,9 @@
1
- import { MethodNotImplementedError } from '@atproto/xrpc-server'
1
+ import { ConnectError } from '@connectrpc/connect'
2
+ import {
3
+ InternalServerError,
4
+ InvalidRequestError,
5
+ MethodNotImplementedError,
6
+ } from '@atproto/xrpc-server'
2
7
  import { AppContext } from '../../../..'
3
8
  import { RolodexClient } from '../../../../rolodex'
4
9
 
@@ -11,3 +16,77 @@ export function assertRolodexOrThrowUnimplemented(
11
16
  )
12
17
  }
13
18
  }
19
+
20
+ /**
21
+ * Converts UPPERCASE_ERROR from Rolodex to PascalCase for XRPC.
22
+ */
23
+ function convertErrorName(reason: string): string {
24
+ switch (reason) {
25
+ case 'INVALID_DID':
26
+ return 'InvalidDid'
27
+ case 'INVALID_LIMIT':
28
+ return 'InvalidLimit'
29
+ case 'INVALID_CURSOR':
30
+ return 'InvalidCursor'
31
+ case 'INVALID_CONTACTS':
32
+ return 'InvalidContacts'
33
+ case 'TOO_MANY_CONTACTS':
34
+ return 'TooManyContacts'
35
+ case 'INVALID_TOKEN':
36
+ return 'InvalidToken'
37
+ case 'RATE_LIMIT_EXCEEDED':
38
+ return 'RateLimitExceeded'
39
+ case 'INVALID_PHONE':
40
+ return 'InvalidPhone'
41
+ case 'INVALID_CODE':
42
+ return 'InvalidCode'
43
+ case 'INTERNAL_ERROR':
44
+ return 'InternalError'
45
+ default:
46
+ return reason
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Helper to call Rolodex client methods and translate RPC errors to XRPC
52
+ * errors.
53
+ *
54
+ * These `reason` values need to stay in sync with the Rolodex service
55
+ */
56
+ export async function callRolodexClient<T>(caller: T) {
57
+ try {
58
+ return await caller
59
+ } catch (e) {
60
+ // might be something we want to handle
61
+ if (e instanceof ConnectError) {
62
+ /**
63
+ * https://connectrpc.com/docs/protocol#error-end-stream
64
+ */
65
+ const details = e.details?.at(0) as
66
+ | {
67
+ debug: {
68
+ reason: string
69
+ message: string
70
+ }
71
+ }
72
+ | undefined
73
+ const reason = details?.debug?.reason // e.g. INVALID_DID
74
+ // Handle known error reasons
75
+ if (reason) {
76
+ const errorName = convertErrorName(reason)
77
+ // NOTE: Don't leak e.message to the response.
78
+
79
+ if (reason === 'INTERNAL_ERROR') {
80
+ throw new InternalServerError('Upstream error', errorName, {
81
+ cause: e,
82
+ })
83
+ } else {
84
+ throw new InvalidRequestError('An error occurred', errorName, {
85
+ cause: e,
86
+ })
87
+ }
88
+ }
89
+ }
90
+ throw e
91
+ }
92
+ }
@@ -1,6 +1,6 @@
1
1
  import { AppContext } from '../../../../context'
2
2
  import { Server } from '../../../../lexicon'
3
- import { assertRolodexOrThrowUnimplemented } from './util'
3
+ import { assertRolodexOrThrowUnimplemented, callRolodexClient } from './util'
4
4
 
5
5
  export default function (server: Server, ctx: AppContext) {
6
6
  server.app.bsky.contact.verifyPhone({
@@ -9,12 +9,13 @@ export default function (server: Server, ctx: AppContext) {
9
9
  assertRolodexOrThrowUnimplemented(ctx)
10
10
 
11
11
  const actor = auth.credentials.iss
12
- // TODO: Error handling.
13
- const res = await ctx.rolodexClient.verifyPhone({
14
- actor,
15
- verificationCode: input.body.code,
16
- phone: input.body.phone,
17
- })
12
+ const res = await callRolodexClient(
13
+ ctx.rolodexClient.verifyPhone({
14
+ actor,
15
+ verificationCode: input.body.code,
16
+ phone: input.body.phone,
17
+ }),
18
+ )
18
19
 
19
20
  return {
20
21
  encoding: 'application/json',
package/src/api/index.ts CHANGED
@@ -16,6 +16,7 @@ import getMatches from './app/bsky/contact/getMatches'
16
16
  import getSyncStatus from './app/bsky/contact/getSyncStatus'
17
17
  import importContacts from './app/bsky/contact/importContacts'
18
18
  import removeData from './app/bsky/contact/removeData'
19
+ import sendNotification from './app/bsky/contact/sendNotification'
19
20
  import startPhoneVerification from './app/bsky/contact/startPhoneVerification'
20
21
  import verifyPhone from './app/bsky/contact/verifyPhone'
21
22
  import getActorFeeds from './app/bsky/feed/getActorFeeds'
@@ -95,6 +96,8 @@ export * as blobResolver from './blob-resolver'
95
96
 
96
97
  export * as external from './external'
97
98
 
99
+ export * as sitemap from './sitemap'
100
+
98
101
  export default function (server: Server, ctx: AppContext) {
99
102
  // app.bsky
100
103
  getTimeline(server, ctx)
@@ -106,6 +109,7 @@ export default function (server: Server, ctx: AppContext) {
106
109
  getSyncStatus(server, ctx)
107
110
  importContacts(server, ctx)
108
111
  removeData(server, ctx)
112
+ sendNotification(server, ctx)
109
113
  startPhoneVerification(server, ctx)
110
114
  verifyPhone(server, ctx)
111
115
  getActorFeeds(server, ctx)
@@ -0,0 +1,76 @@
1
+ import { Readable } from 'node:stream'
2
+ import { Timestamp } from '@bufbuild/protobuf'
3
+ import { Code, ConnectError } from '@connectrpc/connect'
4
+ import express, { RequestHandler, Router } from 'express'
5
+ import { AppContext } from '../context'
6
+ import { httpLogger as log } from '../logger'
7
+ import { SitemapPageType } from '../proto/bsky_pb'
8
+
9
+ export const createRouter = (ctx: AppContext): Router => {
10
+ const router = Router()
11
+ router.get('/external/sitemap/users.xml.gz', userIndexHandler(ctx))
12
+ router.get(
13
+ '/external/sitemap/users/:date/:bucket.xml.gz',
14
+ userPageHandler(ctx),
15
+ )
16
+ return router
17
+ }
18
+
19
+ const userIndexHandler =
20
+ (ctx: AppContext): RequestHandler =>
21
+ async (_req: express.Request, res: express.Response) => {
22
+ try {
23
+ const result = await ctx.dataplane.getSitemapIndex({
24
+ type: SitemapPageType.USER,
25
+ })
26
+ res.set('Content-Type', 'application/gzip')
27
+ res.set('Content-Encoding', 'gzip')
28
+ Readable.from(Buffer.from(result.sitemap)).pipe(res)
29
+ } catch (err) {
30
+ log.error({ err }, 'failed to get sitemap index')
31
+ return res.status(500).send('Internal Server Error')
32
+ }
33
+ }
34
+
35
+ const userPageHandler =
36
+ (ctx: AppContext): RequestHandler =>
37
+ async (req: express.Request, res: express.Response) => {
38
+ const { date, bucket } = req.params
39
+
40
+ // Parse date (YYYY-MM-DD format)
41
+ const dateParts = date.split('-')
42
+ if (dateParts.length !== 3) {
43
+ return res.status(400).send('Invalid date format. Expected YYYY-MM-DD')
44
+ }
45
+
46
+ const year = parseInt(dateParts[0], 10)
47
+ const month = parseInt(dateParts[1], 10)
48
+ const day = parseInt(dateParts[2], 10)
49
+
50
+ if (isNaN(year) || isNaN(month) || isNaN(day)) {
51
+ return res.status(400).send('Invalid date format. Expected YYYY-MM-DD')
52
+ }
53
+
54
+ // Parse bucket (1-indexed)
55
+ const bucketNum = parseInt(bucket, 10)
56
+ if (isNaN(bucketNum) || bucketNum < 1) {
57
+ return res.status(400).send('Invalid bucket number')
58
+ }
59
+
60
+ try {
61
+ const result = await ctx.dataplane.getSitemapPage({
62
+ type: SitemapPageType.USER,
63
+ date: Timestamp.fromDate(new Date(year, month - 1, day)),
64
+ bucket: bucketNum,
65
+ })
66
+ res.set('Content-Type', 'application/gzip')
67
+ res.set('Content-Encoding', 'gzip')
68
+ Readable.from(Buffer.from(result.sitemap)).pipe(res)
69
+ } catch (err) {
70
+ if (err instanceof ConnectError && err.code === Code.NotFound) {
71
+ return res.status(404).send('Sitemap page not found')
72
+ }
73
+ log.error({ err }, 'failed to get sitemap page')
74
+ return res.status(500).send('Internal Server Error')
75
+ }
76
+ }
@@ -22,6 +22,7 @@ import records from './records'
22
22
  import relationships from './relationships'
23
23
  import reposts from './reposts'
24
24
  import search from './search'
25
+ import sitemap from './sitemap'
25
26
  import starterPacks from './starter-packs'
26
27
  import suggestions from './suggestions'
27
28
  import sync from './sync'
@@ -50,6 +51,7 @@ export default (db: Database, idResolver: IdResolver) =>
50
51
  ...relationships(db),
51
52
  ...reposts(db),
52
53
  ...search(db),
54
+ ...sitemap(),
53
55
  ...suggestions(db),
54
56
  ...sync(db),
55
57
  ...threads(db),
@@ -136,12 +136,14 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
136
136
 
137
137
  const status = row?.ageAssuranceStatus ?? 'unknown'
138
138
  let access = row?.ageAssuranceAccess
139
- if (status === 'assured') {
140
- access = 'full'
141
- } else if (status === 'blocked') {
142
- access = 'none'
143
- } else {
144
- access = 'unknown'
139
+ if (!access || access === 'unknown') {
140
+ if (status === 'assured') {
141
+ access = 'full'
142
+ } else if (status === 'blocked') {
143
+ access = 'none'
144
+ } else {
145
+ access = 'unknown'
146
+ }
145
147
  }
146
148
 
147
149
  return {
@@ -0,0 +1,43 @@
1
+ import { gzipSync } from 'node:zlib'
2
+ import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'
3
+ import { Service } from '../../../proto/bsky_connect'
4
+ import { GetSitemapPageRequest } from '../../../proto/bsky_pb'
5
+
6
+ const MOCK_SITEMAP_INDEX = `<?xml version="1.0" encoding="UTF-8"?>
7
+ <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
8
+ <sitemap>
9
+ <loc>https://bsky.app/sitemap/users/2025-01-01/1.xml.gz</loc>
10
+ </sitemap>
11
+ </sitemapindex>`
12
+
13
+ const MOCK_SITEMAP_PAGE = `<?xml version="1.0" encoding="UTF-8"?>
14
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
15
+ <url>
16
+ <loc>https://bsky.app/profile/test.bsky.social</loc>
17
+ </url>
18
+ </urlset>`
19
+
20
+ export default (): Partial<ServiceImpl<typeof Service>> => ({
21
+ async getSitemapIndex() {
22
+ return {
23
+ sitemap: gzipSync(Buffer.from(MOCK_SITEMAP_INDEX)),
24
+ }
25
+ },
26
+ async getSitemapPage(req: GetSitemapPageRequest) {
27
+ const date = req.date?.toDate()
28
+ const isExpectedDate =
29
+ date &&
30
+ date.getFullYear() === 2025 &&
31
+ date.getMonth() === 0 &&
32
+ date.getDate() === 1
33
+ const isExpectedBucket = req.bucket === 1
34
+
35
+ if (!isExpectedDate || !isExpectedBucket) {
36
+ throw new ConnectError('Sitemap page not found', Code.NotFound)
37
+ }
38
+
39
+ return {
40
+ sitemap: gzipSync(Buffer.from(MOCK_SITEMAP_PAGE)),
41
+ }
42
+ },
43
+ })
@@ -273,7 +273,7 @@ export class ActorHydrator {
273
273
  includeTakedowns = false,
274
274
  ): Promise<NotificationDeclarations> {
275
275
  if (!uris.length) return new HydrationMap<NotificationDeclaration>()
276
- const res = await this.dataplane.getActorChatDeclarationRecords({ uris })
276
+ const res = await this.dataplane.getNotificationDeclarationRecords({ uris })
277
277
  return uris.reduce((acc, uri, i) => {
278
278
  const record = parseRecord<NotificationDeclarationRecord>(
279
279
  res.records[i],