@atproto/bsky 0.0.170 → 0.0.172

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 (162) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/api/app/bsky/notification/registerPush.d.ts.map +1 -1
  3. package/dist/api/app/bsky/notification/registerPush.js +6 -7
  4. package/dist/api/app/bsky/notification/registerPush.js.map +1 -1
  5. package/dist/api/app/bsky/notification/unregisterPush.d.ts +4 -0
  6. package/dist/api/app/bsky/notification/unregisterPush.d.ts.map +1 -0
  7. package/dist/api/app/bsky/notification/unregisterPush.js +33 -0
  8. package/dist/api/app/bsky/notification/unregisterPush.js.map +1 -0
  9. package/dist/api/app/bsky/notification/util.d.ts +4 -0
  10. package/dist/api/app/bsky/notification/util.d.ts.map +1 -1
  11. package/dist/api/app/bsky/notification/util.js +14 -1
  12. package/dist/api/app/bsky/notification/util.js.map +1 -1
  13. package/dist/api/app/bsky/unspecced/getAgeAssuranceState.d.ts +4 -0
  14. package/dist/api/app/bsky/unspecced/getAgeAssuranceState.d.ts.map +1 -0
  15. package/dist/api/app/bsky/unspecced/getAgeAssuranceState.js +36 -0
  16. package/dist/api/app/bsky/unspecced/getAgeAssuranceState.js.map +1 -0
  17. package/dist/api/app/bsky/unspecced/initAgeAssurance.d.ts +4 -0
  18. package/dist/api/app/bsky/unspecced/initAgeAssurance.d.ts.map +1 -0
  19. package/dist/api/app/bsky/unspecced/initAgeAssurance.js +59 -0
  20. package/dist/api/app/bsky/unspecced/initAgeAssurance.js.map +1 -0
  21. package/dist/api/external.d.ts +4 -0
  22. package/dist/api/external.d.ts.map +1 -0
  23. package/dist/api/external.js +47 -0
  24. package/dist/api/external.js.map +1 -0
  25. package/dist/api/index.d.ts +1 -0
  26. package/dist/api/index.d.ts.map +1 -1
  27. package/dist/api/index.js +6 -1
  28. package/dist/api/index.js.map +1 -1
  29. package/dist/api/kws/api.d.ts +4 -0
  30. package/dist/api/kws/api.d.ts.map +1 -0
  31. package/dist/api/kws/api.js +60 -0
  32. package/dist/api/kws/api.js.map +1 -0
  33. package/dist/api/kws/index.d.ts +4 -0
  34. package/dist/api/kws/index.d.ts.map +1 -0
  35. package/dist/api/kws/index.js +21 -0
  36. package/dist/api/kws/index.js.map +1 -0
  37. package/dist/api/kws/types.d.ts +100 -0
  38. package/dist/api/kws/types.d.ts.map +1 -0
  39. package/dist/api/kws/types.js +29 -0
  40. package/dist/api/kws/types.js.map +1 -0
  41. package/dist/api/kws/util.d.ts +21 -0
  42. package/dist/api/kws/util.d.ts.map +1 -0
  43. package/dist/api/kws/util.js +78 -0
  44. package/dist/api/kws/util.js.map +1 -0
  45. package/dist/api/kws/webhook.d.ts +5 -0
  46. package/dist/api/kws/webhook.d.ts.map +1 -0
  47. package/dist/api/kws/webhook.js +80 -0
  48. package/dist/api/kws/webhook.js.map +1 -0
  49. package/dist/config.d.ts +12 -0
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/config.js +40 -0
  52. package/dist/config.js.map +1 -1
  53. package/dist/context.d.ts +3 -0
  54. package/dist/context.d.ts.map +1 -1
  55. package/dist/context.js +3 -0
  56. package/dist/context.js.map +1 -1
  57. package/dist/data-plane/bsync/index.d.ts.map +1 -1
  58. package/dist/data-plane/bsync/index.js +52 -33
  59. package/dist/data-plane/bsync/index.js.map +1 -1
  60. package/dist/data-plane/server/db/migrations/20250627T025331240Z-add-actor-age-assurance-columns.d.ts +4 -0
  61. package/dist/data-plane/server/db/migrations/20250627T025331240Z-add-actor-age-assurance-columns.d.ts.map +1 -0
  62. package/dist/data-plane/server/db/migrations/20250627T025331240Z-add-actor-age-assurance-columns.js +22 -0
  63. package/dist/data-plane/server/db/migrations/20250627T025331240Z-add-actor-age-assurance-columns.js.map +1 -0
  64. package/dist/data-plane/server/db/migrations/index.d.ts +1 -0
  65. package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
  66. package/dist/data-plane/server/db/migrations/index.js +2 -0
  67. package/dist/data-plane/server/db/migrations/index.js.map +1 -1
  68. package/dist/data-plane/server/db/tables/actor.d.ts +2 -0
  69. package/dist/data-plane/server/db/tables/actor.d.ts.map +1 -1
  70. package/dist/data-plane/server/db/tables/actor.js.map +1 -1
  71. package/dist/data-plane/server/routes/profile.d.ts.map +1 -1
  72. package/dist/data-plane/server/routes/profile.js +14 -1
  73. package/dist/data-plane/server/routes/profile.js.map +1 -1
  74. package/dist/feature-gates.d.ts +2 -1
  75. package/dist/feature-gates.d.ts.map +1 -1
  76. package/dist/feature-gates.js +1 -0
  77. package/dist/feature-gates.js.map +1 -1
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +5 -0
  80. package/dist/index.js.map +1 -1
  81. package/dist/kws.d.ts +16 -0
  82. package/dist/kws.d.ts.map +1 -0
  83. package/dist/kws.js +86 -0
  84. package/dist/kws.js.map +1 -0
  85. package/dist/lexicon/index.d.ts +8 -2
  86. package/dist/lexicon/index.d.ts.map +1 -1
  87. package/dist/lexicon/index.js +16 -4
  88. package/dist/lexicon/index.js.map +1 -1
  89. package/dist/lexicon/lexicons.d.ts +374 -82
  90. package/dist/lexicon/lexicons.d.ts.map +1 -1
  91. package/dist/lexicon/lexicons.js +191 -42
  92. package/dist/lexicon/lexicons.js.map +1 -1
  93. package/dist/lexicon/types/app/bsky/notification/unregisterPush.d.ts +17 -0
  94. package/dist/lexicon/types/app/bsky/notification/unregisterPush.d.ts.map +1 -0
  95. package/dist/lexicon/types/app/bsky/notification/unregisterPush.js +7 -0
  96. package/dist/lexicon/types/app/bsky/notification/unregisterPush.js.map +1 -0
  97. package/dist/lexicon/types/app/bsky/unspecced/defs.d.ts +32 -0
  98. package/dist/lexicon/types/app/bsky/unspecced/defs.d.ts.map +1 -1
  99. package/dist/lexicon/types/app/bsky/unspecced/defs.js +18 -0
  100. package/dist/lexicon/types/app/bsky/unspecced/defs.js.map +1 -1
  101. package/dist/lexicon/types/app/bsky/unspecced/getAgeAssuranceState.d.ts +18 -0
  102. package/dist/lexicon/types/app/bsky/unspecced/getAgeAssuranceState.d.ts.map +1 -0
  103. package/dist/lexicon/types/app/bsky/unspecced/getAgeAssuranceState.js +7 -0
  104. package/dist/lexicon/types/app/bsky/unspecced/getAgeAssuranceState.js.map +1 -0
  105. package/dist/lexicon/types/app/bsky/unspecced/initAgeAssurance.d.ts +28 -0
  106. package/dist/lexicon/types/app/bsky/unspecced/initAgeAssurance.d.ts.map +1 -0
  107. package/dist/lexicon/types/app/bsky/unspecced/initAgeAssurance.js +7 -0
  108. package/dist/lexicon/types/app/bsky/unspecced/initAgeAssurance.js.map +1 -0
  109. package/dist/proto/bsky_pb.d.ts +33 -0
  110. package/dist/proto/bsky_pb.d.ts.map +1 -1
  111. package/dist/proto/bsky_pb.js +112 -4
  112. package/dist/proto/bsky_pb.js.map +1 -1
  113. package/dist/proto/courier_connect.d.ts +19 -1
  114. package/dist/proto/courier_connect.d.ts.map +1 -1
  115. package/dist/proto/courier_connect.js +18 -0
  116. package/dist/proto/courier_connect.js.map +1 -1
  117. package/dist/proto/courier_pb.d.ts +76 -0
  118. package/dist/proto/courier_pb.d.ts.map +1 -1
  119. package/dist/proto/courier_pb.js +233 -1
  120. package/dist/proto/courier_pb.js.map +1 -1
  121. package/dist/stash.d.ts +1 -0
  122. package/dist/stash.d.ts.map +1 -1
  123. package/dist/stash.js +1 -0
  124. package/dist/stash.js.map +1 -1
  125. package/package.json +7 -4
  126. package/proto/bsky.proto +8 -0
  127. package/proto/courier.proto +18 -0
  128. package/src/api/app/bsky/notification/registerPush.ts +5 -8
  129. package/src/api/app/bsky/notification/unregisterPush.ts +38 -0
  130. package/src/api/app/bsky/notification/util.ts +18 -0
  131. package/src/api/app/bsky/unspecced/getAgeAssuranceState.ts +46 -0
  132. package/src/api/app/bsky/unspecced/initAgeAssurance.ts +71 -0
  133. package/src/api/external.ts +13 -0
  134. package/src/api/index.ts +6 -0
  135. package/src/api/kws/api.ts +92 -0
  136. package/src/api/kws/index.ts +23 -0
  137. package/src/api/kws/types.ts +67 -0
  138. package/src/api/kws/util.ts +111 -0
  139. package/src/api/kws/webhook.ts +107 -0
  140. package/src/config.ts +59 -0
  141. package/src/context.ts +6 -0
  142. package/src/data-plane/bsync/index.ts +69 -33
  143. package/src/data-plane/server/db/migrations/20250627T025331240Z-add-actor-age-assurance-columns.ts +22 -0
  144. package/src/data-plane/server/db/migrations/index.ts +1 -0
  145. package/src/data-plane/server/db/tables/actor.ts +2 -0
  146. package/src/data-plane/server/routes/profile.ts +16 -1
  147. package/src/feature-gates.ts +1 -0
  148. package/src/index.ts +7 -1
  149. package/src/kws.ts +108 -0
  150. package/src/lexicon/index.ts +50 -11
  151. package/src/lexicon/lexicons.ts +201 -43
  152. package/src/lexicon/types/app/bsky/notification/unregisterPush.ts +36 -0
  153. package/src/lexicon/types/app/bsky/unspecced/defs.ts +50 -0
  154. package/src/lexicon/types/app/bsky/unspecced/getAgeAssuranceState.ts +34 -0
  155. package/src/lexicon/types/app/bsky/unspecced/initAgeAssurance.ts +47 -0
  156. package/src/proto/bsky_pb.ts +90 -0
  157. package/src/proto/courier_connect.ts +22 -0
  158. package/src/proto/courier_pb.ts +246 -0
  159. package/src/stash.ts +3 -0
  160. package/tests/views/age-assurance.test.ts +425 -0
  161. package/tsconfig.build.tsbuildinfo +1 -1
  162. package/tsconfig.tests.tsbuildinfo +1 -1
