@atproto/pds 0.4.176 → 0.4.178

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 (119) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/account-manager/db/migrations/007-lexicon-failures-index.d.ts +4 -0
  3. package/dist/account-manager/db/migrations/007-lexicon-failures-index.d.ts.map +1 -0
  4. package/dist/account-manager/db/migrations/007-lexicon-failures-index.js +17 -0
  5. package/dist/account-manager/db/migrations/007-lexicon-failures-index.js.map +1 -0
  6. package/dist/account-manager/db/migrations/index.d.ts +2 -0
  7. package/dist/account-manager/db/migrations/index.d.ts.map +1 -1
  8. package/dist/account-manager/db/migrations/index.js +2 -0
  9. package/dist/account-manager/db/migrations/index.js.map +1 -1
  10. package/dist/account-manager/helpers/lexicon.d.ts.map +1 -1
  11. package/dist/account-manager/helpers/lexicon.js +7 -0
  12. package/dist/account-manager/helpers/lexicon.js.map +1 -1
  13. package/dist/account-manager/helpers/token.d.ts +32 -32
  14. package/dist/account-manager/scope-reference-getter.d.ts +14 -0
  15. package/dist/account-manager/scope-reference-getter.d.ts.map +1 -0
  16. package/dist/account-manager/scope-reference-getter.js +69 -0
  17. package/dist/account-manager/scope-reference-getter.js.map +1 -0
  18. package/dist/actor-store/actor-store.d.ts.map +1 -1
  19. package/dist/actor-store/actor-store.js +4 -1
  20. package/dist/actor-store/actor-store.js.map +1 -1
  21. package/dist/actor-store/blob/transactor.d.ts +2 -2
  22. package/dist/actor-store/blob/transactor.d.ts.map +1 -1
  23. package/dist/actor-store/blob/transactor.js +73 -24
  24. package/dist/actor-store/blob/transactor.js.map +1 -1
  25. package/dist/actor-store/record/reader.d.ts.map +1 -1
  26. package/dist/actor-store/record/reader.js +12 -9
  27. package/dist/actor-store/record/reader.js.map +1 -1
  28. package/dist/actor-store/repo/sql-repo-reader.d.ts.map +1 -1
  29. package/dist/actor-store/repo/sql-repo-reader.js +2 -2
  30. package/dist/actor-store/repo/sql-repo-reader.js.map +1 -1
  31. package/dist/actor-store/repo/sql-repo-transactor.d.ts.map +1 -1
  32. package/dist/actor-store/repo/sql-repo-transactor.js +16 -19
  33. package/dist/actor-store/repo/sql-repo-transactor.js.map +1 -1
  34. package/dist/actor-store/repo/transactor.d.ts.map +1 -1
  35. package/dist/actor-store/repo/transactor.js +11 -15
  36. package/dist/actor-store/repo/transactor.js.map +1 -1
  37. package/dist/api/com/atproto/admin/updateSubjectStatus.js +6 -2
  38. package/dist/api/com/atproto/admin/updateSubjectStatus.js.map +1 -1
  39. package/dist/api/com/atproto/repo/importRepo.d.ts.map +1 -1
  40. package/dist/api/com/atproto/repo/importRepo.js +43 -51
  41. package/dist/api/com/atproto/repo/importRepo.js.map +1 -1
  42. package/dist/auth-verifier.d.ts.map +1 -1
  43. package/dist/auth-verifier.js +2 -12
  44. package/dist/auth-verifier.js.map +1 -1
  45. package/dist/context.d.ts.map +1 -1
  46. package/dist/context.js +20 -4
  47. package/dist/context.js.map +1 -1
  48. package/dist/disk-blobstore.d.ts.map +1 -1
  49. package/dist/disk-blobstore.js +10 -2
  50. package/dist/disk-blobstore.js.map +1 -1
  51. package/dist/lexicon/index.d.ts +49 -0
  52. package/dist/lexicon/index.d.ts.map +1 -1
  53. package/dist/lexicon/index.js +52 -1
  54. package/dist/lexicon/index.js.map +1 -1
  55. package/dist/lexicon/lexicons.d.ts +500 -24
  56. package/dist/lexicon/lexicons.d.ts.map +1 -1
  57. package/dist/lexicon/lexicons.js +344 -7
  58. package/dist/lexicon/lexicons.js.map +1 -1
  59. package/dist/lexicon/types/com/atproto/moderation/defs.d.ts +8 -8
  60. package/dist/lexicon/types/com/atproto/moderation/defs.d.ts.map +1 -1
  61. package/dist/lexicon/types/com/atproto/moderation/defs.js +7 -7
  62. package/dist/lexicon/types/com/atproto/moderation/defs.js.map +1 -1
  63. package/dist/lexicon/types/com/atproto/temp/dereferenceScope.d.ts +24 -0
  64. package/dist/lexicon/types/com/atproto/temp/dereferenceScope.d.ts.map +1 -0
  65. package/dist/lexicon/types/com/atproto/temp/dereferenceScope.js +7 -0
  66. package/dist/lexicon/types/com/atproto/temp/dereferenceScope.js.map +1 -0
  67. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +10 -2
  68. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
  69. package/dist/lexicon/types/tools/ozone/moderation/defs.js +9 -0
  70. package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
  71. package/dist/lexicon/types/tools/ozone/moderation/emitEvent.d.ts +1 -1
  72. package/dist/lexicon/types/tools/ozone/moderation/emitEvent.d.ts.map +1 -1
  73. package/dist/lexicon/types/tools/ozone/moderation/getAccountTimeline.d.ts +1 -1
  74. package/dist/lexicon/types/tools/ozone/moderation/getAccountTimeline.d.ts.map +1 -1
  75. package/dist/lexicon/types/tools/ozone/moderation/getAccountTimeline.js.map +1 -1
  76. package/dist/lexicon/types/tools/ozone/report/defs.d.ts +92 -0
  77. package/dist/lexicon/types/tools/ozone/report/defs.d.ts.map +1 -0
  78. package/dist/lexicon/types/tools/ozone/report/defs.js +98 -0
  79. package/dist/lexicon/types/tools/ozone/report/defs.js.map +1 -0
  80. package/dist/logger.d.ts +1 -0
  81. package/dist/logger.d.ts.map +1 -1
  82. package/dist/logger.js +2 -1
  83. package/dist/logger.js.map +1 -1
  84. package/dist/scripts/rebuild-repo.d.ts.map +1 -1
  85. package/dist/scripts/rebuild-repo.js +3 -5
  86. package/dist/scripts/rebuild-repo.js.map +1 -1
  87. package/dist/scripts/sequencer-recovery/recoverer.js +8 -10
  88. package/dist/scripts/sequencer-recovery/recoverer.js.map +1 -1
  89. package/dist/sequencer/sequencer.js +2 -2
  90. package/dist/sequencer/sequencer.js.map +1 -1
  91. package/package.json +19 -16
  92. package/src/account-manager/db/migrations/007-lexicon-failures-index.ts +14 -0
  93. package/src/account-manager/db/migrations/index.ts +2 -0
  94. package/src/account-manager/helpers/lexicon.ts +14 -1
  95. package/src/account-manager/scope-reference-getter.ts +92 -0
  96. package/src/actor-store/actor-store.ts +5 -9
  97. package/src/actor-store/blob/transactor.ts +115 -42
  98. package/src/actor-store/record/reader.ts +14 -12
  99. package/src/actor-store/repo/sql-repo-reader.ts +12 -14
  100. package/src/actor-store/repo/sql-repo-transactor.ts +17 -23
  101. package/src/actor-store/repo/transactor.ts +29 -32
  102. package/src/api/com/atproto/admin/updateSubjectStatus.ts +7 -7
  103. package/src/api/com/atproto/repo/importRepo.ts +41 -55
  104. package/src/auth-verifier.ts +4 -20
  105. package/src/context.ts +26 -5
  106. package/src/disk-blobstore.ts +20 -3
  107. package/src/lexicon/index.ts +82 -0
  108. package/src/lexicon/lexicons.ts +357 -7
  109. package/src/lexicon/types/com/atproto/moderation/defs.ts +52 -7
  110. package/src/lexicon/types/com/atproto/temp/dereferenceScope.ts +42 -0
  111. package/src/lexicon/types/tools/ozone/moderation/defs.ts +23 -0
  112. package/src/lexicon/types/tools/ozone/moderation/emitEvent.ts +1 -0
  113. package/src/lexicon/types/tools/ozone/moderation/getAccountTimeline.ts +1 -0
  114. package/src/lexicon/types/tools/ozone/report/defs.ts +154 -0
  115. package/src/logger.ts +1 -0
  116. package/src/scripts/rebuild-repo.ts +4 -5
  117. package/src/scripts/sequencer-recovery/recoverer.ts +8 -12
  118. package/src/sequencer/sequencer.ts +3 -3
  119. package/tsconfig.build.tsbuildinfo +1 -1
