@atproto/bsky 0.0.173 → 0.0.174

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 (37) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/api/app/bsky/unspecced/checkHandleAvailability.d.ts +4 -0
  3. package/dist/api/app/bsky/unspecced/checkHandleAvailability.d.ts.map +1 -0
  4. package/dist/api/app/bsky/unspecced/checkHandleAvailability.js +238 -0
  5. package/dist/api/app/bsky/unspecced/checkHandleAvailability.js.map +1 -0
  6. package/dist/api/app/bsky/unspecced/initAgeAssurance.d.ts.map +1 -1
  7. package/dist/api/app/bsky/unspecced/initAgeAssurance.js +20 -0
  8. package/dist/api/app/bsky/unspecced/initAgeAssurance.js.map +1 -1
  9. package/dist/api/index.d.ts.map +1 -1
  10. package/dist/api/index.js +2 -0
  11. package/dist/api/index.js.map +1 -1
  12. package/dist/lexicon/index.d.ts +4 -2
  13. package/dist/lexicon/index.d.ts.map +1 -1
  14. package/dist/lexicon/index.js +8 -4
  15. package/dist/lexicon/index.js.map +1 -1
  16. package/dist/lexicon/lexicons.d.ts +258 -82
  17. package/dist/lexicon/lexicons.d.ts.map +1 -1
  18. package/dist/lexicon/lexicons.js +137 -42
  19. package/dist/lexicon/lexicons.js.map +1 -1
  20. package/dist/lexicon/types/app/bsky/unspecced/checkHandleAvailability.d.ts +58 -0
  21. package/dist/lexicon/types/app/bsky/unspecced/checkHandleAvailability.d.ts.map +1 -0
  22. package/dist/lexicon/types/app/bsky/unspecced/checkHandleAvailability.js +34 -0
  23. package/dist/lexicon/types/app/bsky/unspecced/checkHandleAvailability.js.map +1 -0
  24. package/dist/lexicon/types/app/bsky/unspecced/initAgeAssurance.d.ts +1 -1
  25. package/dist/lexicon/types/app/bsky/unspecced/initAgeAssurance.d.ts.map +1 -1
  26. package/package.json +7 -6
  27. package/src/api/app/bsky/unspecced/checkHandleAvailability.ts +291 -0
  28. package/src/api/app/bsky/unspecced/initAgeAssurance.ts +32 -0
  29. package/src/api/index.ts +2 -0
  30. package/src/lexicon/index.ts +24 -11
  31. package/src/lexicon/lexicons.ts +146 -43
  32. package/src/lexicon/types/app/bsky/unspecced/checkHandleAvailability.ts +99 -0
  33. package/src/lexicon/types/app/bsky/unspecced/initAgeAssurance.ts +1 -1
  34. package/tests/views/age-assurance.test.ts +44 -0
  35. package/tests/views/handle-availability.test.ts +294 -0
  36. package/tsconfig.build.tsbuildinfo +1 -1
  37. package/tsconfig.tests.tsbuildinfo +1 -1
