@atproto/pds 0.4.56 → 0.4.58

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 (67) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/api/com/atproto/repo/applyWrites.d.ts.map +1 -1
  3. package/dist/api/com/atproto/repo/applyWrites.js +43 -7
  4. package/dist/api/com/atproto/repo/applyWrites.js.map +1 -1
  5. package/dist/api/com/atproto/repo/createRecord.d.ts.map +1 -1
  6. package/dist/api/com/atproto/repo/createRecord.js +10 -2
  7. package/dist/api/com/atproto/repo/createRecord.js.map +1 -1
  8. package/dist/api/com/atproto/repo/deleteRecord.d.ts.map +1 -1
  9. package/dist/api/com/atproto/repo/deleteRecord.js +11 -0
  10. package/dist/api/com/atproto/repo/deleteRecord.js.map +1 -1
  11. package/dist/api/com/atproto/repo/putRecord.d.ts.map +1 -1
  12. package/dist/api/com/atproto/repo/putRecord.js +7 -0
  13. package/dist/api/com/atproto/repo/putRecord.js.map +1 -1
  14. package/dist/lexicon/lexicons.d.ts +131 -3
  15. package/dist/lexicon/lexicons.d.ts.map +1 -1
  16. package/dist/lexicon/lexicons.js +143 -6
  17. package/dist/lexicon/lexicons.js.map +1 -1
  18. package/dist/lexicon/types/com/atproto/repo/applyWrites.d.ts +38 -4
  19. package/dist/lexicon/types/com/atproto/repo/applyWrites.d.ts.map +1 -1
  20. package/dist/lexicon/types/com/atproto/repo/applyWrites.js +31 -1
  21. package/dist/lexicon/types/com/atproto/repo/applyWrites.js.map +1 -1
  22. package/dist/lexicon/types/com/atproto/repo/createRecord.d.ts +5 -2
  23. package/dist/lexicon/types/com/atproto/repo/createRecord.d.ts.map +1 -1
  24. package/dist/lexicon/types/com/atproto/repo/defs.d.ts +12 -0
  25. package/dist/lexicon/types/com/atproto/repo/defs.d.ts.map +1 -0
  26. package/dist/lexicon/types/com/atproto/repo/defs.js +16 -0
  27. package/dist/lexicon/types/com/atproto/repo/defs.js.map +1 -0
  28. package/dist/lexicon/types/com/atproto/repo/deleteRecord.d.ts +14 -2
  29. package/dist/lexicon/types/com/atproto/repo/deleteRecord.d.ts.map +1 -1
  30. package/dist/lexicon/types/com/atproto/repo/putRecord.d.ts +5 -2
  31. package/dist/lexicon/types/com/atproto/repo/putRecord.d.ts.map +1 -1
  32. package/dist/lexicon/types/tools/ozone/communication/createTemplate.d.ts +3 -0
  33. package/dist/lexicon/types/tools/ozone/communication/createTemplate.d.ts.map +1 -1
  34. package/dist/lexicon/types/tools/ozone/communication/defs.d.ts +2 -0
  35. package/dist/lexicon/types/tools/ozone/communication/defs.d.ts.map +1 -1
  36. package/dist/lexicon/types/tools/ozone/communication/defs.js.map +1 -1
  37. package/dist/lexicon/types/tools/ozone/communication/updateTemplate.d.ts +3 -0
  38. package/dist/lexicon/types/tools/ozone/communication/updateTemplate.d.ts.map +1 -1
  39. package/dist/lexicon/types/tools/ozone/moderation/emitEvent.d.ts +1 -1
  40. package/dist/lexicon/types/tools/ozone/moderation/emitEvent.d.ts.map +1 -1
  41. package/dist/repo/prepare.d.ts +4 -2
  42. package/dist/repo/prepare.d.ts.map +1 -1
  43. package/dist/repo/prepare.js +30 -14
  44. package/dist/repo/prepare.js.map +1 -1
  45. package/dist/repo/types.d.ts +3 -0
  46. package/dist/repo/types.d.ts.map +1 -1
  47. package/dist/repo/types.js.map +1 -1
  48. package/dist/sequencer/events.d.ts +2 -2
  49. package/package.json +6 -6
  50. package/src/api/com/atproto/repo/applyWrites.ts +41 -0
  51. package/src/api/com/atproto/repo/createRecord.ts +16 -7
  52. package/src/api/com/atproto/repo/deleteRecord.ts +11 -0
  53. package/src/api/com/atproto/repo/putRecord.ts +7 -0
  54. package/src/lexicon/lexicons.ts +143 -6
  55. package/src/lexicon/types/com/atproto/repo/applyWrites.ts +70 -3
  56. package/src/lexicon/types/com/atproto/repo/createRecord.ts +5 -2
  57. package/src/lexicon/types/com/atproto/repo/defs.ts +25 -0
  58. package/src/lexicon/types/com/atproto/repo/deleteRecord.ts +13 -1
  59. package/src/lexicon/types/com/atproto/repo/putRecord.ts +5 -2
  60. package/src/lexicon/types/tools/ozone/communication/createTemplate.ts +3 -0
  61. package/src/lexicon/types/tools/ozone/communication/defs.ts +2 -0
  62. package/src/lexicon/types/tools/ozone/communication/updateTemplate.ts +3 -0
  63. package/src/lexicon/types/tools/ozone/moderation/emitEvent.ts +1 -0
  64. package/src/repo/prepare.ts +31 -13
  65. package/src/repo/types.ts +4 -0
  66. package/tests/crud.test.ts +131 -14
  67. package/tests/sync/sync.test.ts +4 -4
