@atproto/bsky 0.0.172 → 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.
- package/CHANGELOG.md +15 -0
- package/dist/api/app/bsky/unspecced/checkHandleAvailability.d.ts +4 -0
- package/dist/api/app/bsky/unspecced/checkHandleAvailability.d.ts.map +1 -0
- package/dist/api/app/bsky/unspecced/checkHandleAvailability.js +238 -0
- package/dist/api/app/bsky/unspecced/checkHandleAvailability.js.map +1 -0
- package/dist/api/app/bsky/unspecced/initAgeAssurance.d.ts.map +1 -1
- package/dist/api/app/bsky/unspecced/initAgeAssurance.js +71 -9
- package/dist/api/app/bsky/unspecced/initAgeAssurance.js.map +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +2 -0
- package/dist/api/index.js.map +1 -1
- package/dist/kws.d.ts +2 -0
- package/dist/kws.d.ts.map +1 -1
- package/dist/kws.js +10 -2
- package/dist/kws.js.map +1 -1
- package/dist/lexicon/index.d.ts +4 -2
- package/dist/lexicon/index.d.ts.map +1 -1
- package/dist/lexicon/index.js +8 -4
- package/dist/lexicon/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +268 -82
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +145 -42
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/unspecced/checkHandleAvailability.d.ts +58 -0
- package/dist/lexicon/types/app/bsky/unspecced/checkHandleAvailability.d.ts.map +1 -0
- package/dist/lexicon/types/app/bsky/unspecced/checkHandleAvailability.js +34 -0
- package/dist/lexicon/types/app/bsky/unspecced/checkHandleAvailability.js.map +1 -0
- package/dist/lexicon/types/app/bsky/unspecced/initAgeAssurance.d.ts +1 -0
- package/dist/lexicon/types/app/bsky/unspecced/initAgeAssurance.d.ts.map +1 -1
- package/package.json +7 -6
- package/src/api/app/bsky/unspecced/checkHandleAvailability.ts +291 -0
- package/src/api/app/bsky/unspecced/initAgeAssurance.ts +96 -11
- package/src/api/index.ts +2 -0
- package/src/kws.ts +9 -1
- package/src/lexicon/index.ts +24 -11
- package/src/lexicon/lexicons.ts +154 -43
- package/src/lexicon/types/app/bsky/unspecced/checkHandleAvailability.ts +99 -0
- package/src/lexicon/types/app/bsky/unspecced/initAgeAssurance.ts +1 -0
- package/tests/views/age-assurance.test.ts +44 -0
- package/tests/views/handle-availability.test.ts +294 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- 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,6 +23,7 @@ export interface HandlerSuccess {
|
|
|
23
23
|
export interface HandlerError {
|
|
24
24
|
status: number;
|
|
25
25
|
message?: string;
|
|
26
|
+
error?: 'InvalidEmail' | 'DidTooLong' | 'InvalidInitiation';
|
|
26
27
|
}
|
|
27
28
|
export type HandlerOutput = HandlerError | HandlerSuccess;
|
|
28
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;
|
|
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.
|
|
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",
|
|
@@ -52,16 +53,16 @@
|
|
|
52
53
|
"zod": "3.23.8",
|
|
53
54
|
"@atproto-labs/fetch-node": "0.1.9",
|
|
54
55
|
"@atproto-labs/xrpc-utils": "0.0.17",
|
|
55
|
-
"@atproto/api": "^0.15.26",
|
|
56
56
|
"@atproto/common": "^0.4.11",
|
|
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/
|
|
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.
|
|
80
|
+
"@atproto/api": "^0.15.27",
|
|
80
81
|
"@atproto/lex-cli": "^0.9.0",
|
|
81
|
-
"@atproto/pds": "^0.4.
|
|
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
|
+
}
|
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
} from '@atproto/xrpc-server'
|
|
9
9
|
import { AppContext } from '../../../../context'
|
|
10
10
|
import { GateID } from '../../../../feature-gates'
|
|
11
|
+
import { KwsExternalPayloadError } from '../../../../kws'
|
|
11
12
|
import { Server } from '../../../../lexicon'
|
|
13
|
+
import { InputSchema } from '../../../../lexicon/types/app/bsky/unspecced/initAgeAssurance'
|
|
14
|
+
import { httpLogger as log } from '../../../../logger'
|
|
15
|
+
import { ActorInfo } from '../../../../proto/bsky_pb'
|
|
12
16
|
import { KwsExternalPayload } from '../../../kws/types'
|
|
13
17
|
import { createStashEvent, getClientUa } from '../../../kws/util'
|
|
14
18
|
|
|
@@ -30,25 +34,48 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
30
34
|
throw new ForbiddenError()
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
}
|
|
38
49
|
}
|
|
39
50
|
|
|
51
|
+
const { countryCode, email, language } = validateInput(input.body)
|
|
52
|
+
|
|
40
53
|
const attemptId = crypto.randomUUID()
|
|
41
54
|
// Assumes `app.set('trust proxy', ...)` configured with `true` or specific values.
|
|
42
55
|
const initIp = req.ip
|
|
43
56
|
const initUa = getClientUa(req)
|
|
44
57
|
const externalPayload: KwsExternalPayload = { actorDid, attemptId }
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
try {
|
|
60
|
+
await ctx.kwsClient.sendEmail({
|
|
61
|
+
countryCode: countryCode.toUpperCase(),
|
|
62
|
+
email,
|
|
63
|
+
externalPayload,
|
|
64
|
+
language,
|
|
65
|
+
})
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err instanceof KwsExternalPayloadError) {
|
|
68
|
+
log.error(
|
|
69
|
+
{ externalPayload },
|
|
70
|
+
'Age Assurance flow failed because external payload got too long, which is caused by the DID being too long',
|
|
71
|
+
)
|
|
72
|
+
throw new InvalidRequestError(
|
|
73
|
+
'Age Assurance flow failed because DID is too long',
|
|
74
|
+
'DidTooLong',
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
throw err
|
|
78
|
+
}
|
|
52
79
|
|
|
53
80
|
const event = await createStashEvent(ctx, {
|
|
54
81
|
actorDid,
|
|
@@ -69,3 +96,61 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
69
96
|
},
|
|
70
97
|
})
|
|
71
98
|
}
|
|
99
|
+
|
|
100
|
+
// Supported languages for KWS Adult Verification.
|
|
101
|
+
// This list comes from KWS's AV Developer Guide PDF doc.
|
|
102
|
+
const kwsAvSupportedLanguages = [
|
|
103
|
+
'en',
|
|
104
|
+
'ar',
|
|
105
|
+
'zh-Hans',
|
|
106
|
+
'nl',
|
|
107
|
+
'tl',
|
|
108
|
+
'fr',
|
|
109
|
+
'de',
|
|
110
|
+
'id',
|
|
111
|
+
'it',
|
|
112
|
+
'ja',
|
|
113
|
+
'ko',
|
|
114
|
+
'pl',
|
|
115
|
+
'pt-BR',
|
|
116
|
+
'pt',
|
|
117
|
+
'ru',
|
|
118
|
+
'es',
|
|
119
|
+
'th',
|
|
120
|
+
'tr',
|
|
121
|
+
'vi',
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
const validateInput = (input: InputSchema): InputSchema => {
|
|
125
|
+
const { countryCode, email, language } = input
|
|
126
|
+
|
|
127
|
+
if (!isEmailValid(email) || isDisposableEmail(email)) {
|
|
128
|
+
throw new InvalidRequestError(
|
|
129
|
+
'This email address is not supported, please use a different email.',
|
|
130
|
+
'InvalidEmail',
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
countryCode,
|
|
136
|
+
email,
|
|
137
|
+
language: kwsAvSupportedLanguages.includes(language) ? language : 'en',
|
|
138
|
+
}
|
|
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
|
package/src/kws.ts
CHANGED
|
@@ -14,6 +14,9 @@ const authResponseSchema = z.object({
|
|
|
14
14
|
access_token: z.string(),
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
+
const EXTERNAL_PAYLOAD_CHAR_LIMIT = 200
|
|
18
|
+
export class KwsExternalPayloadError extends Error {}
|
|
19
|
+
|
|
17
20
|
export class KwsClient {
|
|
18
21
|
constructor(public cfg: KwsConfig) {}
|
|
19
22
|
|
|
@@ -76,6 +79,11 @@ export class KwsClient {
|
|
|
76
79
|
externalPayload: KwsExternalPayload
|
|
77
80
|
language: string
|
|
78
81
|
}) {
|
|
82
|
+
const serializedExternalPayload = serializeExternalPayload(externalPayload)
|
|
83
|
+
if (serializedExternalPayload.length > EXTERNAL_PAYLOAD_CHAR_LIMIT) {
|
|
84
|
+
throw new KwsExternalPayloadError()
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
const res = await this.fetchWithAuth(
|
|
80
88
|
`${this.cfg.apiOrigin}/v1/verifications/send-email`,
|
|
81
89
|
{
|
|
@@ -86,7 +94,7 @@ export class KwsClient {
|
|
|
86
94
|
},
|
|
87
95
|
body: JSON.stringify({
|
|
88
96
|
email,
|
|
89
|
-
externalPayload:
|
|
97
|
+
externalPayload: serializedExternalPayload,
|
|
90
98
|
language,
|
|
91
99
|
location: countryCode,
|
|
92
100
|
userContext: 'adult',
|