@@ -493,3 +493,249 @@ export class RegisterDeviceTokenResponse extends Message<RegisterDeviceTokenResp
493
493
  return proto3.util.equals(RegisterDeviceTokenResponse, a, b)
494
494
  }
495
495
  }
496
+
497
+ /**
498
+ * @generated from message courier.UnregisterDeviceTokenRequest
499
+ */
500
+ export class UnregisterDeviceTokenRequest extends Message<UnregisterDeviceTokenRequest> {
501
+ /**
502
+ * @generated from field: string did = 1;
503
+ */
504
+ did = ''
505
+
506
+ /**
507
+ * @generated from field: string token = 2;
508
+ */
509
+ token = ''
510
+
511
+ /**
512
+ * @generated from field: string app_id = 3;
513
+ */
514
+ appId = ''
515
+
516
+ /**
517
+ * @generated from field: courier.AppPlatform platform = 4;
518
+ */
519
+ platform = AppPlatform.UNSPECIFIED
520
+
521
+ constructor(data?: PartialMessage<UnregisterDeviceTokenRequest>) {
522
+ super()
523
+ proto3.util.initPartial(data, this)
524
+ }
525
+
526
+ static readonly runtime: typeof proto3 = proto3
527
+ static readonly typeName = 'courier.UnregisterDeviceTokenRequest'
528
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
529
+ { no: 1, name: 'did', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
530
+ { no: 2, name: 'token', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
531
+ { no: 3, name: 'app_id', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
532
+ {
533
+ no: 4,
534
+ name: 'platform',
535
+ kind: 'enum',
536
+ T: proto3.getEnumType(AppPlatform),
537
+ },
538
+ ])
539
+
540
+ static fromBinary(
541
+ bytes: Uint8Array,
542
+ options?: Partial<BinaryReadOptions>,
543
+ ): UnregisterDeviceTokenRequest {
544
+ return new UnregisterDeviceTokenRequest().fromBinary(bytes, options)
545
+ }
546
+
547
+ static fromJson(
548
+ jsonValue: JsonValue,
549
+ options?: Partial<JsonReadOptions>,
550
+ ): UnregisterDeviceTokenRequest {
551
+ return new UnregisterDeviceTokenRequest().fromJson(jsonValue, options)
552
+ }
553
+
554
+ static fromJsonString(
555
+ jsonString: string,
556
+ options?: Partial<JsonReadOptions>,
557
+ ): UnregisterDeviceTokenRequest {
558
+ return new UnregisterDeviceTokenRequest().fromJsonString(
559
+ jsonString,
560
+ options,
561
+ )
562
+ }
563
+
564
+ static equals(
565
+ a:
566
+ | UnregisterDeviceTokenRequest
567
+ | PlainMessage<UnregisterDeviceTokenRequest>
568
+ | undefined,
569
+ b:
570
+ | UnregisterDeviceTokenRequest
571
+ | PlainMessage<UnregisterDeviceTokenRequest>
572
+ | undefined,
573
+ ): boolean {
574
+ return proto3.util.equals(UnregisterDeviceTokenRequest, a, b)
575
+ }
576
+ }
577
+
578
+ /**
579
+ * @generated from message courier.UnregisterDeviceTokenResponse
580
+ */
581
+ export class UnregisterDeviceTokenResponse extends Message<UnregisterDeviceTokenResponse> {
582
+ constructor(data?: PartialMessage<UnregisterDeviceTokenResponse>) {
583
+ super()
584
+ proto3.util.initPartial(data, this)
585
+ }
586
+
587
+ static readonly runtime: typeof proto3 = proto3
588
+ static readonly typeName = 'courier.UnregisterDeviceTokenResponse'
589
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [])
590
+
591
+ static fromBinary(
592
+ bytes: Uint8Array,
593
+ options?: Partial<BinaryReadOptions>,
594
+ ): UnregisterDeviceTokenResponse {
595
+ return new UnregisterDeviceTokenResponse().fromBinary(bytes, options)
596
+ }
597
+
598
+ static fromJson(
599
+ jsonValue: JsonValue,
600
+ options?: Partial<JsonReadOptions>,
601
+ ): UnregisterDeviceTokenResponse {
602
+ return new UnregisterDeviceTokenResponse().fromJson(jsonValue, options)
603
+ }
604
+
605
+ static fromJsonString(
606
+ jsonString: string,
607
+ options?: Partial<JsonReadOptions>,
608
+ ): UnregisterDeviceTokenResponse {
609
+ return new UnregisterDeviceTokenResponse().fromJsonString(
610
+ jsonString,
611
+ options,
612
+ )
613
+ }
614
+
615
+ static equals(
616
+ a:
617
+ | UnregisterDeviceTokenResponse
618
+ | PlainMessage<UnregisterDeviceTokenResponse>
619
+ | undefined,
620
+ b:
621
+ | UnregisterDeviceTokenResponse
622
+ | PlainMessage<UnregisterDeviceTokenResponse>
623
+ | undefined,
624
+ ): boolean {
625
+ return proto3.util.equals(UnregisterDeviceTokenResponse, a, b)
626
+ }
627
+ }
628
+
629
+ /**
630
+ * @generated from message courier.SetAgeRestrictedRequest
631
+ */
632
+ export class SetAgeRestrictedRequest extends Message<SetAgeRestrictedRequest> {
633
+ /**
634
+ * @generated from field: string did = 1;
635
+ */
636
+ did = ''
637
+
638
+ /**
639
+ * @generated from field: bool age_restricted = 2;
640
+ */
641
+ ageRestricted = false
642
+
643
+ constructor(data?: PartialMessage<SetAgeRestrictedRequest>) {
644
+ super()
645
+ proto3.util.initPartial(data, this)
646
+ }
647
+
648
+ static readonly runtime: typeof proto3 = proto3
649
+ static readonly typeName = 'courier.SetAgeRestrictedRequest'
650
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
651
+ { no: 1, name: 'did', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
652
+ {
653
+ no: 2,
654
+ name: 'age_restricted',
655
+ kind: 'scalar',
656
+ T: 8 /* ScalarType.BOOL */,
657
+ },
658
+ ])
659
+
660
+ static fromBinary(
661
+ bytes: Uint8Array,
662
+ options?: Partial<BinaryReadOptions>,
663
+ ): SetAgeRestrictedRequest {
664
+ return new SetAgeRestrictedRequest().fromBinary(bytes, options)
665
+ }
666
+
667
+ static fromJson(
668
+ jsonValue: JsonValue,
669
+ options?: Partial<JsonReadOptions>,
670
+ ): SetAgeRestrictedRequest {
671
+ return new SetAgeRestrictedRequest().fromJson(jsonValue, options)
672
+ }
673
+
674
+ static fromJsonString(
675
+ jsonString: string,
676
+ options?: Partial<JsonReadOptions>,
677
+ ): SetAgeRestrictedRequest {
678
+ return new SetAgeRestrictedRequest().fromJsonString(jsonString, options)
679
+ }
680
+
681
+ static equals(
682
+ a:
683
+ | SetAgeRestrictedRequest
684
+ | PlainMessage<SetAgeRestrictedRequest>
685
+ | undefined,
686
+ b:
687
+ | SetAgeRestrictedRequest
688
+ | PlainMessage<SetAgeRestrictedRequest>
689
+ | undefined,
690
+ ): boolean {
691
+ return proto3.util.equals(SetAgeRestrictedRequest, a, b)
692
+ }
693
+ }
694
+
695
+ /**
696
+ * @generated from message courier.SetAgeRestrictedResponse
697
+ */
698
+ export class SetAgeRestrictedResponse extends Message<SetAgeRestrictedResponse> {
699
+ constructor(data?: PartialMessage<SetAgeRestrictedResponse>) {
700
+ super()
701
+ proto3.util.initPartial(data, this)
702
+ }
703
+
704
+ static readonly runtime: typeof proto3 = proto3
705
+ static readonly typeName = 'courier.SetAgeRestrictedResponse'
706
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [])
707
+
708
+ static fromBinary(
709
+ bytes: Uint8Array,
710
+ options?: Partial<BinaryReadOptions>,
711
+ ): SetAgeRestrictedResponse {
712
+ return new SetAgeRestrictedResponse().fromBinary(bytes, options)
713
+ }
714
+
715
+ static fromJson(
716
+ jsonValue: JsonValue,
717
+ options?: Partial<JsonReadOptions>,
718
+ ): SetAgeRestrictedResponse {
719
+ return new SetAgeRestrictedResponse().fromJson(jsonValue, options)
720
+ }
721
+
722
+ static fromJsonString(
723
+ jsonString: string,
724
+ options?: Partial<JsonReadOptions>,
725
+ ): SetAgeRestrictedResponse {
726
+ return new SetAgeRestrictedResponse().fromJsonString(jsonString, options)
727
+ }
728
+
729
+ static equals(
730
+ a:
731
+ | SetAgeRestrictedResponse
732
+ | PlainMessage<SetAgeRestrictedResponse>
733
+ | undefined,
734
+ b:
735
+ | SetAgeRestrictedResponse
736
+ | PlainMessage<SetAgeRestrictedResponse>
737
+ | undefined,
738
+ ): boolean {
739
+ return proto3.util.equals(SetAgeRestrictedResponse, a, b)
740
+ }
741
+ }
package/src/stash.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  Preferences,
6
6
  SubjectActivitySubscription,