@@ -18,6 +18,8 @@ export interface InputSchema {
18
18
  contentMarkdown: string
19
19
  /** Subject of the message, used in emails. */
20
20
  subject: string
21
+ /** Message language. */
22
+ lang?: string
21
23
  /** DID of the user who is creating the template. */
22
24
  createdBy?: string
23
25
  [k: string]: unknown
@@ -39,6 +41,7 @@ export interface HandlerSuccess {
39
41
  export interface HandlerError {
40
42
  status: number
41
43
  message?: string
44
+ error?: 'DuplicateTemplateName'
42
45
  }
43
46
 
44
47
  export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
@@ -15,6 +15,8 @@ export interface TemplateView {
15
15
  /** Subject of the message, used in emails. */
16
16
  contentMarkdown: string
17
17
  disabled: boolean
18
+ /** Message language. */
19
+ lang?: string
18
20
  /** DID of the user who last updated the template. */
19
21
  lastUpdatedBy: string
20
22
  createdAt: string
@@ -16,6 +16,8 @@ export interface InputSchema {
16
16
  id: string
17
17
  /** Name of the template. */
18
18
  name?: string
19
+ /** Message language. */
20
+ lang?: string
19
21
  /** Content of the template, markdown supported, can contain variable placeholders. */
20
22
  contentMarkdown?: string
21
23
  /** Subject of the message, used in emails. */
@@ -42,6 +44,7 @@ export interface HandlerSuccess {
42
44
  export interface HandlerError {
43
45
  status: number
44
46
  message?: string
47
+ error?: 'DuplicateTemplateName'
45
48
  }
46
49
 
47
50
  export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
@@ -26,6 +26,7 @@ export interface InputSchema {
26
26
  | ToolsOzoneModerationDefs.ModEventMuteReporter
27
27
  | ToolsOzoneModerationDefs.ModEventUnmuteReporter
28
28
  | ToolsOzoneModerationDefs.ModEventReverseTakedown
29
+ | ToolsOzoneModerationDefs.ModEventResolveAppeal
29
30
  | ToolsOzoneModerationDefs.ModEventEmail
30
31
  | ToolsOzoneModerationDefs.ModEventTag
31
32
  | { $type: string; [k: string]: unknown }
@@ -29,6 +29,7 @@ import {
29
29
  InvalidRecordError,
30
30
  PreparedWrite,
31
31
  PreparedBlobRef,
32
+ ValidationStatus,
32
33
  } from './types'
33
34
  import * as lex from '../lexicon/lexicons'
34
35
  import { isRecord as isFeedGenerator } from '../lexicon/types/app/bsky/feed/generator'
@@ -39,7 +40,10 @@ import { isRecord as isList } from '../lexicon/types/app/bsky/graph/list'
39
40
  import { isRecord as isProfile } from '../lexicon/types/app/bsky/actor/profile'
40
41
  import { hasExplicitSlur } from '../handle/explicit-slurs'
41
42
 
42
- export const assertValidRecord = (record: Record<string, unknown>) => {
43
+ export const assertValidRecordWithStatus = (
44
+ record: Record<string, unknown>,
45
+ opts: { requireLexicon: boolean },
46
+ ): ValidationStatus => {
43
47
  if (typeof record.$type !== 'string') {
44
48
  throw new InvalidRecordError('No $type provided')
45
49
  }
@@ -48,7 +52,11 @@ export const assertValidRecord = (record: Record<string, unknown>) => {
48
52
  assertValidCreatedAt(record)
49
53
  } catch (e) {
50
54
  if (e instanceof LexiconDefNotFoundError) {
51
- throw new InvalidRecordError(e.message)
55
+ if (opts.requireLexicon) {
56
+ throw new InvalidRecordError(e.message)
57
+ } else {
58
+ return 'unknown'
59
+ }
52
60
  }
53
61
  throw new InvalidRecordError(
54
62
  `Invalid ${record.$type} record: ${
@@ -56,6 +64,7 @@ export const assertValidRecord = (record: Record<string, unknown>) => {
56
64
  }`,
57
65
  )
58
66
  }
67
+ return 'valid'
59
68
  }
60
69
 
61
70
  // additional more rigorous check on datetimes
@@ -98,12 +107,15 @@ export const prepareCreate = async (opts: {
98
107
  record: RepoRecord
99
108
  validate?: boolean
100
109
  }): Promise<PreparedCreate> => {
101
- const { did, collection, swapCid, validate = true } = opts
102
- const record = setCollectionName(collection, opts.record, validate)
103
- if (validate) {
104
- assertValidRecord(record)
110
+ const { did, collection, swapCid, validate } = opts
111
+ const maybeValidate = validate !== false
112
+ const record = setCollectionName(collection, opts.record, maybeValidate)
113
+ let validationStatus: ValidationStatus
114
+ if (maybeValidate) {
115
+ validationStatus = assertValidRecordWithStatus(record, {
116
+ requireLexicon: validate === true,
117
+ })
105
118
  }
106
-
107
119
  const nextRkey = TID.next()
108
120
  const rkey = opts.rkey || nextRkey.toString()
109
121
  // @TODO: validate against Lexicon record 'key' type, not just overall recordkey syntax
@@ -115,7 +127,8 @@ export const prepareCreate = async (opts: {
115
127
  cid: await cidForSafeRecord(record),
116
128
  swapCid,
117
129
  record,
118
- blobs: blobsForWrite(record, validate),
130
+ blobs: blobsForWrite(record, maybeValidate),
131
+ validationStatus,
119
132
  }
120
133
  }
121
134
 
@@ -127,10 +140,14 @@ export const prepareUpdate = async (opts: {
127
140
  record: RepoRecord
128
141
  validate?: boolean
129
142
  }): Promise<PreparedUpdate> => {
130
- const { did, collection, rkey, swapCid, validate = true } = opts
131
- const record = setCollectionName(collection, opts.record, validate)
132
- if (validate) {
133
- assertValidRecord(record)
143
+ const { did, collection, rkey, swapCid, validate } = opts
144
+ const maybeValidate = validate !== false
145
+ const record = setCollectionName(collection, opts.record, maybeValidate)
146
+ let validationStatus: ValidationStatus
147
+ if (maybeValidate) {
148
+ validationStatus = assertValidRecordWithStatus(record, {
149
+ requireLexicon: validate === true,
150
+ })
134
151
  }
135
152
  assertNoExplicitSlurs(rkey, record)
136
153
  return {
@@ -139,7 +156,8 @@ export const prepareUpdate = async (opts: {
139
156
  cid: await cidForSafeRecord(record),
140
157
  swapCid,
141
158
  record,
142
- blobs: blobsForWrite(record, validate),
159
+ blobs: blobsForWrite(record, maybeValidate),
160
+ validationStatus,
143
161
  }
144
162
  }
145
163
 
package/src/repo/types.ts CHANGED
@@ -3,6 +3,8 @@ import { AtUri } from '@atproto/syntax'
3
3
  import { WriteOpAction } from '@atproto/repo'
4
4
  import { RepoRecord } from '@atproto/lexicon'
5
5
 
6
+ export type ValidationStatus = 'valid' | 'unknown' | undefined
7
+
6
8
  export type BlobConstraint = {
7
9
  accept?: string[]
8
10
  maxSize?: number
@@ -21,6 +23,7 @@ export type PreparedCreate = {
21
23
  swapCid?: CID | null
22
24
  record: RepoRecord
23
25
  blobs: PreparedBlobRef[]
26
+ validationStatus: ValidationStatus
24
27
  }
25
28
 
26
29
  export type PreparedUpdate = {
@@ -30,6 +33,7 @@ export type PreparedUpdate = {
30
33
  swapCid?: CID | null
31
34
  record: RepoRecord
32
35
  blobs: PreparedBlobRef[]
36
+ validationStatus: ValidationStatus
33
37
  }
34
38
 
35
39
  export type PreparedDelete = {
@@ -6,7 +6,7 @@ import { TestNetworkNoAppView } from '@atproto/dev-env'
6
6
  import { cidForCbor, TID, ui8ToArrayBuffer } from '@atproto/common'
7
7
  import { BlobNotFoundError } from '@atproto/repo'
8
8
  import * as Post from '../src/lexicon/types/app/bsky/feed/post'
9
- import { paginateAll } from './_util'
9
+ import { forSnapshot, paginateAll } from './_util'
10
10
  import AppContext from '../src/context'
11
11
  import { ids, lexicons } from '../src/lexicon/lexicons'
12
12
 
@@ -565,8 +565,9 @@ describe('crud operations', () => {
565
565
  expect(got.data.value['$type']).toBe(uri.collection)
566
566
  })
567
567
 
568
- it('requires the schema to be known if validating', async () => {
568
+ it('requires the schema to be known if explicitly validating', async () => {
569
569
  const prom = aliceAgent.api.com.atproto.repo.createRecord({
570
+ validate: true,
570
571
  repo: aliceAgent.accountDid,
571
572
  collection: 'com.example.foobar',
572
573
  record: { $type: 'com.example.foobar' },
@@ -576,6 +577,16 @@ describe('crud operations', () => {
576
577
  )
577
578
  })
578
579
 
580
+ it('does not require the schema to be known if not explicitly validating', async () => {
581
+ const prom = await aliceAgent.api.com.atproto.repo.createRecord({
582
+ // validate not set
583
+ repo: aliceAgent.accountDid,
584
+ collection: 'com.example.foobar',
585
+ record: { $type: 'com.example.foobar' },
586
+ })
587
+ expect(prom.data.validationStatus).toBe('unknown')
588
+ })
589
+
579
590
  it('requires the $type to match the schema', async () => {
580
591
  await expect(
581
592
  aliceAgent.api.com.atproto.repo.createRecord({
@@ -637,6 +648,7 @@ describe('crud operations', () => {
637
648
  describe('unvalidated writes', () => {
638
649
  it('disallows creation of unknown lexicons when validate is set to true', async () => {
639
650
  const attempt = aliceAgent.api.com.atproto.repo.createRecord({
651
+ validate: true,
640
652
  repo: aliceAgent.accountDid,
641
653
  collection: 'com.example.record',
642
654
  record: {
@@ -648,52 +660,157 @@ describe('crud operations', () => {
648
660
  )
649
661
  })
650
662
 
651
- it('allows creation of unknown lexicons when validate is set to false', async () => {
652
- const res = await aliceAgent.api.com.atproto.repo.createRecord({
663
+ it('allows creation of unknown lexicons when validate is not set to true', async () => {
664
+ // validate: default
665
+ const res1 = await aliceAgent.api.com.atproto.repo.createRecord({
653
666
  repo: aliceAgent.accountDid,
654
667
  collection: 'com.example.record',
655
668
  record: {
656
- blah: 'thing',
669
+ blah: 'thing1',
657
670
  },
671
+ })
672
+ expect(res1.data.validationStatus).toBe('unknown')
673
+ const record1 = await ctx.actorStore.read(
674
+ aliceAgent.accountDid,
675
+ (store) =>
676
+ store.record.getRecord(new AtUri(res1.data.uri), res1.data.cid),
677
+ )
678
+ expect(record1?.value).toEqual({
679
+ $type: 'com.example.record',
680
+ blah: 'thing1',
681
+ })
682
+ // validate: false
683
+ const res2 = await aliceAgent.api.com.atproto.repo.createRecord({
658
684
  validate: false,
685
+ repo: aliceAgent.accountDid,
686
+ collection: 'com.example.record',
687
+ record: {
688
+ blah: 'thing2',
689
+ },
659
690
  })
660
- const record = await ctx.actorStore.read(aliceAgent.accountDid, (store) =>
661
- store.record.getRecord(new AtUri(res.data.uri), res.data.cid),
691
+ expect(res2.data.validationStatus).toBeUndefined()
692
+ const record2 = await ctx.actorStore.read(
693
+ aliceAgent.accountDid,
694
+ (store) =>
695
+ store.record.getRecord(new AtUri(res2.data.uri), res2.data.cid),
662
696
  )
663
- expect(record?.value).toEqual({
697
+ expect(record2?.value).toEqual({
664
698
  $type: 'com.example.record',
665
- blah: 'thing',
699
+ blah: 'thing2',
666
700
  })
667
701
  })
668
702
 
669
703
  it('allows update of unknown lexicons when validate is set to false', async () => {
670
704
  const createRes = await aliceAgent.api.com.atproto.repo.createRecord({
705
+ validate: false,
671
706
  repo: aliceAgent.accountDid,
672
707
  collection: 'com.example.record',
673
708
  record: {
674
709
  blah: 'thing',
675
710
  },
676
- validate: false,
677
711
  })
678
712
  const uri = new AtUri(createRes.data.uri)
679
- const updateRes = await aliceAgent.api.com.atproto.repo.putRecord({
713
+ // validate: default
714
+ const updateRes1 = await aliceAgent.api.com.atproto.repo.putRecord({
680
715
  repo: aliceAgent.accountDid,
681
716
  collection: 'com.example.record',
682
717
  rkey: uri.rkey,
683
718
  record: {
684
719
  blah: 'something else',
685
720
  },
721
+ })
722
+ const record1 = await ctx.actorStore.read(
723
+ aliceAgent.accountDid,
724
+ (store) => store.record.getRecord(uri, updateRes1.data.cid),
725
+ )
726
+ expect(record1?.value).toEqual({
727
+ $type: 'com.example.record',
728
+ blah: 'something else',
729
+ })
730
+ // validate: false
731
+ const updateRes2 = await aliceAgent.api.com.atproto.repo.putRecord({
686
732
  validate: false,
733
+ repo: aliceAgent.accountDid,
734
+ collection: 'com.example.record',
735
+ rkey: uri.rkey,
736
+ record: {
737
+ blah: 'something else',
738
+ },
687
739
  })
688
- const record = await ctx.actorStore.read(aliceAgent.accountDid, (store) =>
689
- store.record.getRecord(uri, updateRes.data.cid),
740
+ const record2 = await ctx.actorStore.read(
741
+ aliceAgent.accountDid,
742
+ (store) => store.record.getRecord(uri, updateRes2.data.cid),
690
743
  )
691
- expect(record?.value).toEqual({
744
+ expect(record2?.value).toEqual({
692
745
  $type: 'com.example.record',
693
746
  blah: 'something else',
694
747
  })
695
748
  })
696
749
 
750
+ it('applyWrites returns results with validation status', async () => {
751
+ const existing1 = await aliceAgent.api.com.atproto.repo.createRecord({
752
+ validate: false,
753
+ repo: aliceAgent.accountDid,
754
+ collection: 'com.example.record',
755
+ record: {
756
+ blah: 'thing1',
757
+ },
758
+ })
759
+ const existing2 = await aliceAgent.api.com.atproto.repo.createRecord({
760
+ validate: false,
761
+ repo: aliceAgent.accountDid,
762
+ collection: 'com.example.record',
763
+ record: {
764
+ blah: 'thing2',
765
+ },
766
+ })
767
+ const {
768
+ data: { results },
769
+ } = await aliceAgent.com.atproto.repo.applyWrites({
770
+ repo: aliceAgent.accountDid,
771
+ writes: [
772
+ {
773
+ $type: `${ids.ComAtprotoRepoApplyWrites}#create`,
774
+ action: 'create',
775
+ collection: ids.AppBskyFeedPost,
776
+ value: {
777
+ $type: ids.AppBskyFeedPost,
778
+ text: '👋',
779
+ createdAt: new Date().toISOString(),
780
+ },
781
+ },
782
+ {
783
+ $type: `${ids.ComAtprotoRepoApplyWrites}#update`,
784
+ action: 'update',
785
+ collection: 'com.example.record',
786
+ rkey: new AtUri(existing1.data.uri).rkey,
787
+ value: {},
788
+ },
789
+ {
790
+ $type: `${ids.ComAtprotoRepoApplyWrites}#delete`,
791
+ action: 'delete',
792
+ collection: 'com.example.record',
793
+ rkey: new AtUri(existing2.data.uri).rkey,
794
+ },
795
+ ],
796
+ })
797
+ expect(forSnapshot(results)).toEqual([
798
+ {
799
+ $type: `${ids.ComAtprotoRepoApplyWrites}#createResult`,
800
+ cid: 'cids(0)',
801
+ uri: 'record(0)',
802
+ validationStatus: 'valid',
803
+ },
804
+ {
805
+ $type: `${ids.ComAtprotoRepoApplyWrites}#updateResult`,
806
+ cid: 'cids(1)',
807
+ uri: 'record(1)',
808
+ validationStatus: 'unknown',
809
+ },
810
+ { $type: `${ids.ComAtprotoRepoApplyWrites}#deleteResult` },
811
+ ])
812
+ })
813
+
697
814
  it('correctly associates images with unknown record types', async () => {
698
815
  const file = await fs.readFile(
699
816
  '../dev-env/src/seed/img/key-portrait-small.jpg',
@@ -1,6 +1,6 @@
1
1
  import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env'
2
2
  import { AtpAgent } from '@atproto/api'
3
- import { TID } from '@atproto/common'
3
+ import { cidForCbor, TID } from '@atproto/common'
4
4
  import { Keypair, randomStr } from '@atproto/crypto'
5
5
  import * as repo from '@atproto/repo'
6
6
  import { MemoryBlockstore } from '@atproto/repo'
@@ -169,10 +169,10 @@ describe('repo sync', () => {
169
169
  const claim = {
170
170
  collection,
171
171
  rkey,
172
- record: repoData[collection][rkey],
172
+ cid: await cidForCbor(repoData[collection][rkey]),
173
173
  }
174
174
  expect(records.length).toBe(1)
175
- expect(records[0].record).toEqual(claim.record)
175
+ expect(await cidForCbor(records[0].record)).toEqual(claim.cid)
176
176
  const result = await repo.verifyProofs(
177
177
  new Uint8Array(car.data),
178
178
  [claim],
@@ -194,7 +194,7 @@ describe('repo sync', () => {
194
194
  const claim = {
195
195
  collection,
196
196
  rkey,
197
- record: null,
197
+ cid: null,
198
198
  }
199
199
  const result = await repo.verifyProofs(
200
200
  new Uint8Array(car.data),