@atproto/pds 0.4.226 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/account-manager/account-manager.d.ts +19 -5
  3. package/dist/account-manager/account-manager.d.ts.map +1 -1
  4. package/dist/account-manager/account-manager.js +94 -12
  5. package/dist/account-manager/account-manager.js.map +1 -1
  6. package/dist/account-manager/helpers/account.d.ts +2 -0
  7. package/dist/account-manager/helpers/account.d.ts.map +1 -1
  8. package/dist/account-manager/helpers/account.js +4 -0
  9. package/dist/account-manager/helpers/account.js.map +1 -1
  10. package/dist/account-manager/oauth-store.d.ts +5 -1
  11. package/dist/account-manager/oauth-store.d.ts.map +1 -1
  12. package/dist/account-manager/oauth-store.js +50 -1
  13. package/dist/account-manager/oauth-store.js.map +1 -1
  14. package/dist/api/app/bsky/actor/getPreferences.d.ts.map +1 -1
  15. package/dist/api/app/bsky/actor/getPreferences.js +7 -2
  16. package/dist/api/app/bsky/actor/getPreferences.js.map +1 -1
  17. package/dist/api/app/bsky/actor/putPreferences.d.ts.map +1 -1
  18. package/dist/api/app/bsky/actor/putPreferences.js +7 -2
  19. package/dist/api/app/bsky/actor/putPreferences.js.map +1 -1
  20. package/dist/api/com/atproto/admin/updateAccountEmail.js +1 -1
  21. package/dist/api/com/atproto/admin/updateAccountEmail.js.map +1 -1
  22. package/dist/api/com/atproto/server/confirmEmail.d.ts.map +1 -1
  23. package/dist/api/com/atproto/server/confirmEmail.js +20 -27
  24. package/dist/api/com/atproto/server/confirmEmail.js.map +1 -1
  25. package/dist/api/com/atproto/server/getServiceAuth.d.ts.map +1 -1
  26. package/dist/api/com/atproto/server/getServiceAuth.js +4 -0
  27. package/dist/api/com/atproto/server/getServiceAuth.js.map +1 -1
  28. package/dist/api/com/atproto/server/requestEmailConfirmation.d.ts +3 -1
  29. package/dist/api/com/atproto/server/requestEmailConfirmation.d.ts.map +1 -1
  30. package/dist/api/com/atproto/server/requestEmailConfirmation.js +44 -39
  31. package/dist/api/com/atproto/server/requestEmailConfirmation.js.map +1 -1
  32. package/dist/api/com/atproto/server/requestEmailUpdate.d.ts +3 -1
  33. package/dist/api/com/atproto/server/requestEmailUpdate.d.ts.map +1 -1
  34. package/dist/api/com/atproto/server/requestEmailUpdate.js +51 -47
  35. package/dist/api/com/atproto/server/requestEmailUpdate.js.map +1 -1
  36. package/dist/api/com/atproto/server/updateEmail.d.ts.map +1 -1
  37. package/dist/api/com/atproto/server/updateEmail.js +32 -46
  38. package/dist/api/com/atproto/server/updateEmail.js.map +1 -1
  39. package/dist/config/config.d.ts +5 -2
  40. package/dist/config/config.d.ts.map +1 -1
  41. package/dist/config/config.js +50 -46
  42. package/dist/config/config.js.map +1 -1
  43. package/dist/config/env.d.ts +1 -0
  44. package/dist/config/env.d.ts.map +1 -1
  45. package/dist/config/env.js +1 -0
  46. package/dist/config/env.js.map +1 -1
  47. package/dist/context.d.ts.map +1 -1
  48. package/dist/context.js +2 -2
  49. package/dist/context.js.map +1 -1
  50. package/dist/lexicons/app/bsky/embed/external.defs.d.ts +5 -0
  51. package/dist/lexicons/app/bsky/embed/external.defs.d.ts.map +1 -1
  52. package/dist/lexicons/app/bsky/embed/external.defs.js +4 -0
  53. package/dist/lexicons/app/bsky/embed/external.defs.js.map +1 -1
  54. package/dist/lexicons/chat/bsky/actor/getStatus.defs.d.ts +2 -0
  55. package/dist/lexicons/chat/bsky/actor/getStatus.defs.d.ts.map +1 -1
  56. package/dist/lexicons/chat/bsky/actor/getStatus.defs.js +1 -0
  57. package/dist/lexicons/chat/bsky/actor/getStatus.defs.js.map +1 -1
  58. package/dist/lexicons/chat/bsky/convo/defs.defs.d.ts +4 -0
  59. package/dist/lexicons/chat/bsky/convo/defs.defs.d.ts.map +1 -1
  60. package/dist/lexicons/chat/bsky/convo/defs.defs.js +1 -0
  61. package/dist/lexicons/chat/bsky/convo/defs.defs.js.map +1 -1
  62. package/dist/lexicons/com/atproto/server/getServiceAuth.defs.d.ts +2 -2
  63. package/dist/lexicons/com/atproto/server/getServiceAuth.defs.js +1 -1
  64. package/dist/lexicons/com/atproto/server/getServiceAuth.defs.js.map +1 -1
  65. package/dist/mailer/index.d.ts +5 -3
  66. package/dist/mailer/index.d.ts.map +1 -1
  67. package/dist/mailer/index.js +20 -9
  68. package/dist/mailer/index.js.map +1 -1
  69. package/dist/mailer/templates/confirm-email.js +11 -3
  70. package/dist/mailer/templates/confirm-email.js.map +2 -2
  71. package/dist/mailer/templates/delete-account.js +2 -2
  72. package/dist/mailer/templates/delete-account.js.map +2 -2
  73. package/dist/mailer/templates/plc-operation.js +2 -2
  74. package/dist/mailer/templates/plc-operation.js.map +2 -2
  75. package/dist/mailer/templates/reset-password.js +2 -2
  76. package/dist/mailer/templates/reset-password.js.map +2 -2
  77. package/dist/mailer/templates/update-email.js +2 -2
  78. package/dist/mailer/templates/update-email.js.map +2 -2
  79. package/dist/mailer/templates.d.ts +11 -0
  80. package/dist/mailer/templates.d.ts.map +1 -1
  81. package/dist/mailer/templates.js.map +1 -1
  82. package/dist/pipethrough.d.ts +3 -0
  83. package/dist/pipethrough.d.ts.map +1 -1
  84. package/dist/pipethrough.js +25 -9
  85. package/dist/pipethrough.js.map +1 -1
  86. package/package.json +12 -11
  87. package/src/account-manager/account-manager.ts +136 -15
  88. package/src/account-manager/helpers/account.ts +9 -1
  89. package/src/account-manager/oauth-store.ts +80 -1
  90. package/src/api/app/bsky/actor/getPreferences.ts +11 -2
  91. package/src/api/app/bsky/actor/putPreferences.ts +11 -2
  92. package/src/api/com/atproto/admin/updateAccountEmail.ts +1 -1
  93. package/src/api/com/atproto/server/confirmEmail.ts +24 -29
  94. package/src/api/com/atproto/server/getServiceAuth.ts +7 -0
  95. package/src/api/com/atproto/server/requestEmailConfirmation.ts +55 -48
  96. package/src/api/com/atproto/server/requestEmailUpdate.ts +64 -48
  97. package/src/api/com/atproto/server/updateEmail.ts +32 -62
  98. package/src/config/config.ts +69 -57
  99. package/src/config/env.ts +3 -0
  100. package/src/context.ts +2 -1
  101. package/src/mailer/index.ts +35 -11
  102. package/src/mailer/templates/confirm-email.hbs +18 -17
  103. package/src/mailer/templates/delete-account.hbs +6 -6
  104. package/src/mailer/templates/plc-operation.hbs +6 -6
  105. package/src/mailer/templates/reset-password.hbs +7 -7
  106. package/src/mailer/templates/update-email.hbs +6 -6
  107. package/src/mailer/templates.ts +12 -0
  108. package/src/pipethrough.ts +33 -12
  109. package/tests/account-manager.test.ts +89 -8
  110. package/tests/app-passwords.test.ts +5 -5
  111. package/tests/get-service-auth.test.ts +81 -0
  112. package/tests/proxied/proxy-header.test.ts +1 -0
  113. package/tests/proxied/proxy-oauth-aud.test.ts +175 -0