7
7
  } from './lexicon/types/app/bsky/notification/defs'
8
+ import { AgeAssuranceEvent } from './lexicon/types/app/bsky/unspecced/defs'
8
9
  import { Method } from './proto/bsync_pb'
9
10
 
10
11
  type PickNSID<T extends { $type?: string }> = Exclude<T['$type'], undefined>
@@ -14,6 +15,8 @@ export const Namespaces = {
14
15
  'app.bsky.notification.defs#preferences' satisfies PickNSID<Preferences>,
15
16
  AppBskyNotificationDefsSubjectActivitySubscription:
16
17
  'app.bsky.notification.defs#subjectActivitySubscription' satisfies PickNSID<SubjectActivitySubscription>,
18
+ AppBskyUnspeccedDefsAgeAssuranceEvent:
19
+ 'app.bsky.unspecced.defs#ageAssuranceEvent' satisfies PickNSID<AgeAssuranceEvent>,
17
20
  }
18
21
 
19
22
  export type Namespace = (typeof Namespaces)[keyof typeof Namespaces]
@@ -0,0 +1,425 @@
1
+ import crypto from 'node:crypto'
2
+ import { once } from 'node:events'
3
+ import { Server, createServer } from 'node:http'
4
+ import { AddressInfo } from 'node:net'
5
+ import express, { Application } from 'express'
6
+ import Statsig from 'statsig-node'
7
+ import { AtpAgent } from '@atproto/api'
8
+ import { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'
9
+ import {
10
+ KwsExternalPayload,
11
+ KwsVerificationQuery,
12
+ KwsWebhookBody,
13
+ } from '../../src/api/kws/types'
14
+ import {
15
+ parseExternalPayload,
16
+ serializeExternalPayload,
17
+ } from '../../src/api/kws/util'
18
+ import { GateID } from '../../src/feature-gates'
19
+ import { ids } from '../../src/lexicon/lexicons'
20
+
21
+ type Database = TestNetwork['bsky']['db']
22
+
23
+ describe('age assurance views', () => {
24
+ const verificationSecret = 'verificationSecret'
25
+ const webhookSecret = 'webhookSecret'
26
+ const attemptId = crypto.randomUUID()
27
+ const redirectUrl = 'https://bsky.app/intent/age-assurance'
28
+
29
+ let network: TestNetwork
30
+ let db: Database
31
+ let agent: AtpAgent
32
+ let sc: SeedClient
33
+
34
+ let actorDid: string
35
+
36
+ let kwsServer: MockKwsServer
37
+ const authMock = jest.fn()
38
+ const sendEmailMock = jest.fn()
39
+
40
+ beforeAll(async () => {
41
+ kwsServer = new MockKwsServer({
42
+ verificationSecret,
43
+ webhookSecret,
44
+ authMock,
45
+ sendEmailMock,
46
+ })
47
+ await kwsServer.listen()
48
+
49
+ network = await TestNetwork.create({
50
+ dbPostgresSchema: 'bsky_views_age_assurance',
51
+ bsky: {
52
+ statsigEnv: 'test',
53
+ statsigKey: 'secret-key',
54
+ kws: {
55
+ apiKey: 'apiKey',
56
+ apiOrigin: kwsServer.url,
57
+ authOrigin: kwsServer.url,
58
+ clientId: 'clientId',
59
+ redirectUrl,
60
+ userAgent: 'userAgent',
61
+ verificationSecret,
62
+ webhookSecret,
63
+ },
64
+ },
65
+ })
66
+ Statsig.overrideGate(GateID.AgeAssurance, true)
67
+ db = network.bsky.db
68
+ agent = network.bsky.getClient()
69
+ sc = network.getSeedClient()
70
+ await basicSeed(sc)
71
+ await network.processAll()
72
+
73
+ actorDid = sc.dids.alice
74
+ })
75
+
76
+ beforeEach(async () => {
77
+ // Default mocks for KWS endpoints.
78
+ authMock.mockImplementation(
79
+ (_req: express.Request, res: express.Response) =>
80
+ res.json({
81
+ access_token:
82
+ 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.INVALID',
83
+ expires_in: 3600,
84
+ }),
85
+ )
86
+ sendEmailMock.mockImplementation(
87
+ (_req: express.Request, res: express.Response) => {
88
+ res.json({})
89
+ },
90
+ )
91
+ })
92
+
93
+ afterEach(async () => {
94
+ jest.resetAllMocks()
95
+ await clearPrivateData(db)
96
+ await clearActorAgeAssurance(db)
97
+ })
98
+
99
+ afterAll(async () => {
100
+ await network.close()
101
+ await kwsServer.stop()
102
+ })
103
+
104
+ const getAgeAssurance = async (did: string) => {
105
+ const { data } = await agent.app.bsky.unspecced.getAgeAssuranceState(
106
+ {},
107
+ {
108
+ headers: await network.serviceHeaders(
109
+ did,
110
+ ids.AppBskyUnspeccedGetAgeAssuranceState,
111
+ ),
112
+ },
113
+ )
114
+ return data
115
+ }
116
+
117
+ const initAgeAssurance = async (did: string, email?: string) => {
118
+ const { data } = await agent.app.bsky.unspecced.initAgeAssurance(
119
+ {
120
+ email: email ?? sc.accounts[did].email,
121
+ language: 'en',
122
+ countryCode: 'CC',
123
+ },
124
+ {
125
+ headers: await network.serviceHeaders(
126
+ did,
127
+ ids.AppBskyUnspeccedInitAgeAssurance,
128
+ ),
129
+ },
130
+ )
131
+ return data
132
+ }
133
+
134
+ describe('parsing external payload', () => {
135
+ it('fails if actorDid is missing', () => {
136
+ const serialized = JSON.stringify({
137
+ attemptId,
138
+ } satisfies Partial<KwsExternalPayload>)
139
+
140
+ expect(() => parseExternalPayload(serialized)).toThrow(
141
+ `Invalid external payload`,
142
+ )
143
+ })
144
+
145
+ it('fails if attemptId is missing', () => {
146
+ const serialized = JSON.stringify({
147
+ actorDid,
148
+ } satisfies Partial<KwsExternalPayload>)
149
+
150
+ expect(() => parseExternalPayload(serialized)).toThrow(
151
+ `Invalid external payload`,
152
+ )
153
+ })
154
+
155
+ it('fails if extra field is present', () => {
156
+ const serialized = JSON.stringify({
157
+ actorDid,
158
+ attemptId,
159
+ extra: 'field',
160
+ } satisfies KwsExternalPayload & { extra: string })
161
+
162
+ expect(() => parseExternalPayload(serialized)).toThrow(
163
+ `Invalid external payload`,
164
+ )
165
+ })
166
+
167
+ it('does not fail if all fields are set', () => {
168
+ const externalPayload: KwsExternalPayload = {
169
+ actorDid,
170
+ attemptId,
171
+ }
172
+ const serialized = JSON.stringify(externalPayload)
173
+
174
+ const parsed = parseExternalPayload(serialized)
175
+ expect(parsed).toStrictEqual(externalPayload)
176
+ })
177
+ })
178
+
179
+ it('fetches AA state correctly if user never did the flow', async () => {
180
+ const aliceState = await getAgeAssurance(actorDid)
181
+ expect(aliceState).toEqual({
182
+ status: 'unknown',
183
+ })
184
+ })
185
+
186
+ it('validates email used for AA flow', async () => {
187
+ await expect(initAgeAssurance(actorDid, 'invalid-email')).rejects.toThrow(
188
+ 'This email address is not supported,',
189
+ )
190
+ })
191
+
192
+ describe('verification response flow', () => {
193
+ it('performs the AA flow', async () => {
194
+ const state0 = await getAgeAssurance(actorDid)
195
+ expect(state0).toStrictEqual({
196
+ status: 'unknown',
197
+ })
198
+
199
+ const state1 = await initAgeAssurance(actorDid)
200
+ expect(state1).toStrictEqual({
201
+ status: 'pending',
202
+ lastInitiatedAt: expect.any(String),
203
+ })
204
+ expect(sendEmailMock).toHaveBeenCalledTimes(1)
205
+
206
+ const externalPayload: KwsExternalPayload = {
207
+ actorDid,
208
+ attemptId,
209
+ }
210
+ const status = { verified: true }
211
+ const verificationRes = await kwsServer.callVerificationResponse(
212
+ network.bsky.url,
213
+ { externalPayload, status },
214
+ )
215
+ expect(verificationRes.status).toBe(302)
216
+ expect(verificationRes.headers.get('Location')).toBe(
217
+ `${redirectUrl}?actorDid=${encodeURIComponent(actorDid)}&result=success`,
218
+ )
219
+
220
+ const state2 = await getAgeAssurance(actorDid)
221
+ expect(state2).toStrictEqual({
222
+ status: 'assured',
223
+ lastInitiatedAt: expect.any(String),
224
+ })
225
+ })
226
+
227
+ it('does not assure if the verification response has status not verified', async () => {
228
+ await initAgeAssurance(actorDid)
229
+
230
+ const externalPayload: KwsExternalPayload = {
231
+ actorDid,
232
+ attemptId,
233
+ }
234
+ const status = { verified: false }
235
+ const verificationRes = await kwsServer.callVerificationResponse(
236
+ network.bsky.url,
237
+ { externalPayload, status },
238
+ )
239
+ expect(verificationRes.status).toBe(302)
240
+ expect(verificationRes.headers.get('Location')).toBe(
241
+ `${redirectUrl}?result=unknown`,
242
+ )
243
+
244
+ const state = await getAgeAssurance(actorDid)
245
+ expect(state).toStrictEqual({
246
+ status: 'pending',
247
+ lastInitiatedAt: expect.any(String),
248
+ })
249
+ })
250
+ })
251
+
252
+ describe('webhook flow', () => {
253
+ it('performs the AA flow', async () => {
254
+ const state0 = await getAgeAssurance(actorDid)
255
+ expect(state0).toStrictEqual({
256
+ status: 'unknown',
257
+ })
258
+
259
+ const state1 = await initAgeAssurance(actorDid)
260
+ expect(state1).toStrictEqual({
261
+ status: 'pending',
262
+ lastInitiatedAt: expect.any(String),
263
+ })
264
+ expect(sendEmailMock).toHaveBeenCalledTimes(1)
265
+
266
+ const webhookRes = await kwsServer.callWebhook(network.bsky.url, {
267
+ payload: {
268
+ externalPayload: {
269
+ actorDid,
270
+ attemptId,
271
+ },
272
+ status: {
273
+ verified: true,
274
+ },
275
+ },
276
+ })
277
+ expect(webhookRes.status).toBe(200)
278
+
279
+ const state2 = await getAgeAssurance(actorDid)
280
+ expect(state2).toStrictEqual({
281
+ status: 'assured',
282
+ lastInitiatedAt: expect.any(String),
283
+ })
284
+ })
285
+
286
+ it('does not assure if the webhook has status not verified', async () => {
287
+ await initAgeAssurance(actorDid)
288
+
289
+ const webhookRes = await kwsServer.callWebhook(network.bsky.url, {
290
+ payload: {
291
+ externalPayload: {
292
+ actorDid,
293
+ attemptId,
294
+ },
295
+ status: {
296
+ verified: false,
297
+ },
298
+ },
299
+ })
300
+ expect(webhookRes.status).toBe(500)
301
+
302
+ const state = await getAgeAssurance(actorDid)
303
+ expect(state).toStrictEqual({
304
+ status: 'pending',
305
+ lastInitiatedAt: expect.any(String),
306
+ })
307
+ })
308
+ })
309
+ })
310
+
311
+ const clearPrivateData = async (db: Database) => {
312
+ await db.db.deleteFrom('private_data').execute()
313
+ }
314
+
315
+ const clearActorAgeAssurance = async (db: Database) => {
316
+ await db.db
317
+ .updateTable('actor')
318
+ .set({
319
+ ageAssuranceStatus: null,
320
+ ageAssuranceLastInitiatedAt: null,
321
+ })
322
+ .execute()
323
+ }
324
+
325
+ class MockKwsServer {
326
+ private verificationSecret: string
327
+ private webhookSecret: string
328
+ private app: Application
329
+ private server: Server
330
+
331
+ constructor({
332
+ verificationSecret,
333
+ webhookSecret,
334
+ authMock,
335
+ sendEmailMock,
336
+ }: {
337
+ verificationSecret: string
338
+ webhookSecret: string
339
+ authMock: jest.Mock
340
+ sendEmailMock: jest.Mock
341
+ }) {
342
+ this.verificationSecret = verificationSecret
343
+ this.webhookSecret = webhookSecret
344
+
345
+ this.app = express()
346
+ .post('/auth/realms/kws/protocol/openid-connect/token', (req, res) =>
347
+ authMock(req, res),
348
+ )
349
+ .post('/v1/verifications/send-email', (req, res) =>
350
+ sendEmailMock(req, res),
351
+ )
352
+
353
+ this.server = createServer(this.app)
354
+ }
355
+
356
+ async listen(port?: number) {
357
+ this.server.listen(port)
358
+ await once(this.server, 'listening')
359
+ }
360
+
361
+ async stop() {
362
+ this.server.close()
363
+ await once(this.server, 'close')
364
+ }
365
+
366
+ callVerificationResponse(
367
+ bskyUrl: string,
368
+ query: Omit<KwsVerificationQuery, 'signature'>,
369
+ ) {
370
+ const externalPayloadJson = JSON.stringify(query.externalPayload)
371
+ const statusJson = JSON.stringify(query.status)
372
+
373
+ const sig = crypto
374
+ .createHmac('sha256', this.verificationSecret)
375
+ .update(`${statusJson}:${externalPayloadJson}`)
376
+ .digest('hex')
377
+
378
+ const queryString = new URLSearchParams({
379
+ externalPayload: externalPayloadJson,
380
+ signature: sig,
381
+ status: statusJson,
382
+ }).toString()
383
+
384
+ return fetch(
385
+ `${bskyUrl}/external/kws/age-assurance-verification?${queryString}`,
386
+ {
387
+ method: 'GET',
388
+ redirect: 'manual',
389
+ },
390
+ )
391
+ }
392
+
393
+ callWebhook(bskyUrl: string, body: KwsWebhookBody): Promise<Response> {
394
+ const withSerializedExternalPayload = {
395
+ ...body,
396
+ payload: {
397
+ ...body.payload,
398
+ externalPayload: serializeExternalPayload(body.payload.externalPayload),
399
+ },
400
+ }
401
+ const bodyBuffer = Buffer.from(
402
+ JSON.stringify(withSerializedExternalPayload),
403
+ )
404
+
405
+ const timestamp = new Date().valueOf()
406
+ const sig = crypto
407
+ .createHmac('sha256', this.webhookSecret)
408
+ .update(`${timestamp}.${bodyBuffer}`)
409
+ .digest('hex')
410
+
411
+ return fetch(`${bskyUrl}/external/kws/age-assurance-webhook`, {
412
+ method: 'POST',
413
+ body: bodyBuffer,
414
+ headers: {
415
+ 'x-kws-signature': `t=${timestamp},v1=${sig}`,
416
+ 'Content-Type': 'application/json',
417
+ },
418
+ })
419
+ }
420
+
421
+ get url() {
422
+ const address = this.server.address() as AddressInfo
423
+ return `http://localhost:${address.port}`
424
+ }
425
+ }