@@ -0,0 +1,58 @@
1
+ /**
2
+ * GENERATED CODE - DO NOT MODIFY
3
+ */
4
+ import { type ValidationResult } from '@atproto/lexicon';
5
+ import { type $Typed } from '../../../../util';
6
+ export type QueryParams = {
7
+ /** Tentative handle. Will be checked for availability or used to build handle suggestions. */
8
+ handle: string;
9
+ /** User-provided email. Might be used to build handle suggestions. */
10
+ email?: string;
11
+ /** User-provided birth date. Might be used to build handle suggestions. */
12
+ birthDate?: string;
13
+ };
14
+ export type InputSchema = undefined;
15
+ export interface OutputSchema {
16
+ /** Echo of the input handle. */
17
+ handle: string;
18
+ result: $Typed<ResultAvailable> | $Typed<ResultUnavailable> | {
19
+ $type: string;
20
+ };
21
+ }
22
+ export type HandlerInput = void;
23
+ export interface HandlerSuccess {
24
+ encoding: 'application/json';
25
+ body: OutputSchema;
26
+ headers?: {
27
+ [key: string]: string;
28
+ };
29
+ }
30
+ export interface HandlerError {
31
+ status: number;
32
+ message?: string;
33
+ error?: 'InvalidEmail';
34
+ }
35
+ export type HandlerOutput = HandlerError | HandlerSuccess;
36
+ /** Indicates the provided handle is available. */
37
+ export interface ResultAvailable {
38
+ $type?: 'app.bsky.unspecced.checkHandleAvailability#resultAvailable';
39
+ }
40
+ export declare function isResultAvailable<V>(v: V): v is import("../../../../util").$TypedObject<V, "app.bsky.unspecced.checkHandleAvailability", "resultAvailable">;
41
+ export declare function validateResultAvailable<V>(v: V): ValidationResult<ResultAvailable & V>;
42
+ /** Indicates the provided handle is unavailable and gives suggestions of available handles. */
43
+ export interface ResultUnavailable {
44
+ $type?: 'app.bsky.unspecced.checkHandleAvailability#resultUnavailable';
45
+ /** List of suggested handles based on the provided inputs. */
46
+ suggestions: Suggestion[];
47
+ }
48
+ export declare function isResultUnavailable<V>(v: V): v is import("../../../../util").$TypedObject<V, "app.bsky.unspecced.checkHandleAvailability", "resultUnavailable">;
49
+ export declare function validateResultUnavailable<V>(v: V): ValidationResult<ResultUnavailable & V>;
50
+ export interface Suggestion {
51
+ $type?: 'app.bsky.unspecced.checkHandleAvailability#suggestion';
52
+ handle: string;
53
+ /** Method used to build this suggestion. Should be considered opaque to clients. Can be used for metrics. */
54
+ method: string;
55
+ }
56
+ export declare function isSuggestion<V>(v: V): v is import("../../../../util").$TypedObject<V, "app.bsky.unspecced.checkHandleAvailability", "suggestion">;
57
+ export declare function validateSuggestion<V>(v: V): ValidationResult<Suggestion & V>;
58
+ //# sourceMappingURL=checkHandleAvailability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkHandleAvailability.d.ts","sourceRoot":"","sources":["../../../../../../src/lexicon/types/app/bsky/unspecced/checkHandleAvailability.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,KAAK,gBAAgB,EAAW,MAAM,kBAAkB,CAAA;AAGjE,OAAO,EACL,KAAK,MAAM,EAGZ,MAAM,kBAAkB,CAAA;AAMzB,MAAM,MAAM,WAAW,GAAG;IACxB,8FAA8F;IAC9F,MAAM,EAAE,MAAM,CAAA;IACd,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AACD,MAAM,MAAM,WAAW,GAAG,SAAS,CAAA;AAEnC,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EACF,MAAM,CAAC,eAAe,CAAC,GACvB,MAAM,CAAC,iBAAiB,CAAC,GACzB;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CACtB;AAED,MAAM,MAAM,YAAY,GAAG,IAAI,CAAA;AAE/B,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,IAAI,EAAE,YAAY,CAAA;IAClB,OAAO,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAA;CACpC;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,cAAc,CAAA;CACvB;AAED,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,cAAc,CAAA;AAEzD,kDAAkD;AAClD,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,4DAA4D,CAAA;CACrE;AAID,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,oHAExC;AAED,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,yCAE9C;AAED,+FAA+F;AAC/F,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,8DAA8D,CAAA;IACtE,8DAA8D;IAC9D,WAAW,EAAE,UAAU,EAAE,CAAA;CAC1B;AAID,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,sHAE1C;AAED,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,2CAEhD;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,uDAAuD,CAAA;IAC/D,MAAM,EAAE,MAAM,CAAA;IACd,6GAA6G;IAC7G,MAAM,EAAE,MAAM,CAAA;CACf;AAID,wBAAgB,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,+GAEnC;AAED,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,oCAEzC"}
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isResultAvailable = isResultAvailable;
4
+ exports.validateResultAvailable = validateResultAvailable;
5
+ exports.isResultUnavailable = isResultUnavailable;
6
+ exports.validateResultUnavailable = validateResultUnavailable;
7
+ exports.isSuggestion = isSuggestion;
8
+ exports.validateSuggestion = validateSuggestion;
9
+ const lexicons_1 = require("../../../../lexicons");
10
+ const util_1 = require("../../../../util");
11
+ const is$typed = util_1.is$typed, validate = lexicons_1.validate;
12
+ const id = 'app.bsky.unspecced.checkHandleAvailability';
13
+ const hashResultAvailable = 'resultAvailable';
14
+ function isResultAvailable(v) {
15
+ return is$typed(v, id, hashResultAvailable);
16
+ }
17
+ function validateResultAvailable(v) {
18
+ return validate(v, id, hashResultAvailable);
19
+ }
20
+ const hashResultUnavailable = 'resultUnavailable';
21
+ function isResultUnavailable(v) {
22
+ return is$typed(v, id, hashResultUnavailable);
23
+ }
24
+ function validateResultUnavailable(v) {
25
+ return validate(v, id, hashResultUnavailable);
26
+ }
27
+ const hashSuggestion = 'suggestion';
28
+ function isSuggestion(v) {
29
+ return is$typed(v, id, hashSuggestion);
30
+ }
31
+ function validateSuggestion(v) {
32
+ return validate(v, id, hashSuggestion);
33
+ }
34
+ //# sourceMappingURL=checkHandleAvailability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkHandleAvailability.js","sourceRoot":"","sources":["../../../../../../src/lexicon/types/app/bsky/unspecced/checkHandleAvailability.ts"],"names":[],"mappings":";;AA0DA,8CAEC;AAED,0DAEC;AAWD,kDAEC;AAED,8DAEC;AAWD,oCAEC;AAED,gDAEC;AA7FD,mDAA4D;AAC5D,2CAIyB;AAEzB,MAAM,QAAQ,GAAG,eAAS,EACxB,QAAQ,GAAG,mBAAS,CAAA;AACtB,MAAM,EAAE,GAAG,4CAA4C,CAAA;AA0CvD,MAAM,mBAAmB,GAAG,iBAAiB,CAAA;AAE7C,SAAgB,iBAAiB,CAAI,CAAI;IACvC,OAAO,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,mBAAmB,CAAC,CAAA;AAC7C,CAAC;AAED,SAAgB,uBAAuB,CAAI,CAAI;IAC7C,OAAO,QAAQ,CAAsB,CAAC,EAAE,EAAE,EAAE,mBAAmB,CAAC,CAAA;AAClE,CAAC;AASD,MAAM,qBAAqB,GAAG,mBAAmB,CAAA;AAEjD,SAAgB,mBAAmB,CAAI,CAAI;IACzC,OAAO,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,qBAAqB,CAAC,CAAA;AAC/C,CAAC;AAED,SAAgB,yBAAyB,CAAI,CAAI;IAC/C,OAAO,QAAQ,CAAwB,CAAC,EAAE,EAAE,EAAE,qBAAqB,CAAC,CAAA;AACtE,CAAC;AASD,MAAM,cAAc,GAAG,YAAY,CAAA;AAEnC,SAAgB,YAAY,CAAI,CAAI;IAClC,OAAO,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,cAAc,CAAC,CAAA;AACxC,CAAC;AAED,SAAgB,kBAAkB,CAAI,CAAI;IACxC,OAAO,QAAQ,CAAiB,CAAC,EAAE,EAAE,EAAE,cAAc,CAAC,CAAA;AACxD,CAAC"}
@@ -23,7 +23,7 @@ export interface HandlerSuccess {
23
23
  export interface HandlerError {
24
24
  status: number;
25
25
  message?: string;
26
- error?: 'InvalidEmail' | 'DidTooLong';
26
+ error?: 'InvalidEmail' | 'DidTooLong' | 'InvalidInitiation';
27
27
  }
28
28
  export type HandlerOutput = HandlerError | HandlerSuccess;
29
29
  //# sourceMappingURL=initAgeAssurance.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"initAgeAssurance.d.ts","sourceRoot":"","sources":["../../../../../../src/lexicon/types/app/bsky/unspecced/initAgeAssurance.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,KAAK,oBAAoB,MAAM,WAAW,CAAA;AAMtD,MAAM,MAAM,WAAW,GAAG,EAAE,CAAA;AAE5B,MAAM,WAAW,WAAW;IAC1B,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAA;IAChB,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,MAAM,YAAY,GAAG,oBAAoB,CAAC,iBAAiB,CAAA;AAEjE,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,IAAI,EAAE,WAAW,CAAA;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,IAAI,EAAE,YAAY,CAAA;IAClB,OAAO,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAA;CACpC;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,cAAc,GAAG,YAAY,CAAA;CACtC;AAED,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,cAAc,CAAA"}
1
+ {"version":3,"file":"initAgeAssurance.d.ts","sourceRoot":"","sources":["../../../../../../src/lexicon/types/app/bsky/unspecced/initAgeAssurance.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,KAAK,oBAAoB,MAAM,WAAW,CAAA;AAMtD,MAAM,MAAM,WAAW,GAAG,EAAE,CAAA;AAE5B,MAAM,WAAW,WAAW;IAC1B,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAA;IAChB,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,MAAM,YAAY,GAAG,oBAAoB,CAAC,iBAAiB,CAAA;AAEjE,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,IAAI,EAAE,WAAW,CAAA;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,IAAI,EAAE,YAAY,CAAA;IAClB,OAAO,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAA;CACpC;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,cAAc,GAAG,YAAY,GAAG,mBAAmB,CAAA;CAC5D;AAED,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,cAAc,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/bsky",
3
- "version": "0.0.173",
3
+ "version": "0.0.174",
4
4
  "license": "MIT",
5
5
  "description": "Reference implementation of app.bsky App View (Bluesky API)",
6
6
  "keywords": [
@@ -37,6 +37,7 @@
37
37
  "jose": "^5.0.1",
38
38
  "key-encoder": "^2.0.3",
39
39
  "kysely": "^0.22.0",
40
+ "leo-profanity": "^1.8.0",
40
41
  "multiformats": "^9.9.0",
41
42
  "murmurhash": "^2.0.1",
42
43
  "p-queue": "^6.6.2",
@@ -53,15 +54,15 @@
53
54
  "@atproto-labs/fetch-node": "0.1.9",
54
55
  "@atproto-labs/xrpc-utils": "0.0.17",
55
56
  "@atproto/common": "^0.4.11",
56
- "@atproto/api": "^0.15.26",
57
57
  "@atproto/crypto": "^0.4.4",
58
58
  "@atproto/did": "^0.1.5",
59
59
  "@atproto/identity": "^0.4.8",
60
60
  "@atproto/lexicon": "^0.4.12",
61
- "@atproto/repo": "^0.8.5",
62
61
  "@atproto/sync": "^0.1.29",
63
62
  "@atproto/syntax": "^0.4.0",
64
- "@atproto/xrpc-server": "^0.9.0"
63
+ "@atproto/repo": "^0.8.5",
64
+ "@atproto/xrpc-server": "^0.9.0",
65
+ "@atproto/api": "^0.15.27"
65
66
  },
66
67
  "devDependencies": {
67
68
  "@bufbuild/buf": "^1.28.1",
@@ -76,9 +77,9 @@
76
77
  "jest": "^28.1.2",
77
78
  "ts-node": "^10.8.2",
78
79
  "typescript": "^5.6.3",
79
- "@atproto/api": "^0.15.26",
80
+ "@atproto/api": "^0.15.27",
80
81
  "@atproto/lex-cli": "^0.9.0",
81
- "@atproto/pds": "^0.4.161",
82
+ "@atproto/pds": "^0.4.162",
82
83
  "@atproto/xrpc": "^0.7.1"
83
84
  },
84
85
  "scripts": {
@@ -0,0 +1,291 @@
1
+ import { isEmailValid } from '@hapi/address'
2
+ import filter from 'leo-profanity'
3
+ import * as ident from '@atproto/syntax'
4
+ import { InvalidRequestError } from '@atproto/xrpc-server'
5
+ import { AppContext } from '../../../../context'
6
+ import { Server } from '../../../../lexicon'
7
+ import {
8
+ QueryParams,
9
+ Suggestion,
10
+ } from '../../../../lexicon/types/app/bsky/unspecced/checkHandleAvailability'
11
+
12
+ // THIS IS A TEMPORARY UNSPECCED ROUTE
13
+ export default function (server: Server, ctx: AppContext) {
14
+ server.app.bsky.unspecced.checkHandleAvailability({
15
+ handler: async ({ params }) => {
16
+ const { birthDate, email, handle } = validateParams(params)
17
+
18
+ if (isSlur(handle)) {
19
+ return {
20
+ encoding: 'application/json',
21
+ body: {
22
+ handle,
23
+ result: {
24
+ $type:
25
+ 'app.bsky.unspecced.checkHandleAvailability#resultUnavailable',
26
+ suggestions: [],
27
+ },
28
+ },
29
+ }
30
+ }
31
+
32
+ const [did] = await ctx.hydrator.actor.getDids([handle], {
33
+ lookupUnidirectional: true,
34
+ })
35
+ if (!did) {
36
+ return {
37
+ encoding: 'application/json',
38
+ body: {
39
+ handle,
40
+ result: {
41
+ $type:
42
+ 'app.bsky.unspecced.checkHandleAvailability#resultAvailable',
43
+ },
44
+ },
45
+ }
46
+ }
47
+
48
+ const suggestions = await getSuggestions(ctx, handle, email, birthDate)
49
+
50
+ return {
51
+ encoding: 'application/json',
52
+ body: {
53
+ handle,
54
+ result: {
55
+ $type:
56
+ 'app.bsky.unspecced.checkHandleAvailability#resultUnavailable',
57
+ suggestions,
58
+ },
59
+ },
60
+ }
61
+ },
62
+ })
63
+ }
64
+
65
+ const validateParams = (params: QueryParams) => {
66
+ const { email } = params
67
+ if (email && !isEmailValid(email)) {
68
+ throw new InvalidRequestError('Invalid email address.', 'InvalidEmail')
69
+ }
70
+
71
+ return {
72
+ birthDate: params.birthDate,
73
+ email,
74
+ handle: ident.normalizeHandle(params.handle),
75
+ }
76
+ }
77
+
78
+ const uniqueSuggestions = (suggestions: Suggestion[]): Suggestion[] =>
79
+ suggestions.filter((s0, i, ss) => {
80
+ return ss.findIndex((s1) => s0.handle === s1.handle) === i
81
+ })
82
+
83
+ /** Gets the target number of suggestions, ensuring uniqueness and availability. */
84
+ const getSuggestions = async (
85
+ ctx: AppContext,
86
+ tentativeHandle: string,
87
+ email: string | undefined,
88
+ birthDate: string | undefined,
89
+ ): Promise<Suggestion[]> => {
90
+ const [subdomain, ...rest] = tentativeHandle.split('.')
91
+ const domain = rest.join('.')
92
+
93
+ let suggestions: Suggestion[] = []
94
+
95
+ const want = 5
96
+ let attempt = 0
97
+
98
+ const deterministic = await availableSuggestions(
99
+ ctx,
100
+ deterministicSuggestions(subdomain, email, birthDate),
101
+ tentativeHandle,
102
+ domain,
103
+ )
104
+ suggestions.push(...deterministic)
105
+
106
+ while (suggestions.length < want && attempt < 3) {
107
+ const random = await availableSuggestions(
108
+ ctx,
109
+ randomSuggestions(subdomain),
110
+ tentativeHandle,
111
+ domain,
112
+ )
113
+ suggestions.push(...random)
114
+
115
+ suggestions = uniqueSuggestions([...suggestions, ...random])
116
+ attempt++
117
+ }
118
+
119
+ return suggestions.slice(0, want)
120
+ }
121
+
122
+ type IntermediateSuggestion = {
123
+ subdomain: string
124
+ method: string
125
+ }
126
+
127
+ const availableSuggestions = async (
128
+ ctx: AppContext,
129
+ suggestions: IntermediateSuggestion[],
130
+ tentativeHandle: string,
131
+ domain: string,
132
+ ): Promise<Suggestion[]> => {
133
+ const join = (subdomain: string, domain: string) => `${subdomain}.${domain}`
134
+
135
+ const validSuggestions = suggestions
136
+ .filter((s) => {
137
+ // @TODO: from a magic number in the PDS code.
138
+ if (s.subdomain.length < 3) return false
139
+
140
+ // @TODO: from a magic number in the PDS code.
141
+ if (s.subdomain.length > 18) return false
142
+
143
+ const handle = join(s.subdomain, domain)
144
+ // @TODO: from a magic number in the entryway code.
145
+ if (handle.length > 30) return false
146
+
147
+ // Only valid, and not the tentative one.
148
+ return ident.isValidHandle(handle) && handle !== tentativeHandle
149
+ })
150
+ .map(
151
+ (s): Suggestion => ({
152
+ handle: join(s.subdomain, domain),
153
+ method: s.method,
154
+ }),
155
+ )
156
+
157
+ const dids = await ctx.hydrator.actor.getDids(
158
+ validSuggestions.map((s) => s.handle),
159
+ {
160
+ lookupUnidirectional: true,
161
+ },
162
+ )
163
+ return validSuggestions.filter((_, i) => !dids[i])
164
+ }
165
+
166
+ const deterministicSuggestions = (
167
+ subdomain: string,
168
+ email: string | undefined,
169
+ birthDate: string | undefined,
170
+ ): IntermediateSuggestion[] => {
171
+ const localPart = email
172
+ ?.split('@')[0]
173
+ .toLowerCase()
174
+ .replace('.', '-')
175
+ .replace(/[^a-zA-Z0-9-]/g, '')
176
+ const year = getYear(birthDate)
177
+
178
+ return [
179
+ ...suggestAppendDigits('handle_yob', subdomain, year),
180
+ ...suggestValue('email', localPart),
181
+ ...suggestAppendDigits('email_yob', localPart, year),
182
+ ]
183
+ }
184
+
185
+ const randomSuggestions = (subdomain: string): IntermediateSuggestion[] => [
186
+ ...suggestHyphens('hyphen', subdomain),
187
+ ...suggestAppendRandomDigits('random_digits', subdomain),
188
+ ]
189
+
190
+ const avoidDigits = ['69']
191
+
192
+ const getYear = (d: string | undefined): string | undefined => {
193
+ if (!d) return undefined
194
+
195
+ const date = new Date(d)
196
+ if (isNaN(date.getTime())) return undefined
197
+
198
+ const year = date.getFullYear().toString().slice(-2)
199
+ return avoidDigits.includes(year) ? undefined : year
200
+ }
201
+
202
+ const suggestValue = (
203
+ method: string,
204
+ s: string | undefined,
205
+ ): IntermediateSuggestion[] => {
206
+ if (!s) return []
207
+ return [{ subdomain: `${s}`, method }]
208
+ }
209
+
210
+ const suggestAppendDigits = (
211
+ method: string,
212
+ s: string | undefined,
213
+ d: string | undefined,
214
+ ): IntermediateSuggestion[] => {
215
+ if (!s || !d) return []
216
+
217
+ // If s already ends in digits, add an hyphen before appending the number.
218
+ const separator = /\d$/.test(s) ? '-' : ''
219
+ return [{ subdomain: `${s}${separator}${d}`, method }]
220
+ }
221
+
222
+ const suggestAppendRandomDigits = (
223
+ method: string,
224
+ s: string,
225
+ ): IntermediateSuggestion[] => {
226
+ const ss: IntermediateSuggestion[] = []
227
+ const want = 2
228
+ let got = 0
229
+ while (got < want) {
230
+ const randomDigits = Math.floor(Math.random() * 100).toString()
231
+ if (avoidDigits.includes(randomDigits)) continue
232
+ ss.push(...suggestAppendDigits(method, s, randomDigits))
233
+ got++
234
+ }
235
+ return ss
236
+ }
237
+
238
+ const suggestHyphens = (
239
+ method: string,
240
+ s: string,
241
+ ): IntermediateSuggestion[] => {
242
+ const ss: IntermediateSuggestion[] = []
243
+ // 2 suggestions or less, if the string is too short.
244
+ const want = Math.min(Math.floor(s.length / 2), 2)
245
+ let got = 0
246
+
247
+ while (got < want) {
248
+ // Exclude first and last character to avoid leading/trailing hyphens.
249
+ for (let i = 1; i < s.length && got < want; i++) {
250
+ // Randomly skip some combinations.
251
+ if (Math.random() > 0.5) continue
252
+
253
+ const left = s.slice(0, i)
254
+ const right = s.slice(i)
255
+ if (isSlur(left) || isSlur(right)) {
256
+ // Skip but count to avoid infinite loop.
257
+ got++
258
+ continue
259
+ }
260
+ ss.push({ subdomain: `${left}-${right}`, method })
261
+ got++
262
+ }
263
+ }
264
+
265
+ return ss
266
+ }
267
+
268
+ // regexes taken from: https://github.com/Blank-Cheque/Slurs
269
+ /* eslint-disable no-misleading-character-class */
270
+ const explicitSlurRegexes = [
271
+ /\b[cĆćĈĉČčĊċÇçḈḉȻȼꞒꞓꟄꞔƇƈɕ][hĤĥȞȟḦḧḢḣḨḩḤḥḪḫH̱ẖĦħⱧⱨꞪɦꞕΗНн][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][kḰḱǨǩĶķḲḳḴḵƘƙⱩⱪᶄꝀꝁꝂꝃꝄꝅꞢꞣ][sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/,
272
+ /\b[cĆćĈĉČčĊċÇçḈḉȻȼꞒꞓꟄꞔƇƈɕ][ÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOo0]{2}[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/,
273
+ /\b[fḞḟƑƒꞘꞙᵮᶂ][aÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa@4][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶGgqꝖꝗꝘꝙɋʠ]{1,2}([ÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOo0e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEeiÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][tŤťṪṫŢţṬṭȚțṰṱṮṯŦŧȾⱦƬƭƮʈT̈ẗᵵƫȶ]{1,2}([rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][yÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ]|[rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe])?)?[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/,
274
+ /\b[kḰḱǨǩĶķḲḳḴḵƘƙⱩⱪᶄꝀꝁꝂꝃꝄꝅꞢꞣ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLlyÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ][kḰḱǨǩĶķḲḳḴḵƘƙⱩⱪᶄꝀꝁꝂꝃꝄꝅꞢꞣ][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe]([rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][yÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ]|[rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe])?[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]*\b/,
275
+ /\b[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLloÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOoІіa4ÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶGgqꝖꝗꝘꝙɋʠ]{2}(l[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe]t|[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEeaÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ]?|n[ÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOo0][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶGgqꝖꝗꝘꝙɋʠ]|[a4ÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa]?)?[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/,
276
+ /[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLloÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOoІіa4ÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶGgqꝖꝗꝘꝙɋʠ]{2}(l[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe]t|[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ])[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?/,
277
+ /\b[tŤťṪṫŢţṬṭȚțṰṱṮṯŦŧȾⱦƬƭƮʈT̈ẗᵵƫȶ][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][aÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa4]+[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn]{1,2}([iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe]|[yÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ]|[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ])[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/,
278
+ ]
279
+
280
+ const isSlur = (handle: string): boolean => {
281
+ return (
282
+ filter.check(handle) ||
283
+ explicitSlurRegexes.some(
284
+ (reg) =>
285
+ reg.test(handle) ||
286
+ reg.test(
287
+ handle.replaceAll('.', '').replaceAll('-', '').replaceAll('_', ''),
288
+ ),
289
+ )
290
+ )
291
+ }
@@ -12,6 +12,7 @@ import { KwsExternalPayloadError } from '../../../../kws'
12
12
  import { Server } from '../../../../lexicon'
13
13
  import { InputSchema } from '../../../../lexicon/types/app/bsky/unspecced/initAgeAssurance'
14
14
  import { httpLogger as log } from '../../../../logger'
15
+ import { ActorInfo } from '../../../../proto/bsky_pb'
15
16
  import { KwsExternalPayload } from '../../../kws/types'
16
17
  import { createStashEvent, getClientUa } from '../../../kws/util'
17
18
 
@@ -33,6 +34,20 @@ export default function (server: Server, ctx: AppContext) {
33
34
  throw new ForbiddenError()
34
35
  }
35
36
 
37
+ const actorInfo = await getAgeVerificationState(ctx, actorDid)
38
+
39
+ if (actorInfo?.ageAssuranceStatus) {
40
+ if (
41
+ actorInfo.ageAssuranceStatus.status !== 'unknown' &&
42
+ actorInfo.ageAssuranceStatus.status !== 'pending'
43
+ ) {
44
+ throw new InvalidRequestError(
45
+ `Cannot initiate age assurance flow from current state: ${actorInfo.ageAssuranceStatus.status}`,
46
+ 'InvalidInitiation',
47
+ )
48
+ }
49
+ }
50
+
36
51
  const { countryCode, email, language } = validateInput(input.body)
37
52
 
38
53
  const attemptId = crypto.randomUUID()
@@ -122,3 +137,20 @@ const validateInput = (input: InputSchema): InputSchema => {
122
137
  language: kwsAvSupportedLanguages.includes(language) ? language : 'en',
123
138
  }
124
139
  }
140
+
141
+ const getAgeVerificationState = async (
142
+ ctx: AppContext,
143
+ actorDid: string,
144
+ ): Promise<ActorInfo | undefined> => {
145
+ try {
146
+ const res = await ctx.dataplane.getActors({
147
+ dids: [actorDid],
148
+ returnAgeAssuranceForDids: [actorDid],
149
+ skipCacheForDids: [actorDid],
150
+ })
151
+
152
+ return res.actors[0]
153
+ } catch (err) {
154
+ return undefined
155
+ }
156
+ }
package/src/api/index.ts CHANGED
@@ -51,6 +51,7 @@ import putPreferences from './app/bsky/notification/putPreferences'
51
51
  import putPreferencesV2 from './app/bsky/notification/putPreferencesV2'
52
52
  import registerPush from './app/bsky/notification/registerPush'
53
53
  import updateSeen from './app/bsky/notification/updateSeen'
54
+ import checkHandleAvailability from './app/bsky/unspecced/checkHandleAvailability'
54
55
  import getAgeAssuranceState from './app/bsky/unspecced/getAgeAssuranceState'
55
56
  import getConfig from './app/bsky/unspecced/getConfig'
56
57
  import getPopularFeedGenerators from './app/bsky/unspecced/getPopularFeedGenerators'
@@ -142,6 +143,7 @@ export default function (server: Server, ctx: AppContext) {
142
143
  getConfig(server, ctx)
143
144
  getPopularFeedGenerators(server, ctx)
144
145
  getTaggedSuggestions(server, ctx)
146
+ checkHandleAvailability(server, ctx)
145
147
  getAgeAssuranceState(server, ctx)
146
148
  initAgeAssurance(server, ctx)
147
149
  // com.atproto
@@ -109,8 +109,8 @@ import * as AppBskyFeedGetFeedGenerators from './types/app/bsky/feed/getFeedGene
109
109
  import * as AppBskyFeedGetFeedSkeleton from './types/app/bsky/feed/getFeedSkeleton.js'
110
110
  import * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes.js'
111
111
  import * as AppBskyFeedGetListFeed from './types/app/bsky/feed/getListFeed.js'
112
- import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread.js'
113
112
  import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts.js'
113
+ import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread.js'
114
114
  import * as AppBskyFeedGetQuotes from './types/app/bsky/feed/getQuotes.js'
115
115
  import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy.js'
116
116
  import * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds.js'
@@ -149,6 +149,7 @@ import * as AppBskyNotificationPutPreferencesV2 from './types/app/bsky/notificat
149
149
  import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush.js'
150
150
  import * as AppBskyNotificationUnregisterPush from './types/app/bsky/notification/unregisterPush.js'
151
151
  import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen.js'
152
+ import * as AppBskyUnspeccedCheckHandleAvailability from './types/app/bsky/unspecced/checkHandleAvailability.js'
152
153
  import * as AppBskyUnspeccedGetAgeAssuranceState from './types/app/bsky/unspecced/getAgeAssuranceState.js'
153
154
  import * as AppBskyUnspeccedGetConfig from './types/app/bsky/unspecced/getConfig.js'
154
155
  import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators.js'
@@ -1600,27 +1601,27 @@ export class AppBskyFeedNS {
1600
1601
  return this._server.xrpc.method(nsid, cfg)
1601
1602
  }
1602
1603
 
1603
- getPostThread<A extends Auth = void>(
1604
+ getPosts<A extends Auth = void>(
1604
1605
  cfg: MethodConfigOrHandler<
1605
1606
  A,
1606
- AppBskyFeedGetPostThread.QueryParams,
1607
- AppBskyFeedGetPostThread.HandlerInput,
1608
- AppBskyFeedGetPostThread.HandlerOutput
1607
+ AppBskyFeedGetPosts.QueryParams,
1608
+ AppBskyFeedGetPosts.HandlerInput,
1609
+ AppBskyFeedGetPosts.HandlerOutput
1609
1610
  >,
1610
1611
  ) {
1611
- const nsid = 'app.bsky.feed.getPostThread' // @ts-ignore
1612
+ const nsid = 'app.bsky.feed.getPosts' // @ts-ignore
1612
1613
  return this._server.xrpc.method(nsid, cfg)
1613
1614
  }
1614
1615
 
1615
- getPosts<A extends Auth = void>(
1616
+ getPostThread<A extends Auth = void>(
1616
1617
  cfg: MethodConfigOrHandler<
1617
1618
  A,
1618
- AppBskyFeedGetPosts.QueryParams,
1619
- AppBskyFeedGetPosts.HandlerInput,
1620
- AppBskyFeedGetPosts.HandlerOutput
1619
+ AppBskyFeedGetPostThread.QueryParams,
1620
+ AppBskyFeedGetPostThread.HandlerInput,
1621
+ AppBskyFeedGetPostThread.HandlerOutput
1621
1622
  >,
1622
1623
  ) {
1623
- const nsid = 'app.bsky.feed.getPosts' // @ts-ignore
1624
+ const nsid = 'app.bsky.feed.getPostThread' // @ts-ignore
1624
1625
  return this._server.xrpc.method(nsid, cfg)
1625
1626
  }
1626
1627
 
@@ -2120,6 +2121,18 @@ export class AppBskyUnspeccedNS {
2120
2121
  this._server = server
2121
2122
  }
2122
2123
 
2124
+ checkHandleAvailability<A extends Auth = void>(
2125
+ cfg: MethodConfigOrHandler<
2126
+ A,
2127
+ AppBskyUnspeccedCheckHandleAvailability.QueryParams,
2128
+ AppBskyUnspeccedCheckHandleAvailability.HandlerInput,
2129
+ AppBskyUnspeccedCheckHandleAvailability.HandlerOutput
2130
+ >,
2131
+ ) {
2132
+ const nsid = 'app.bsky.unspecced.checkHandleAvailability' // @ts-ignore
2133
+ return this._server.xrpc.method(nsid, cfg)
2134
+ }
2135
+
2123
2136
  getAgeAssuranceState<A extends Auth = void>(
2124
2137
  cfg: MethodConfigOrHandler<
2125
2138
  A,