From 5ad3a906f6c0d9b01dc1cc018ef5a07123d28cff Mon Sep 17 00:00:00 2001
From: dholms
Date: Fri, 8 Sep 2023 10:48:47 -0500
Subject: [PATCH 01/18] lexicons
---
lexicons/com/atproto/server/confirmEmail.json | 22 ++++++++++++++++
.../server/requestEmailConfirmation.json | 10 ++++++++
.../atproto/server/requestEmailUpdate.json | 10 ++++++++
lexicons/com/atproto/server/updateEmail.json | 25 +++++++++++++++++++
4 files changed, 67 insertions(+)
create mode 100644 lexicons/com/atproto/server/confirmEmail.json
create mode 100644 lexicons/com/atproto/server/requestEmailConfirmation.json
create mode 100644 lexicons/com/atproto/server/requestEmailUpdate.json
create mode 100644 lexicons/com/atproto/server/updateEmail.json
diff --git a/lexicons/com/atproto/server/confirmEmail.json b/lexicons/com/atproto/server/confirmEmail.json
new file mode 100644
index 00000000000..ad064cf64b9
--- /dev/null
+++ b/lexicons/com/atproto/server/confirmEmail.json
@@ -0,0 +1,22 @@
+{
+ "lexicon": 1,
+ "id": "com.atproto.server.confirmEmail",
+ "defs": {
+ "main": {
+ "type": "procedure",
+ "description": "Confirm an email using a token from com.atproto.server.requestEmailConfirmation.",
+ "input": {
+ "encoding": "application/json",
+ "schema": {
+ "type": "object",
+ "required": ["email", "token"],
+ "properties": {
+ "email": { "type": "string" },
+ "token": { "type": "string" }
+ }
+ }
+ },
+ "errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }]
+ }
+ }
+}
diff --git a/lexicons/com/atproto/server/requestEmailConfirmation.json b/lexicons/com/atproto/server/requestEmailConfirmation.json
new file mode 100644
index 00000000000..4b2470bf59b
--- /dev/null
+++ b/lexicons/com/atproto/server/requestEmailConfirmation.json
@@ -0,0 +1,10 @@
+{
+ "lexicon": 1,
+ "id": "com.atproto.server.requestEmailConfirmation",
+ "defs": {
+ "main": {
+ "type": "procedure",
+ "description": "Request an email with a code to confirm ownership of email"
+ }
+ }
+}
diff --git a/lexicons/com/atproto/server/requestEmailUpdate.json b/lexicons/com/atproto/server/requestEmailUpdate.json
new file mode 100644
index 00000000000..37c435d72e5
--- /dev/null
+++ b/lexicons/com/atproto/server/requestEmailUpdate.json
@@ -0,0 +1,10 @@
+{
+ "lexicon": 1,
+ "id": "com.atproto.server.requestEmailUpdate",
+ "defs": {
+ "main": {
+ "type": "procedure",
+ "description": "Request a token in order to update email."
+ }
+ }
+}
diff --git a/lexicons/com/atproto/server/updateEmail.json b/lexicons/com/atproto/server/updateEmail.json
new file mode 100644
index 00000000000..2cd84a3e5bc
--- /dev/null
+++ b/lexicons/com/atproto/server/updateEmail.json
@@ -0,0 +1,25 @@
+{
+ "lexicon": 1,
+ "id": "com.atproto.server.updateEmail",
+ "defs": {
+ "main": {
+ "type": "procedure",
+ "description": "Update an account's email.",
+ "input": {
+ "encoding": "application/json",
+ "schema": {
+ "type": "object",
+ "required": ["email"],
+ "properties": {
+ "email": { "type": "string" },
+ "token": {
+ "type": "string",
+ "description": " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed."
+ }
+ }
+ }
+ },
+ "errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }]
+ }
+ }
+}
From f97e3abdad98e6672ddb6f0d2eb02c41da142cd6 Mon Sep 17 00:00:00 2001
From: dholms
Date: Fri, 8 Sep 2023 10:49:50 -0500
Subject: [PATCH 02/18] codegen
---
packages/api/docs/labels.md | 54 +-
.../api/docs/moderation-behaviors/posts.md | 529 +++++++++++++++++-
.../api/docs/moderation-behaviors/profiles.md | 188 ++++++-
packages/api/src/client/index.ts | 52 ++
packages/api/src/client/lexicons.ts | 95 ++++
.../types/com/atproto/server/confirmEmail.ts | 47 ++
.../server/requestEmailConfirmation.ts | 28 +
.../com/atproto/server/requestEmailUpdate.ts | 28 +
.../types/com/atproto/server/updateEmail.ts | 48 ++
packages/bsky/src/lexicon/index.ts | 48 ++
packages/bsky/src/lexicon/lexicons.ts | 95 ++++
.../types/com/atproto/server/confirmEmail.ts | 40 ++
.../server/requestEmailConfirmation.ts | 31 +
.../com/atproto/server/requestEmailUpdate.ts | 31 +
.../types/com/atproto/server/updateEmail.ts | 41 ++
packages/pds/src/lexicon/index.ts | 48 ++
packages/pds/src/lexicon/lexicons.ts | 95 ++++
.../types/com/atproto/server/confirmEmail.ts | 40 ++
.../server/requestEmailConfirmation.ts | 31 +
.../com/atproto/server/requestEmailUpdate.ts | 31 +
.../types/com/atproto/server/updateEmail.ts | 41 ++
21 files changed, 1587 insertions(+), 54 deletions(-)
create mode 100644 packages/api/src/client/types/com/atproto/server/confirmEmail.ts
create mode 100644 packages/api/src/client/types/com/atproto/server/requestEmailConfirmation.ts
create mode 100644 packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts
create mode 100644 packages/api/src/client/types/com/atproto/server/updateEmail.ts
create mode 100644 packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts
create mode 100644 packages/bsky/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts
create mode 100644 packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts
create mode 100644 packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts
create mode 100644 packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts
create mode 100644 packages/pds/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts
create mode 100644 packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts
create mode 100644 packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts
diff --git a/packages/api/docs/labels.md b/packages/api/docs/labels.md
index a2d8806b566..9531df460c1 100644
--- a/packages/api/docs/labels.md
+++ b/packages/api/docs/labels.md
@@ -1,44 +1,44 @@
-# Labels
+ # Labels
+
+ This document is a reference for the labels used in the SDK.
-This document is a reference for the labels used in the SDK.
+ **⚠️ Note**: These labels are still in development and may change over time. Not all are currently in use.
-**⚠️ Note**: These labels are still in development and may change over time. Not all are currently in use.
+ ## Key
-## Key
+ ### Label Preferences
-### Label Preferences
+ The possible client interpretations for a label.
-The possible client interpretations for a label.
+ - ignore
Do nothing with the label.
+ - warn
Provide some form of warning on the content (see "On Warn" behavior).
+ - hide
Remove the content from feeds and apply the warning when directly viewed.
-- ignore
Do nothing with the label.
-- warn
Provide some form of warning on the content (see "On Warn" behavior).
-- hide
Remove the content from feeds and apply the warning when directly viewed.
+ Each label specifies which preferences it can support. If a label is not configurable, it must have only own supported preference.
-Each label specifies which preferences it can support. If a label is not configurable, it must have only own supported preference.
+ ### Configurable?
-### Configurable?
+ Non-configurable labels cannot have their preference changed by the user.
-Non-configurable labels cannot have their preference changed by the user.
+ ### Flags
-### Flags
+ Additional behaviors which a label can adopt.
-Additional behaviors which a label can adopt.
+ - no-override
The user cannot click through any covering of content created by the label.
+ - adult
The user must have adult content enabled to configure the label. If adult content is not enabled, the label must adopt the strictest preference.
-- no-override
The user cannot click through any covering of content created by the label.
-- adult
The user must have adult content enabled to configure the label. If adult content is not enabled, the label must adopt the strictest preference.
+ ### On Warn
-### On Warn
+ The kind of UI behavior used when a warning must be applied.
-The kind of UI behavior used when a warning must be applied.
+ - blur
Hide all of the content behind an interstitial.
+ - blur-media
Hide only the media within the content (ie images) behind an interstitial.
+ - alert
Display a descriptive warning but do not hide the content.
+ - null
Do nothing.
-- blur
Hide all of the content behind an interstitial.
-- blur-media
Hide only the media within the content (ie images) behind an interstitial.
-- alert
Display a descriptive warning but do not hide the content.
-- null
Do nothing.
-
-## Label Behaviors
+ ## Label Behaviors
@@ -267,7 +267,7 @@ The kind of UI behavior used when a warning must be applied.
-## Label Group Descriptions
+ ## Label Group Descriptions
@@ -312,7 +312,7 @@ The kind of UI behavior used when a warning must be applied.
-## Label Descriptions
+ ## Label Descriptions
@@ -535,4 +535,4 @@ The kind of UI behavior used when a warning must be applied.
on content
Misleading
The moderators believe this account is spreading misleading information.
-
+
\ No newline at end of file
diff --git a/packages/api/docs/moderation-behaviors/posts.md b/packages/api/docs/moderation-behaviors/posts.md
index 5ddcf9ff602..ef3c6c7fc5d 100644
--- a/packages/api/docs/moderation-behaviors/posts.md
+++ b/packages/api/docs/moderation-behaviors/posts.md
@@ -38,12 +38,17 @@ Key:
+
|
+
|
+
+
+
Imperative label ('!hide') on author profile |
@@ -51,6 +56,7 @@ Key:
|
+
|
🚫
@@ -58,9 +64,13 @@ Key:
|
+
|
+
+
+
Imperative label ('!hide') on author account |
@@ -76,9 +86,13 @@ Key:
|
+
|
+
+
+
Imperative label ('!hide') on quoted post |
@@ -86,9 +100,11 @@ Key:
|
+
|
+
|
🚫
@@ -96,6 +112,9 @@ Key:
|
+
+
+
Imperative label ('!hide') on quoted author account |
@@ -103,9 +122,11 @@ Key:
|
+
|
+
|
🚫
@@ -113,6 +134,9 @@ Key:
|
+
+
+
Imperative label ('!no-promote') on post |
@@ -120,15 +144,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Imperative label ('!no-promote') on author profile |
@@ -136,15 +166,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Imperative label ('!no-promote') on author account |
@@ -152,15 +188,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Imperative label ('!no-promote') on quoted post |
@@ -168,15 +210,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Imperative label ('!no-promote') on quoted author account |
@@ -184,15 +232,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Imperative label ('!warn') on post |
@@ -204,12 +258,17 @@ Key:
|
+
|
+
|
+
+
+
Imperative label ('!warn') on author profile |
@@ -217,6 +276,7 @@ Key:
|
+
|
✋
@@ -224,9 +284,13 @@ Key:
|
+
|
+
+
+
Imperative label ('!warn') on author account |
@@ -242,9 +306,13 @@ Key:
|
+
|
+
+
+
Imperative label ('!warn') on quoted post |
@@ -252,9 +320,11 @@ Key:
|
+
|
+
|
✋
@@ -262,6 +332,9 @@ Key:
|
+
+
+
Imperative label ('!warn') on quoted author account |
@@ -269,9 +342,11 @@ Key:
|
+
|
+
|
✋
@@ -279,6 +354,8 @@ Key:
|
+
+
Scenario | Filter | Content | Avatar | Embed |
Blur label ('intolerant') on post (hide) |
@@ -291,12 +368,17 @@ Key:
+
|
+
|
+
+
+
Blur label ('intolerant') on author profile (hide) |
@@ -304,6 +386,7 @@ Key:
|
+
|
✋
@@ -311,9 +394,13 @@ Key:
|
+
|
+
+
+
Blur label ('intolerant') on author account (hide) |
@@ -329,9 +416,13 @@ Key:
|
+
|
+
+
+
Blur label ('intolerant') on quoted post (hide) |
@@ -339,9 +430,11 @@ Key:
|
+
|
+
|
✋
@@ -349,6 +442,9 @@ Key:
|
+
+
+
Blur label ('intolerant') on quoted author account (hide) |
@@ -356,9 +452,11 @@ Key:
|
+
|
+
|
✋
@@ -366,6 +464,9 @@ Key:
|
+
+
+
Blur label ('intolerant') on post (warn) |
@@ -377,12 +478,17 @@ Key:
|
+
|
+
|
+
+
+
Blur label ('intolerant') on author profile (warn) |
@@ -390,6 +496,7 @@ Key:
|
+
|
✋
@@ -397,9 +504,13 @@ Key:
|
+
|
+
+
+
Blur label ('intolerant') on author account (warn) |
@@ -415,9 +526,13 @@ Key:
|
+
|
+
+
+
Blur label ('intolerant') on quoted post (warn) |
@@ -425,9 +540,11 @@ Key:
|
+
|
+
|
✋
@@ -435,6 +552,9 @@ Key:
|
+
+
+
Blur label ('intolerant') on quoted author account (warn) |
@@ -442,9 +562,11 @@ Key:
|
+
|
+
|
✋
@@ -452,6 +574,9 @@ Key:
|
+
+
+
Blur label ('intolerant') on post (ignore) |
@@ -459,15 +584,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Blur label ('intolerant') on author profile (ignore) |
@@ -475,15 +606,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Blur label ('intolerant') on author account (ignore) |
@@ -491,15 +628,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Blur label ('intolerant') on quoted post (ignore) |
@@ -507,15 +650,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Blur label ('intolerant') on quoted author account (ignore) |
@@ -523,15 +672,20 @@ Key:
|
+
|
+
|
+
|
+
+
Scenario | Filter | Content | Avatar | Embed |
Blur-media label ('porn') on post (hide) |
@@ -540,9 +694,11 @@ Key:
+
|
+
|
✋
@@ -550,6 +706,9 @@ Key:
|
+
+
+
Blur-media label ('porn') on author profile (hide) |
@@ -557,6 +716,7 @@ Key:
|
+
|
✋
@@ -564,9 +724,13 @@ Key:
|
+
|
+
+
+
Blur-media label ('porn') on author account (hide) |
@@ -574,6 +738,7 @@ Key:
|
+
|
✋
@@ -585,6 +750,9 @@ Key:
|
+
+
+
Blur-media label ('porn') on quoted post (hide) |
@@ -592,9 +760,11 @@ Key:
|
+
|
+
|
✋
@@ -602,6 +772,9 @@ Key:
|
+
+
+
Blur-media label ('porn') on quoted author account (hide) |
@@ -609,15 +782,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Blur-media label ('porn') on post (warn) |
@@ -625,9 +804,11 @@ Key:
|
+
|
+
|
✋
@@ -635,6 +816,9 @@ Key:
|
+
+
+
Blur-media label ('porn') on author profile (warn) |
@@ -642,6 +826,7 @@ Key:
|
+
|
✋
@@ -649,9 +834,13 @@ Key:
|
+
|
+
+
+
Blur-media label ('porn') on author account (warn) |
@@ -659,6 +848,7 @@ Key:
|
+
|
✋
@@ -670,6 +860,9 @@ Key:
|
+
+
+
Blur-media label ('porn') on quoted post (warn) |
@@ -677,9 +870,11 @@ Key:
|
+
|
+
|
✋
@@ -687,6 +882,9 @@ Key:
|
+
+
+
Blur-media label ('porn') on quoted author account (warn) |
@@ -694,15 +892,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Blur-media label ('porn') on post (ignore) |
@@ -710,15 +914,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Blur-media label ('porn') on author profile (ignore) |
@@ -726,15 +936,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Blur-media label ('porn') on author account (ignore) |
@@ -742,15 +958,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Blur-media label ('porn') on quoted post (ignore) |
@@ -758,15 +980,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Blur-media label ('porn') on quoted author account (ignore) |
@@ -774,15 +1002,20 @@ Key:
|
+
|
+
|
+
|
+
+
Scenario | Filter | Content | Avatar | Embed |
Notice label ('scam') on post (hide) |
@@ -792,16 +1025,20 @@ Key:
🪧
-
|
+
|
+
|
+
+
+
Notice label ('scam') on author profile (hide) |
@@ -809,17 +1046,21 @@ Key:
|
+
|
🪧
-
|
+
|
+
+
+
Notice label ('scam') on author account (hide) |
@@ -828,18 +1069,20 @@ Key:
|
🪧
-
|
🪧
-
|
+
|
+
+
+
Notice label ('scam') on quoted post (hide) |
@@ -847,17 +1090,21 @@ Key:
|
+
|
+
|
🪧
-
|
+
+
+
Notice label ('scam') on quoted author account (hide) |
@@ -865,17 +1112,21 @@ Key:
|
+
|
+
|
🪧
-
|
+
+
+
Notice label ('scam') on post (warn) |
@@ -884,16 +1135,20 @@ Key:
|
🪧
-
|
+
|
+
|
+
+
+
Notice label ('scam') on author profile (warn) |
@@ -901,17 +1156,21 @@ Key:
|
+
|
🪧
-
|
+
|
+
+
+
Notice label ('scam') on author account (warn) |
@@ -920,18 +1179,20 @@ Key:
|
🪧
-
|
🪧
-
|
+
|
+
+
+
Notice label ('scam') on quoted post (warn) |
@@ -939,17 +1200,21 @@ Key:
|
+
|
+
|
🪧
-
|
+
+
+
Notice label ('scam') on quoted author account (warn) |
@@ -957,17 +1222,21 @@ Key:
|
+
|
+
|
🪧
-
|
+
+
+
Notice label ('scam') on post (ignore) |
@@ -975,15 +1244,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Notice label ('scam') on author profile (ignore) |
@@ -991,15 +1266,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Notice label ('scam') on author account (ignore) |
@@ -1007,15 +1288,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Notice label ('scam') on quoted post (ignore) |
@@ -1023,15 +1310,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Notice label ('scam') on quoted author account (ignore) |
@@ -1039,15 +1332,20 @@ Key:
|
+
|
+
|
+
|
+
+
Scenario | Filter | Content | Avatar | Embed |
Adult-only label on post when adult content is disabled |
@@ -1056,9 +1354,11 @@ Key:
+
|
+
|
🚫
@@ -1066,6 +1366,9 @@ Key:
|
+
+
+
Adult-only label on author profile when adult content is disabled |
@@ -1073,6 +1376,7 @@ Key:
|
+
|
🚫
@@ -1080,9 +1384,13 @@ Key:
|
+
|
+
+
+
Adult-only label on author account when adult content is disabled |
@@ -1090,6 +1398,7 @@ Key:
|
+
|
🚫
@@ -1101,6 +1410,9 @@ Key:
|
+
+
+
Adult-only label on quoted post when adult content is disabled |
@@ -1108,9 +1420,11 @@ Key:
|
+
|
+
|
🚫
@@ -1118,6 +1432,9 @@ Key:
|
+
+
+
Adult-only label on quoted author account when adult content is disabled |
@@ -1125,15 +1442,20 @@ Key:
|
+
|
+
|
+
|
+
+
Scenario | Filter | Content | Avatar | Embed |
Self-post: Imperative label ('!hide') on post |
@@ -1146,12 +1468,17 @@ Key:
+
|
+
|
+
+
+
Self-post: Imperative label ('!hide') on author profile |
@@ -1159,15 +1486,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Self-post: Imperative label ('!hide') on author account |
@@ -1175,15 +1508,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Self-post: Imperative label ('!hide') on quoted post |
@@ -1191,9 +1530,11 @@ Key:
|
+
|
+
|
✋
@@ -1201,6 +1542,9 @@ Key:
|
+
+
+
Self-post: Imperative label ('!hide') on quoted author account |
@@ -1208,15 +1552,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Self-post: Imperative label ('!warn') on post |
@@ -1228,12 +1578,17 @@ Key:
|
+
|
+
|
+
+
+
Self-post: Imperative label ('!warn') on author profile |
@@ -1241,15 +1596,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Self-post: Imperative label ('!warn') on author account |
@@ -1257,15 +1618,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Self-post: Imperative label ('!warn') on quoted post |
@@ -1273,9 +1640,11 @@ Key:
|
+
|
+
|
✋
@@ -1283,6 +1652,9 @@ Key:
|
+
+
+
Self-post: Imperative label ('!warn') on quoted author account |
@@ -1290,15 +1662,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Self-post: Blur-media label ('porn') on post (hide) |
@@ -1306,9 +1684,11 @@ Key:
|
+
|
+
|
✋
@@ -1316,6 +1696,9 @@ Key:
|
+
+
+
Self-post: Blur-media label ('porn') on author profile (hide) |
@@ -1323,15 +1706,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Self-post: Blur-media label ('porn') on author account (hide) |
@@ -1339,15 +1728,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Self-post: Blur-media label ('porn') on quoted post (hide) |
@@ -1355,9 +1750,11 @@ Key:
|
+
|
+
|
✋
@@ -1365,6 +1762,9 @@ Key:
|
+
+
+
Self-post: Blur-media label ('porn') on quoted author account (hide) |
@@ -1372,15 +1772,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Self-post: Blur-media label ('porn') on post (warn) |
@@ -1388,9 +1794,11 @@ Key:
|
+
|
+
|
✋
@@ -1398,6 +1806,9 @@ Key:
|
+
+
+
Self-post: Blur-media label ('porn') on author profile (warn) |
@@ -1405,15 +1816,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Self-post: Blur-media label ('porn') on author account (warn) |
@@ -1421,15 +1838,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Self-post: Blur-media label ('porn') on quoted post (warn) |
@@ -1437,9 +1860,11 @@ Key:
|
+
|
+
|
✋
@@ -1447,6 +1872,9 @@ Key:
|
+
+
+
Self-post: Blur-media label ('porn') on quoted author account (warn) |
@@ -1454,15 +1882,20 @@ Key:
|
+
|
+
|
+
|
+
+
Scenario | Filter | Content | Avatar | Embed |
Post with blocked author |
@@ -1479,9 +1912,13 @@ Key:
+
|
+
+
+
Post with blocked quoted author |
@@ -1489,9 +1926,11 @@ Key:
|
+
|
+
|
🚫
@@ -1499,6 +1938,9 @@ Key:
|
+
+
+
Post with author blocking user |
@@ -1514,9 +1956,13 @@ Key:
|
+
|
+
+
+
Post with quoted author blocking user |
@@ -1524,9 +1970,11 @@ Key:
|
+
|
+
|
🚫
@@ -1534,6 +1982,9 @@ Key:
|
+
+
+
Post with muted author |
@@ -1545,12 +1996,17 @@ Key:
|
+
|
+
|
+
+
+
Post with muted quoted author |
@@ -1558,9 +2014,11 @@ Key:
|
+
|
+
|
✋
@@ -1568,6 +2026,9 @@ Key:
|
+
+
+
Post with muted-by-list author |
@@ -1579,12 +2040,17 @@ Key:
|
+
|
+
|
+
+
+
Post with muted-by-list quoted author |
@@ -1592,9 +2058,11 @@ Key:
|
+
|
+
|
✋
@@ -1602,6 +2070,8 @@ Key:
|
+
+
Scenario | Filter | Content | Avatar | Embed |
Prioritization: post with blocking & blocked-by author |
@@ -1618,9 +2088,13 @@ Key:
+
|
+
+
+
Prioritization: post with blocking & blocked-by quoted author |
@@ -1628,9 +2102,11 @@ Key:
|
+
|
+
|
🚫
@@ -1638,6 +2114,9 @@ Key:
|
+
+
+
Prioritization: '!hide' label on post by blocked user |
@@ -1653,9 +2132,13 @@ Key:
|
+
|
+
+
+
Prioritization: '!hide' label on quoted post, post by blocked user |
@@ -1675,6 +2158,9 @@ Key:
|
+
+
+
Prioritization: '!hide' and 'intolerant' labels on post (hide) |
@@ -1686,12 +2172,17 @@ Key:
|
+
|
+
|
+
+
+
Prioritization: '!warn' and 'intolerant' labels on post (hide) |
@@ -1703,12 +2194,17 @@ Key:
|
+
|
+
|
+
+
+
Prioritization: '!hide' and 'porn' labels on post (hide) |
@@ -1720,12 +2216,17 @@ Key:
|
+
|
+
|
+
+
+
Prioritization: '!warn' and 'porn' labels on post (hide) |
@@ -1733,9 +2234,11 @@ Key:
|
+
|
+
|
✋
@@ -1743,4 +2246,4 @@ Key:
|
-
+
\ No newline at end of file
diff --git a/packages/api/docs/moderation-behaviors/profiles.md b/packages/api/docs/moderation-behaviors/profiles.md
index b8d7c94ce91..3d2f9af96b3 100644
--- a/packages/api/docs/moderation-behaviors/profiles.md
+++ b/packages/api/docs/moderation-behaviors/profiles.md
@@ -38,6 +38,7 @@ Key:
+
|
🚫
@@ -45,6 +46,9 @@ Key:
|
+
+
+
Imperative label ('!hide') on profile |
@@ -52,6 +56,7 @@ Key:
|
+
|
🚫
@@ -63,6 +68,9 @@ Key:
|
+
+
+
Imperative label ('!no-promote') on account |
@@ -70,15 +78,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Imperative label ('!no-promote') on profile |
@@ -86,15 +100,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Imperative label ('!warn') on account |
@@ -106,6 +126,7 @@ Key:
|
+
|
✋
@@ -113,6 +134,9 @@ Key:
|
+
+
+
Imperative label ('!warn') on profile |
@@ -120,6 +144,7 @@ Key:
|
+
|
✋
@@ -131,6 +156,8 @@ Key:
|
+
+
Scenario | Filter | Account | Profile | Avatar |
Blur label ('intolerant') on account (hide) |
@@ -143,6 +170,7 @@ Key:
+
|
✋
@@ -150,6 +178,9 @@ Key:
|
+
+
+
Blur label ('intolerant') on profile (hide) |
@@ -157,6 +188,7 @@ Key:
|
+
|
✋
@@ -168,6 +200,9 @@ Key:
|
+
+
+
Blur label ('intolerant') on account (warn) |
@@ -179,6 +214,7 @@ Key:
|
+
|
✋
@@ -186,6 +222,9 @@ Key:
|
+
+
+
Blur label ('intolerant') on profile (warn) |
@@ -193,6 +232,7 @@ Key:
|
+
|
✋
@@ -204,6 +244,9 @@ Key:
|
+
+
+
Blur label ('intolerant') on account (ignore) |
@@ -211,15 +254,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Blur label ('intolerant') on profile (ignore) |
@@ -227,15 +276,20 @@ Key:
|
+
|
+
|
+
|
+
+
Scenario | Filter | Account | Profile | Avatar |
Blur-media label ('porn') on account (hide) |
@@ -248,6 +302,7 @@ Key:
+
|
✋
@@ -255,6 +310,9 @@ Key:
|
+
+
+
Blur-media label ('porn') on profile (hide) |
@@ -262,9 +320,11 @@ Key:
|
+
|
+
|
✋
@@ -272,6 +332,9 @@ Key:
|
+
+
+
Blur-media label ('porn') on account (warn) |
@@ -283,6 +346,7 @@ Key:
|
+
|
✋
@@ -290,6 +354,9 @@ Key:
|
+
+
+
Blur-media label ('porn') on profile (warn) |
@@ -297,9 +364,11 @@ Key:
|
+
|
+
|
✋
@@ -307,6 +376,9 @@ Key:
|
+
+
+
Blur-media label ('porn') on account (ignore) |
@@ -314,15 +386,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Blur-media label ('porn') on profile (ignore) |
@@ -330,15 +408,20 @@ Key:
|
+
|
+
|
+
|
+
+
Scenario | Filter | Account | Profile | Avatar |
Notice label ('scam') on account (hide) |
@@ -348,18 +431,20 @@ Key:
🪧
-
|
+
|
🪧
-
|
+
+
+
Notice label ('scam') on profile (hide) |
@@ -367,19 +452,21 @@ Key:
|
+
|
🪧
-
|
🪧
-
|
+
+
+
Notice label ('scam') on account (warn) |
@@ -388,18 +475,20 @@ Key:
|
🪧
-
|
+
|
🪧
-
|
+
+
+
Notice label ('scam') on profile (warn) |
@@ -407,19 +496,21 @@ Key:
|
+
|
🪧
-
|
🪧
-
|
+
+
+
Notice label ('scam') on account (ignore) |
@@ -427,15 +518,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Notice label ('scam') on profile (ignore) |
@@ -443,15 +540,20 @@ Key:
|
+
|
+
|
+
|
+
+
Scenario | Filter | Account | Profile | Avatar |
Adult-only label on account when adult content is disabled |
@@ -464,6 +566,7 @@ Key:
+
|
🚫
@@ -471,6 +574,9 @@ Key:
|
+
+
+
Adult-only label on profile when adult content is disabled |
@@ -478,9 +584,11 @@ Key:
|
+
|
+
|
🚫
@@ -488,6 +596,8 @@ Key:
|
+
+
Scenario | Filter | Account | Profile | Avatar |
Self-profile: !hide on account |
@@ -497,18 +607,20 @@ Key:
🪧
-
|
+
|
🪧
-
|
+
+
+
Self-profile: !hide on profile |
@@ -516,19 +628,20 @@ Key:
|
+
|
🪧
-
|
🪧
-
|
+
+
Scenario | Filter | Account | Profile | Avatar |
Mute/block: Blocking user |
@@ -537,9 +650,11 @@ Key:
+
|
+
|
🚫
@@ -547,6 +662,9 @@ Key:
|
+
+
+
Mute/block: Blocked by user |
@@ -554,9 +672,11 @@ Key:
|
+
|
+
|
🚫
@@ -564,6 +684,9 @@ Key:
|
+
+
+
Mute/block: Muted user |
@@ -571,15 +694,21 @@ Key:
|
+
|
+
|
+
|
+
+
+
Mute/block: Muted-by-list user |
@@ -587,15 +716,20 @@ Key:
|
+
|
+
|
+
|
+
+
Scenario | Filter | Account | Profile | Avatar |
Prioritization: blocking & blocked-by user |
@@ -604,9 +738,11 @@ Key:
+
|
+
|
🚫
@@ -614,6 +750,9 @@ Key:
|
+
+
+
Prioritization: '!hide' label on account of blocked user |
@@ -625,6 +764,7 @@ Key:
|
+
|
🚫
@@ -632,6 +772,9 @@ Key:
|
+
+
+
Prioritization: '!hide' and 'intolerant' labels on account (hide) |
@@ -643,6 +786,7 @@ Key:
|
+
|
🚫
@@ -650,6 +794,9 @@ Key:
|
+
+
+
Prioritization: '!warn' and 'intolerant' labels on account (hide) |
@@ -661,6 +808,7 @@ Key:
|
+
|
✋
@@ -668,6 +816,9 @@ Key:
|
+
+
+
Prioritization: '!warn' and 'porn' labels on account (hide) |
@@ -679,6 +830,7 @@ Key:
|
+
|
✋
@@ -686,6 +838,9 @@ Key:
|
+
+
+
Prioritization: intolerant label on account (hide) and scam label on profile (warn) |
@@ -698,7 +853,6 @@ Key:
|
🪧
-
|
✋
@@ -706,6 +860,9 @@ Key:
|
+
+
+
Prioritization: !hide on account, !warn on profile |
@@ -725,6 +882,9 @@ Key:
|
+
+
+
Prioritization: !warn on account, !hide on profile |
@@ -744,4 +904,4 @@ Key:
|
-
+
\ No newline at end of file
diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts
index f9ebf0ade63..768ab3e087e 100644
--- a/packages/api/src/client/index.ts
+++ b/packages/api/src/client/index.ts
@@ -41,6 +41,7 @@ import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords
import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord'
import * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef'
import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob'
+import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail'
import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount'
import * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword'
import * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode'
@@ -55,9 +56,12 @@ import * as ComAtprotoServerGetSession from './types/com/atproto/server/getSessi
import * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords'
import * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession'
import * as ComAtprotoServerRequestAccountDelete from './types/com/atproto/server/requestAccountDelete'
+import * as ComAtprotoServerRequestEmailConfirmation from './types/com/atproto/server/requestEmailConfirmation'
+import * as ComAtprotoServerRequestEmailUpdate from './types/com/atproto/server/requestEmailUpdate'
import * as ComAtprotoServerRequestPasswordReset from './types/com/atproto/server/requestPasswordReset'
import * as ComAtprotoServerResetPassword from './types/com/atproto/server/resetPassword'
import * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/revokeAppPassword'
+import * as ComAtprotoServerUpdateEmail from './types/com/atproto/server/updateEmail'
import * as ComAtprotoSyncGetBlob from './types/com/atproto/sync/getBlob'
import * as ComAtprotoSyncGetBlocks from './types/com/atproto/sync/getBlocks'
import * as ComAtprotoSyncGetCheckout from './types/com/atproto/sync/getCheckout'
@@ -163,6 +167,7 @@ export * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords
export * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord'
export * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef'
export * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob'
+export * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail'
export * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount'
export * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword'
export * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode'
@@ -177,9 +182,12 @@ export * as ComAtprotoServerGetSession from './types/com/atproto/server/getSessi
export * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords'
export * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession'
export * as ComAtprotoServerRequestAccountDelete from './types/com/atproto/server/requestAccountDelete'
+export * as ComAtprotoServerRequestEmailConfirmation from './types/com/atproto/server/requestEmailConfirmation'
+export * as ComAtprotoServerRequestEmailUpdate from './types/com/atproto/server/requestEmailUpdate'
export * as ComAtprotoServerRequestPasswordReset from './types/com/atproto/server/requestPasswordReset'
export * as ComAtprotoServerResetPassword from './types/com/atproto/server/resetPassword'
export * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/revokeAppPassword'
+export * as ComAtprotoServerUpdateEmail from './types/com/atproto/server/updateEmail'
export * as ComAtprotoSyncGetBlob from './types/com/atproto/sync/getBlob'
export * as ComAtprotoSyncGetBlocks from './types/com/atproto/sync/getBlocks'
export * as ComAtprotoSyncGetCheckout from './types/com/atproto/sync/getCheckout'
@@ -699,6 +707,17 @@ export class ServerNS {
this._service = service
}
+ confirmEmail(
+ data?: ComAtprotoServerConfirmEmail.InputSchema,
+ opts?: ComAtprotoServerConfirmEmail.CallOptions,
+ ): Promise {
+ return this._service.xrpc
+ .call('com.atproto.server.confirmEmail', opts?.qp, data, opts)
+ .catch((e) => {
+ throw ComAtprotoServerConfirmEmail.toKnownErr(e)
+ })
+ }
+
createAccount(
data?: ComAtprotoServerCreateAccount.InputSchema,
opts?: ComAtprotoServerCreateAccount.CallOptions,
@@ -842,6 +861,28 @@ export class ServerNS {
})
}
+ requestEmailConfirmation(
+ data?: ComAtprotoServerRequestEmailConfirmation.InputSchema,
+ opts?: ComAtprotoServerRequestEmailConfirmation.CallOptions,
+ ): Promise {
+ return this._service.xrpc
+ .call('com.atproto.server.requestEmailConfirmation', opts?.qp, data, opts)
+ .catch((e) => {
+ throw ComAtprotoServerRequestEmailConfirmation.toKnownErr(e)
+ })
+ }
+
+ requestEmailUpdate(
+ data?: ComAtprotoServerRequestEmailUpdate.InputSchema,
+ opts?: ComAtprotoServerRequestEmailUpdate.CallOptions,
+ ): Promise {
+ return this._service.xrpc
+ .call('com.atproto.server.requestEmailUpdate', opts?.qp, data, opts)
+ .catch((e) => {
+ throw ComAtprotoServerRequestEmailUpdate.toKnownErr(e)
+ })
+ }
+
requestPasswordReset(
data?: ComAtprotoServerRequestPasswordReset.InputSchema,
opts?: ComAtprotoServerRequestPasswordReset.CallOptions,
@@ -874,6 +915,17 @@ export class ServerNS {
throw ComAtprotoServerRevokeAppPassword.toKnownErr(e)
})
}
+
+ updateEmail(
+ data?: ComAtprotoServerUpdateEmail.InputSchema,
+ opts?: ComAtprotoServerUpdateEmail.CallOptions,
+ ): Promise {
+ return this._service.xrpc
+ .call('com.atproto.server.updateEmail', opts?.qp, data, opts)
+ .catch((e) => {
+ throw ComAtprotoServerUpdateEmail.toKnownErr(e)
+ })
+ }
}
export class SyncNS {
diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts
index e15d7aba1ce..17ad93b38c7 100644
--- a/packages/api/src/client/lexicons.ts
+++ b/packages/api/src/client/lexicons.ts
@@ -2238,6 +2238,40 @@ export const schemaDict = {
},
},
},
+ ComAtprotoServerConfirmEmail: {
+ lexicon: 1,
+ id: 'com.atproto.server.confirmEmail',
+ defs: {
+ main: {
+ type: 'procedure',
+ description:
+ 'Confirm an email using a token from com.atproto.server.requestEmailConfirmation.',
+ input: {
+ encoding: 'application/json',
+ schema: {
+ type: 'object',
+ required: ['email', 'token'],
+ properties: {
+ email: {
+ type: 'string',
+ },
+ token: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ errors: [
+ {
+ name: 'ExpiredToken',
+ },
+ {
+ name: 'InvalidToken',
+ },
+ ],
+ },
+ },
+ },
ComAtprotoServerCreateAccount: {
lexicon: 1,
id: 'com.atproto.server.createAccount',
@@ -2850,6 +2884,27 @@ export const schemaDict = {
},
},
},
+ ComAtprotoServerRequestEmailConfirmation: {
+ lexicon: 1,
+ id: 'com.atproto.server.requestEmailConfirmation',
+ defs: {
+ main: {
+ type: 'procedure',
+ description:
+ 'Request an email with a code to confirm ownership of email',
+ },
+ },
+ },
+ ComAtprotoServerRequestEmailUpdate: {
+ lexicon: 1,
+ id: 'com.atproto.server.requestEmailUpdate',
+ defs: {
+ main: {
+ type: 'procedure',
+ description: 'Request a token in order to update email.',
+ },
+ },
+ },
ComAtprotoServerRequestPasswordReset: {
lexicon: 1,
id: 'com.atproto.server.requestPasswordReset',
@@ -2927,6 +2982,41 @@ export const schemaDict = {
},
},
},
+ ComAtprotoServerUpdateEmail: {
+ lexicon: 1,
+ id: 'com.atproto.server.updateEmail',
+ defs: {
+ main: {
+ type: 'procedure',
+ description: "Update an account's email.",
+ input: {
+ encoding: 'application/json',
+ schema: {
+ type: 'object',
+ required: ['email'],
+ properties: {
+ email: {
+ type: 'string',
+ },
+ token: {
+ type: 'string',
+ description:
+ " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.",
+ },
+ },
+ },
+ },
+ errors: [
+ {
+ name: 'ExpiredToken',
+ },
+ {
+ name: 'InvalidToken',
+ },
+ ],
+ },
+ },
+ },
ComAtprotoSyncGetBlob: {
lexicon: 1,
id: 'com.atproto.sync.getBlob',
@@ -6715,6 +6805,7 @@ export const ids = {
ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord',
ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',
ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob',
+ ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail',
ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount',
ComAtprotoServerCreateAppPassword: 'com.atproto.server.createAppPassword',
ComAtprotoServerCreateInviteCode: 'com.atproto.server.createInviteCode',
@@ -6731,10 +6822,14 @@ export const ids = {
ComAtprotoServerRefreshSession: 'com.atproto.server.refreshSession',
ComAtprotoServerRequestAccountDelete:
'com.atproto.server.requestAccountDelete',
+ ComAtprotoServerRequestEmailConfirmation:
+ 'com.atproto.server.requestEmailConfirmation',
+ ComAtprotoServerRequestEmailUpdate: 'com.atproto.server.requestEmailUpdate',
ComAtprotoServerRequestPasswordReset:
'com.atproto.server.requestPasswordReset',
ComAtprotoServerResetPassword: 'com.atproto.server.resetPassword',
ComAtprotoServerRevokeAppPassword: 'com.atproto.server.revokeAppPassword',
+ ComAtprotoServerUpdateEmail: 'com.atproto.server.updateEmail',
ComAtprotoSyncGetBlob: 'com.atproto.sync.getBlob',
ComAtprotoSyncGetBlocks: 'com.atproto.sync.getBlocks',
ComAtprotoSyncGetCheckout: 'com.atproto.sync.getCheckout',
diff --git a/packages/api/src/client/types/com/atproto/server/confirmEmail.ts b/packages/api/src/client/types/com/atproto/server/confirmEmail.ts
new file mode 100644
index 00000000000..64a9d6811bb
--- /dev/null
+++ b/packages/api/src/client/types/com/atproto/server/confirmEmail.ts
@@ -0,0 +1,47 @@
+/**
+ * GENERATED CODE - DO NOT MODIFY
+ */
+import { Headers, XRPCError } from '@atproto/xrpc'
+import { ValidationResult, BlobRef } from '@atproto/lexicon'
+import { isObj, hasProp } from '../../../../util'
+import { lexicons } from '../../../../lexicons'
+import { CID } from 'multiformats/cid'
+
+export interface QueryParams {}
+
+export interface InputSchema {
+ email: string
+ token: string
+ [k: string]: unknown
+}
+
+export interface CallOptions {
+ headers?: Headers
+ qp?: QueryParams
+ encoding: 'application/json'
+}
+
+export interface Response {
+ success: boolean
+ headers: Headers
+}
+
+export class ExpiredTokenError extends XRPCError {
+ constructor(src: XRPCError) {
+ super(src.status, src.error, src.message, src.headers)
+ }
+}
+
+export class InvalidTokenError extends XRPCError {
+ constructor(src: XRPCError) {
+ super(src.status, src.error, src.message, src.headers)
+ }
+}
+
+export function toKnownErr(e: any) {
+ if (e instanceof XRPCError) {
+ if (e.error === 'ExpiredToken') return new ExpiredTokenError(e)
+ if (e.error === 'InvalidToken') return new InvalidTokenError(e)
+ }
+ return e
+}
diff --git a/packages/api/src/client/types/com/atproto/server/requestEmailConfirmation.ts b/packages/api/src/client/types/com/atproto/server/requestEmailConfirmation.ts
new file mode 100644
index 00000000000..ef2ed1ac47c
--- /dev/null
+++ b/packages/api/src/client/types/com/atproto/server/requestEmailConfirmation.ts
@@ -0,0 +1,28 @@
+/**
+ * GENERATED CODE - DO NOT MODIFY
+ */
+import { Headers, XRPCError } from '@atproto/xrpc'
+import { ValidationResult, BlobRef } from '@atproto/lexicon'
+import { isObj, hasProp } from '../../../../util'
+import { lexicons } from '../../../../lexicons'
+import { CID } from 'multiformats/cid'
+
+export interface QueryParams {}
+
+export type InputSchema = undefined
+
+export interface CallOptions {
+ headers?: Headers
+ qp?: QueryParams
+}
+
+export interface Response {
+ success: boolean
+ headers: Headers
+}
+
+export function toKnownErr(e: any) {
+ if (e instanceof XRPCError) {
+ }
+ return e
+}
diff --git a/packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts b/packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts
new file mode 100644
index 00000000000..ef2ed1ac47c
--- /dev/null
+++ b/packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts
@@ -0,0 +1,28 @@
+/**
+ * GENERATED CODE - DO NOT MODIFY
+ */
+import { Headers, XRPCError } from '@atproto/xrpc'
+import { ValidationResult, BlobRef } from '@atproto/lexicon'
+import { isObj, hasProp } from '../../../../util'
+import { lexicons } from '../../../../lexicons'
+import { CID } from 'multiformats/cid'
+
+export interface QueryParams {}
+
+export type InputSchema = undefined
+
+export interface CallOptions {
+ headers?: Headers
+ qp?: QueryParams
+}
+
+export interface Response {
+ success: boolean
+ headers: Headers
+}
+
+export function toKnownErr(e: any) {
+ if (e instanceof XRPCError) {
+ }
+ return e
+}
diff --git a/packages/api/src/client/types/com/atproto/server/updateEmail.ts b/packages/api/src/client/types/com/atproto/server/updateEmail.ts
new file mode 100644
index 00000000000..6b7c476d0a9
--- /dev/null
+++ b/packages/api/src/client/types/com/atproto/server/updateEmail.ts
@@ -0,0 +1,48 @@
+/**
+ * GENERATED CODE - DO NOT MODIFY
+ */
+import { Headers, XRPCError } from '@atproto/xrpc'
+import { ValidationResult, BlobRef } from '@atproto/lexicon'
+import { isObj, hasProp } from '../../../../util'
+import { lexicons } from '../../../../lexicons'
+import { CID } from 'multiformats/cid'
+
+export interface QueryParams {}
+
+export interface InputSchema {
+ email: string
+ /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */
+ token?: string
+ [k: string]: unknown
+}
+
+export interface CallOptions {
+ headers?: Headers
+ qp?: QueryParams
+ encoding: 'application/json'
+}
+
+export interface Response {
+ success: boolean
+ headers: Headers
+}
+
+export class ExpiredTokenError extends XRPCError {
+ constructor(src: XRPCError) {
+ super(src.status, src.error, src.message, src.headers)
+ }
+}
+
+export class InvalidTokenError extends XRPCError {
+ constructor(src: XRPCError) {
+ super(src.status, src.error, src.message, src.headers)
+ }
+}
+
+export function toKnownErr(e: any) {
+ if (e instanceof XRPCError) {
+ if (e.error === 'ExpiredToken') return new ExpiredTokenError(e)
+ if (e.error === 'InvalidToken') return new InvalidTokenError(e)
+ }
+ return e
+}
diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts
index a99d4d6e51b..be1b593b5ef 100644
--- a/packages/bsky/src/lexicon/index.ts
+++ b/packages/bsky/src/lexicon/index.ts
@@ -39,6 +39,7 @@ import * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord'
import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords'
import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord'
import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob'
+import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail'
import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount'
import * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword'
import * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode'
@@ -52,9 +53,12 @@ import * as ComAtprotoServerGetSession from './types/com/atproto/server/getSessi
import * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords'
import * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession'
import * as ComAtprotoServerRequestAccountDelete from './types/com/atproto/server/requestAccountDelete'
+import * as ComAtprotoServerRequestEmailConfirmation from './types/com/atproto/server/requestEmailConfirmation'
+import * as ComAtprotoServerRequestEmailUpdate from './types/com/atproto/server/requestEmailUpdate'
import * as ComAtprotoServerRequestPasswordReset from './types/com/atproto/server/requestPasswordReset'
import * as ComAtprotoServerResetPassword from './types/com/atproto/server/resetPassword'
import * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/revokeAppPassword'
+import * as ComAtprotoServerUpdateEmail from './types/com/atproto/server/updateEmail'
import * as ComAtprotoSyncGetBlob from './types/com/atproto/sync/getBlob'
import * as ComAtprotoSyncGetBlocks from './types/com/atproto/sync/getBlocks'
import * as ComAtprotoSyncGetCheckout from './types/com/atproto/sync/getCheckout'
@@ -554,6 +558,17 @@ export class ServerNS {
this._server = server
}
+ confirmEmail(
+ cfg: ConfigOf<
+ AV,
+ ComAtprotoServerConfirmEmail.Handler>,
+ ComAtprotoServerConfirmEmail.HandlerReqCtx>
+ >,
+ ) {
+ const nsid = 'com.atproto.server.confirmEmail' // @ts-ignore
+ return this._server.xrpc.method(nsid, cfg)
+ }
+
createAccount(
cfg: ConfigOf<
AV,
@@ -697,6 +712,28 @@ export class ServerNS {
return this._server.xrpc.method(nsid, cfg)
}
+ requestEmailConfirmation(
+ cfg: ConfigOf<
+ AV,
+ ComAtprotoServerRequestEmailConfirmation.Handler>,
+ ComAtprotoServerRequestEmailConfirmation.HandlerReqCtx>
+ >,
+ ) {
+ const nsid = 'com.atproto.server.requestEmailConfirmation' // @ts-ignore
+ return this._server.xrpc.method(nsid, cfg)
+ }
+
+ requestEmailUpdate(
+ cfg: ConfigOf<
+ AV,
+ ComAtprotoServerRequestEmailUpdate.Handler>,
+ ComAtprotoServerRequestEmailUpdate.HandlerReqCtx>
+ >,
+ ) {
+ const nsid = 'com.atproto.server.requestEmailUpdate' // @ts-ignore
+ return this._server.xrpc.method(nsid, cfg)
+ }
+
requestPasswordReset(
cfg: ConfigOf<
AV,
@@ -729,6 +766,17 @@ export class ServerNS {
const nsid = 'com.atproto.server.revokeAppPassword' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
+
+ updateEmail(
+ cfg: ConfigOf<
+ AV,
+ ComAtprotoServerUpdateEmail.Handler>,
+ ComAtprotoServerUpdateEmail.HandlerReqCtx>
+ >,
+ ) {
+ const nsid = 'com.atproto.server.updateEmail' // @ts-ignore
+ return this._server.xrpc.method(nsid, cfg)
+ }
}
export class SyncNS {
diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts
index e15d7aba1ce..17ad93b38c7 100644
--- a/packages/bsky/src/lexicon/lexicons.ts
+++ b/packages/bsky/src/lexicon/lexicons.ts
@@ -2238,6 +2238,40 @@ export const schemaDict = {
},
},
},
+ ComAtprotoServerConfirmEmail: {
+ lexicon: 1,
+ id: 'com.atproto.server.confirmEmail',
+ defs: {
+ main: {
+ type: 'procedure',
+ description:
+ 'Confirm an email using a token from com.atproto.server.requestEmailConfirmation.',
+ input: {
+ encoding: 'application/json',
+ schema: {
+ type: 'object',
+ required: ['email', 'token'],
+ properties: {
+ email: {
+ type: 'string',
+ },
+ token: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ errors: [
+ {
+ name: 'ExpiredToken',
+ },
+ {
+ name: 'InvalidToken',
+ },
+ ],
+ },
+ },
+ },
ComAtprotoServerCreateAccount: {
lexicon: 1,
id: 'com.atproto.server.createAccount',
@@ -2850,6 +2884,27 @@ export const schemaDict = {
},
},
},
+ ComAtprotoServerRequestEmailConfirmation: {
+ lexicon: 1,
+ id: 'com.atproto.server.requestEmailConfirmation',
+ defs: {
+ main: {
+ type: 'procedure',
+ description:
+ 'Request an email with a code to confirm ownership of email',
+ },
+ },
+ },
+ ComAtprotoServerRequestEmailUpdate: {
+ lexicon: 1,
+ id: 'com.atproto.server.requestEmailUpdate',
+ defs: {
+ main: {
+ type: 'procedure',
+ description: 'Request a token in order to update email.',
+ },
+ },
+ },
ComAtprotoServerRequestPasswordReset: {
lexicon: 1,
id: 'com.atproto.server.requestPasswordReset',
@@ -2927,6 +2982,41 @@ export const schemaDict = {
},
},
},
+ ComAtprotoServerUpdateEmail: {
+ lexicon: 1,
+ id: 'com.atproto.server.updateEmail',
+ defs: {
+ main: {
+ type: 'procedure',
+ description: "Update an account's email.",
+ input: {
+ encoding: 'application/json',
+ schema: {
+ type: 'object',
+ required: ['email'],
+ properties: {
+ email: {
+ type: 'string',
+ },
+ token: {
+ type: 'string',
+ description:
+ " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.",
+ },
+ },
+ },
+ },
+ errors: [
+ {
+ name: 'ExpiredToken',
+ },
+ {
+ name: 'InvalidToken',
+ },
+ ],
+ },
+ },
+ },
ComAtprotoSyncGetBlob: {
lexicon: 1,
id: 'com.atproto.sync.getBlob',
@@ -6715,6 +6805,7 @@ export const ids = {
ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord',
ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',
ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob',
+ ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail',
ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount',
ComAtprotoServerCreateAppPassword: 'com.atproto.server.createAppPassword',
ComAtprotoServerCreateInviteCode: 'com.atproto.server.createInviteCode',
@@ -6731,10 +6822,14 @@ export const ids = {
ComAtprotoServerRefreshSession: 'com.atproto.server.refreshSession',
ComAtprotoServerRequestAccountDelete:
'com.atproto.server.requestAccountDelete',
+ ComAtprotoServerRequestEmailConfirmation:
+ 'com.atproto.server.requestEmailConfirmation',
+ ComAtprotoServerRequestEmailUpdate: 'com.atproto.server.requestEmailUpdate',
ComAtprotoServerRequestPasswordReset:
'com.atproto.server.requestPasswordReset',
ComAtprotoServerResetPassword: 'com.atproto.server.resetPassword',
ComAtprotoServerRevokeAppPassword: 'com.atproto.server.revokeAppPassword',
+ ComAtprotoServerUpdateEmail: 'com.atproto.server.updateEmail',
ComAtprotoSyncGetBlob: 'com.atproto.sync.getBlob',
ComAtprotoSyncGetBlocks: 'com.atproto.sync.getBlocks',
ComAtprotoSyncGetCheckout: 'com.atproto.sync.getCheckout',
diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts b/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts
new file mode 100644
index 00000000000..6d2d89db7d0
--- /dev/null
+++ b/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts
@@ -0,0 +1,40 @@
+/**
+ * GENERATED CODE - DO NOT MODIFY
+ */
+import express from 'express'
+import { ValidationResult, BlobRef } from '@atproto/lexicon'
+import { lexicons } from '../../../../lexicons'
+import { isObj, hasProp } from '../../../../util'
+import { CID } from 'multiformats/cid'
+import { HandlerAuth } from '@atproto/xrpc-server'
+
+export interface QueryParams {}
+
+export interface InputSchema {
+ email: string
+ token: string
+ [k: string]: unknown
+}
+
+export interface HandlerInput {
+ encoding: 'application/json'
+ body: InputSchema
+}
+
+export interface HandlerError {
+ status: number
+ message?: string
+ error?: 'ExpiredToken' | 'InvalidToken'
+}
+
+export type HandlerOutput = HandlerError | void
+export type HandlerReqCtx = {
+ auth: HA
+ params: QueryParams
+ input: HandlerInput
+ req: express.Request
+ res: express.Response
+}
+export type Handler = (
+ ctx: HandlerReqCtx,
+) => Promise | HandlerOutput
diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts
new file mode 100644
index 00000000000..e4244870425
--- /dev/null
+++ b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts
@@ -0,0 +1,31 @@
+/**
+ * GENERATED CODE - DO NOT MODIFY
+ */
+import express from 'express'
+import { ValidationResult, BlobRef } from '@atproto/lexicon'
+import { lexicons } from '../../../../lexicons'
+import { isObj, hasProp } from '../../../../util'
+import { CID } from 'multiformats/cid'
+import { HandlerAuth } from '@atproto/xrpc-server'
+
+export interface QueryParams {}
+
+export type InputSchema = undefined
+export type HandlerInput = undefined
+
+export interface HandlerError {
+ status: number
+ message?: string
+}
+
+export type HandlerOutput = HandlerError | void
+export type HandlerReqCtx = {
+ auth: HA
+ params: QueryParams
+ input: HandlerInput
+ req: express.Request
+ res: express.Response
+}
+export type Handler = (
+ ctx: HandlerReqCtx,
+) => Promise | HandlerOutput
diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts
new file mode 100644
index 00000000000..e4244870425
--- /dev/null
+++ b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts
@@ -0,0 +1,31 @@
+/**
+ * GENERATED CODE - DO NOT MODIFY
+ */
+import express from 'express'
+import { ValidationResult, BlobRef } from '@atproto/lexicon'
+import { lexicons } from '../../../../lexicons'
+import { isObj, hasProp } from '../../../../util'
+import { CID } from 'multiformats/cid'
+import { HandlerAuth } from '@atproto/xrpc-server'
+
+export interface QueryParams {}
+
+export type InputSchema = undefined
+export type HandlerInput = undefined
+
+export interface HandlerError {
+ status: number
+ message?: string
+}
+
+export type HandlerOutput = HandlerError | void
+export type HandlerReqCtx = {
+ auth: HA
+ params: QueryParams
+ input: HandlerInput
+ req: express.Request
+ res: express.Response
+}
+export type Handler = (
+ ctx: HandlerReqCtx,
+) => Promise | HandlerOutput
diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts b/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts
new file mode 100644
index 00000000000..c2c99ea01c8
--- /dev/null
+++ b/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts
@@ -0,0 +1,41 @@
+/**
+ * GENERATED CODE - DO NOT MODIFY
+ */
+import express from 'express'
+import { ValidationResult, BlobRef } from '@atproto/lexicon'
+import { lexicons } from '../../../../lexicons'
+import { isObj, hasProp } from '../../../../util'
+import { CID } from 'multiformats/cid'
+import { HandlerAuth } from '@atproto/xrpc-server'
+
+export interface QueryParams {}
+
+export interface InputSchema {
+ email: string
+ /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */
+ token?: string
+ [k: string]: unknown
+}
+
+export interface HandlerInput {
+ encoding: 'application/json'
+ body: InputSchema
+}
+
+export interface HandlerError {
+ status: number
+ message?: string
+ error?: 'ExpiredToken' | 'InvalidToken'
+}
+
+export type HandlerOutput = HandlerError | void
+export type HandlerReqCtx = {
+ auth: HA
+ params: QueryParams
+ input: HandlerInput
+ req: express.Request
+ res: express.Response
+}
+export type Handler = (
+ ctx: HandlerReqCtx,
+) => Promise | HandlerOutput
diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts
index a99d4d6e51b..be1b593b5ef 100644
--- a/packages/pds/src/lexicon/index.ts
+++ b/packages/pds/src/lexicon/index.ts
@@ -39,6 +39,7 @@ import * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord'
import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords'
import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord'
import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob'
+import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail'
import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount'
import * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword'
import * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode'
@@ -52,9 +53,12 @@ import * as ComAtprotoServerGetSession from './types/com/atproto/server/getSessi
import * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords'
import * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession'
import * as ComAtprotoServerRequestAccountDelete from './types/com/atproto/server/requestAccountDelete'
+import * as ComAtprotoServerRequestEmailConfirmation from './types/com/atproto/server/requestEmailConfirmation'
+import * as ComAtprotoServerRequestEmailUpdate from './types/com/atproto/server/requestEmailUpdate'
import * as ComAtprotoServerRequestPasswordReset from './types/com/atproto/server/requestPasswordReset'
import * as ComAtprotoServerResetPassword from './types/com/atproto/server/resetPassword'
import * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/revokeAppPassword'
+import * as ComAtprotoServerUpdateEmail from './types/com/atproto/server/updateEmail'
import * as ComAtprotoSyncGetBlob from './types/com/atproto/sync/getBlob'
import * as ComAtprotoSyncGetBlocks from './types/com/atproto/sync/getBlocks'
import * as ComAtprotoSyncGetCheckout from './types/com/atproto/sync/getCheckout'
@@ -554,6 +558,17 @@ export class ServerNS {
this._server = server
}
+ confirmEmail(
+ cfg: ConfigOf<
+ AV,
+ ComAtprotoServerConfirmEmail.Handler>,
+ ComAtprotoServerConfirmEmail.HandlerReqCtx>
+ >,
+ ) {
+ const nsid = 'com.atproto.server.confirmEmail' // @ts-ignore
+ return this._server.xrpc.method(nsid, cfg)
+ }
+
createAccount(
cfg: ConfigOf<
AV,
@@ -697,6 +712,28 @@ export class ServerNS {
return this._server.xrpc.method(nsid, cfg)
}
+ requestEmailConfirmation(
+ cfg: ConfigOf<
+ AV,
+ ComAtprotoServerRequestEmailConfirmation.Handler>,
+ ComAtprotoServerRequestEmailConfirmation.HandlerReqCtx>
+ >,
+ ) {
+ const nsid = 'com.atproto.server.requestEmailConfirmation' // @ts-ignore
+ return this._server.xrpc.method(nsid, cfg)
+ }
+
+ requestEmailUpdate(
+ cfg: ConfigOf<
+ AV,
+ ComAtprotoServerRequestEmailUpdate.Handler>,
+ ComAtprotoServerRequestEmailUpdate.HandlerReqCtx>
+ >,
+ ) {
+ const nsid = 'com.atproto.server.requestEmailUpdate' // @ts-ignore
+ return this._server.xrpc.method(nsid, cfg)
+ }
+
requestPasswordReset(
cfg: ConfigOf<
AV,
@@ -729,6 +766,17 @@ export class ServerNS {
const nsid = 'com.atproto.server.revokeAppPassword' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
+
+ updateEmail(
+ cfg: ConfigOf<
+ AV,
+ ComAtprotoServerUpdateEmail.Handler>,
+ ComAtprotoServerUpdateEmail.HandlerReqCtx>
+ >,
+ ) {
+ const nsid = 'com.atproto.server.updateEmail' // @ts-ignore
+ return this._server.xrpc.method(nsid, cfg)
+ }
}
export class SyncNS {
diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts
index e15d7aba1ce..17ad93b38c7 100644
--- a/packages/pds/src/lexicon/lexicons.ts
+++ b/packages/pds/src/lexicon/lexicons.ts
@@ -2238,6 +2238,40 @@ export const schemaDict = {
},
},
},
+ ComAtprotoServerConfirmEmail: {
+ lexicon: 1,
+ id: 'com.atproto.server.confirmEmail',
+ defs: {
+ main: {
+ type: 'procedure',
+ description:
+ 'Confirm an email using a token from com.atproto.server.requestEmailConfirmation.',
+ input: {
+ encoding: 'application/json',
+ schema: {
+ type: 'object',
+ required: ['email', 'token'],
+ properties: {
+ email: {
+ type: 'string',
+ },
+ token: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ errors: [
+ {
+ name: 'ExpiredToken',
+ },
+ {
+ name: 'InvalidToken',
+ },
+ ],
+ },
+ },
+ },
ComAtprotoServerCreateAccount: {
lexicon: 1,
id: 'com.atproto.server.createAccount',
@@ -2850,6 +2884,27 @@ export const schemaDict = {
},
},
},
+ ComAtprotoServerRequestEmailConfirmation: {
+ lexicon: 1,
+ id: 'com.atproto.server.requestEmailConfirmation',
+ defs: {
+ main: {
+ type: 'procedure',
+ description:
+ 'Request an email with a code to confirm ownership of email',
+ },
+ },
+ },
+ ComAtprotoServerRequestEmailUpdate: {
+ lexicon: 1,
+ id: 'com.atproto.server.requestEmailUpdate',
+ defs: {
+ main: {
+ type: 'procedure',
+ description: 'Request a token in order to update email.',
+ },
+ },
+ },
ComAtprotoServerRequestPasswordReset: {
lexicon: 1,
id: 'com.atproto.server.requestPasswordReset',
@@ -2927,6 +2982,41 @@ export const schemaDict = {
},
},
},
+ ComAtprotoServerUpdateEmail: {
+ lexicon: 1,
+ id: 'com.atproto.server.updateEmail',
+ defs: {
+ main: {
+ type: 'procedure',
+ description: "Update an account's email.",
+ input: {
+ encoding: 'application/json',
+ schema: {
+ type: 'object',
+ required: ['email'],
+ properties: {
+ email: {
+ type: 'string',
+ },
+ token: {
+ type: 'string',
+ description:
+ " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.",
+ },
+ },
+ },
+ },
+ errors: [
+ {
+ name: 'ExpiredToken',
+ },
+ {
+ name: 'InvalidToken',
+ },
+ ],
+ },
+ },
+ },
ComAtprotoSyncGetBlob: {
lexicon: 1,
id: 'com.atproto.sync.getBlob',
@@ -6715,6 +6805,7 @@ export const ids = {
ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord',
ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',
ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob',
+ ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail',
ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount',
ComAtprotoServerCreateAppPassword: 'com.atproto.server.createAppPassword',
ComAtprotoServerCreateInviteCode: 'com.atproto.server.createInviteCode',
@@ -6731,10 +6822,14 @@ export const ids = {
ComAtprotoServerRefreshSession: 'com.atproto.server.refreshSession',
ComAtprotoServerRequestAccountDelete:
'com.atproto.server.requestAccountDelete',
+ ComAtprotoServerRequestEmailConfirmation:
+ 'com.atproto.server.requestEmailConfirmation',
+ ComAtprotoServerRequestEmailUpdate: 'com.atproto.server.requestEmailUpdate',
ComAtprotoServerRequestPasswordReset:
'com.atproto.server.requestPasswordReset',
ComAtprotoServerResetPassword: 'com.atproto.server.resetPassword',
ComAtprotoServerRevokeAppPassword: 'com.atproto.server.revokeAppPassword',
+ ComAtprotoServerUpdateEmail: 'com.atproto.server.updateEmail',
ComAtprotoSyncGetBlob: 'com.atproto.sync.getBlob',
ComAtprotoSyncGetBlocks: 'com.atproto.sync.getBlocks',
ComAtprotoSyncGetCheckout: 'com.atproto.sync.getCheckout',
diff --git a/packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts b/packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts
new file mode 100644
index 00000000000..6d2d89db7d0
--- /dev/null
+++ b/packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts
@@ -0,0 +1,40 @@
+/**
+ * GENERATED CODE - DO NOT MODIFY
+ */
+import express from 'express'
+import { ValidationResult, BlobRef } from '@atproto/lexicon'
+import { lexicons } from '../../../../lexicons'
+import { isObj, hasProp } from '../../../../util'
+import { CID } from 'multiformats/cid'
+import { HandlerAuth } from '@atproto/xrpc-server'
+
+export interface QueryParams {}
+
+export interface InputSchema {
+ email: string
+ token: string
+ [k: string]: unknown
+}
+
+export interface HandlerInput {
+ encoding: 'application/json'
+ body: InputSchema
+}
+
+export interface HandlerError {
+ status: number
+ message?: string
+ error?: 'ExpiredToken' | 'InvalidToken'
+}
+
+export type HandlerOutput = HandlerError | void
+export type HandlerReqCtx = {
+ auth: HA
+ params: QueryParams
+ input: HandlerInput
+ req: express.Request
+ res: express.Response
+}
+export type Handler = (
+ ctx: HandlerReqCtx,
+) => Promise | HandlerOutput
diff --git a/packages/pds/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts b/packages/pds/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts
new file mode 100644
index 00000000000..e4244870425
--- /dev/null
+++ b/packages/pds/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts
@@ -0,0 +1,31 @@
+/**
+ * GENERATED CODE - DO NOT MODIFY
+ */
+import express from 'express'
+import { ValidationResult, BlobRef } from '@atproto/lexicon'
+import { lexicons } from '../../../../lexicons'
+import { isObj, hasProp } from '../../../../util'
+import { CID } from 'multiformats/cid'
+import { HandlerAuth } from '@atproto/xrpc-server'
+
+export interface QueryParams {}
+
+export type InputSchema = undefined
+export type HandlerInput = undefined
+
+export interface HandlerError {
+ status: number
+ message?: string
+}
+
+export type HandlerOutput = HandlerError | void
+export type HandlerReqCtx = {
+ auth: HA
+ params: QueryParams
+ input: HandlerInput
+ req: express.Request
+ res: express.Response
+}
+export type Handler = (
+ ctx: HandlerReqCtx,
+) => Promise | HandlerOutput
diff --git a/packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts b/packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts
new file mode 100644
index 00000000000..e4244870425
--- /dev/null
+++ b/packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts
@@ -0,0 +1,31 @@
+/**
+ * GENERATED CODE - DO NOT MODIFY
+ */
+import express from 'express'
+import { ValidationResult, BlobRef } from '@atproto/lexicon'
+import { lexicons } from '../../../../lexicons'
+import { isObj, hasProp } from '../../../../util'
+import { CID } from 'multiformats/cid'
+import { HandlerAuth } from '@atproto/xrpc-server'
+
+export interface QueryParams {}
+
+export type InputSchema = undefined
+export type HandlerInput = undefined
+
+export interface HandlerError {
+ status: number
+ message?: string
+}
+
+export type HandlerOutput = HandlerError | void
+export type HandlerReqCtx = {
+ auth: HA
+ params: QueryParams
+ input: HandlerInput
+ req: express.Request
+ res: express.Response
+}
+export type Handler = (
+ ctx: HandlerReqCtx,
+) => Promise | HandlerOutput
diff --git a/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts b/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts
new file mode 100644
index 00000000000..c2c99ea01c8
--- /dev/null
+++ b/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts
@@ -0,0 +1,41 @@
+/**
+ * GENERATED CODE - DO NOT MODIFY
+ */
+import express from 'express'
+import { ValidationResult, BlobRef } from '@atproto/lexicon'
+import { lexicons } from '../../../../lexicons'
+import { isObj, hasProp } from '../../../../util'
+import { CID } from 'multiformats/cid'
+import { HandlerAuth } from '@atproto/xrpc-server'
+
+export interface QueryParams {}
+
+export interface InputSchema {
+ email: string
+ /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */
+ token?: string
+ [k: string]: unknown
+}
+
+export interface HandlerInput {
+ encoding: 'application/json'
+ body: InputSchema
+}
+
+export interface HandlerError {
+ status: number
+ message?: string
+ error?: 'ExpiredToken' | 'InvalidToken'
+}
+
+export type HandlerOutput = HandlerError | void
+export type HandlerReqCtx = {
+ auth: HA
+ params: QueryParams
+ input: HandlerInput
+ req: express.Request
+ res: express.Response
+}
+export type Handler = (
+ ctx: HandlerReqCtx,
+) => Promise | HandlerOutput
From ea1e6cceaadd20ed0ef57e00115775d86ddc340c Mon Sep 17 00:00:00 2001
From: dholms
Date: Fri, 8 Sep 2023 16:46:20 -0500
Subject: [PATCH 03/18] email templates
---
packages/pds/src/mailer/index.ts | 16 +
.../src/mailer/templates/confirm-email.hbs | 382 ++++++++++++++++++
.../pds/src/mailer/templates/update-email.hbs | 381 +++++++++++++++++
3 files changed, 779 insertions(+)
create mode 100644 packages/pds/src/mailer/templates/confirm-email.hbs
create mode 100644 packages/pds/src/mailer/templates/update-email.hbs
diff --git a/packages/pds/src/mailer/index.ts b/packages/pds/src/mailer/index.ts
index 99059f6f02e..6c77fc8964c 100644
--- a/packages/pds/src/mailer/index.ts
+++ b/packages/pds/src/mailer/index.ts
@@ -24,6 +24,8 @@ export class ServerMailer {
this.templates = {
resetPassword: this.compile('reset-password'),
deleteAccount: this.compile('delete-account'),
+ confirmEmail: this.compile('confirm-email'),
+ updateEmail: this.compile('update-email'),
}
}
@@ -51,6 +53,20 @@ export class ServerMailer {
})
}
+ async sendConfirmEmail(params: { token: string }, mailOpts: Mail.Options) {
+ return this.sendTemplate('confirmEmail', params, {
+ subject: 'Email Confirmation',
+ ...mailOpts,
+ })
+ }
+
+ async sendUpdateEmail(params: { token: string }, mailOpts: Mail.Options) {
+ return this.sendTemplate('updateEmail', params, {
+ subject: 'Email Update Requested',
+ ...mailOpts,
+ })
+ }
+
private async sendTemplate(templateName, params, mailOpts: Mail.Options) {
const html = this.templates[templateName]({
...params,
diff --git a/packages/pds/src/mailer/templates/confirm-email.hbs b/packages/pds/src/mailer/templates/confirm-email.hbs
new file mode 100644
index 00000000000..cf0f123b0f6
--- /dev/null
+++ b/packages/pds/src/mailer/templates/confirm-email.hbs
@@ -0,0 +1,382 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Confirm your Bluesky email
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{token}}
+ |
+
+
+
+ |
+
+
+
+
+
+ To confirm this email for
+ your account, please enter the
+ above code in the app.
+
+ |
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/pds/src/mailer/templates/update-email.hbs b/packages/pds/src/mailer/templates/update-email.hbs
new file mode 100644
index 00000000000..a7c22fd5ca6
--- /dev/null
+++ b/packages/pds/src/mailer/templates/update-email.hbs
@@ -0,0 +1,381 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Update your Bluesky email
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{token}}
+ |
+
+
+
+ |
+
+
+
+
+
+ To the email for your account, please enter the
+ above code alongside your new email in the app.
+
+ |
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
\ No newline at end of file
From c4cf9fab664bcb226d6e5e9f9ef7a3e4a66eb24a Mon Sep 17 00:00:00 2001
From: dholms
Date: Fri, 8 Sep 2023 16:50:21 -0500
Subject: [PATCH 04/18] request routes
---
.../atproto/server/requestEmailUpdate.json | 12 +++++-
packages/api/src/client/lexicons.ts | 12 ++++++
.../com/atproto/server/requestEmailUpdate.ts | 6 +++
packages/bsky/src/lexicon/lexicons.ts | 12 ++++++
.../com/atproto/server/requestEmailUpdate.ts | 14 ++++++-
.../atproto/server/requestAccountDelete.ts | 4 +-
.../server/requestEmailConfirmation.ts | 27 +++++++++++++
.../com/atproto/server/requestEmailUpdate.ts | 38 +++++++++++++++++++
packages/pds/src/db/database-schema.ts | 2 +
packages/pds/src/db/tables/email-token.ts | 14 +++++++
packages/pds/src/db/tables/user-account.ts | 1 +
packages/pds/src/lexicon/lexicons.ts | 12 ++++++
.../com/atproto/server/requestEmailUpdate.ts | 14 ++++++-
13 files changed, 163 insertions(+), 5 deletions(-)
create mode 100644 packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts
create mode 100644 packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts
create mode 100644 packages/pds/src/db/tables/email-token.ts
diff --git a/lexicons/com/atproto/server/requestEmailUpdate.json b/lexicons/com/atproto/server/requestEmailUpdate.json
index 37c435d72e5..4cc1a86f612 100644
--- a/lexicons/com/atproto/server/requestEmailUpdate.json
+++ b/lexicons/com/atproto/server/requestEmailUpdate.json
@@ -4,7 +4,17 @@
"defs": {
"main": {
"type": "procedure",
- "description": "Request a token in order to update email."
+ "description": "Request a token in order to update email.",
+ "output": {
+ "encoding": "application/json",
+ "schema": {
+ "type": "object",
+ "required": ["tokenRequired"],
+ "properties": {
+ "tokenRequired": { "type": "boolean" }
+ }
+ }
+ }
}
}
}
diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts
index 17ad93b38c7..ff72487df77 100644
--- a/packages/api/src/client/lexicons.ts
+++ b/packages/api/src/client/lexicons.ts
@@ -2902,6 +2902,18 @@ export const schemaDict = {
main: {
type: 'procedure',
description: 'Request a token in order to update email.',
+ output: {
+ encoding: 'application/json',
+ schema: {
+ type: 'object',
+ required: ['tokenRequired'],
+ properties: {
+ tokenRequired: {
+ type: 'boolean',
+ },
+ },
+ },
+ },
},
},
},
diff --git a/packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts b/packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts
index ef2ed1ac47c..30d84002cf2 100644
--- a/packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts
+++ b/packages/api/src/client/types/com/atproto/server/requestEmailUpdate.ts
@@ -11,6 +11,11 @@ export interface QueryParams {}
export type InputSchema = undefined
+export interface OutputSchema {
+ tokenRequired: boolean
+ [k: string]: unknown
+}
+
export interface CallOptions {
headers?: Headers
qp?: QueryParams
@@ -19,6 +24,7 @@ export interface CallOptions {
export interface Response {
success: boolean
headers: Headers
+ data: OutputSchema
}
export function toKnownErr(e: any) {
diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts
index 17ad93b38c7..ff72487df77 100644
--- a/packages/bsky/src/lexicon/lexicons.ts
+++ b/packages/bsky/src/lexicon/lexicons.ts
@@ -2902,6 +2902,18 @@ export const schemaDict = {
main: {
type: 'procedure',
description: 'Request a token in order to update email.',
+ output: {
+ encoding: 'application/json',
+ schema: {
+ type: 'object',
+ required: ['tokenRequired'],
+ properties: {
+ tokenRequired: {
+ type: 'boolean',
+ },
+ },
+ },
+ },
},
},
},
diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts
index e4244870425..6876d44ca46 100644
--- a/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts
+++ b/packages/bsky/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts
@@ -11,14 +11,26 @@ import { HandlerAuth } from '@atproto/xrpc-server'
export interface QueryParams {}
export type InputSchema = undefined
+
+export interface OutputSchema {
+ tokenRequired: boolean
+ [k: string]: unknown
+}
+
export type HandlerInput = undefined
+export interface HandlerSuccess {
+ encoding: 'application/json'
+ body: OutputSchema
+ headers?: { [key: string]: string }
+}
+
export interface HandlerError {
status: number
message?: string
}
-export type HandlerOutput = HandlerError | void
+export type HandlerOutput = HandlerError | HandlerSuccess
export type HandlerReqCtx = {
auth: HA
params: QueryParams
diff --git a/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts b/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts
index 61870c8c3ca..a448d97c02e 100644
--- a/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts
+++ b/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts
@@ -8,12 +8,12 @@ export default function (server: Server, ctx: AppContext) {
auth: ctx.accessVerifierCheckTakedown,
handler: async ({ auth }) => {
const did = auth.credentials.did
- const token = getRandomToken().toUpperCase()
- const requestedAt = new Date().toISOString()
const user = await ctx.services.account(ctx.db).getAccount(did)
if (!user) {
throw new InvalidRequestError('user not found')
}
+ const token = getRandomToken().toUpperCase()
+ const requestedAt = new Date().toISOString()
await ctx.db.db
.insertInto('delete_account_token')
.values({ did, token, requestedAt })
diff --git a/packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts b/packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts
new file mode 100644
index 00000000000..3d2299bd28b
--- /dev/null
+++ b/packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts
@@ -0,0 +1,27 @@
+import { InvalidRequestError } from '@atproto/xrpc-server'
+import { Server } from '../../../../lexicon'
+import AppContext from '../../../../context'
+import { getRandomToken } from './util'
+
+export default function (server: Server, ctx: AppContext) {
+ server.com.atproto.server.requestEmailConfirmation({
+ auth: ctx.accessVerifierCheckTakedown,
+ handler: async ({ auth }) => {
+ const did = auth.credentials.did
+ const user = await ctx.services.account(ctx.db).getAccount(did)
+ if (!user) {
+ throw new InvalidRequestError('user not found')
+ }
+ const token = getRandomToken().toUpperCase()
+ const requestedAt = new Date().toISOString()
+ await ctx.db.db
+ .insertInto('email_token')
+ .values({ purpose: 'confirm_email', did, token, requestedAt })
+ .onConflict((oc) =>
+ oc.columns(['purpose', 'did']).doUpdateSet({ token, requestedAt }),
+ )
+ .execute()
+ await ctx.mailer.sendConfirmEmail({ token }, { to: user.email })
+ },
+ })
+}
diff --git a/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts b/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts
new file mode 100644
index 00000000000..e2fa5f36b7a
--- /dev/null
+++ b/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts
@@ -0,0 +1,38 @@
+import { InvalidRequestError } from '@atproto/xrpc-server'
+import { Server } from '../../../../lexicon'
+import AppContext from '../../../../context'
+import { getRandomToken } from './util'
+
+export default function (server: Server, ctx: AppContext) {
+ server.com.atproto.server.requestEmailUpdate({
+ auth: ctx.accessVerifierCheckTakedown,
+ handler: async ({ auth }) => {
+ const did = auth.credentials.did
+ const user = await ctx.services.account(ctx.db).getAccount(did)
+ if (!user) {
+ throw new InvalidRequestError('user not found')
+ }
+
+ const tokenRequired = user.emailConfirmedAt !== null
+ if (tokenRequired) {
+ const token = getRandomToken().toUpperCase()
+ const requestedAt = new Date().toISOString()
+ await ctx.db.db
+ .insertInto('email_token')
+ .values({ purpose: 'update_email', did, token, requestedAt })
+ .onConflict((oc) =>
+ oc.columns(['purpose', 'did']).doUpdateSet({ token, requestedAt }),
+ )
+ .execute()
+ await ctx.mailer.sendUpdateEmail({ token }, { to: user.email })
+ }
+
+ return {
+ encoding: 'application/json',
+ body: {
+ tokenRequired,
+ },
+ }
+ },
+ })
+}
diff --git a/packages/pds/src/db/database-schema.ts b/packages/pds/src/db/database-schema.ts
index f77df6b21ad..bebb13eb84d 100644
--- a/packages/pds/src/db/database-schema.ts
+++ b/packages/pds/src/db/database-schema.ts
@@ -15,6 +15,7 @@ import * as notification from './tables/user-notification'
import * as blob from './tables/blob'
import * as repoBlob from './tables/repo-blob'
import * as deleteAccountToken from './tables/delete-account-token'
+import * as emailToken from './tables/email-token'
import * as moderation from './tables/moderation'
import * as mute from './tables/mute'
import * as listMute from './tables/list-mute'
@@ -43,6 +44,7 @@ export type DatabaseSchemaType = appView.DatabaseSchemaType &
blob.PartialDB &
repoBlob.PartialDB &
deleteAccountToken.PartialDB &
+ emailToken.PartialDB &
moderation.PartialDB &
mute.PartialDB &
listMute.PartialDB &
diff --git a/packages/pds/src/db/tables/email-token.ts b/packages/pds/src/db/tables/email-token.ts
new file mode 100644
index 00000000000..01679d83e97
--- /dev/null
+++ b/packages/pds/src/db/tables/email-token.ts
@@ -0,0 +1,14 @@
+export interface EmailToken {
+ purpose:
+ | 'confirm_email'
+ | 'update_email'
+ | 'reset_password'
+ | 'delete_account'
+ did: string
+ token: string
+ requestedAt: string
+}
+
+export const tableName = 'email_token'
+
+export type PartialDB = { [tableName]: EmailToken }
diff --git a/packages/pds/src/db/tables/user-account.ts b/packages/pds/src/db/tables/user-account.ts
index 665521efc08..ef9fdbecb3c 100644
--- a/packages/pds/src/db/tables/user-account.ts
+++ b/packages/pds/src/db/tables/user-account.ts
@@ -5,6 +5,7 @@ export interface UserAccount {
email: string
passwordScrypt: string
createdAt: string
+ emailConfirmedAt: string | null
passwordResetToken: string | null
passwordResetGrantedAt: string | null
invitesDisabled: Generated<0 | 1>
diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts
index 17ad93b38c7..ff72487df77 100644
--- a/packages/pds/src/lexicon/lexicons.ts
+++ b/packages/pds/src/lexicon/lexicons.ts
@@ -2902,6 +2902,18 @@ export const schemaDict = {
main: {
type: 'procedure',
description: 'Request a token in order to update email.',
+ output: {
+ encoding: 'application/json',
+ schema: {
+ type: 'object',
+ required: ['tokenRequired'],
+ properties: {
+ tokenRequired: {
+ type: 'boolean',
+ },
+ },
+ },
+ },
},
},
},
diff --git a/packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts b/packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts
index e4244870425..6876d44ca46 100644
--- a/packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts
+++ b/packages/pds/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts
@@ -11,14 +11,26 @@ import { HandlerAuth } from '@atproto/xrpc-server'
export interface QueryParams {}
export type InputSchema = undefined
+
+export interface OutputSchema {
+ tokenRequired: boolean
+ [k: string]: unknown
+}
+
export type HandlerInput = undefined
+export interface HandlerSuccess {
+ encoding: 'application/json'
+ body: OutputSchema
+ headers?: { [key: string]: string }
+}
+
export interface HandlerError {
status: number
message?: string
}
-export type HandlerOutput = HandlerError | void
+export type HandlerOutput = HandlerError | HandlerSuccess
export type HandlerReqCtx = {
auth: HA
params: QueryParams
From 57aec2267d6359aeafb0b672873dc590f510ff6e Mon Sep 17 00:00:00 2001
From: dholms
Date: Fri, 8 Sep 2023 17:19:09 -0500
Subject: [PATCH 05/18] impl
---
packages/common-web/src/times.ts | 4 ++
.../api/com/atproto/server/confirmEmail.ts | 25 ++++++++++
.../pds/src/api/com/atproto/server/index.ts | 10 ++++
.../server/requestEmailConfirmation.ts | 13 ++---
.../com/atproto/server/requestEmailUpdate.ts | 13 ++---
.../src/api/com/atproto/server/updateEmail.ts | 42 +++++++++++++++++
packages/pds/src/db/tables/email-token.ts | 16 ++++---
packages/pds/src/services/account/index.ts | 47 ++++++++++++++++++-
8 files changed, 143 insertions(+), 27 deletions(-)
create mode 100644 packages/pds/src/api/com/atproto/server/confirmEmail.ts
create mode 100644 packages/pds/src/api/com/atproto/server/updateEmail.ts
diff --git a/packages/common-web/src/times.ts b/packages/common-web/src/times.ts
index 09bb26efc97..90366277fdf 100644
--- a/packages/common-web/src/times.ts
+++ b/packages/common-web/src/times.ts
@@ -2,3 +2,7 @@ export const SECOND = 1000
export const MINUTE = SECOND * 60
export const HOUR = MINUTE * 60
export const DAY = HOUR * 24
+
+export const lessThanAgoMs = (time: Date, range: number) => {
+ return Date.now() < time.getTime() + range
+}
diff --git a/packages/pds/src/api/com/atproto/server/confirmEmail.ts b/packages/pds/src/api/com/atproto/server/confirmEmail.ts
new file mode 100644
index 00000000000..3e42931ae0f
--- /dev/null
+++ b/packages/pds/src/api/com/atproto/server/confirmEmail.ts
@@ -0,0 +1,25 @@
+import { Server } from '../../../../lexicon'
+import AppContext from '../../../../context'
+
+export default function (server: Server, ctx: AppContext) {
+ server.com.atproto.server.confirmEmail({
+ auth: ctx.accessVerifierCheckTakedown,
+ handler: async ({ auth, input }) => {
+ const did = auth.credentials.did
+ const { token } = input.body
+
+ await ctx.services
+ .account(ctx.db)
+ .assertValidToken(did, 'confirm_email', token)
+
+ await ctx.db.transaction(async (dbTxn) => {
+ await ctx.services.account(dbTxn).deleteEmailToken(did, 'confirm_email')
+ await dbTxn.db
+ .updateTable('user_account')
+ .set({ emailConfirmedAt: new Date().toISOString() })
+ .where('did', '=', did)
+ .execute()
+ })
+ },
+ })
+}
diff --git a/packages/pds/src/api/com/atproto/server/index.ts b/packages/pds/src/api/com/atproto/server/index.ts
index 9a49216f71c..210d0f45461 100644
--- a/packages/pds/src/api/com/atproto/server/index.ts
+++ b/packages/pds/src/api/com/atproto/server/index.ts
@@ -14,6 +14,12 @@ import deleteAccount from './deleteAccount'
import requestPasswordReset from './requestPasswordReset'
import resetPassword from './resetPassword'
+import requestEmailConfirmation from './requestEmailConfirmation'
+import confirmEmail from './confirmEmail'
+
+import requestEmailUpdate from './requestEmailUpdate'
+import updateEmail from './updateEmail'
+
import createSession from './createSession'
import deleteSession from './deleteSession'
import getSession from './getSession'
@@ -33,6 +39,10 @@ export default function (server: Server, ctx: AppContext) {
deleteAccount(server, ctx)
requestPasswordReset(server, ctx)
resetPassword(server, ctx)
+ requestEmailConfirmation(server, ctx)
+ confirmEmail(server, ctx)
+ requestEmailUpdate(server, ctx)
+ updateEmail(server, ctx)
createSession(server, ctx)
deleteSession(server, ctx)
getSession(server, ctx)
diff --git a/packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts b/packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts
index 3d2299bd28b..aa7b632569e 100644
--- a/packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts
+++ b/packages/pds/src/api/com/atproto/server/requestEmailConfirmation.ts
@@ -1,7 +1,6 @@
import { InvalidRequestError } from '@atproto/xrpc-server'
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
-import { getRandomToken } from './util'
export default function (server: Server, ctx: AppContext) {
server.com.atproto.server.requestEmailConfirmation({
@@ -12,15 +11,9 @@ export default function (server: Server, ctx: AppContext) {
if (!user) {
throw new InvalidRequestError('user not found')
}
- const token = getRandomToken().toUpperCase()
- const requestedAt = new Date().toISOString()
- await ctx.db.db
- .insertInto('email_token')
- .values({ purpose: 'confirm_email', did, token, requestedAt })
- .onConflict((oc) =>
- oc.columns(['purpose', 'did']).doUpdateSet({ token, requestedAt }),
- )
- .execute()
+ const token = await ctx.services
+ .account(ctx.db)
+ .createEmailToken(did, 'confirm_email')
await ctx.mailer.sendConfirmEmail({ token }, { to: user.email })
},
})
diff --git a/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts b/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts
index e2fa5f36b7a..e38b69a6d7e 100644
--- a/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts
+++ b/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts
@@ -1,7 +1,6 @@
import { InvalidRequestError } from '@atproto/xrpc-server'
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
-import { getRandomToken } from './util'
export default function (server: Server, ctx: AppContext) {
server.com.atproto.server.requestEmailUpdate({
@@ -15,15 +14,9 @@ export default function (server: Server, ctx: AppContext) {
const tokenRequired = user.emailConfirmedAt !== null
if (tokenRequired) {
- const token = getRandomToken().toUpperCase()
- const requestedAt = new Date().toISOString()
- await ctx.db.db
- .insertInto('email_token')
- .values({ purpose: 'update_email', did, token, requestedAt })
- .onConflict((oc) =>
- oc.columns(['purpose', 'did']).doUpdateSet({ token, requestedAt }),
- )
- .execute()
+ const token = await ctx.services
+ .account(ctx.db)
+ .createEmailToken(did, 'update_email')
await ctx.mailer.sendUpdateEmail({ token }, { to: user.email })
}
diff --git a/packages/pds/src/api/com/atproto/server/updateEmail.ts b/packages/pds/src/api/com/atproto/server/updateEmail.ts
new file mode 100644
index 00000000000..d89c45cbbac
--- /dev/null
+++ b/packages/pds/src/api/com/atproto/server/updateEmail.ts
@@ -0,0 +1,42 @@
+import { Server } from '../../../../lexicon'
+import AppContext from '../../../../context'
+import { InvalidRequestError } from '@atproto/xrpc-server'
+
+export default function (server: Server, ctx: AppContext) {
+ server.com.atproto.server.updateEmail({
+ auth: ctx.accessVerifierCheckTakedown,
+ handler: async ({ auth, input }) => {
+ const did = auth.credentials.did
+ const { token, email } = input.body
+ const user = await ctx.services.account(ctx.db).getAccount(did)
+ if (!user) {
+ throw new InvalidRequestError('user not found')
+ }
+ // require valid token
+ if (user.emailConfirmedAt !== null) {
+ if (!token) {
+ throw new InvalidRequestError(
+ 'confirmation token required',
+ 'TokenRequired',
+ )
+ }
+ await ctx.services
+ .account(ctx.db)
+ .assertValidToken(did, 'confirm_email', token)
+ }
+
+ await ctx.db.transaction(async (dbTxn) => {
+ if (token) {
+ await ctx.services
+ .account(dbTxn)
+ .deleteEmailToken(did, 'update_email')
+ }
+ await dbTxn.db
+ .updateTable('user_account')
+ .set({ email, emailConfirmedAt: null })
+ .where('did', '=', did)
+ .execute()
+ })
+ },
+ })
+}
diff --git a/packages/pds/src/db/tables/email-token.ts b/packages/pds/src/db/tables/email-token.ts
index 01679d83e97..d962bbf1cc5 100644
--- a/packages/pds/src/db/tables/email-token.ts
+++ b/packages/pds/src/db/tables/email-token.ts
@@ -1,12 +1,16 @@
+import { Generated } from 'kysely'
+
+export type EmailTokenPurpose =
+ | 'confirm_email'
+ | 'update_email'
+ | 'reset_password'
+ | 'delete_account'
+
export interface EmailToken {
- purpose:
- | 'confirm_email'
- | 'update_email'
- | 'reset_password'
- | 'delete_account'
+ purpose: EmailTokenPurpose
did: string
token: string
- requestedAt: string
+ requestedAt: Generated
}
export const tableName = 'email_token'
diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts
index dd28af01df5..3d955f8d1c9 100644
--- a/packages/pds/src/services/account/index.ts
+++ b/packages/pds/src/services/account/index.ts
@@ -12,7 +12,9 @@ import * as sequencer from '../../sequencer'
import { AppPassword } from '../../lexicon/types/com/atproto/server/createAppPassword'
import { randomStr } from '@atproto/crypto'
import { InvalidRequestError } from '@atproto/xrpc-server'
-import { NotEmptyArray } from '@atproto/common'
+import { MINUTE, NotEmptyArray, lessThanAgoMs } from '@atproto/common'
+import { EmailTokenPurpose } from '../../db/tables/email-token'
+import { getRandomToken } from '../../api/com/atproto/server/util'
export class AccountService {
constructor(public db: Database) {}
@@ -555,6 +557,49 @@ export class AccountService {
}, {} as Record)
}
+ async createEmailToken(
+ did: string,
+ purpose: EmailTokenPurpose,
+ ): Promise {
+ const token = getRandomToken().toUpperCase()
+ await this.db.db
+ .insertInto('email_token')
+ .values({ purpose, did, token })
+ .onConflict((oc) => oc.columns(['purpose', 'did']).doUpdateSet({ token }))
+ .execute()
+ return token
+ }
+
+ async deleteEmailToken(did: string, purpose: EmailTokenPurpose) {
+ await this.db.db
+ .deleteFrom('email_token')
+ .where('did', '=', did)
+ .where('purpose', '=', purpose)
+ .executeTakeFirst()
+ }
+
+ async assertValidToken(
+ did: string,
+ purpose: EmailTokenPurpose,
+ token: string,
+ expirationLen = 15 * MINUTE,
+ ) {
+ const res = await this.db.db
+ .selectFrom('email_token')
+ .selectAll()
+ .where('purpose', '=', purpose)
+ .where('did', '=', did)
+ .where('token', '=', token)
+ .executeTakeFirst()
+ if (!res) {
+ throw new InvalidRequestError('Token is invalid', 'InvalidToken')
+ }
+ const expired = !lessThanAgoMs(res.requestedAt, expirationLen)
+ if (expired) {
+ throw new InvalidRequestError('Token is expired', 'ExpiredToken')
+ }
+ }
+
async getLastSeenNotifs(did: string): Promise {
const res = await this.db.db
.selectFrom('user_state')
From 5dfd97f81dd6247ef18aa423de930a2e65e7f606 Mon Sep 17 00:00:00 2001
From: dholms
Date: Fri, 8 Sep 2023 17:50:26 -0500
Subject: [PATCH 06/18] migration
---
.../20230908T224408678Z-email-tokens.ts | 18 ++++++++++++++++++
packages/pds/src/db/migrations/index.ts | 1 +
packages/pds/src/db/tables/email-token.ts | 4 +---
packages/pds/src/services/account/index.ts | 2 +-
4 files changed, 21 insertions(+), 4 deletions(-)
create mode 100644 packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts
diff --git a/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts b/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts
new file mode 100644
index 00000000000..f32eef3f54e
--- /dev/null
+++ b/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts
@@ -0,0 +1,18 @@
+import { Kysely } from 'kysely'
+import { Dialect } from '..'
+
+export async function up(db: Kysely, dialect: Dialect): Promise {
+ const timestamp = dialect === 'sqlite' ? 'datetime' : 'timestamptz'
+ await db.schema
+ .createTable('email_token')
+ .addColumn('purpose', 'varchar', (col) => col.notNull())
+ .addColumn('did', 'varchar', (col) => col.notNull())
+ .addColumn('token', 'varchar', (col) => col.notNull())
+ .addColumn('requestedAt', timestamp, (col) => col.notNull())
+ .addPrimaryKeyConstraint('email_token_pkey', ['purpose', 'did'])
+ .execute()
+}
+
+export async function down(db: Kysely): Promise {
+ await db.schema.dropTable('email_token').execute()
+}
diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts
index e7e521e986a..f4401936e34 100644
--- a/packages/pds/src/db/migrations/index.ts
+++ b/packages/pds/src/db/migrations/index.ts
@@ -65,3 +65,4 @@ export * as _20230818T134357818Z from './20230818T134357818Z-runtime-flags'
export * as _20230824T182048120Z from './20230824T182048120Z-remove-post-hierarchy'
export * as _20230825T142507884Z from './20230825T142507884Z-blob-tempkey-idx'
export * as _20230828T153013575Z from './20230828T153013575Z-repo-history-rewrite'
+export * as _20230908T224408678Z from './20230908T224408678Z-email-tokens'
diff --git a/packages/pds/src/db/tables/email-token.ts b/packages/pds/src/db/tables/email-token.ts
index d962bbf1cc5..b8f42bde198 100644
--- a/packages/pds/src/db/tables/email-token.ts
+++ b/packages/pds/src/db/tables/email-token.ts
@@ -1,5 +1,3 @@
-import { Generated } from 'kysely'
-
export type EmailTokenPurpose =
| 'confirm_email'
| 'update_email'
@@ -10,7 +8,7 @@ export interface EmailToken {
purpose: EmailTokenPurpose
did: string
token: string
- requestedAt: Generated
+ requestedAt: Date
}
export const tableName = 'email_token'
diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts
index 3d955f8d1c9..59115c9b6e5 100644
--- a/packages/pds/src/services/account/index.ts
+++ b/packages/pds/src/services/account/index.ts
@@ -564,7 +564,7 @@ export class AccountService {
const token = getRandomToken().toUpperCase()
await this.db.db
.insertInto('email_token')
- .values({ purpose, did, token })
+ .values({ purpose, did, token, requestedAt: new Date() })
.onConflict((oc) => oc.columns(['purpose', 'did']).doUpdateSet({ token }))
.execute()
return token
From a80966f56083f7ce2d296d4c44b21ffb6087e165 Mon Sep 17 00:00:00 2001
From: dholms
Date: Fri, 8 Sep 2023 17:53:09 -0500
Subject: [PATCH 07/18] tidy
---
.../pds/src/api/com/atproto/server/updateEmail.ts | 14 ++++++--------
packages/pds/src/services/account/index.ts | 2 +-
2 files changed, 7 insertions(+), 9 deletions(-)
diff --git a/packages/pds/src/api/com/atproto/server/updateEmail.ts b/packages/pds/src/api/com/atproto/server/updateEmail.ts
index d89c45cbbac..095b89ce2b0 100644
--- a/packages/pds/src/api/com/atproto/server/updateEmail.ts
+++ b/packages/pds/src/api/com/atproto/server/updateEmail.ts
@@ -26,16 +26,14 @@ export default function (server: Server, ctx: AppContext) {
}
await ctx.db.transaction(async (dbTxn) => {
+ const accntSrvce = ctx.services.account(dbTxn)
+
if (token) {
- await ctx.services
- .account(dbTxn)
- .deleteEmailToken(did, 'update_email')
+ await accntSrvce.deleteEmailToken(did, 'update_email')
+ }
+ if (user.email !== email) {
+ await accntSrvce.updateEmail(did, email)
}
- await dbTxn.db
- .updateTable('user_account')
- .set({ email, emailConfirmedAt: null })
- .where('did', '=', did)
- .execute()
})
},
})
diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts
index 59115c9b6e5..a2f85c63998 100644
--- a/packages/pds/src/services/account/index.ts
+++ b/packages/pds/src/services/account/index.ts
@@ -200,7 +200,7 @@ export class AccountService {
async updateEmail(did: string, email: string) {
await this.db.db
.updateTable('user_account')
- .set({ email: email.toLowerCase() })
+ .set({ email: email.toLowerCase(), emailConfirmedAt: null })
.where('did', '=', did)
.executeTakeFirst()
}
From 96d0f01624694bf78ef82d0170db7a13f16c2389 Mon Sep 17 00:00:00 2001
From: dholms
Date: Fri, 8 Sep 2023 18:18:31 -0500
Subject: [PATCH 08/18] tests
---
lexicons/com/atproto/server/confirmEmail.json | 7 +-
.../com/atproto/server/createSession.json | 3 +-
lexicons/com/atproto/server/getSession.json | 3 +-
lexicons/com/atproto/server/updateEmail.json | 6 +-
packages/api/src/client/lexicons.ts | 15 ++
.../types/com/atproto/server/confirmEmail.ts | 14 ++
.../types/com/atproto/server/createSession.ts | 1 +
.../types/com/atproto/server/getSession.ts | 1 +
.../types/com/atproto/server/updateEmail.ts | 7 +
packages/bsky/src/lexicon/lexicons.ts | 15 ++
.../types/com/atproto/server/confirmEmail.ts | 2 +-
.../types/com/atproto/server/createSession.ts | 1 +
.../types/com/atproto/server/getSession.ts | 1 +
.../types/com/atproto/server/updateEmail.ts | 2 +-
.../api/com/atproto/server/confirmEmail.ts | 11 +-
.../api/com/atproto/server/createSession.ts | 1 +
.../src/api/com/atproto/server/getSession.ts | 7 +-
packages/pds/src/lexicon/lexicons.ts | 15 ++
.../types/com/atproto/server/confirmEmail.ts | 2 +-
.../types/com/atproto/server/createSession.ts | 1 +
.../types/com/atproto/server/getSession.ts | 1 +
.../types/com/atproto/server/updateEmail.ts | 2 +-
packages/pds/tests/email-confirmation.test.ts | 177 ++++++++++++++++++
23 files changed, 285 insertions(+), 10 deletions(-)
create mode 100644 packages/pds/tests/email-confirmation.test.ts
diff --git a/lexicons/com/atproto/server/confirmEmail.json b/lexicons/com/atproto/server/confirmEmail.json
index ad064cf64b9..12d6fc57b57 100644
--- a/lexicons/com/atproto/server/confirmEmail.json
+++ b/lexicons/com/atproto/server/confirmEmail.json
@@ -16,7 +16,12 @@
}
}
},
- "errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }]
+ "errors": [
+ { "name": "UserNotFound" },
+ { "name": "ExpiredToken" },
+ { "name": "InvalidToken" },
+ { "name": "InvalidEmail" }
+ ]
}
}
}
diff --git a/lexicons/com/atproto/server/createSession.json b/lexicons/com/atproto/server/createSession.json
index fc416ddabae..7d877cec91c 100644
--- a/lexicons/com/atproto/server/createSession.json
+++ b/lexicons/com/atproto/server/createSession.json
@@ -29,7 +29,8 @@
"refreshJwt": { "type": "string" },
"handle": { "type": "string", "format": "handle" },
"did": { "type": "string", "format": "did" },
- "email": { "type": "string" }
+ "email": { "type": "string" },
+ "emailConfirmed": { "type": "boolean" }
}
}
},
diff --git a/lexicons/com/atproto/server/getSession.json b/lexicons/com/atproto/server/getSession.json
index 55b129be3df..5d2b6ee82fd 100644
--- a/lexicons/com/atproto/server/getSession.json
+++ b/lexicons/com/atproto/server/getSession.json
@@ -13,7 +13,8 @@
"properties": {
"handle": { "type": "string", "format": "handle" },
"did": { "type": "string", "format": "did" },
- "email": { "type": "string" }
+ "email": { "type": "string" },
+ "emailConfirmed": {"type": "boolean" }
}
}
}
diff --git a/lexicons/com/atproto/server/updateEmail.json b/lexicons/com/atproto/server/updateEmail.json
index 2cd84a3e5bc..276baf42670 100644
--- a/lexicons/com/atproto/server/updateEmail.json
+++ b/lexicons/com/atproto/server/updateEmail.json
@@ -19,7 +19,11 @@
}
}
},
- "errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }]
+ "errors": [
+ { "name": "ExpiredToken" },
+ { "name": "InvalidToken" },
+ { "name": "TokenRequired" }
+ ]
}
}
}
diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts
index ff72487df77..6520cfc6fd0 100644
--- a/packages/api/src/client/lexicons.ts
+++ b/packages/api/src/client/lexicons.ts
@@ -2262,12 +2262,18 @@ export const schemaDict = {
},
},
errors: [
+ {
+ name: 'UserNotFound',
+ },
{
name: 'ExpiredToken',
},
{
name: 'InvalidToken',
},
+ {
+ name: 'InvalidEmail',
+ },
],
},
},
@@ -2556,6 +2562,9 @@ export const schemaDict = {
email: {
type: 'string',
},
+ emailConfirmed: {
+ type: 'boolean',
+ },
},
},
},
@@ -2786,6 +2795,9 @@ export const schemaDict = {
email: {
type: 'string',
},
+ emailConfirmed: {
+ type: 'boolean',
+ },
},
},
},
@@ -3025,6 +3037,9 @@ export const schemaDict = {
{
name: 'InvalidToken',
},
+ {
+ name: 'TokenRequired',
+ },
],
},
},
diff --git a/packages/api/src/client/types/com/atproto/server/confirmEmail.ts b/packages/api/src/client/types/com/atproto/server/confirmEmail.ts
index 64a9d6811bb..cacb946a28c 100644
--- a/packages/api/src/client/types/com/atproto/server/confirmEmail.ts
+++ b/packages/api/src/client/types/com/atproto/server/confirmEmail.ts
@@ -26,6 +26,12 @@ export interface Response {
headers: Headers
}
+export class UserNotFoundError extends XRPCError {
+ constructor(src: XRPCError) {
+ super(src.status, src.error, src.message, src.headers)
+ }
+}
+
export class ExpiredTokenError extends XRPCError {
constructor(src: XRPCError) {
super(src.status, src.error, src.message, src.headers)
@@ -38,10 +44,18 @@ export class InvalidTokenError extends XRPCError {
}
}
+export class InvalidEmailError extends XRPCError {
+ constructor(src: XRPCError) {
+ super(src.status, src.error, src.message, src.headers)
+ }
+}
+
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
+ if (e.error === 'UserNotFound') return new UserNotFoundError(e)
if (e.error === 'ExpiredToken') return new ExpiredTokenError(e)
if (e.error === 'InvalidToken') return new InvalidTokenError(e)
+ if (e.error === 'InvalidEmail') return new InvalidEmailError(e)
}
return e
}
diff --git a/packages/api/src/client/types/com/atproto/server/createSession.ts b/packages/api/src/client/types/com/atproto/server/createSession.ts
index d86f2aef1d4..08d2bcd6225 100644
--- a/packages/api/src/client/types/com/atproto/server/createSession.ts
+++ b/packages/api/src/client/types/com/atproto/server/createSession.ts
@@ -22,6 +22,7 @@ export interface OutputSchema {
handle: string
did: string
email?: string
+ emailConfirmed?: boolean
[k: string]: unknown
}
diff --git a/packages/api/src/client/types/com/atproto/server/getSession.ts b/packages/api/src/client/types/com/atproto/server/getSession.ts
index c15836dfb77..91d51860982 100644
--- a/packages/api/src/client/types/com/atproto/server/getSession.ts
+++ b/packages/api/src/client/types/com/atproto/server/getSession.ts
@@ -15,6 +15,7 @@ export interface OutputSchema {
handle: string
did: string
email?: string
+ emailConfirmed?: boolean
[k: string]: unknown
}
diff --git a/packages/api/src/client/types/com/atproto/server/updateEmail.ts b/packages/api/src/client/types/com/atproto/server/updateEmail.ts
index 6b7c476d0a9..b16864fe684 100644
--- a/packages/api/src/client/types/com/atproto/server/updateEmail.ts
+++ b/packages/api/src/client/types/com/atproto/server/updateEmail.ts
@@ -39,10 +39,17 @@ export class InvalidTokenError extends XRPCError {
}
}
+export class TokenRequiredError extends XRPCError {
+ constructor(src: XRPCError) {
+ super(src.status, src.error, src.message, src.headers)
+ }
+}
+
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
if (e.error === 'ExpiredToken') return new ExpiredTokenError(e)
if (e.error === 'InvalidToken') return new InvalidTokenError(e)
+ if (e.error === 'TokenRequired') return new TokenRequiredError(e)
}
return e
}
diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts
index ff72487df77..6520cfc6fd0 100644
--- a/packages/bsky/src/lexicon/lexicons.ts
+++ b/packages/bsky/src/lexicon/lexicons.ts
@@ -2262,12 +2262,18 @@ export const schemaDict = {
},
},
errors: [
+ {
+ name: 'UserNotFound',
+ },
{
name: 'ExpiredToken',
},
{
name: 'InvalidToken',
},
+ {
+ name: 'InvalidEmail',
+ },
],
},
},
@@ -2556,6 +2562,9 @@ export const schemaDict = {
email: {
type: 'string',
},
+ emailConfirmed: {
+ type: 'boolean',
+ },
},
},
},
@@ -2786,6 +2795,9 @@ export const schemaDict = {
email: {
type: 'string',
},
+ emailConfirmed: {
+ type: 'boolean',
+ },
},
},
},
@@ -3025,6 +3037,9 @@ export const schemaDict = {
{
name: 'InvalidToken',
},
+ {
+ name: 'TokenRequired',
+ },
],
},
},
diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts b/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts
index 6d2d89db7d0..b92a1cb66ca 100644
--- a/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts
+++ b/packages/bsky/src/lexicon/types/com/atproto/server/confirmEmail.ts
@@ -24,7 +24,7 @@ export interface HandlerInput {
export interface HandlerError {
status: number
message?: string
- error?: 'ExpiredToken' | 'InvalidToken'
+ error?: 'UserNotFound' | 'ExpiredToken' | 'InvalidToken' | 'InvalidEmail'
}
export type HandlerOutput = HandlerError | void
diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts b/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts
index b836551f301..037900346a1 100644
--- a/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts
+++ b/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts
@@ -23,6 +23,7 @@ export interface OutputSchema {
handle: string
did: string
email?: string
+ emailConfirmed?: boolean
[k: string]: unknown
}
diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/getSession.ts b/packages/bsky/src/lexicon/types/com/atproto/server/getSession.ts
index 388fb5eae9d..7f066a500bf 100644
--- a/packages/bsky/src/lexicon/types/com/atproto/server/getSession.ts
+++ b/packages/bsky/src/lexicon/types/com/atproto/server/getSession.ts
@@ -16,6 +16,7 @@ export interface OutputSchema {
handle: string
did: string
email?: string
+ emailConfirmed?: boolean
[k: string]: unknown
}
diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts b/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts
index c2c99ea01c8..17025f45a98 100644
--- a/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts
+++ b/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts
@@ -25,7 +25,7 @@ export interface HandlerInput {
export interface HandlerError {
status: number
message?: string
- error?: 'ExpiredToken' | 'InvalidToken'
+ error?: 'ExpiredToken' | 'InvalidToken' | 'TokenRequired'
}
export type HandlerOutput = HandlerError | void
diff --git a/packages/pds/src/api/com/atproto/server/confirmEmail.ts b/packages/pds/src/api/com/atproto/server/confirmEmail.ts
index 3e42931ae0f..b46402b1079 100644
--- a/packages/pds/src/api/com/atproto/server/confirmEmail.ts
+++ b/packages/pds/src/api/com/atproto/server/confirmEmail.ts
@@ -1,13 +1,22 @@
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
+import { InvalidRequestError } from '@atproto/xrpc-server'
export default function (server: Server, ctx: AppContext) {
server.com.atproto.server.confirmEmail({
auth: ctx.accessVerifierCheckTakedown,
handler: async ({ auth, input }) => {
const did = auth.credentials.did
- const { token } = input.body
+ const { token, email } = input.body
+ const user = await ctx.services.account(ctx.db).getAccount(did)
+ if (!user) {
+ throw new InvalidRequestError('user not found')
+ }
+
+ if (user.email !== email.toLowerCase()) {
+ throw new InvalidRequestError('invalid email', 'InvalidEmail')
+ }
await ctx.services
.account(ctx.db)
.assertValidToken(did, 'confirm_email', token)
diff --git a/packages/pds/src/api/com/atproto/server/createSession.ts b/packages/pds/src/api/com/atproto/server/createSession.ts
index aee0063d86c..50791f23c7e 100644
--- a/packages/pds/src/api/com/atproto/server/createSession.ts
+++ b/packages/pds/src/api/com/atproto/server/createSession.ts
@@ -68,6 +68,7 @@ export default function (server: Server, ctx: AppContext) {
did: user.did,
handle: user.handle,
email: user.email,
+ emailConfirmed: !!user.emailConfirmedAt,
accessJwt: access.jwt,
refreshJwt: refresh.jwt,
},
diff --git a/packages/pds/src/api/com/atproto/server/getSession.ts b/packages/pds/src/api/com/atproto/server/getSession.ts
index bfcccc97657..eb33180e6ff 100644
--- a/packages/pds/src/api/com/atproto/server/getSession.ts
+++ b/packages/pds/src/api/com/atproto/server/getSession.ts
@@ -15,7 +15,12 @@ export default function (server: Server, ctx: AppContext) {
}
return {
encoding: 'application/json',
- body: { handle: user.handle, did: user.did, email: user.email },
+ body: {
+ handle: user.handle,
+ did: user.did,
+ email: user.email,
+ emailConfirmed: !!user.emailConfirmedAt,
+ },
}
},
})
diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts
index ff72487df77..6520cfc6fd0 100644
--- a/packages/pds/src/lexicon/lexicons.ts
+++ b/packages/pds/src/lexicon/lexicons.ts
@@ -2262,12 +2262,18 @@ export const schemaDict = {
},
},
errors: [
+ {
+ name: 'UserNotFound',
+ },
{
name: 'ExpiredToken',
},
{
name: 'InvalidToken',
},
+ {
+ name: 'InvalidEmail',
+ },
],
},
},
@@ -2556,6 +2562,9 @@ export const schemaDict = {
email: {
type: 'string',
},
+ emailConfirmed: {
+ type: 'boolean',
+ },
},
},
},
@@ -2786,6 +2795,9 @@ export const schemaDict = {
email: {
type: 'string',
},
+ emailConfirmed: {
+ type: 'boolean',
+ },
},
},
},
@@ -3025,6 +3037,9 @@ export const schemaDict = {
{
name: 'InvalidToken',
},
+ {
+ name: 'TokenRequired',
+ },
],
},
},
diff --git a/packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts b/packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts
index 6d2d89db7d0..b92a1cb66ca 100644
--- a/packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts
+++ b/packages/pds/src/lexicon/types/com/atproto/server/confirmEmail.ts
@@ -24,7 +24,7 @@ export interface HandlerInput {
export interface HandlerError {
status: number
message?: string
- error?: 'ExpiredToken' | 'InvalidToken'
+ error?: 'UserNotFound' | 'ExpiredToken' | 'InvalidToken' | 'InvalidEmail'
}
export type HandlerOutput = HandlerError | void
diff --git a/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts b/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts
index b836551f301..037900346a1 100644
--- a/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts
+++ b/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts
@@ -23,6 +23,7 @@ export interface OutputSchema {
handle: string
did: string
email?: string
+ emailConfirmed?: boolean
[k: string]: unknown
}
diff --git a/packages/pds/src/lexicon/types/com/atproto/server/getSession.ts b/packages/pds/src/lexicon/types/com/atproto/server/getSession.ts
index 388fb5eae9d..7f066a500bf 100644
--- a/packages/pds/src/lexicon/types/com/atproto/server/getSession.ts
+++ b/packages/pds/src/lexicon/types/com/atproto/server/getSession.ts
@@ -16,6 +16,7 @@ export interface OutputSchema {
handle: string
did: string
email?: string
+ emailConfirmed?: boolean
[k: string]: unknown
}
diff --git a/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts b/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts
index c2c99ea01c8..17025f45a98 100644
--- a/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts
+++ b/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts
@@ -25,7 +25,7 @@ export interface HandlerInput {
export interface HandlerError {
status: number
message?: string
- error?: 'ExpiredToken' | 'InvalidToken'
+ error?: 'ExpiredToken' | 'InvalidToken' | 'TokenRequired'
}
export type HandlerOutput = HandlerError | void
diff --git a/packages/pds/tests/email-confirmation.test.ts b/packages/pds/tests/email-confirmation.test.ts
new file mode 100644
index 00000000000..9c283c8a2ee
--- /dev/null
+++ b/packages/pds/tests/email-confirmation.test.ts
@@ -0,0 +1,177 @@
+import { once, EventEmitter } from 'events'
+import Mail from 'nodemailer/lib/mailer'
+import AtpAgent from '@atproto/api'
+import { SeedClient } from './seeds/client'
+import userSeed from './seeds/users'
+import { ServerMailer } from '../src/mailer'
+import { TestNetworkNoAppView } from '@atproto/dev-env'
+import {
+ ComAtprotoServerConfirmEmail,
+ ComAtprotoServerUpdateEmail,
+} from '@atproto/api'
+
+describe('email confirmation', () => {
+ let network: TestNetworkNoAppView
+ let agent: AtpAgent
+ let sc: SeedClient
+
+ let mailer: ServerMailer
+ const mailCatcher = new EventEmitter()
+ let _origSendMail
+
+ let alice
+
+ beforeAll(async () => {
+ network = await TestNetworkNoAppView.create({
+ dbPostgresSchema: 'email_confirmation',
+ })
+ mailer = network.pds.ctx.mailer
+ agent = network.pds.getClient()
+ sc = new SeedClient(agent)
+ await userSeed(sc)
+ alice = sc.accounts[sc.dids.alice]
+
+ // Catch emails for use in tests
+ _origSendMail = mailer.transporter.sendMail
+ mailer.transporter.sendMail = async (opts) => {
+ const result = await _origSendMail.call(mailer.transporter, opts)
+ mailCatcher.emit('mail', opts)
+ return result
+ }
+ })
+
+ afterAll(async () => {
+ mailer.transporter.sendMail = _origSendMail
+ await network.close()
+ })
+
+ const getMailFrom = async (promise): Promise => {
+ const result = await Promise.all([once(mailCatcher, 'mail'), promise])
+ return result[0][0]
+ }
+
+ const getTokenFromMail = (mail: Mail.Options) =>
+ mail.html?.toString().match(/>([a-z0-9]{5}-[a-z0-9]{5}) {
+ const res = await agent.api.com.atproto.server.requestEmailUpdate(
+ undefined,
+ { headers: sc.getHeaders(alice.did) },
+ )
+ expect(res.data.tokenRequired).toBe(false)
+
+ await agent.api.com.atproto.server.updateEmail(
+ {
+ email: 'new-alice@example.com',
+ },
+ { headers: sc.getHeaders(alice.did), encoding: 'application/json' },
+ )
+ const session = await agent.api.com.atproto.server.getSession(
+ {},
+ { headers: sc.getHeaders(alice.did) },
+ )
+ expect(session.data.email).toEqual('new-alice@example.com')
+ expect(session.data.emailConfirmed).toEqual(false)
+ alice.email = session.data.email
+ })
+
+ let confirmToken
+
+ it('requests email confirmation', async () => {
+ const mail = await getMailFrom(
+ agent.api.com.atproto.server.requestEmailConfirmation(undefined, {
+ headers: sc.getHeaders(alice.did),
+ }),
+ )
+ expect(mail.to).toEqual(alice.email)
+ expect(mail.html).toEqual('Confirm your Bluesky email')
+ confirmToken = getTokenFromMail(mail)
+ expect(confirmToken).toBeDefined()
+ })
+
+ it('fails email confirmation with a bad token', async () => {
+ const attempt = agent.api.com.atproto.server.confirmEmail({
+ email: 'new-alice@example.com',
+ token: '123456',
+ })
+ await expect(attempt).rejects.toThrow(
+ ComAtprotoServerConfirmEmail.InvalidTokenError,
+ )
+ })
+
+ it('fails email confirmation with a bad token', async () => {
+ const attempt = agent.api.com.atproto.server.confirmEmail({
+ email: 'fake-alice@example.com',
+ token: confirmToken,
+ })
+ await expect(attempt).rejects.toThrow(
+ ComAtprotoServerConfirmEmail.InvalidEmailError,
+ )
+ })
+
+ it('confirms email', async () => {
+ await agent.api.com.atproto.server.confirmEmail({
+ email: 'new-alice@example.com',
+ token: confirmToken,
+ })
+ const session = await agent.api.com.atproto.server.getSession(
+ {},
+ { headers: sc.getHeaders(alice.did) },
+ )
+ expect(session.data.emailConfirmed).toBe(true)
+ })
+
+ it('disallows email update without token when verified', async () => {
+ const attempt = agent.api.com.atproto.server.updateEmail(
+ {
+ email: 'new-alice-2@example.com',
+ },
+ { headers: sc.getHeaders(alice.did), encoding: 'application/json' },
+ )
+ await expect(attempt).rejects.toThrow(
+ ComAtprotoServerUpdateEmail.TokenRequiredError,
+ )
+ })
+
+ let updateToken
+
+ it('requests email update', async () => {
+ const mail = await getMailFrom(async () => {
+ const res = await agent.api.com.atproto.server.requestEmailUpdate(
+ undefined,
+ {
+ headers: sc.getHeaders(alice.did),
+ },
+ )
+ expect(res.data.tokenRequired).toBe(true)
+ })
+ expect(mail.to).toEqual(alice.email)
+ expect(mail.html).toEqual('Update your Bluesky email')
+ updateToken = getTokenFromMail(mail)
+ expect(updateToken).toBeDefined()
+ })
+
+ it('fails email update with a bad token', async () => {
+ const attempt = agent.api.com.atproto.server.updateEmail({
+ email: 'new-alice-2@example.com',
+ token: '123456',
+ })
+ await expect(attempt).rejects.toThrow(
+ ComAtprotoServerUpdateEmail.InvalidTokenError,
+ )
+ })
+
+ it('updates email', async () => {
+ await agent.api.com.atproto.server.updateEmail({
+ email: 'new-alice-2@example.com',
+ token: updateToken,
+ })
+
+ const session = await agent.api.com.atproto.server.getSession(
+ {},
+ { headers: sc.getHeaders(alice.did) },
+ )
+ expect(session.data.email).toBe('new-alice-2@example.com')
+ expect(session.data.emailConfirmed).toBe(false)
+ })
+})
From 81a3f56571f34f21a6394e9d6d92e58d83715a3a Mon Sep 17 00:00:00 2001
From: dholms
Date: Fri, 8 Sep 2023 18:41:24 -0500
Subject: [PATCH 09/18] tidy & bugfixes
---
.../com/atproto/server/requestEmailUpdate.ts | 2 +-
.../src/api/com/atproto/server/updateEmail.ts | 4 +-
.../20230908T224408678Z-email-tokens.ts | 9 +++
packages/pds/tests/email-confirmation.test.ts | 72 ++++++++++++-------
4 files changed, 60 insertions(+), 27 deletions(-)
diff --git a/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts b/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts
index e38b69a6d7e..bcc65303f41 100644
--- a/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts
+++ b/packages/pds/src/api/com/atproto/server/requestEmailUpdate.ts
@@ -12,7 +12,7 @@ export default function (server: Server, ctx: AppContext) {
throw new InvalidRequestError('user not found')
}
- const tokenRequired = user.emailConfirmedAt !== null
+ const tokenRequired = !!user.emailConfirmedAt
if (tokenRequired) {
const token = await ctx.services
.account(ctx.db)
diff --git a/packages/pds/src/api/com/atproto/server/updateEmail.ts b/packages/pds/src/api/com/atproto/server/updateEmail.ts
index 095b89ce2b0..c87ffa16b82 100644
--- a/packages/pds/src/api/com/atproto/server/updateEmail.ts
+++ b/packages/pds/src/api/com/atproto/server/updateEmail.ts
@@ -13,7 +13,7 @@ export default function (server: Server, ctx: AppContext) {
throw new InvalidRequestError('user not found')
}
// require valid token
- if (user.emailConfirmedAt !== null) {
+ if (user.emailConfirmedAt) {
if (!token) {
throw new InvalidRequestError(
'confirmation token required',
@@ -22,7 +22,7 @@ export default function (server: Server, ctx: AppContext) {
}
await ctx.services
.account(ctx.db)
- .assertValidToken(did, 'confirm_email', token)
+ .assertValidToken(did, 'update_email', token)
}
await ctx.db.transaction(async (dbTxn) => {
diff --git a/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts b/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts
index f32eef3f54e..d775f6f0a9c 100644
--- a/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts
+++ b/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts
@@ -11,8 +11,17 @@ export async function up(db: Kysely, dialect: Dialect): Promise {
.addColumn('requestedAt', timestamp, (col) => col.notNull())
.addPrimaryKeyConstraint('email_token_pkey', ['purpose', 'did'])
.execute()
+
+ await db.schema
+ .alterTable('user_account')
+ .addColumn('emailConfirmedAt', 'varchar')
+ .execute()
}
export async function down(db: Kysely): Promise {
await db.schema.dropTable('email_token').execute()
+ await db.schema
+ .alterTable('user_account')
+ .dropColumn('emailConfirmedAt')
+ .execute()
}
diff --git a/packages/pds/tests/email-confirmation.test.ts b/packages/pds/tests/email-confirmation.test.ts
index 9c283c8a2ee..00d4acfc99d 100644
--- a/packages/pds/tests/email-confirmation.test.ts
+++ b/packages/pds/tests/email-confirmation.test.ts
@@ -53,6 +53,14 @@ describe('email confirmation', () => {
const getTokenFromMail = (mail: Mail.Options) =>
mail.html?.toString().match(/>([a-z0-9]{5}-[a-z0-9]{5}) {
+ const session = await agent.api.com.atproto.server.getSession(
+ {},
+ { headers: sc.getHeaders(alice.did) },
+ )
+ expect(session.data.emailConfirmed).toEqual(false)
+ })
+
it('allows email update without token when unverified', async () => {
const res = await agent.api.com.atproto.server.requestEmailUpdate(
undefined,
@@ -84,36 +92,45 @@ describe('email confirmation', () => {
}),
)
expect(mail.to).toEqual(alice.email)
- expect(mail.html).toEqual('Confirm your Bluesky email')
+ expect(mail.html).toContain('Confirm your Bluesky email')
confirmToken = getTokenFromMail(mail)
expect(confirmToken).toBeDefined()
})
it('fails email confirmation with a bad token', async () => {
- const attempt = agent.api.com.atproto.server.confirmEmail({
- email: 'new-alice@example.com',
- token: '123456',
- })
+ const attempt = agent.api.com.atproto.server.confirmEmail(
+ {
+ email: 'new-alice@example.com',
+ token: '123456',
+ },
+ { headers: sc.getHeaders(alice.did), encoding: 'application/json' },
+ )
await expect(attempt).rejects.toThrow(
ComAtprotoServerConfirmEmail.InvalidTokenError,
)
})
it('fails email confirmation with a bad token', async () => {
- const attempt = agent.api.com.atproto.server.confirmEmail({
- email: 'fake-alice@example.com',
- token: confirmToken,
- })
+ const attempt = agent.api.com.atproto.server.confirmEmail(
+ {
+ email: 'fake-alice@example.com',
+ token: confirmToken,
+ },
+ { headers: sc.getHeaders(alice.did), encoding: 'application/json' },
+ )
await expect(attempt).rejects.toThrow(
ComAtprotoServerConfirmEmail.InvalidEmailError,
)
})
it('confirms email', async () => {
- await agent.api.com.atproto.server.confirmEmail({
- email: 'new-alice@example.com',
- token: confirmToken,
- })
+ await agent.api.com.atproto.server.confirmEmail(
+ {
+ email: 'new-alice@example.com',
+ token: confirmToken,
+ },
+ { headers: sc.getHeaders(alice.did), encoding: 'application/json' },
+ )
const session = await agent.api.com.atproto.server.getSession(
{},
{ headers: sc.getHeaders(alice.did) },
@@ -136,7 +153,7 @@ describe('email confirmation', () => {
let updateToken
it('requests email update', async () => {
- const mail = await getMailFrom(async () => {
+ const reqUpdate = async () => {
const res = await agent.api.com.atproto.server.requestEmailUpdate(
undefined,
{
@@ -144,28 +161,35 @@ describe('email confirmation', () => {
},
)
expect(res.data.tokenRequired).toBe(true)
- })
+ }
+ const mail = await getMailFrom(reqUpdate())
expect(mail.to).toEqual(alice.email)
- expect(mail.html).toEqual('Update your Bluesky email')
+ expect(mail.html).toContain('Update your Bluesky email')
updateToken = getTokenFromMail(mail)
expect(updateToken).toBeDefined()
})
it('fails email update with a bad token', async () => {
- const attempt = agent.api.com.atproto.server.updateEmail({
- email: 'new-alice-2@example.com',
- token: '123456',
- })
+ const attempt = agent.api.com.atproto.server.updateEmail(
+ {
+ email: 'new-alice-2@example.com',
+ token: '123456',
+ },
+ { headers: sc.getHeaders(alice.did), encoding: 'application/json' },
+ )
await expect(attempt).rejects.toThrow(
ComAtprotoServerUpdateEmail.InvalidTokenError,
)
})
it('updates email', async () => {
- await agent.api.com.atproto.server.updateEmail({
- email: 'new-alice-2@example.com',
- token: updateToken,
- })
+ await agent.api.com.atproto.server.updateEmail(
+ {
+ email: 'new-alice-2@example.com',
+ token: updateToken,
+ },
+ { headers: sc.getHeaders(alice.did), encoding: 'application/json' },
+ )
const session = await agent.api.com.atproto.server.getSession(
{},
From e2f7a9a852795438f083bdbfb0a5157d9075781a Mon Sep 17 00:00:00 2001
From: dholms
Date: Fri, 8 Sep 2023 18:42:35 -0500
Subject: [PATCH 10/18] format
---
lexicons/com/atproto/server/getSession.json | 2 +-
lexicons/com/atproto/server/updateEmail.json | 2 +-
packages/api/docs/labels.md | 54 +-
.../api/docs/moderation-behaviors/posts.md | 529 +-----------------
.../api/docs/moderation-behaviors/profiles.md | 188 +------
.../src/mailer/templates/confirm-email.hbs | 6 +-
.../pds/src/mailer/templates/update-email.hbs | 6 +-
7 files changed, 63 insertions(+), 724 deletions(-)
diff --git a/lexicons/com/atproto/server/getSession.json b/lexicons/com/atproto/server/getSession.json
index 5d2b6ee82fd..7ff5569eb1b 100644
--- a/lexicons/com/atproto/server/getSession.json
+++ b/lexicons/com/atproto/server/getSession.json
@@ -14,7 +14,7 @@
"handle": { "type": "string", "format": "handle" },
"did": { "type": "string", "format": "did" },
"email": { "type": "string" },
- "emailConfirmed": {"type": "boolean" }
+ "emailConfirmed": { "type": "boolean" }
}
}
}
diff --git a/lexicons/com/atproto/server/updateEmail.json b/lexicons/com/atproto/server/updateEmail.json
index 276baf42670..c51aef5dd68 100644
--- a/lexicons/com/atproto/server/updateEmail.json
+++ b/lexicons/com/atproto/server/updateEmail.json
@@ -12,7 +12,7 @@
"required": ["email"],
"properties": {
"email": { "type": "string" },
- "token": {
+ "token": {
"type": "string",
"description": " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed."
}
diff --git a/packages/api/docs/labels.md b/packages/api/docs/labels.md
index 9531df460c1..a2d8806b566 100644
--- a/packages/api/docs/labels.md
+++ b/packages/api/docs/labels.md
@@ -1,44 +1,44 @@
- # Labels
-
- This document is a reference for the labels used in the SDK.
+# Labels
- **⚠️ Note**: These labels are still in development and may change over time. Not all are currently in use.
+This document is a reference for the labels used in the SDK.
- ## Key
+**⚠️ Note**: These labels are still in development and may change over time. Not all are currently in use.
- ### Label Preferences
+## Key
- The possible client interpretations for a label.
+### Label Preferences
- - ignore
Do nothing with the label.
- - warn
Provide some form of warning on the content (see "On Warn" behavior).
- - hide
Remove the content from feeds and apply the warning when directly viewed.
+The possible client interpretations for a label.
- Each label specifies which preferences it can support. If a label is not configurable, it must have only own supported preference.
+- ignore
Do nothing with the label.
+- warn
Provide some form of warning on the content (see "On Warn" behavior).
+- hide
Remove the content from feeds and apply the warning when directly viewed.
- ### Configurable?
+Each label specifies which preferences it can support. If a label is not configurable, it must have only own supported preference.
- Non-configurable labels cannot have their preference changed by the user.
+### Configurable?
- ### Flags
+Non-configurable labels cannot have their preference changed by the user.
- Additional behaviors which a label can adopt.
+### Flags
- - no-override
The user cannot click through any covering of content created by the label.
- - adult
The user must have adult content enabled to configure the label. If adult content is not enabled, the label must adopt the strictest preference.
+Additional behaviors which a label can adopt.
- ### On Warn
+- no-override
The user cannot click through any covering of content created by the label.
+- adult
The user must have adult content enabled to configure the label. If adult content is not enabled, the label must adopt the strictest preference.
- The kind of UI behavior used when a warning must be applied.
+### On Warn
- - blur
Hide all of the content behind an interstitial.
- - blur-media
Hide only the media within the content (ie images) behind an interstitial.
- - alert
Display a descriptive warning but do not hide the content.
- - null
Do nothing.
+The kind of UI behavior used when a warning must be applied.
- ## Label Behaviors
+- blur
Hide all of the content behind an interstitial.
+- blur-media
Hide only the media within the content (ie images) behind an interstitial.
+- alert
Display a descriptive warning but do not hide the content.
+- null
Do nothing.
+
+## Label Behaviors
- ## Label Group Descriptions
+## Label Group Descriptions
- ## Label Descriptions
+## Label Descriptions
@@ -535,4 +535,4 @@
on content
Misleading
The moderators believe this account is spreading misleading information.
-
\ No newline at end of file
+
diff --git a/packages/api/docs/moderation-behaviors/posts.md b/packages/api/docs/moderation-behaviors/posts.md
index ef3c6c7fc5d..5ddcf9ff602 100644
--- a/packages/api/docs/moderation-behaviors/posts.md
+++ b/packages/api/docs/moderation-behaviors/posts.md
@@ -38,17 +38,12 @@ Key:
-
|
-
|
-
-
-
Imperative label ('!hide') on author profile |
@@ -56,7 +51,6 @@ Key:
|
-
|
🚫
@@ -64,13 +58,9 @@ Key:
|
-
|
-
-
-
Imperative label ('!hide') on author account |
@@ -86,13 +76,9 @@ Key:
|
-
|
-
-
-
Imperative label ('!hide') on quoted post |
@@ -100,11 +86,9 @@ Key:
|
-
|
-
|
🚫
@@ -112,9 +96,6 @@ Key:
|
-
-
-
Imperative label ('!hide') on quoted author account |
@@ -122,11 +103,9 @@ Key:
|
-
|
-
|
🚫
@@ -134,9 +113,6 @@ Key:
|
-
-
-
Imperative label ('!no-promote') on post |
@@ -144,21 +120,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Imperative label ('!no-promote') on author profile |
@@ -166,21 +136,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Imperative label ('!no-promote') on author account |
@@ -188,21 +152,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Imperative label ('!no-promote') on quoted post |
@@ -210,21 +168,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Imperative label ('!no-promote') on quoted author account |
@@ -232,21 +184,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Imperative label ('!warn') on post |
@@ -258,17 +204,12 @@ Key:
|
-
|
-
|
-
-
-
Imperative label ('!warn') on author profile |
@@ -276,7 +217,6 @@ Key:
|
-
|
✋
@@ -284,13 +224,9 @@ Key:
|
-
|
-
-
-
Imperative label ('!warn') on author account |
@@ -306,13 +242,9 @@ Key:
|
-
|
-
-
-
Imperative label ('!warn') on quoted post |
@@ -320,11 +252,9 @@ Key:
|
-
|
-
|
✋
@@ -332,9 +262,6 @@ Key:
|
-
-
-
Imperative label ('!warn') on quoted author account |
@@ -342,11 +269,9 @@ Key:
|
-
|
-
|
✋
@@ -354,8 +279,6 @@ Key:
|
-
-
Scenario | Filter | Content | Avatar | Embed |
Blur label ('intolerant') on post (hide) |
@@ -368,17 +291,12 @@ Key:
-
|
-
|
-
-
-
Blur label ('intolerant') on author profile (hide) |
@@ -386,7 +304,6 @@ Key:
|
-
|
✋
@@ -394,13 +311,9 @@ Key:
|
-
|
-
-
-
Blur label ('intolerant') on author account (hide) |
@@ -416,13 +329,9 @@ Key:
|
-
|
-
-
-
Blur label ('intolerant') on quoted post (hide) |
@@ -430,11 +339,9 @@ Key:
|
-
|
-
|
✋
@@ -442,9 +349,6 @@ Key:
|
-
-
-
Blur label ('intolerant') on quoted author account (hide) |
@@ -452,11 +356,9 @@ Key:
|
-
|
-
|
✋
@@ -464,9 +366,6 @@ Key:
|
-
-
-
Blur label ('intolerant') on post (warn) |
@@ -478,17 +377,12 @@ Key:
|
-
|
-
|
-
-
-
Blur label ('intolerant') on author profile (warn) |
@@ -496,7 +390,6 @@ Key:
|
-
|
✋
@@ -504,13 +397,9 @@ Key:
|
-
|
-
-
-
Blur label ('intolerant') on author account (warn) |
@@ -526,13 +415,9 @@ Key:
|
-
|
-
-
-
Blur label ('intolerant') on quoted post (warn) |
@@ -540,11 +425,9 @@ Key:
|
-
|
-
|
✋
@@ -552,9 +435,6 @@ Key:
|
-
-
-
Blur label ('intolerant') on quoted author account (warn) |
@@ -562,11 +442,9 @@ Key:
|
-
|
-
|
✋
@@ -574,9 +452,6 @@ Key:
|
-
-
-
Blur label ('intolerant') on post (ignore) |
@@ -584,21 +459,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Blur label ('intolerant') on author profile (ignore) |
@@ -606,21 +475,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Blur label ('intolerant') on author account (ignore) |
@@ -628,21 +491,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Blur label ('intolerant') on quoted post (ignore) |
@@ -650,21 +507,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Blur label ('intolerant') on quoted author account (ignore) |
@@ -672,20 +523,15 @@ Key:
|
-
|
-
|
-
|
-
-
Scenario | Filter | Content | Avatar | Embed |
Blur-media label ('porn') on post (hide) |
@@ -694,11 +540,9 @@ Key:
-
|
-
|
✋
@@ -706,9 +550,6 @@ Key:
|
-
-
-
Blur-media label ('porn') on author profile (hide) |
@@ -716,7 +557,6 @@ Key:
|
-
|
✋
@@ -724,13 +564,9 @@ Key:
|
-
|
-
-
-
Blur-media label ('porn') on author account (hide) |
@@ -738,7 +574,6 @@ Key:
|
-
|
✋
@@ -750,9 +585,6 @@ Key:
|
-
-
-
Blur-media label ('porn') on quoted post (hide) |
@@ -760,11 +592,9 @@ Key:
|
-
|
-
|
✋
@@ -772,9 +602,6 @@ Key:
|
-
-
-
Blur-media label ('porn') on quoted author account (hide) |
@@ -782,21 +609,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Blur-media label ('porn') on post (warn) |
@@ -804,11 +625,9 @@ Key:
|
-
|
-
|
✋
@@ -816,9 +635,6 @@ Key:
|
-
-
-
Blur-media label ('porn') on author profile (warn) |
@@ -826,7 +642,6 @@ Key:
|
-
|
✋
@@ -834,13 +649,9 @@ Key:
|
-
|
-
-
-
Blur-media label ('porn') on author account (warn) |
@@ -848,7 +659,6 @@ Key:
|
-
|
✋
@@ -860,9 +670,6 @@ Key:
|
-
-
-
Blur-media label ('porn') on quoted post (warn) |
@@ -870,11 +677,9 @@ Key:
|
-
|
-
|
✋
@@ -882,9 +687,6 @@ Key:
|
-
-
-
Blur-media label ('porn') on quoted author account (warn) |
@@ -892,21 +694,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Blur-media label ('porn') on post (ignore) |
@@ -914,21 +710,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Blur-media label ('porn') on author profile (ignore) |
@@ -936,21 +726,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Blur-media label ('porn') on author account (ignore) |
@@ -958,21 +742,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Blur-media label ('porn') on quoted post (ignore) |
@@ -980,21 +758,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Blur-media label ('porn') on quoted author account (ignore) |
@@ -1002,20 +774,15 @@ Key:
|
-
|
-
|
-
|
-
-
Scenario | Filter | Content | Avatar | Embed |
Notice label ('scam') on post (hide) |
@@ -1025,20 +792,16 @@ Key:
🪧
+
|
-
|
-
|
-
-
-
Notice label ('scam') on author profile (hide) |
@@ -1046,21 +809,17 @@ Key:
|
-
|
🪧
+
|
-
|
-
-
-
Notice label ('scam') on author account (hide) |
@@ -1069,20 +828,18 @@ Key:
|
🪧
+
|
🪧
+
|
-
|
-
-
-
Notice label ('scam') on quoted post (hide) |
@@ -1090,21 +847,17 @@ Key:
|
-
|
-
|
🪧
+
|
-
-
-
Notice label ('scam') on quoted author account (hide) |
@@ -1112,21 +865,17 @@ Key:
|
-
|
-
|
🪧
+
|
-
-
-
Notice label ('scam') on post (warn) |
@@ -1135,20 +884,16 @@ Key:
|
🪧
+
|
-
|
-
|
-
-
-
Notice label ('scam') on author profile (warn) |
@@ -1156,21 +901,17 @@ Key:
|
-
|
🪧
+
|
-
|
-
-
-
Notice label ('scam') on author account (warn) |
@@ -1179,20 +920,18 @@ Key:
|
🪧
+
|
🪧
+
|
-
|
-
-
-
Notice label ('scam') on quoted post (warn) |
@@ -1200,21 +939,17 @@ Key:
|
-
|
-
|
🪧
+
|
-
-
-
Notice label ('scam') on quoted author account (warn) |
@@ -1222,21 +957,17 @@ Key:
|
-
|
-
|
🪧
+
|
-
-
-
Notice label ('scam') on post (ignore) |
@@ -1244,21 +975,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Notice label ('scam') on author profile (ignore) |
@@ -1266,21 +991,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Notice label ('scam') on author account (ignore) |
@@ -1288,21 +1007,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Notice label ('scam') on quoted post (ignore) |
@@ -1310,21 +1023,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Notice label ('scam') on quoted author account (ignore) |
@@ -1332,20 +1039,15 @@ Key:
|
-
|
-
|
-
|
-
-
Scenario | Filter | Content | Avatar | Embed |
Adult-only label on post when adult content is disabled |
@@ -1354,11 +1056,9 @@ Key:
-
|
-
|
🚫
@@ -1366,9 +1066,6 @@ Key:
|
-
-
-
Adult-only label on author profile when adult content is disabled |
@@ -1376,7 +1073,6 @@ Key:
|
-
|
🚫
@@ -1384,13 +1080,9 @@ Key:
|
-
|
-
-
-
Adult-only label on author account when adult content is disabled |
@@ -1398,7 +1090,6 @@ Key:
|
-
|
🚫
@@ -1410,9 +1101,6 @@ Key:
|
-
-
-
Adult-only label on quoted post when adult content is disabled |
@@ -1420,11 +1108,9 @@ Key:
|
-
|
-
|
🚫
@@ -1432,9 +1118,6 @@ Key:
|
-
-
-
Adult-only label on quoted author account when adult content is disabled |
@@ -1442,20 +1125,15 @@ Key:
|
-
|
-
|
-
|
-
-
Scenario | Filter | Content | Avatar | Embed |
Self-post: Imperative label ('!hide') on post |
@@ -1468,17 +1146,12 @@ Key:
-
|
-
|
-
-
-
Self-post: Imperative label ('!hide') on author profile |
@@ -1486,21 +1159,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Self-post: Imperative label ('!hide') on author account |
@@ -1508,21 +1175,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Self-post: Imperative label ('!hide') on quoted post |
@@ -1530,11 +1191,9 @@ Key:
|
-
|
-
|
✋
@@ -1542,9 +1201,6 @@ Key:
|
-
-
-
Self-post: Imperative label ('!hide') on quoted author account |
@@ -1552,21 +1208,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Self-post: Imperative label ('!warn') on post |
@@ -1578,17 +1228,12 @@ Key:
|
-
|
-
|
-
-
-
Self-post: Imperative label ('!warn') on author profile |
@@ -1596,21 +1241,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Self-post: Imperative label ('!warn') on author account |
@@ -1618,21 +1257,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Self-post: Imperative label ('!warn') on quoted post |
@@ -1640,11 +1273,9 @@ Key:
|
-
|
-
|
✋
@@ -1652,9 +1283,6 @@ Key:
|
-
-
-
Self-post: Imperative label ('!warn') on quoted author account |
@@ -1662,21 +1290,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Self-post: Blur-media label ('porn') on post (hide) |
@@ -1684,11 +1306,9 @@ Key:
|
-
|
-
|
✋
@@ -1696,9 +1316,6 @@ Key:
|
-
-
-
Self-post: Blur-media label ('porn') on author profile (hide) |
@@ -1706,21 +1323,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Self-post: Blur-media label ('porn') on author account (hide) |
@@ -1728,21 +1339,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Self-post: Blur-media label ('porn') on quoted post (hide) |
@@ -1750,11 +1355,9 @@ Key:
|
-
|
-
|
✋
@@ -1762,9 +1365,6 @@ Key:
|
-
-
-
Self-post: Blur-media label ('porn') on quoted author account (hide) |
@@ -1772,21 +1372,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Self-post: Blur-media label ('porn') on post (warn) |
@@ -1794,11 +1388,9 @@ Key:
|
-
|
-
|
✋
@@ -1806,9 +1398,6 @@ Key:
|
-
-
-
Self-post: Blur-media label ('porn') on author profile (warn) |
@@ -1816,21 +1405,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Self-post: Blur-media label ('porn') on author account (warn) |
@@ -1838,21 +1421,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Self-post: Blur-media label ('porn') on quoted post (warn) |
@@ -1860,11 +1437,9 @@ Key:
|
-
|
-
|
✋
@@ -1872,9 +1447,6 @@ Key:
|
-
-
-
Self-post: Blur-media label ('porn') on quoted author account (warn) |
@@ -1882,20 +1454,15 @@ Key:
|
-
|
-
|
-
|
-
-
Scenario | Filter | Content | Avatar | Embed |
Post with blocked author |
@@ -1912,13 +1479,9 @@ Key:
-
|
-
-
-
Post with blocked quoted author |
@@ -1926,11 +1489,9 @@ Key:
|
-
|
-
|
🚫
@@ -1938,9 +1499,6 @@ Key:
|
-
-
-
Post with author blocking user |
@@ -1956,13 +1514,9 @@ Key:
|
-
|
-
-
-
Post with quoted author blocking user |
@@ -1970,11 +1524,9 @@ Key:
|
-
|
-
|
🚫
@@ -1982,9 +1534,6 @@ Key:
|
-
-
-
Post with muted author |
@@ -1996,17 +1545,12 @@ Key:
|
-
|
-
|
-
-
-
Post with muted quoted author |
@@ -2014,11 +1558,9 @@ Key:
|
-
|
-
|
✋
@@ -2026,9 +1568,6 @@ Key:
|
-
-
-
Post with muted-by-list author |
@@ -2040,17 +1579,12 @@ Key:
|
-
|
-
|
-
-
-
Post with muted-by-list quoted author |
@@ -2058,11 +1592,9 @@ Key:
|
-
|
-
|
✋
@@ -2070,8 +1602,6 @@ Key:
|
-
-
Scenario | Filter | Content | Avatar | Embed |
Prioritization: post with blocking & blocked-by author |
@@ -2088,13 +1618,9 @@ Key:
-
|
-
-
-
Prioritization: post with blocking & blocked-by quoted author |
@@ -2102,11 +1628,9 @@ Key:
|
-
|
-
|
🚫
@@ -2114,9 +1638,6 @@ Key:
|
-
-
-
Prioritization: '!hide' label on post by blocked user |
@@ -2132,13 +1653,9 @@ Key:
|
-
|
-
-
-
Prioritization: '!hide' label on quoted post, post by blocked user |
@@ -2158,9 +1675,6 @@ Key:
|
-
-
-
Prioritization: '!hide' and 'intolerant' labels on post (hide) |
@@ -2172,17 +1686,12 @@ Key:
|
-
|
-
|
-
-
-
Prioritization: '!warn' and 'intolerant' labels on post (hide) |
@@ -2194,17 +1703,12 @@ Key:
|
-
|
-
|
-
-
-
Prioritization: '!hide' and 'porn' labels on post (hide) |
@@ -2216,17 +1720,12 @@ Key:
|
-
|
-
|
-
-
-
Prioritization: '!warn' and 'porn' labels on post (hide) |
@@ -2234,11 +1733,9 @@ Key:
|
-
|
-
|
✋
@@ -2246,4 +1743,4 @@ Key:
|
-
\ No newline at end of file
+
diff --git a/packages/api/docs/moderation-behaviors/profiles.md b/packages/api/docs/moderation-behaviors/profiles.md
index 3d2f9af96b3..b8d7c94ce91 100644
--- a/packages/api/docs/moderation-behaviors/profiles.md
+++ b/packages/api/docs/moderation-behaviors/profiles.md
@@ -38,7 +38,6 @@ Key:
-
|
🚫
@@ -46,9 +45,6 @@ Key:
|
-
-
-
Imperative label ('!hide') on profile |
@@ -56,7 +52,6 @@ Key:
|
-
|
🚫
@@ -68,9 +63,6 @@ Key:
|
-
-
-
Imperative label ('!no-promote') on account |
@@ -78,21 +70,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Imperative label ('!no-promote') on profile |
@@ -100,21 +86,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Imperative label ('!warn') on account |
@@ -126,7 +106,6 @@ Key:
|
-
|
✋
@@ -134,9 +113,6 @@ Key:
|
-
-
-
Imperative label ('!warn') on profile |
@@ -144,7 +120,6 @@ Key:
|
-
|
✋
@@ -156,8 +131,6 @@ Key:
|
-
-
Scenario | Filter | Account | Profile | Avatar |
Blur label ('intolerant') on account (hide) |
@@ -170,7 +143,6 @@ Key:
-
|
✋
@@ -178,9 +150,6 @@ Key:
|
-
-
-
Blur label ('intolerant') on profile (hide) |
@@ -188,7 +157,6 @@ Key:
|
-
|
✋
@@ -200,9 +168,6 @@ Key:
|
-
-
-
Blur label ('intolerant') on account (warn) |
@@ -214,7 +179,6 @@ Key:
|
-
|
✋
@@ -222,9 +186,6 @@ Key:
|
-
-
-
Blur label ('intolerant') on profile (warn) |
@@ -232,7 +193,6 @@ Key:
|
-
|
✋
@@ -244,9 +204,6 @@ Key:
|
-
-
-
Blur label ('intolerant') on account (ignore) |
@@ -254,21 +211,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Blur label ('intolerant') on profile (ignore) |
@@ -276,20 +227,15 @@ Key:
|
-
|
-
|
-
|
-
-
Scenario | Filter | Account | Profile | Avatar |
Blur-media label ('porn') on account (hide) |
@@ -302,7 +248,6 @@ Key:
-
|
✋
@@ -310,9 +255,6 @@ Key:
|
-
-
-
Blur-media label ('porn') on profile (hide) |
@@ -320,11 +262,9 @@ Key:
|
-
|
-
|
✋
@@ -332,9 +272,6 @@ Key:
|
-
-
-
Blur-media label ('porn') on account (warn) |
@@ -346,7 +283,6 @@ Key:
|
-
|
✋
@@ -354,9 +290,6 @@ Key:
|
-
-
-
Blur-media label ('porn') on profile (warn) |
@@ -364,11 +297,9 @@ Key:
|
-
|
-
|
✋
@@ -376,9 +307,6 @@ Key:
|
-
-
-
Blur-media label ('porn') on account (ignore) |
@@ -386,21 +314,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Blur-media label ('porn') on profile (ignore) |
@@ -408,20 +330,15 @@ Key:
|
-
|
-
|
-
|
-
-
Scenario | Filter | Account | Profile | Avatar |
Notice label ('scam') on account (hide) |
@@ -431,20 +348,18 @@ Key:
🪧
+
|
-
|
🪧
+
|
-
-
-
Notice label ('scam') on profile (hide) |
@@ -452,21 +367,19 @@ Key:
|
-
|
🪧
+
|
🪧
+
|
-
-
-
Notice label ('scam') on account (warn) |
@@ -475,20 +388,18 @@ Key:
|
🪧
+
|
-
|
🪧
+
|
-
-
-
Notice label ('scam') on profile (warn) |
@@ -496,21 +407,19 @@ Key:
|
-
|
🪧
+
|
🪧
+
|
-
-
-
Notice label ('scam') on account (ignore) |
@@ -518,21 +427,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Notice label ('scam') on profile (ignore) |
@@ -540,20 +443,15 @@ Key:
|
-
|
-
|
-
|
-
-
Scenario | Filter | Account | Profile | Avatar |
Adult-only label on account when adult content is disabled |
@@ -566,7 +464,6 @@ Key:
-
|
🚫
@@ -574,9 +471,6 @@ Key:
|
-
-
-
Adult-only label on profile when adult content is disabled |
@@ -584,11 +478,9 @@ Key:
|
-
|
-
|
🚫
@@ -596,8 +488,6 @@ Key:
|
-
-
Scenario | Filter | Account | Profile | Avatar |
Self-profile: !hide on account |
@@ -607,20 +497,18 @@ Key:
🪧
+
|
-
|
🪧
+
|
-
-
-
Self-profile: !hide on profile |
@@ -628,20 +516,19 @@ Key:
|
-
|
🪧
+
|
🪧
+
|
-
-
Scenario | Filter | Account | Profile | Avatar |
Mute/block: Blocking user |
@@ -650,11 +537,9 @@ Key:
-
|
-
|
🚫
@@ -662,9 +547,6 @@ Key:
|
-
-
-
Mute/block: Blocked by user |
@@ -672,11 +554,9 @@ Key:
|
-
|
-
|
🚫
@@ -684,9 +564,6 @@ Key:
|
-
-
-
Mute/block: Muted user |
@@ -694,21 +571,15 @@ Key:
|
-
|
-
|
-
|
-
-
-
Mute/block: Muted-by-list user |
@@ -716,20 +587,15 @@ Key:
|
-
|
-
|
-
|
-
-
Scenario | Filter | Account | Profile | Avatar |
Prioritization: blocking & blocked-by user |
@@ -738,11 +604,9 @@ Key:
-
|
-
|
🚫
@@ -750,9 +614,6 @@ Key:
|
-
-
-
Prioritization: '!hide' label on account of blocked user |
@@ -764,7 +625,6 @@ Key:
|
-
|
🚫
@@ -772,9 +632,6 @@ Key:
|
-
-
-
Prioritization: '!hide' and 'intolerant' labels on account (hide) |
@@ -786,7 +643,6 @@ Key:
|
-
|
🚫
@@ -794,9 +650,6 @@ Key:
|
-
-
-
Prioritization: '!warn' and 'intolerant' labels on account (hide) |
@@ -808,7 +661,6 @@ Key:
|
-
|
✋
@@ -816,9 +668,6 @@ Key:
|
-
-
-
Prioritization: '!warn' and 'porn' labels on account (hide) |
@@ -830,7 +679,6 @@ Key:
|
-
|
✋
@@ -838,9 +686,6 @@ Key:
|
-
-
-
Prioritization: intolerant label on account (hide) and scam label on profile (warn) |
@@ -853,6 +698,7 @@ Key:
|
🪧
+
|
✋
@@ -860,9 +706,6 @@ Key:
|
-
-
-
Prioritization: !hide on account, !warn on profile |
@@ -882,9 +725,6 @@ Key:
|
-
-
-
Prioritization: !warn on account, !hide on profile |
@@ -904,4 +744,4 @@ Key:
|
-
\ No newline at end of file
+
diff --git a/packages/pds/src/mailer/templates/confirm-email.hbs b/packages/pds/src/mailer/templates/confirm-email.hbs
index cf0f123b0f6..ee062a40e07 100644
--- a/packages/pds/src/mailer/templates/confirm-email.hbs
+++ b/packages/pds/src/mailer/templates/confirm-email.hbs
@@ -326,9 +326,9 @@
class="text-gray-700"
style="line-height: 24px; font-size: 16px; color: #4a5568; width: 100%; margin: 0;"
align="left"
- >To confirm this email for
- your account, please enter the
- above code in the app.
+ >To confirm this email for your
+ account, please enter the above
+ code in the app.
diff --git a/packages/pds/src/mailer/templates/update-email.hbs b/packages/pds/src/mailer/templates/update-email.hbs
index a7c22fd5ca6..f49947125be 100644
--- a/packages/pds/src/mailer/templates/update-email.hbs
+++ b/packages/pds/src/mailer/templates/update-email.hbs
@@ -326,8 +326,10 @@
class="text-gray-700"
style="line-height: 24px; font-size: 16px; color: #4a5568; width: 100%; margin: 0;"
align="left"
- >To the email for your account, please enter the
- above code alongside your new email in the app.
+ >To the email for your account,
+ please enter the above code
+ alongside your new email in the
+ app.
From b796ad2f566e4f2459672233d5f8da34068fe958 Mon Sep 17 00:00:00 2001
From: dholms
Date: Fri, 8 Sep 2023 18:46:56 -0500
Subject: [PATCH 11/18] fix api test
---
packages/api/tests/agent.test.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/packages/api/tests/agent.test.ts b/packages/api/tests/agent.test.ts
index 2444e7e0c61..cb8989724ef 100644
--- a/packages/api/tests/agent.test.ts
+++ b/packages/api/tests/agent.test.ts
@@ -56,6 +56,7 @@ describe('agent', () => {
did: res.data.did,
handle: res.data.handle,
email: 'user1@test.com',
+ emailConfirmed: false,
})
expect(events.length).toEqual(1)
@@ -100,6 +101,7 @@ describe('agent', () => {
did: res1.data.did,
handle: res1.data.handle,
email,
+ emailConfirmed: false,
})
expect(events.length).toEqual(2)
@@ -142,6 +144,7 @@ describe('agent', () => {
did: res1.data.did,
handle: res1.data.handle,
email: res1.data.email,
+ emailConfirmed: false,
})
expect(events.length).toEqual(2)
From b2232712caeaddb89b88cb56b8bc3407c58348bd Mon Sep 17 00:00:00 2001
From: dholms
Date: Fri, 8 Sep 2023 18:48:27 -0500
Subject: [PATCH 12/18] fix auth test
---
packages/pds/tests/auth.test.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/packages/pds/tests/auth.test.ts b/packages/pds/tests/auth.test.ts
index c1883d5a7f7..ae78f3d5619 100644
--- a/packages/pds/tests/auth.test.ts
+++ b/packages/pds/tests/auth.test.ts
@@ -68,6 +68,7 @@ describe('auth', () => {
did: account.did,
handle: account.handle,
email,
+ emailConfirmed: false,
})
// Valid refresh token
const nextSession = await refreshSession(account.refreshJwt)
@@ -96,6 +97,7 @@ describe('auth', () => {
did: session.did,
handle: session.handle,
email,
+ emailConfirmed: false,
})
// Valid refresh token
const nextSession = await refreshSession(session.refreshJwt)
@@ -139,6 +141,7 @@ describe('auth', () => {
did: session.did,
handle: session.handle,
email,
+ emailConfirmed: false,
})
// Valid refresh token
const nextSession = await refreshSession(session.refreshJwt)
From 49b9c23eb0da9d0fee78f0ae4bdea047d0c2895f Mon Sep 17 00:00:00 2001
From: dholms
Date: Fri, 8 Sep 2023 19:27:21 -0500
Subject: [PATCH 13/18] impl
---
.../api/com/atproto/server/deleteAccount.ts | 66 ++----------------
.../atproto/server/requestAccountDelete.ts | 13 +---
.../atproto/server/requestPasswordReset.ts | 14 +---
.../api/com/atproto/server/resetPassword.ts | 68 ++-----------------
packages/pds/src/db/database-schema.ts | 2 -
.../20230908T224408678Z-email-tokens.ts | 33 +++++++++
.../pds/src/db/tables/delete-account-token.ts | 9 ---
packages/pds/src/db/tables/user-account.ts | 2 -
packages/pds/src/services/account/index.ts | 23 ++++++-
packages/pds/tests/account.test.ts | 17 +++--
10 files changed, 82 insertions(+), 165 deletions(-)
delete mode 100644 packages/pds/src/db/tables/delete-account-token.ts
diff --git a/packages/pds/src/api/com/atproto/server/deleteAccount.ts b/packages/pds/src/api/com/atproto/server/deleteAccount.ts
index 9ebfcfa7fdf..4d12edb1b32 100644
--- a/packages/pds/src/api/com/atproto/server/deleteAccount.ts
+++ b/packages/pds/src/api/com/atproto/server/deleteAccount.ts
@@ -2,7 +2,6 @@ import { AuthRequiredError } from '@atproto/xrpc-server'
import { Server } from '../../../../lexicon'
import { TAKEDOWN } from '../../../../lexicon/types/com/atproto/admin/defs'
import AppContext from '../../../../context'
-import Database from '../../../../db'
import { MINUTE } from '@atproto/common'
const REASON_ACCT_DELETION = 'ACCOUNT DELETION'
@@ -22,40 +21,18 @@ export default function (server: Server, ctx: AppContext) {
throw new AuthRequiredError('Invalid did or password')
}
- const tokenInfo = await ctx.db.db
- .selectFrom('did_handle')
- .innerJoin(
- 'delete_account_token as token',
- 'token.did',
- 'did_handle.did',
- )
- .where('did_handle.did', '=', did)
- .where('token.token', '=', token.toUpperCase())
- .select([
- 'token.token as token',
- 'token.requestedAt as requestedAt',
- 'token.did as did',
- ])
- .executeTakeFirst()
-
- if (!tokenInfo) {
- return createInvalidTokenError()
- }
+ await ctx.services
+ .account(ctx.db)
+ .assertValidToken(did, 'delete_account', token)
const now = new Date()
- const requestedAt = new Date(tokenInfo.requestedAt)
- const expiresAt = new Date(requestedAt.getTime() + 15 * minsToMs)
- if (now > expiresAt) {
- await removeDeleteToken(ctx.db, tokenInfo.did)
- return createExpiredTokenError()
- }
-
await ctx.db.transaction(async (dbTxn) => {
+ const accountService = ctx.services.account(dbTxn)
const moderationTxn = ctx.services.moderation(dbTxn)
const [currentAction] = await moderationTxn.getCurrentActions({ did })
if (currentAction?.action === TAKEDOWN) {
// Do not disturb an existing takedown, continue with account deletion
- return await removeDeleteToken(dbTxn, did)
+ return await accountService.deleteEmailToken(did, 'delete_account')
}
if (currentAction) {
// Reverse existing action to replace it with a self-takedown
@@ -74,7 +51,7 @@ export default function (server: Server, ctx: AppContext) {
createdAt: now,
})
await moderationTxn.takedownRepo({ did, takedownId: takedown.id })
- await removeDeleteToken(dbTxn, did)
+ await accountService.deleteEmailToken(did, 'delete_account')
})
ctx.backgroundQueue.add(async (db) => {
@@ -90,34 +67,3 @@ export default function (server: Server, ctx: AppContext) {
},
})
}
-
-type ErrorResponse = {
- status: number
- error: string
- message: string
-}
-
-const minsToMs = 60 * 1000
-
-const createInvalidTokenError = (): ErrorResponse & {
- error: 'InvalidToken'
-} => ({
- status: 400,
- error: 'InvalidToken',
- message: 'Token is invalid',
-})
-
-const createExpiredTokenError = (): ErrorResponse & {
- error: 'ExpiredToken'
-} => ({
- status: 400,
- error: 'ExpiredToken',
- message: 'The password reset token has expired',
-})
-
-const removeDeleteToken = async (db: Database, did: string) => {
- await db.db
- .deleteFrom('delete_account_token')
- .where('delete_account_token.did', '=', did)
- .execute()
-}
diff --git a/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts b/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts
index a448d97c02e..c438c32f69f 100644
--- a/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts
+++ b/packages/pds/src/api/com/atproto/server/requestAccountDelete.ts
@@ -1,7 +1,6 @@
import { InvalidRequestError } from '@atproto/xrpc-server'
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
-import { getRandomToken } from './util'
export default function (server: Server, ctx: AppContext) {
server.com.atproto.server.requestAccountDelete({
@@ -12,15 +11,9 @@ export default function (server: Server, ctx: AppContext) {
if (!user) {
throw new InvalidRequestError('user not found')
}
- const token = getRandomToken().toUpperCase()
- const requestedAt = new Date().toISOString()
- await ctx.db.db
- .insertInto('delete_account_token')
- .values({ did, token, requestedAt })
- .onConflict((oc) =>
- oc.column('did').doUpdateSet({ token, requestedAt }),
- )
- .execute()
+ const token = await ctx.services
+ .account(ctx.db)
+ .createEmailToken(did, 'delete_account')
await ctx.mailer.sendAccountDelete({ token }, { to: user.email })
},
})
diff --git a/packages/pds/src/api/com/atproto/server/requestPasswordReset.ts b/packages/pds/src/api/com/atproto/server/requestPasswordReset.ts
index 5d81e43c68b..61b17ebb9a9 100644
--- a/packages/pds/src/api/com/atproto/server/requestPasswordReset.ts
+++ b/packages/pds/src/api/com/atproto/server/requestPasswordReset.ts
@@ -1,6 +1,5 @@
import AppContext from '../../../../context'
import { Server } from '../../../../lexicon'
-import { getRandomToken } from './util'
export default function (server: Server, ctx: AppContext) {
server.com.atproto.server.requestPasswordReset(async ({ input }) => {
@@ -9,16 +8,9 @@ export default function (server: Server, ctx: AppContext) {
const user = await ctx.services.account(ctx.db).getAccountByEmail(email)
if (user) {
- const token = getRandomToken().toUpperCase()
- const grantedAt = new Date().toISOString()
- await ctx.db.db
- .updateTable('user_account')
- .where('did', '=', user.did)
- .set({
- passwordResetToken: token,
- passwordResetGrantedAt: grantedAt,
- })
- .execute()
+ const token = await ctx.services
+ .account(ctx.db)
+ .createEmailToken(user.did, 'reset_password')
await ctx.mailer.sendResetPassword(
{ handle: user.handle, token },
{ to: user.email },
diff --git a/packages/pds/src/api/com/atproto/server/resetPassword.ts b/packages/pds/src/api/com/atproto/server/resetPassword.ts
index de8d10382c0..a84b6249a3c 100644
--- a/packages/pds/src/api/com/atproto/server/resetPassword.ts
+++ b/packages/pds/src/api/com/atproto/server/resetPassword.ts
@@ -1,6 +1,5 @@
import AppContext from '../../../../context'
import { Server } from '../../../../lexicon'
-import Database from '../../../../db'
import { MINUTE } from '@atproto/common'
export default function (server: Server, ctx: AppContext) {
@@ -14,69 +13,16 @@ export default function (server: Server, ctx: AppContext) {
handler: async ({ input }) => {
const { token, password } = input.body
- const tokenInfo = await ctx.db.db
- .selectFrom('user_account')
- .select(['did', 'passwordResetGrantedAt'])
- .where('passwordResetToken', '=', token.toUpperCase())
- .executeTakeFirst()
-
- if (!tokenInfo?.passwordResetGrantedAt) {
- return createInvalidTokenError()
- }
-
- const now = new Date()
- const grantedAt = new Date(tokenInfo.passwordResetGrantedAt)
- const expiresAt = new Date(grantedAt.getTime() + 15 * minsToMs)
-
- if (now > expiresAt) {
- await unsetResetToken(ctx.db, tokenInfo.did)
- return createExpiredTokenError()
- }
+ const did = await ctx.services
+ .account(ctx.db)
+ .assertValidTokenAndFindDid('reset_password', token)
await ctx.db.transaction(async (dbTxn) => {
- await unsetResetToken(dbTxn, tokenInfo.did)
- await ctx.services
- .account(dbTxn)
- .updateUserPassword(tokenInfo.did, password)
- await await ctx.services
- .auth(dbTxn)
- .revokeRefreshTokensByDid(tokenInfo.did)
+ const accountService = ctx.services.account(ctx.db)
+ await accountService.updateUserPassword(did, password)
+ await accountService.deleteEmailToken(did, 'reset_password')
+ await ctx.services.auth(dbTxn).revokeRefreshTokensByDid(did)
})
},
})
}
-
-type ErrorResponse = {
- status: number
- error: string
- message: string
-}
-
-const minsToMs = 60 * 1000
-
-const createInvalidTokenError = (): ErrorResponse & {
- error: 'InvalidToken'
-} => ({
- status: 400,
- error: 'InvalidToken',
- message: 'Token is invalid',
-})
-
-const createExpiredTokenError = (): ErrorResponse & {
- error: 'ExpiredToken'
-} => ({
- status: 400,
- error: 'ExpiredToken',
- message: 'The password reset token has expired',
-})
-
-const unsetResetToken = async (db: Database, did: string) => {
- await db.db
- .updateTable('user_account')
- .where('did', '=', did)
- .set({
- passwordResetToken: null,
- passwordResetGrantedAt: null,
- })
- .execute()
-}
diff --git a/packages/pds/src/db/database-schema.ts b/packages/pds/src/db/database-schema.ts
index bebb13eb84d..b5ac5c6d265 100644
--- a/packages/pds/src/db/database-schema.ts
+++ b/packages/pds/src/db/database-schema.ts
@@ -14,7 +14,6 @@ import * as inviteCode from './tables/invite-code'
import * as notification from './tables/user-notification'
import * as blob from './tables/blob'
import * as repoBlob from './tables/repo-blob'
-import * as deleteAccountToken from './tables/delete-account-token'
import * as emailToken from './tables/email-token'
import * as moderation from './tables/moderation'
import * as mute from './tables/mute'
@@ -43,7 +42,6 @@ export type DatabaseSchemaType = appView.DatabaseSchemaType &
notification.PartialDB &
blob.PartialDB &
repoBlob.PartialDB &
- deleteAccountToken.PartialDB &
emailToken.PartialDB &
moderation.PartialDB &
mute.PartialDB &
diff --git a/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts b/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts
index d775f6f0a9c..c18b9002965 100644
--- a/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts
+++ b/packages/pds/src/db/migrations/20230908T224408678Z-email-tokens.ts
@@ -12,16 +12,49 @@ export async function up(db: Kysely, dialect: Dialect): Promise {
.addPrimaryKeyConstraint('email_token_pkey', ['purpose', 'did'])
.execute()
+ // for lookups where we don't know the did
+ await db.schema.createIndex('email_token_token_idx').on('token').execute()
+
await db.schema
.alterTable('user_account')
.addColumn('emailConfirmedAt', 'varchar')
.execute()
+
+ await db.schema
+ .alterTable('user_account')
+ .dropColumn('passwordResetToken')
+ .execute()
+
+ await db.schema
+ .alterTable('user_account')
+ .dropColumn('passwordResetGrantedAt')
+ .execute()
+
+ await db.schema.dropTable('delete_account_token').execute()
}
export async function down(db: Kysely): Promise {
await db.schema.dropTable('email_token').execute()
+
await db.schema
.alterTable('user_account')
.dropColumn('emailConfirmedAt')
.execute()
+
+ await db.schema
+ .alterTable('user_account')
+ .addColumn('passwordResetToken', 'varchar')
+ .execute()
+
+ await db.schema
+ .alterTable('user_account')
+ .addColumn('passwordResetGrantedAt', 'varchar')
+ .execute()
+
+ await db.schema
+ .createTable('delete_account_token')
+ .addColumn('did', 'varchar', (col) => col.primaryKey())
+ .addColumn('token', 'varchar', (col) => col.notNull())
+ .addColumn('requestedAt', 'varchar', (col) => col.notNull())
+ .execute()
}
diff --git a/packages/pds/src/db/tables/delete-account-token.ts b/packages/pds/src/db/tables/delete-account-token.ts
deleted file mode 100644
index da748c639a7..00000000000
--- a/packages/pds/src/db/tables/delete-account-token.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export interface DeleteAccountToken {
- did: string
- token: string
- requestedAt: string
-}
-
-export const tableName = 'delete_account_token'
-
-export type PartialDB = { [tableName]: DeleteAccountToken }
diff --git a/packages/pds/src/db/tables/user-account.ts b/packages/pds/src/db/tables/user-account.ts
index ef9fdbecb3c..808663ca468 100644
--- a/packages/pds/src/db/tables/user-account.ts
+++ b/packages/pds/src/db/tables/user-account.ts
@@ -6,8 +6,6 @@ export interface UserAccount {
passwordScrypt: string
createdAt: string
emailConfirmedAt: string | null
- passwordResetToken: string | null
- passwordResetGrantedAt: string | null
invitesDisabled: Generated<0 | 1>
inviteNote: string | null
}
diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts
index a2f85c63998..d8c1d02385c 100644
--- a/packages/pds/src/services/account/index.ts
+++ b/packages/pds/src/services/account/index.ts
@@ -589,7 +589,7 @@ export class AccountService {
.selectAll()
.where('purpose', '=', purpose)
.where('did', '=', did)
- .where('token', '=', token)
+ .where('token', '=', token.toUpperCase())
.executeTakeFirst()
if (!res) {
throw new InvalidRequestError('Token is invalid', 'InvalidToken')
@@ -600,6 +600,27 @@ export class AccountService {
}
}
+ async assertValidTokenAndFindDid(
+ purpose: EmailTokenPurpose,
+ token: string,
+ expirationLen = 15 * MINUTE,
+ ): Promise {
+ const res = await this.db.db
+ .selectFrom('email_token')
+ .selectAll()
+ .where('purpose', '=', purpose)
+ .where('token', '=', token.toUpperCase())
+ .executeTakeFirst()
+ if (!res) {
+ throw new InvalidRequestError('Token is invalid', 'InvalidToken')
+ }
+ const expired = !lessThanAgoMs(res.requestedAt, expirationLen)
+ if (expired) {
+ throw new InvalidRequestError('Token is expired', 'ExpiredToken')
+ }
+ return res.did
+ }
+
async getLastSeenNotifs(did: string): Promise {
const res = await this.db.db
.selectFrom('user_state')
diff --git a/packages/pds/tests/account.test.ts b/packages/pds/tests/account.test.ts
index 78a769b6e9f..75a679e384f 100644
--- a/packages/pds/tests/account.test.ts
+++ b/packages/pds/tests/account.test.ts
@@ -506,24 +506,23 @@ describe('account', () => {
it('allows only unexpired password reset tokens', async () => {
await agent.api.com.atproto.server.requestPasswordReset({ email })
- const user = await db.db
- .updateTable('user_account')
- .where('email', '=', email)
+ const res = await db.db
+ .updateTable('email_token')
+ .where('purpose', '=', 'reset_password')
+ .where('did', '=', did)
.set({
- passwordResetGrantedAt: new Date(
- Date.now() - 16 * minsToMs,
- ).toISOString(),
+ requestedAt: new Date(Date.now() - 16 * minsToMs),
})
- .returning(['passwordResetToken'])
+ .returning(['token'])
.executeTakeFirst()
- if (!user?.passwordResetToken) {
+ if (!res?.token) {
throw new Error('Missing reset token')
}
// Use of expired token fails
await expect(
agent.api.com.atproto.server.resetPassword({
- token: user.passwordResetToken,
+ token: res.token,
password: passwordAlt,
}),
).rejects.toThrow(ComAtprotoServerResetPassword.ExpiredTokenError)
From 8f903beb091a17c1aaa7259df27bc27cae9b50bd Mon Sep 17 00:00:00 2001
From: dholms
Date: Wed, 27 Sep 2023 17:49:08 -0500
Subject: [PATCH 14/18] update constraint name
---
.../src/db/migrations/20230926T195532354Z-email-tokens.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/packages/pds/src/db/migrations/20230926T195532354Z-email-tokens.ts b/packages/pds/src/db/migrations/20230926T195532354Z-email-tokens.ts
index ce8e6574731..4200e64477b 100644
--- a/packages/pds/src/db/migrations/20230926T195532354Z-email-tokens.ts
+++ b/packages/pds/src/db/migrations/20230926T195532354Z-email-tokens.ts
@@ -10,7 +10,10 @@ export async function up(db: Kysely, dialect: Dialect): Promise {
.addColumn('token', 'varchar', (col) => col.notNull())
.addColumn('requestedAt', timestamp, (col) => col.notNull())
.addPrimaryKeyConstraint('email_token_pkey', ['purpose', 'did'])
- .addUniqueConstraint('email_token_token_unique', ['purpose', 'token'])
+ .addUniqueConstraint('email_token_purpose_token_unique', [
+ 'purpose',
+ 'token',
+ ])
.execute()
await db.schema
From b9168c34276536aa20a4e209b1c04165b8453aa4 Mon Sep 17 00:00:00 2001
From: dholms
Date: Thu, 28 Sep 2023 12:58:48 -0500
Subject: [PATCH 15/18] temporarily disable unconfirmed updates
---
.../src/api/com/atproto/server/updateEmail.ts | 3 ++
packages/pds/tests/email-confirmation.test.ts | 30 ++++++++++++++++---
2 files changed, 29 insertions(+), 4 deletions(-)
diff --git a/packages/pds/src/api/com/atproto/server/updateEmail.ts b/packages/pds/src/api/com/atproto/server/updateEmail.ts
index c87ffa16b82..477a849fcf0 100644
--- a/packages/pds/src/api/com/atproto/server/updateEmail.ts
+++ b/packages/pds/src/api/com/atproto/server/updateEmail.ts
@@ -12,6 +12,9 @@ export default function (server: Server, ctx: AppContext) {
if (!user) {
throw new InvalidRequestError('user not found')
}
+ if (!user.emailConfirmedAt) {
+ throw new InvalidRequestError('email must be confirmed (temporary)')
+ }
// require valid token
if (user.emailConfirmedAt) {
if (!token) {
diff --git a/packages/pds/tests/email-confirmation.test.ts b/packages/pds/tests/email-confirmation.test.ts
index 284e40d9123..bbcbdfa8e80 100644
--- a/packages/pds/tests/email-confirmation.test.ts
+++ b/packages/pds/tests/email-confirmation.test.ts
@@ -61,28 +61,50 @@ describe('email confirmation', () => {
expect(session.data.emailConfirmed).toEqual(false)
})
- it('allows email update without token when unverified', async () => {
+ it('disallows email update when unverified', async () => {
const res = await agent.api.com.atproto.server.requestEmailUpdate(
undefined,
{ headers: sc.getHeaders(alice.did) },
)
expect(res.data.tokenRequired).toBe(false)
- await agent.api.com.atproto.server.updateEmail(
+ const attempt = agent.api.com.atproto.server.updateEmail(
{
email: 'new-alice@example.com',
},
{ headers: sc.getHeaders(alice.did), encoding: 'application/json' },
)
+ await expect(attempt).rejects.toThrow()
const session = await agent.api.com.atproto.server.getSession(
{},
{ headers: sc.getHeaders(alice.did) },
)
- expect(session.data.email).toEqual('new-alice@example.com')
+ expect(session.data.email).toEqual(alice.email)
expect(session.data.emailConfirmed).toEqual(false)
- alice.email = session.data.email
})
+ // it('allows email update without token when unverified', async () => {
+ // const res = await agent.api.com.atproto.server.requestEmailUpdate(
+ // undefined,
+ // { headers: sc.getHeaders(alice.did) },
+ // )
+ // expect(res.data.tokenRequired).toBe(false)
+
+ // await agent.api.com.atproto.server.updateEmail(
+ // {
+ // email: 'new-alice@example.com',
+ // },
+ // { headers: sc.getHeaders(alice.did), encoding: 'application/json' },
+ // )
+ // const session = await agent.api.com.atproto.server.getSession(
+ // {},
+ // { headers: sc.getHeaders(alice.did) },
+ // )
+ // expect(session.data.email).toEqual('new-alice@example.com')
+ // expect(session.data.emailConfirmed).toEqual(false)
+ // alice.email = session.data.email
+ // })
+
let confirmToken
it('requests email confirmation', async () => {
From 5127187c54882736a908f57ee65f2fad192735e6 Mon Sep 17 00:00:00 2001
From: dholms
Date: Thu, 28 Sep 2023 13:03:31 -0500
Subject: [PATCH 16/18] tidy
---
lexicons/com/atproto/server/updateEmail.json | 2 +-
packages/api/src/client/lexicons.ts | 2 +-
packages/api/src/client/types/com/atproto/server/updateEmail.ts | 2 +-
packages/bsky/src/lexicon/lexicons.ts | 2 +-
.../bsky/src/lexicon/types/com/atproto/server/updateEmail.ts | 2 +-
packages/pds/src/lexicon/lexicons.ts | 2 +-
.../pds/src/lexicon/types/com/atproto/server/updateEmail.ts | 2 +-
7 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/lexicons/com/atproto/server/updateEmail.json b/lexicons/com/atproto/server/updateEmail.json
index c51aef5dd68..88872698910 100644
--- a/lexicons/com/atproto/server/updateEmail.json
+++ b/lexicons/com/atproto/server/updateEmail.json
@@ -14,7 +14,7 @@
"email": { "type": "string" },
"token": {
"type": "string",
- "description": " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed."
+ "description": "Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed."
}
}
}
diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts
index 1a763baa6ba..77b4939d758 100644
--- a/packages/api/src/client/lexicons.ts
+++ b/packages/api/src/client/lexicons.ts
@@ -3029,7 +3029,7 @@ export const schemaDict = {
token: {
type: 'string',
description:
- " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.",
+ "Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.",
},
},
},
diff --git a/packages/api/src/client/types/com/atproto/server/updateEmail.ts b/packages/api/src/client/types/com/atproto/server/updateEmail.ts
index b16864fe684..92aef734e20 100644
--- a/packages/api/src/client/types/com/atproto/server/updateEmail.ts
+++ b/packages/api/src/client/types/com/atproto/server/updateEmail.ts
@@ -11,7 +11,7 @@ export interface QueryParams {}
export interface InputSchema {
email: string
- /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */
+ /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */
token?: string
[k: string]: unknown
}
diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts
index 1a763baa6ba..77b4939d758 100644
--- a/packages/bsky/src/lexicon/lexicons.ts
+++ b/packages/bsky/src/lexicon/lexicons.ts
@@ -3029,7 +3029,7 @@ export const schemaDict = {
token: {
type: 'string',
description:
- " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.",
+ "Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.",
},
},
},
diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts b/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts
index 17025f45a98..c88bd3021b2 100644
--- a/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts
+++ b/packages/bsky/src/lexicon/types/com/atproto/server/updateEmail.ts
@@ -12,7 +12,7 @@ export interface QueryParams {}
export interface InputSchema {
email: string
- /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */
+ /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */
token?: string
[k: string]: unknown
}
diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts
index 1a763baa6ba..77b4939d758 100644
--- a/packages/pds/src/lexicon/lexicons.ts
+++ b/packages/pds/src/lexicon/lexicons.ts
@@ -3029,7 +3029,7 @@ export const schemaDict = {
token: {
type: 'string',
description:
- " Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.",
+ "Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.",
},
},
},
diff --git a/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts b/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts
index 17025f45a98..c88bd3021b2 100644
--- a/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts
+++ b/packages/pds/src/lexicon/types/com/atproto/server/updateEmail.ts
@@ -12,7 +12,7 @@ export interface QueryParams {}
export interface InputSchema {
email: string
- /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */
+ /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */
token?: string
[k: string]: unknown
}
From e51ad36e8765d478c6533bd39caef63236dd3cce Mon Sep 17 00:00:00 2001
From: dholms
Date: Thu, 28 Sep 2023 13:10:59 -0500
Subject: [PATCH 17/18] fix some tests
---
packages/bsky/tests/indexing.test.ts | 3 ++-
packages/pds/tests/moderation.test.ts | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/packages/bsky/tests/indexing.test.ts b/packages/bsky/tests/indexing.test.ts
index cee3ed5a768..f5c8083df09 100644
--- a/packages/bsky/tests/indexing.test.ts
+++ b/packages/bsky/tests/indexing.test.ts
@@ -648,8 +648,9 @@ describe('indexing', () => {
headers: sc.getHeaders(alice),
})
const { token } = await network.pds.ctx.db.db
- .selectFrom('delete_account_token')
+ .selectFrom('email_token')
.selectAll()
+ .where('purpose', '=', 'delete_account')
.where('did', '=', alice)
.executeTakeFirstOrThrow()
await pdsAgent.api.com.atproto.server.deleteAccount({
diff --git a/packages/pds/tests/moderation.test.ts b/packages/pds/tests/moderation.test.ts
index edbb23c6578..80536a6933f 100644
--- a/packages/pds/tests/moderation.test.ts
+++ b/packages/pds/tests/moderation.test.ts
@@ -286,7 +286,8 @@ describe('moderation', () => {
headers: sc.getHeaders(deleteme.did),
})
const { token: deletionToken } = await server.ctx.db.db
- .selectFrom('delete_account_token')
+ .selectFrom('email_token')
+ .where('purpose', '=', 'delete_account')
.where('did', '=', deleteme.did)
.selectAll()
.executeTakeFirstOrThrow()
From e6ddbf5fada336fd53e895f30e094c3112a94b26 Mon Sep 17 00:00:00 2001
From: dholms
Date: Thu, 28 Sep 2023 13:20:40 -0500
Subject: [PATCH 18/18] validate email syntax
---
.../pds/src/api/com/atproto/server/updateEmail.ts | 6 ++++++
packages/pds/tests/email-confirmation.test.ts | 13 +++++++++++++
2 files changed, 19 insertions(+)
diff --git a/packages/pds/src/api/com/atproto/server/updateEmail.ts b/packages/pds/src/api/com/atproto/server/updateEmail.ts
index 477a849fcf0..1873f5e0157 100644
--- a/packages/pds/src/api/com/atproto/server/updateEmail.ts
+++ b/packages/pds/src/api/com/atproto/server/updateEmail.ts
@@ -1,6 +1,7 @@
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
import { InvalidRequestError } from '@atproto/xrpc-server'
+import disposable from 'disposable-email'
export default function (server: Server, ctx: AppContext) {
server.com.atproto.server.updateEmail({
@@ -8,6 +9,11 @@ export default function (server: Server, ctx: AppContext) {
handler: async ({ auth, input }) => {
const did = auth.credentials.did
const { token, email } = input.body
+ if (!disposable.validate(email)) {
+ throw new InvalidRequestError(
+ 'This email address is not supported, please use a different email.',
+ )
+ }
const user = await ctx.services.account(ctx.db).getAccount(did)
if (!user) {
throw new InvalidRequestError('user not found')
diff --git a/packages/pds/tests/email-confirmation.test.ts b/packages/pds/tests/email-confirmation.test.ts
index bbcbdfa8e80..fc3c4caadcd 100644
--- a/packages/pds/tests/email-confirmation.test.ts
+++ b/packages/pds/tests/email-confirmation.test.ts
@@ -204,6 +204,19 @@ describe('email confirmation', () => {
)
})
+ it('fails email update with a badly formatted email', async () => {
+ const attempt = agent.api.com.atproto.server.updateEmail(
+ {
+ email: 'bad-email@disposeamail.com',
+ token: updateToken,
+ },
+ { headers: sc.getHeaders(alice.did), encoding: 'application/json' },
+ )
+ await expect(attempt).rejects.toThrow(
+ 'This email address is not supported, please use a different email.',
+ )
+ })
+
it('updates email', async () => {
await agent.api.com.atproto.server.updateEmail(
{