@@ -1,22 +1,38 @@
1
1
  import { SendMailOptions, Transporter } from 'nodemailer'
2
2
  import { htmlToText } from 'nodemailer-html-to-text'
3
- import { ServerConfig } from '../config/index.js'
3
+ import { BrandingConfig, EmailConfig } from '../config/index.js'
4
4
  import { mailerLogger } from '../logger.js'
5
5
  import * as templates from './templates.js'
6
6
 
7
7
  // @TODO Add support for i18n
8
8
 
9
+ const DEFAULT_LOGO_URL =
10
+ 'https://bsky.social/about/images/email/email_logo_default.png'
11
+ const DEFAULT_MARK_URL =
12
+ 'https://bsky.social/about/images/email/email_mark_dark.png'
13
+ const DEFAULT_HOME_URL = 'https://bsky.app'
14
+ const DEFAULT_PRIMARY_COLOR = '#067df7'
15
+
9
16
  export class ServerMailer {
17
+ private readonly config: templates.Config
18
+
10
19
  constructor(
11
20
  public readonly transporter: Transporter,
12
- private readonly config: ServerConfig,
21
+ private readonly email: EmailConfig | null,
22
+ branding: BrandingConfig,
13
23
  ) {
14
24
  transporter.use('compile', htmlToText())
15
- }
16
25
 
17
- // The returned config can be used inside email templates.
18
- static getEmailConfig(_config: ServerConfig) {
19
- return {}
26
+ this.config = {
27
+ serviceName: branding.name ?? 'Bluesky',
28
+ homeUrl:
29
+ branding.links?.find((link) => link.rel === 'canonical')?.href ??
30
+ DEFAULT_HOME_URL,
31
+ logoUrl: branding.logo ?? DEFAULT_LOGO_URL,
32
+ markUrl: branding.logo ?? DEFAULT_MARK_URL,
33
+ primaryColor: branding.colors?.primary ?? DEFAULT_PRIMARY_COLOR,
34
+ showBskyAppEmailConfirmationLink: email?.disableConfirmationLink !== true,
35
+ }
20
36
  }
21
37
 
22
38
  async sendResetPassword(
@@ -39,14 +55,22 @@ export class ServerMailer {
39
55
  })
40
56
  }