@@ -7,7 +7,7 @@ import { IdResolver, getDidKeyFromMultibase } from '@atproto/identity'
7
7
  import {
8
8
  OAuthError,
9
9
  OAuthVerifier,
10
- VerifyTokenClaimsOptions,
10
+ VerifyTokenPayloadOptions,
11
11
  WWWAuthenticateError,
12
12
  } from '@atproto/oauth-provider'
13
13
  import {
@@ -39,7 +39,6 @@ import {
39
39
  } from './auth-output'
40
40
  import { ACCESS_STANDARD, AuthScope, isAuthScope } from './auth-scope'
41
41
  import { softDeleted } from './db'
42
- import { oauthLogger } from './logger'
43
42
  import { appendVary } from './util/http'
44
43
  import { WithRequired } from './util/types'
45
44
 
@@ -338,7 +337,7 @@ export class AuthVerifier {
338
337
  OAuthOutput,
339
338
  P
340
339
  > {
341
- const verifyTokenOptions: VerifyTokenClaimsOptions = {
340
+ const verifyTokenOptions: VerifyTokenPayloadOptions = {
342
341
  audience: [this.dids.pds],
343
342
  scope: ['atproto'],
344
343
  }
@@ -358,7 +357,7 @@ export class AuthVerifier {
358
357
  const originalUrl = req.originalUrl || req.url || '/'
359
358
  const url = new URL(originalUrl, this._publicUrl)
360
359
 
361
- const { tokenClaims, dpopProof } = await this.oauthVerifier
360
+ const { scope, sub: did } = await this.oauthVerifier
362
361
  .authenticateRequest(
363
362
  req.method || 'GET',
364
363
  url,
@@ -383,28 +382,13 @@ export class AuthVerifier {
383
382
  throw err
384
383
  })
385
384
 
386
- // @TODO drop this once oauth provider no longer accepts DPoP proof with
387
- // query or fragment in "htu" claim.
388
- if (dpopProof?.htu.match(/[?#]/)) {
389
- oauthLogger.info(
390
- {
391
- client_id: tokenClaims.client_id,
392
- htu: dpopProof.htu,
393
- },
394
- 'DPoP proof "htu" contains query or fragment',
395
- )
396
- }
397
-
398
- const { sub: did } = tokenClaims
399
385
  if (typeof did !== 'string' || !did.startsWith('did:')) {
400
386
  throw new InvalidRequestError('Malformed token', 'InvalidToken')
401
387
  }
402
388
 
403
389
  await this.verifyStatus(did, verifyStatusOptions)
404
390
 
405
- const permissions = new ScopePermissionsTransition(
406
- tokenClaims.scope?.split(' '),
407
- )
391
+ const permissions = new ScopePermissionsTransition(scope?.split(' '))
408
392
 
409
393
  // Should never happen
410
394
  if (!permissions.scopes.has('atproto')) {
package/src/context.ts CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  } from '@atproto-labs/fetch-node'
33
33
  import { AccountManager } from './account-manager/account-manager'
34
34
  import { OAuthStore } from './account-manager/oauth-store'
35
+ import { ScopeReferenceGetter } from './account-manager/scope-reference-getter'
35
36
  import { ActorStore } from './actor-store/actor-store'
36
37
  import { authPassthru, forwardedFor } from './api/proxy'
37
38
  import {
@@ -46,7 +47,7 @@ import { Crawlers } from './crawlers'
46
47
  import { DidSqliteCache } from './did-cache'
47
48
  import { DiskBlobStore } from './disk-blobstore'
48
49
  import { ImageUrlBuilder } from './image/image-url-builder'
49
- import { fetchLogger, lexiconResolverLogger } from './logger'
50
+ import { fetchLogger, lexiconResolverLogger, oauthLogger } from './logger'
50
51
  import { ServerMailer } from './mailer'
51
52
  import { ModerationMailer } from './mailer/moderation'
52
53
  import { LocalViewer, LocalViewerCreator } from './read-after-write/viewer'
@@ -397,10 +398,11 @@ export class AppContext {
397
398
  protected_resources: [new URL(cfg.oauth.issuer).origin],
398
399
  },
399
400
  // If the PDS is both an authorization server & resource server (no
400
- // entryway), there is no need to use JWTs as access tokens. Instead,
401
- // the PDS can use tokenId as access tokens. This allows the PDS to
402
- // always use up-to-date token data from the token store.
403
- accessTokenMode: AccessTokenMode.light,
401
+ // entryway), we can afford to check the token validity on every
402
+ // request. This allows revoked tokens to be rejected immediately.
403
+ // This also allows JWT to be shorter since some claims (notably the
404
+ // "scope" claim) do not need to be included in the token.
405
+ accessTokenMode: AccessTokenMode.stateful,
404
406
 
405
407
  getClientInfo(clientId) {
406
408
  return {
@@ -410,6 +412,10 @@ export class AppContext {
410
412
  })
411
413
  : undefined
412
414
 
415
+ const scopeRefGetter = entrywayAgent
416
+ ? new ScopeReferenceGetter(entrywayAgent, redisScratch)
417
+ : undefined
418
+
413
419
  const oauthVerifier: OAuthVerifier =
414
420
  oauthProvider ?? // OAuthProvider extends OAuthVerifier
415
421
  new OAuthVerifier({
@@ -417,6 +423,21 @@ export class AppContext {
417
423
  keyset: [await JoseKey.fromKeyLike(jwtPublicKey!, undefined, 'ES256K')],
418
424
  dpopSecret: secrets.dpopSecret,
419
425
  redis: redisScratch,
426
+ onDecodeToken: scopeRefGetter
427
+ ? async ({ payload, dpopProof }) => {
428
+ // @TODO drop this once oauth provider no longer accepts DPoP proof with
429
+ // query or fragment in "htu" claim.
430
+ if (dpopProof?.htu.match(/[?#]/)) {
431
+ oauthLogger.info(
432
+ { htu: dpopProof.htu, client_id: payload.client_id },
433
+ 'DPoP proof "htu" contains query or fragment',
434
+ )
435
+ }
436
+
437
+ const scope = await scopeRefGetter.dereference(payload.scope)
438
+ return { ...payload, scope }
439
+ }
440
+ : undefined,
420
441
  })
421
442
 
422
443
  const authVerifier = new AuthVerifier(
@@ -3,10 +3,16 @@ import fs from 'node:fs/promises'
3
3
  import path from 'node:path'
4
4
  import stream from 'node:stream'
5
5
  import { CID } from 'multiformats/cid'
6
- import { fileExists, isErrnoException, rmIfExists } from '@atproto/common'
6
+ import {
7
+ aggregateErrors,
8
+ chunkArray,
9
+ fileExists,
10
+ isErrnoException,
11
+ rmIfExists,
12
+ } from '@atproto/common'
7
13
  import { randomStr } from '@atproto/crypto'
8
14
  import { BlobNotFoundError, BlobStore } from '@atproto/repo'
9
- import { httpLogger as log } from './logger'
15
+ import { blobStoreLogger as log } from './logger'
10
16
 
11
17
  export class DiskBlobStore implements BlobStore {
12
18
  constructor(
@@ -137,7 +143,18 @@ export class DiskBlobStore implements BlobStore {
137
143
  }
138
144
 
139
145
  async deleteMany(cids: CID[]): Promise<void> {
140
- await Promise.all(cids.map((cid) => this.delete(cid)))
146
+ const errors: unknown[] = []
147
+ for (const chunk of chunkArray(cids, 500)) {
148
+ await Promise.all(
149
+ chunk.map((cid) =>
150
+ this.delete(cid).catch((err) => {
151
+ log.error({ err, cid: cid.toString() }, 'error deleting blob')
152
+ errors.push(err)
153
+ }),
154
+ ),
155
+ )
156
+ }
157
+ if (errors.length) throw aggregateErrors(errors)
141
158
  }
142
159
 
143
160
  async deleteAll(): Promise<void> {
@@ -198,6 +198,7 @@ import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscrib
198
198
  import * as ComAtprotoTempAddReservedHandle from './types/com/atproto/temp/addReservedHandle.js'
199
199
  import * as ComAtprotoTempCheckHandleAvailability from './types/com/atproto/temp/checkHandleAvailability.js'
200
200
  import * as ComAtprotoTempCheckSignupQueue from './types/com/atproto/temp/checkSignupQueue.js'
201
+ import * as ComAtprotoTempDereferenceScope from './types/com/atproto/temp/dereferenceScope.js'
201
202
  import * as ComAtprotoTempFetchLabels from './types/com/atproto/temp/fetchLabels.js'
202
203
  import * as ComAtprotoTempRequestPhoneVerification from './types/com/atproto/temp/requestPhoneVerification.js'
203
204
  import * as ComAtprotoTempRevokeAccountCredentials from './types/com/atproto/temp/revokeAccountCredentials.js'
@@ -289,6 +290,75 @@ export const TOOLS_OZONE_MODERATION = {
289
290
  DefsTimelineEventPlcTombstone:
290
291
  'tools.ozone.moderation.defs#timelineEventPlcTombstone',
291
292
  }
293
+ export const TOOLS_OZONE_REPORT = {
294
+ DefsReasonAppeal: 'tools.ozone.report.defs#reasonAppeal',
295
+ DefsReasonViolenceAnimalWelfare:
296
+ 'tools.ozone.report.defs#reasonViolenceAnimalWelfare',
297
+ DefsReasonViolenceThreats: 'tools.ozone.report.defs#reasonViolenceThreats',
298
+ DefsReasonViolenceGraphicContent:
299
+ 'tools.ozone.report.defs#reasonViolenceGraphicContent',
300
+ DefsReasonViolenceSelfHarm: 'tools.ozone.report.defs#reasonViolenceSelfHarm',
301
+ DefsReasonViolenceGlorification:
302
+ 'tools.ozone.report.defs#reasonViolenceGlorification',
303
+ DefsReasonViolenceExtremistContent:
304
+ 'tools.ozone.report.defs#reasonViolenceExtremistContent',
305
+ DefsReasonViolenceTrafficking:
306
+ 'tools.ozone.report.defs#reasonViolenceTrafficking',
307
+ DefsReasonViolenceOther: 'tools.ozone.report.defs#reasonViolenceOther',
308
+ DefsReasonSexualAbuseContent:
309
+ 'tools.ozone.report.defs#reasonSexualAbuseContent',
310
+ DefsReasonSexualNCII: 'tools.ozone.report.defs#reasonSexualNCII',
311
+ DefsReasonSexualSextortion: 'tools.ozone.report.defs#reasonSexualSextortion',
312
+ DefsReasonSexualDeepfake: 'tools.ozone.report.defs#reasonSexualDeepfake',
313
+ DefsReasonSexualAnimal: 'tools.ozone.report.defs#reasonSexualAnimal',
314
+ DefsReasonSexualUnlabeled: 'tools.ozone.report.defs#reasonSexualUnlabeled',
315
+ DefsReasonSexualOther: 'tools.ozone.report.defs#reasonSexualOther',
316
+ DefsReasonChildSafetyCSAM: 'tools.ozone.report.defs#reasonChildSafetyCSAM',
317
+ DefsReasonChildSafetyGroom: 'tools.ozone.report.defs#reasonChildSafetyGroom',
318
+ DefsReasonChildSafetyMinorPrivacy:
319
+ 'tools.ozone.report.defs#reasonChildSafetyMinorPrivacy',
320
+ DefsReasonChildSafetyEndangerment:
321
+ 'tools.ozone.report.defs#reasonChildSafetyEndangerment',
322
+ DefsReasonChildSafetyHarassment:
323
+ 'tools.ozone.report.defs#reasonChildSafetyHarassment',
324
+ DefsReasonChildSafetyPromotion:
325
+ 'tools.ozone.report.defs#reasonChildSafetyPromotion',
326
+ DefsReasonChildSafetyOther: 'tools.ozone.report.defs#reasonChildSafetyOther',
327
+ DefsReasonHarassmentTroll: 'tools.ozone.report.defs#reasonHarassmentTroll',
328
+ DefsReasonHarassmentTargeted:
329
+ 'tools.ozone.report.defs#reasonHarassmentTargeted',
330
+ DefsReasonHarassmentHateSpeech:
331
+ 'tools.ozone.report.defs#reasonHarassmentHateSpeech',
332
+ DefsReasonHarassmentDoxxing:
333
+ 'tools.ozone.report.defs#reasonHarassmentDoxxing',
334
+ DefsReasonHarassmentOther: 'tools.ozone.report.defs#reasonHarassmentOther',
335
+ DefsReasonMisleadingBot: 'tools.ozone.report.defs#reasonMisleadingBot',
336
+ DefsReasonMisleadingImpersonation:
337
+ 'tools.ozone.report.defs#reasonMisleadingImpersonation',
338
+ DefsReasonMisleadingSpam: 'tools.ozone.report.defs#reasonMisleadingSpam',
339
+ DefsReasonMisleadingScam: 'tools.ozone.report.defs#reasonMisleadingScam',
340
+ DefsReasonMisleadingSyntheticContent:
341
+ 'tools.ozone.report.defs#reasonMisleadingSyntheticContent',
342
+ DefsReasonMisleadingMisinformation:
343
+ 'tools.ozone.report.defs#reasonMisleadingMisinformation',
344
+ DefsReasonMisleadingOther: 'tools.ozone.report.defs#reasonMisleadingOther',
345
+ DefsReasonRuleSiteSecurity: 'tools.ozone.report.defs#reasonRuleSiteSecurity',
346
+ DefsReasonRuleStolenContent:
347
+ 'tools.ozone.report.defs#reasonRuleStolenContent',
348
+ DefsReasonRuleProhibitedSales:
349
+ 'tools.ozone.report.defs#reasonRuleProhibitedSales',
350
+ DefsReasonRuleBanEvasion: 'tools.ozone.report.defs#reasonRuleBanEvasion',
351
+ DefsReasonRuleOther: 'tools.ozone.report.defs#reasonRuleOther',
352
+ DefsReasonCivicElectoralProcess:
353
+ 'tools.ozone.report.defs#reasonCivicElectoralProcess',
354
+ DefsReasonCivicDisclosure: 'tools.ozone.report.defs#reasonCivicDisclosure',
355
+ DefsReasonCivicInterference:
356
+ 'tools.ozone.report.defs#reasonCivicInterference',
357
+ DefsReasonCivicMisinformation:
358
+ 'tools.ozone.report.defs#reasonCivicMisinformation',
359
+ DefsReasonCivicImpersonation:
360
+ 'tools.ozone.report.defs#reasonCivicImpersonation',
361
+ }
292
362
  export const TOOLS_OZONE_TEAM = {
293
363
  DefsRoleAdmin: 'tools.ozone.team.defs#roleAdmin',
294
364
  DefsRoleModerator: 'tools.ozone.team.defs#roleModerator',
@@ -2843,6 +2913,18 @@ export class ComAtprotoTempNS {
2843
2913
  return this._server.xrpc.method(nsid, cfg)
2844
2914
  }
2845
2915
 
2916
+ dereferenceScope<A extends Auth = void>(
2917
+ cfg: MethodConfigOrHandler<
2918
+ A,
2919
+ ComAtprotoTempDereferenceScope.QueryParams,
2920
+ ComAtprotoTempDereferenceScope.HandlerInput,
2921
+ ComAtprotoTempDereferenceScope.HandlerOutput
2922
+ >,
2923
+ ) {
2924
+ const nsid = 'com.atproto.temp.dereferenceScope' // @ts-ignore
2925
+ return this._server.xrpc.method(nsid, cfg)
2926
+ }
2927
+
2846
2928
  fetchLabels<A extends Auth = void>(
2847
2929
  cfg: MethodConfigOrHandler<
2848
2930
  A,