41
57
 
42
- async sendConfirmEmail(params: { token: string }, mailOpts: SendMailOptions) {
58
+ async sendConfirmEmail(
59
+ params: { token: string; locale?: string },
60
+ mailOpts: SendMailOptions,
61
+ ) {
62
+ // @TODO (later) handle locale in the template
43
63
  await this.sendTemplate('confirmEmail', params, {
44
64
  subject: 'Email Confirmation',
45
65
  ...mailOpts,
46
66
  })
47
67
  }
48
68
 
49
- async sendUpdateEmail(params: { token: string }, mailOpts: SendMailOptions) {
69
+ async sendUpdateEmail(
70
+ params: { token: string; locale?: string },
71
+ mailOpts: SendMailOptions,
72
+ ) {
73
+ // @TODO (later) handle locale in the template
50
74
  await this.sendTemplate('updateEmail', params, {
51
75
  subject: 'Email Update Requested',
52
76
  ...mailOpts,
@@ -67,14 +91,14 @@ export class ServerMailer {
67
91
  ) {
68
92
  const html = templates[templateName]({
69
93
  ...params,
70
- config: ServerMailer.getEmailConfig(this.config),
94
+ config: this.config,
71
95
  } as any)
72
96
  const res = await this.transporter.sendMail({
73
97
  ...mailOpts,
74
- from: mailOpts.from ?? this.config.email?.fromAddress,
98
+ from: mailOpts.from ?? this.email?.fromAddress,
75
99
  html,
76
100
  })
77
- if (!this.config.email?.smtpUrl) {
101
+ if (!this.email?.smtpUrl) {
78
102
  mailerLogger.debug(
79
103
  'No SMTP URL has been configured. Intended to send email:\n' +
80
104
  JSON.stringify(res, null, 2),
@@ -4,14 +4,12 @@
4
4
  <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
5
5
  <meta name="x-apple-disable-message-reformatting" />
6
6
  <title>Confirm your email</title>
7
- <meta
8
- name="description"
9
- content="To confirm your email, enter the code provided in the app."
10
- />
7
+ <meta name="description" content="{{token}} is your verification code." />
11
8
  </head>
12
9
  <div
13
10
  style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0"
14
- >To confirm your email, enter the code provided in the app.<div
11
+ >{{token}}
12
+ is your verification code.<div
15
13
  > ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏</div>
16
14
  </div>
17
15
 
@@ -42,8 +40,8 @@
42
40
  <tbody>
43
41
  <tr>
44
42
  <td><img
45
- alt="Bluesky"
46
- src="https://bsky.social/about/images/email/email_logo_default.png"
43
+ alt="{{config.serviceName}}"
44
+ src="{{config.logoUrl}}"
47
45
  style="display:block;outline:none;border:none;text-decoration:none;width:110px;margin:0 auto"
48
46
  /></td>
49
47
  </tr>
@@ -67,12 +65,15 @@
67
65
  <p
68
66
  style="font-size:16px;line-height:1.4;margin:0px 0px;letter-spacing:0.25px;color:hsl(211, 24%, 34.2%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;padding-top:12px;padding-bottom:12px;padding-right:32px"
69
67
  >To confirm this email for your account, please enter the
70
- code below in the app or<!-- -->
71
- <a
72
- href="https://bsky.app/intent/verify-email?code={{token}}"
73
- style="color:#067df7;text-decoration:none;text-decoration-line:underline;font-size:16px;letter-spacing:0.25px"
74
- target="_blank"
75
- >click here.</a></p><code
68
+ code below in the app{{#if
69
+ config.showBskyAppEmailConfirmationLink
70
+ }}
71
+ or<!-- -->
72
+ <a
73
+ href="https://bsky.app/intent/verify-email?code={{token}}"
74
+ style="color:{{config.primaryColor}};text-decoration:none;text-decoration-line:underline;font-size:16px;letter-spacing:0.25px"
75
+ target="_blank"
76
+ >click here</a>{{/if}}.</p><code
76
77
  style="display:block;padding:16px;border-radius:8px;border-width:1px;border-style:solid;background-color:hsl(211, 20%, 95.3%);border-color:hsl(211, 20%, 85.89999999999999%);font-size:14px;letter-spacing:0.25px;font-family:monospace;text-transform:uppercase"
77
78
  >{{token}}</code>
78
79
  <p
@@ -109,17 +110,17 @@
109
110
  <p
110
111
  style="font-size:14px;line-height:1.4;margin:0px 0px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;letter-spacing:0.25px"
111
112
  ><a
112
- href="https://bsky.app"
113
+ href="{{config.homeUrl}}"
113
114
  style="color:hsl(211, 20%, 53%);text-decoration:none;text-decoration-line:underline;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0;font-size:14px;letter-spacing:0.25px"
114
115
  target="_blank"
115
- >Bluesky</a>, the social internet</p>
116
+ >{{config.serviceName}}</a></p>
116
117
  </td>
117
118
  <td
118
119
  data-id="__react-email-column"
119
120
  style="width:24px"
120
121
  ><img
121
- alt="🦋"
122
- src="https://bsky.social/about/images/email/email_mark_dark.png"
122
+ alt="{{config.serviceName}}"
123
+ src="{{config.markUrl}}"
123
124
  style="display:block;outline:none;border:none;text-decoration:none;width:24px"
124
125
  /></td>
125
126
  </tr>
@@ -43,8 +43,8 @@
43
43
  <tbody>
44
44
  <tr>
45
45
  <td><img
46
- alt="Bluesky"
47
- src="https://bsky.social/about/images/email/email_logo_default.png"
46
+ alt="{{config.serviceName}}"
47
+ src="{{config.logoUrl}}"
48
48
  style="display:block;outline:none;border:none;text-decoration:none;width:110px;margin:0 auto"
49
49
  /></td>
50
50
  </tr>
@@ -108,17 +108,17 @@
108
108
  <p
109
109
  style="font-size:14px;line-height:1.4;margin:0px 0px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;letter-spacing:0.25px"
110
110
  ><a
111
- href="https://bsky.app"
111
+ href="{{config.homeUrl}}"
112
112
  style="color:hsl(211, 20%, 53%);text-decoration:none;text-decoration-line:underline;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0;font-size:14px;letter-spacing:0.25px"
113
113
  target="_blank"
114
- >Bluesky</a>, the social internet</p>
114
+ >{{config.serviceName}}</a></p>
115
115
  </td>
116
116
  <td
117
117
  data-id="__react-email-column"
118
118
  style="width:24px"
119
119
  ><img
120
- alt="🦋"
121
- src="https://bsky.social/about/images/email/email_mark_dark.png"
120
+ alt="{{config.serviceName}}"
121
+ src="{{config.markUrl}}"
122
122
  style="display:block;outline:none;border:none;text-decoration:none;width:24px"
123
123
  /></td>
124
124
  </tr>
@@ -42,8 +42,8 @@
42
42
  <tbody>
43
43
  <tr>
44
44
  <td><img
45
- alt="Bluesky"
46
- src="https://bsky.social/about/images/email/email_logo_default.png"
45
+ alt="{{config.serviceName}}"
46
+ src="{{config.logoUrl}}"
47
47
  style="display:block;outline:none;border:none;text-decoration:none;width:110px;margin:0 auto"
48
48
  /></td>
49
49
  </tr>
@@ -105,17 +105,17 @@
105
105
  <p
106
106
  style="font-size:14px;line-height:1.4;margin:0px 0px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;letter-spacing:0.25px"
107
107
  ><a
108
- href="https://bsky.app"
108
+ href="{{config.homeUrl}}"
109
109
  style="color:hsl(211, 20%, 53%);text-decoration:none;text-decoration-line:underline;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0;font-size:14px;letter-spacing:0.25px"
110
110
  target="_blank"
111
- >Bluesky</a>, the social internet</p>
111
+ >{{config.serviceName}}</a></p>
112
112
  </td>
113
113
  <td
114
114
  data-id="__react-email-column"
115
115
  style="width:24px"
116
116
  ><img
117
- alt="🦋"
118
- src="https://bsky.social/about/images/email/email_mark_dark.png"
117
+ alt="{{config.serviceName}}"
118
+ src="{{config.markUrl}}"
119
119
  style="display:block;outline:none;border:none;text-decoration:none;width:24px"
120
120
  /></td>
121
121
  </tr>
@@ -42,8 +42,8 @@
42
42
  <tbody>
43
43
  <tr>
44
44
  <td><img
45
- alt="Bluesky"
46
- src="https://bsky.social/about/images/email/email_logo_default.png"
45
+ alt="{{config.serviceName}}"
46
+ src="{{config.logoUrl}}"
47
47
  style="display:block;outline:none;border:none;text-decoration:none;width:110px;margin:0 auto"
48
48
  /></td>
49
49
  </tr>
@@ -68,7 +68,7 @@
68
68
  style="font-size:16px;line-height:1.4;margin:0px 0px;letter-spacing:0.25px;color:hsl(211, 24%, 34.2%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;padding-top:12px;padding-bottom:12px"
69
69
  >We received a request to reset the password for the account<!-- -->
70
70
  <span
71
- style="color:hsl(211, 99%, 53%)"
71
+ style="color:{{config.primaryColor}}"
72
72
  >@<!-- -->{{handle}}<!-- -->.</span></p><code
73
73
  style="display:block;padding:16px;border-radius:8px;border-width:1px;border-style:solid;background-color:hsl(211, 20%, 95.3%);border-color:hsl(211, 20%, 85.89999999999999%);font-size:14px;letter-spacing:0.25px;font-family:monospace;text-transform:uppercase"
74
74
  >{{token}}</code>
@@ -106,17 +106,17 @@
106
106
  <p
107
107
  style="font-size:14px;line-height:1.4;margin:0px 0px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;letter-spacing:0.25px"
108
108
  ><a
109
- href="https://bsky.app"
109
+ href="{{config.homeUrl}}"
110
110
  style="color:hsl(211, 20%, 53%);text-decoration:none;text-decoration-line:underline;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0;font-size:14px;letter-spacing:0.25px"
111
111
  target="_blank"
112
- >Bluesky</a>, the social internet</p>
112
+ >{{config.serviceName}}</a></p>
113
113
  </td>
114
114
  <td
115
115
  data-id="__react-email-column"
116
116
  style="width:24px"
117
117
  ><img
118
- alt="🦋"
119
- src="https://bsky.social/about/images/email/email_mark_dark.png"
118
+ alt="{{config.serviceName}}"
119
+ src="{{config.markUrl}}"
120
120
  style="display:block;outline:none;border:none;text-decoration:none;width:24px"
121
121
  /></td>
122
122
  </tr>
@@ -43,8 +43,8 @@
43
43
  <tbody>
44
44
  <tr>
45
45
  <td><img
46
- alt="Bluesky"
47
- src="https://bsky.social/about/images/email/email_logo_default.png"
46
+ alt="{{config.serviceName}}"
47
+ src="{{config.logoUrl}}"
48
48
  style="display:block;outline:none;border:none;text-decoration:none;width:110px;margin:0 auto"
49
49
  /></td>
50
50
  </tr>
@@ -105,17 +105,17 @@
105
105
  <p
106
106
  style="font-size:14px;line-height:1.4;margin:0px 0px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;letter-spacing:0.25px"
107
107
  ><a
108
- href="https://bsky.app"
108
+ href="{{config.homeUrl}}"
109
109
  style="color:hsl(211, 20%, 53%);text-decoration:none;text-decoration-line:underline;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0;font-size:14px;letter-spacing:0.25px"
110
110
  target="_blank"
111
- >Bluesky</a>, the social internet</p>
111
+ >{{config.serviceName}}</a></p>
112
112
  </td>
113
113
  <td
114
114
  data-id="__react-email-column"
115
115
  style="width:24px"
116
116
  ><img
117
- alt="🦋"
118
- src="https://bsky.social/about/images/email/email_mark_dark.png"
117
+ alt="{{config.serviceName}}"
118
+ src="{{config.markUrl}}"
119
119
  style="display:block;outline:none;border:none;text-decoration:none;width:24px"
120
120
  /></td>
121
121
  </tr>
@@ -3,3 +3,15 @@ export { default as deleteAccount } from './templates/delete-account.js'
3
3
  export { default as confirmEmail } from './templates/confirm-email.js'
4
4
  export { default as updateEmail } from './templates/update-email.js'
5
5
  export { default as plcOperation } from './templates/plc-operation.js'
6
+
7
+ /**
8
+ * Common config variable that will be injected into all templates.
9
+ */
10
+ export type Config = {
11
+ serviceName: string
12
+ homeUrl: string
13
+ logoUrl: string
14
+ markUrl: string
15
+ primaryColor: string
16
+ showBskyAppEmailConfirmationLink: boolean
17
+ }
@@ -56,9 +56,23 @@ export const proxyHandler = (ctx: AppContext): CatchallHandler => {
56
56
  throw new InvalidRequestError('Bad token method', 'InvalidToken')
57
57
  }
58
58
 
59
- const { url: origin, did: aud } = await parseProxyInfo(ctx, req, lxm)
60
-
61
- const authResult = await performAuth({ req, res, params: { lxm, aud } })
59
+ const {
60
+ url: origin,
61
+ did,
62
+ serviceId,
63
+ } = await parseProxyInfo(ctx, req, lxm)
64
+ // Phase 1 of service auth updates: the scope check sees the combined
65
+ // did#serviceId form (so OAuth callers' rpc:?aud=did#service scopes
66
+ // match), while the outbound service-auth JWT keeps bare-DID aud
67
+ // regardless of session type.
68
+ const scopeAud = `${did}#${serviceId}`
69
+ const tokenAud = did
70
+
71
+ const authResult = await performAuth({
72
+ req,
73
+ res,
74
+ params: { lxm, aud: scopeAud },
75
+ })
62
76
 
63
77
  const { credentials } = excludeErrorResult(authResult)
64
78
 
@@ -80,7 +94,7 @@ export const proxyHandler = (ctx: AppContext): CatchallHandler => {
80
94
  'content-encoding': body && req.headers['content-encoding'],
81
95
  'content-length': body && req.headers['content-length'],
82
96
 
83
- authorization: `Bearer ${await ctx.serviceAuthJwt(credentials.did, aud, lxm)}`,
97
+ authorization: `Bearer ${await ctx.serviceAuthJwt(credentials.did, tokenAud, lxm)}`,
84
98
  }
85
99
 
86
100
  const dispatchOptions: Dispatcher.RequestOptions = {
@@ -216,18 +230,25 @@ export function computeProxyTo(
216
230
  throw new InvalidRequestError(`No service configured for ${lxm}`)
217
231
  }
218
232
 
233
+ // Bare-DID portion of `proxyTo`, suitable as a service-auth JWT audience
234
+ // (Phase 1 of service auth updates).
235
+ export function bareDidFromProxyTo(proxyTo: string): string {
236
+ const hashIndex = proxyTo.indexOf('#')
237
+ return hashIndex === -1 ? proxyTo : proxyTo.slice(0, hashIndex)
238
+ }
239
+
219
240
  export async function parseProxyInfo(
220
241
  ctx: AppContext,
221
242
  req: Request,
222
243
  lxm: string,
223
- ): Promise<{ url: string; did: string }> {
244
+ ): Promise<{ url: string; did: string; serviceId: string }> {
224
245
  // /!\ Hot path
225
246
 
226
247
  const proxyToHeader = req.header('atproto-proxy')
227
248
  if (proxyToHeader) return parseProxyHeader(ctx, proxyToHeader)
228
249
 
229
- const { serviceInfo } = defaultService(ctx, lxm)
230
- if (serviceInfo) return serviceInfo
250
+ const { serviceId, serviceInfo } = defaultService(ctx, lxm)
251
+ if (serviceInfo) return { ...serviceInfo, serviceId }
231
252
 
232
253
  throw new InvalidRequestError(`No service configured for ${lxm}`)
233
254
  }
@@ -236,7 +257,7 @@ export const parseProxyHeader = async (
236
257
  // Using subset of AppContext for testing purposes
237
258
  ctx: Pick<AppContext, 'cfg' | 'idResolver'>,
238
259
  proxyTo: string,
239
- ): Promise<{ did: string; url: string }> => {
260
+ ): Promise<{ did: string; url: string; serviceId: string }> => {
240
261
  // /!\ Hot path
241
262
 
242
263
  const hashIndex = proxyTo.indexOf('#')
@@ -260,13 +281,14 @@ export const parseProxyHeader = async (
260
281
  }
261
282
 
262
283
  const did = proxyTo.slice(0, hashIndex)
284
+ const serviceId = proxyTo.slice(hashIndex + 1)
263
285
 
264
286
  // Special case a configured appview, while still proxying correctly any other appview
265
287
  if (
266
288
  ctx.cfg.bskyAppView &&
267
289
  proxyTo === `${ctx.cfg.bskyAppView.did}#bsky_appview`
268
290
  ) {
269
- return { did, url: ctx.cfg.bskyAppView.url }
291
+ return { did, url: ctx.cfg.bskyAppView.url, serviceId }
270
292
  }
271
293
 
272
294
  const didDoc = await ctx.idResolver.did.resolve(did)
@@ -274,13 +296,12 @@ export const parseProxyHeader = async (
274
296
  throw new InvalidRequestError('could not resolve proxy did')
275
297
  }
276
298
 
277
- const serviceId = proxyTo.slice(hashIndex)
278
- const url = getServiceEndpoint(didDoc, { id: serviceId })
299
+ const url = getServiceEndpoint(didDoc, { id: `#${serviceId}` })
279
300
  if (!url) {
280
301
  throw new InvalidRequestError('could not resolve proxy did service url')
281
302
  }
282
303
 
283
- return { did, url }
304
+ return { did, url, serviceId }
284
305
  }
285
306
 
286
307
  /**
@@ -108,12 +108,6 @@ describe('account manager', () => {
108
108
  })
109
109
 
110
110
  it('allows changing the password', async () => {
111
- const sendResetPasswordMock = jest
112
- .spyOn(network.pds.ctx.mailer, 'sendResetPassword')
113
- .mockImplementation(async () => {
114
- // noop
115
- })
116
-
117
111
  await using page = await PageHelper.from(browser, { languages })
118
112
 
119
113
  await page.goto(new URL('/account', network.pds.url))
@@ -122,7 +116,11 @@ describe('account manager', () => {
122
116
 
123
117
  await page.clickOnText('Mot de passe', 'a')
124
118
 
125
- expect(sendResetPasswordMock).toHaveBeenCalledTimes(0)
119
+ using sendResetPasswordMock = jest
120
+ .spyOn(network.pds.ctx.mailer, 'sendResetPassword')
121
+ .mockImplementation(async () => {
122
+ // noop
123
+ })
126
124
 
127
125
  await page.clickOnText('Envoyer le code')
128
126
 
@@ -145,7 +143,90 @@ describe('account manager', () => {
145
143
  'Réinitialisation du mot de passe réussie',
146
144
  'div',
147
145
  )
146
+ })
147
+
148
+ it('allows validating the email address', async () => {
149
+ await using page = await PageHelper.from(browser, { languages })
150
+
151
+ await page.goto(new URL('/account', network.pds.url))
152
+
153
+ await page.clickOnText('Vérifier maintenant', 'button')
154
+
155
+ using sendConfirmEmailMock = jest
156
+ .spyOn(network.pds.ctx.mailer, 'sendConfirmEmail')
157
+ .mockImplementation(async () => {
158
+ // noop
159
+ })
160
+
161
+ await page.clickOnText('Envoyer le code', 'button')
162
+
163
+ await page.waitForNetworkIdle()
164
+
165
+ expect(sendConfirmEmailMock).toHaveBeenCalledTimes(1)
166
+
167
+ const [params] = sendConfirmEmailMock.mock.lastCall!
168
+ expect(params).toEqual({
169
+ locale: 'fr',
170
+ token: expect.any(String),
171
+ })
172
+
173
+ await page.typeInInput('code', params.token)
174
+ await page.clickOnText('Soumettre')
175
+
176
+ await page.ensureTextVisibility('Adresse email vérifiée', 'div')
177
+ })
178
+
179
+ it('allows changing the email address', async () => {
180
+ await using page = await PageHelper.from(browser, { languages })
181
+
182
+ await page.goto(new URL('/account', network.pds.url))
183
+
184
+ await page.clickOnText('Email', 'a')
185
+
186
+ using sendUpdateEmailMock = jest
187
+ .spyOn(network.pds.ctx.mailer, 'sendUpdateEmail')
188
+ .mockImplementation(async () => {
189
+ // noop
190
+ })
191
+
192
+ using sendConfirmEmailMock = jest
193
+ .spyOn(network.pds.ctx.mailer, 'sendConfirmEmail')
194
+ .mockImplementation(async () => {
195
+ // noop
196
+ })
197
+
198
+ await page.clickOnText('Envoyer le code', 'button')
199
+
200
+ await page.waitForNetworkIdle()
201
+
202
+ expect(sendUpdateEmailMock).toHaveBeenCalledTimes(1)
203
+
204
+ const [updateParams] = sendUpdateEmailMock.mock.lastCall!
205
+ expect(updateParams).toEqual({
206
+ locale: 'fr',
207
+ token: expect.any(String),
208
+ })
209
+
210
+ await page.typeInInput('code', updateParams.token)
211
+ await page.typeInInput('email', 'bob-new-email@example.com')
212
+ await page.clickOnText('Soumettre')
213
+
214
+ await page.ensureTextVisibility(
215
+ "Modification de l'adresse email réussie",
216
+ 'div',
217
+ )
218
+
219
+ expect(sendConfirmEmailMock).toHaveBeenCalledTimes(1)
220
+
221
+ const [confirmParams] = sendConfirmEmailMock.mock.lastCall!
222
+ expect(confirmParams).toEqual({
223
+ locale: 'fr',
224
+ token: expect.any(String),
225
+ })
226
+
227
+ await page.typeInInput('code', confirmParams.token)
228
+ await page.clickOnText('Soumettre')
148
229
 
149
- sendResetPasswordMock.mockRestore()
230
+ await page.ensureTextVisibility('Adresse email vérifiée', 'div')
150
231
  })
151
232
  })
@@ -115,7 +115,7 @@ describe('app_passwords', () => {
115
115
  it('restricts service auth token methods for non-privileged access tokens', async () => {
116
116
  const attemptCaseSensitive = appAgent.api.com.atproto.server.getServiceAuth(
117
117
  {
118
- aud: 'did:example:test',
118
+ aud: network.pds.ctx.cfg.service.did,
119
119
  lxm: 'com.atproto.server.createAccount',
120
120
  },
121
121
  )
@@ -124,7 +124,7 @@ describe('app_passwords', () => {
124
124
  )
125
125
  const attemptCaseInsensitive =
126
126
  appAgent.api.com.atproto.server.getServiceAuth({
127
- aud: 'did:example:test',
127
+ aud: network.pds.ctx.cfg.service.did,
128
128
  lxm: 'com.atproto.server.createaccount',
129
129
  })
130
130
  await expect(attemptCaseInsensitive).rejects.toThrow(
@@ -134,7 +134,7 @@ describe('app_passwords', () => {
134
134
 
135
135
  it('allows privileged service auth token scopes for privileged access tokens', async () => {
136
136
  await priviAgent.api.com.atproto.server.getServiceAuth({
137
- aud: 'did:example:test',
137
+ aud: network.pds.ctx.cfg.service.did,
138
138
  lxm: 'com.atproto.server.createAccount',
139
139
  })
140
140
  })
@@ -165,7 +165,7 @@ describe('app_passwords', () => {
165
165
 
166
166
  // allows privileged app passwords or higher
167
167
  const priviAttempt = appAgent.api.com.atproto.server.getServiceAuth({
168
- aud: 'did:example:test',
168
+ aud: network.pds.ctx.cfg.service.did,
169
169
  lxm: 'com.atproto.server.createAccount',
170
170
  })
171
171
  await expect(priviAttempt).rejects.toThrow(
@@ -211,7 +211,7 @@ describe('app_passwords', () => {
211
211
 
212
212
  // allows privileged app passwords or higher
213
213
  await priviAgent.api.com.atproto.server.getServiceAuth({
214
- aud: 'did:example:test',
214
+ aud: network.pds.ctx.cfg.service.did,
215
215
  })
216
216
 
217
217
  // allows only full access auth