@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,44 +1,39 @@
1
- import { InvalidRequestError, Server } from '@atproto/xrpc-server'
1
+ import { Server } from '@atproto/xrpc-server'
2
2
  import { AppContext } from '../../../../context.js'
3
3
  import { com } from '../../../../lexicons/index.js'
4
+ import { requestEmailConfirmationAuth } from './requestEmailConfirmation.js'
4
5
 
5
6
  export default function (server: Server, ctx: AppContext) {
6
- server.add(com.atproto.server.confirmEmail, {
7
- auth: ctx.authVerifier.authorization({
8
- checkTakedown: true,
9
- authorize: (permissions) => {
10
- permissions.assertAccount({ attr: 'email', action: 'manage' })
11
- },
12
- }),
13
- handler: async ({ auth, input: { body }, req }) => {
14
- const { did } = auth.credentials
7
+ const { entrywayClient } = ctx
15
8
 
16
- const user = await ctx.accountManager.getAccount(did, {
17
- includeDeactivated: true,
18
- })
19
- if (!user) {
20
- throw new InvalidRequestError('user not found', 'AccountNotFound')
21
- }
9
+ // @NOTE Ensure that both endpoints use the same authentication logic
10
+ const auth = requestEmailConfirmationAuth(ctx)
22
11
 
23
- if (ctx.entrywayClient) {
12
+ if (entrywayClient) {
13
+ server.add(com.atproto.server.confirmEmail, {
14
+ auth,
15
+ handler: async ({ auth, input: { body }, req }) => {
24
16
  const { headers } = await ctx.entrywayAuthHeaders(
25
17
  req,
26
18
  auth.credentials.did,
27
19
  com.atproto.server.confirmEmail.$lxm,
28
20
  )
29
- await ctx.entrywayClient.xrpc(com.atproto.server.confirmEmail, {
21
+ await entrywayClient.xrpc(com.atproto.server.confirmEmail, {
30
22
  headers,
31
23
  body,
32
24
  })
33
- return
34
- }
35
-
36
- const { token, email } = body
37
-
38
- if (user.email !== email.toLowerCase()) {
39
- throw new InvalidRequestError('invalid email', 'InvalidEmail')
40
- }
41
- await ctx.accountManager.confirmEmail({ did, token })
42
- },
43
- })
25
+ },
26
+ })
27
+ } else {
28
+ server.add(com.atproto.server.confirmEmail, {
29
+ auth,
30
+ handler: async ({ auth, input: { body } }) => {
31
+ await ctx.accountManager.confirmEmail(
32
+ auth.credentials.did,
33
+ body.email,
34
+ body.token,
35
+ )
36
+ },
37
+ })
38
+ }
44
39
  }
@@ -1,4 +1,5 @@
1
1
  import { HOUR, MINUTE } from '@atproto/common'
2
+ import { isAtprotoDid, isAtprotoDidRefAbsolute } from '@atproto/did'
2
3
  import { l } from '@atproto/lex'
3
4
  import {
4
5
  InvalidRequestError,
@@ -34,6 +35,12 @@ export default function (server: Server, ctx: AppContext) {
34
35
  // @NOTE "exp" is expressed in seconds since epoch, not milliseconds
35
36
  const { aud, exp, lxm = null } = params
36
37
 
38
+ if (!isAtprotoDid(aud) && !isAtprotoDidRefAbsolute(aud)) {
39
+ throw new InvalidRequestError(
40
+ 'aud must be a valid atproto DID or did#serviceId reference',
41
+ )
42
+ }
43
+
37
44
  // Takendown accounts should not be able to generate service auth tokens except for methods necessary for account migration
38
45
  if (auth.credentials.type === 'access') {
39
46
  // @NOTE We should probably use "ForbiddenError" here. Using
@@ -1,61 +1,68 @@
1
1
  import { DAY, HOUR } from '@atproto/common'
2
- import { InvalidRequestError, Server } from '@atproto/xrpc-server'
2
+ import {
3
+ MethodAuthVerifier,
4
+ MethodRateLimit,
5
+ Server,
6
+ } from '@atproto/xrpc-server'
7
+ import { AccessOutput, OAuthOutput } from '../../../../auth-output.js'
3
8
  import { AppContext } from '../../../../context.js'
4
9
  import { com } from '../../../../lexicons/index.js'
5
10
 
11
+ // Exposed as a utility to ensure auth in confirmEmail and requestEmailConfirmation
12
+ export function requestEmailConfirmationAuth(
13
+ ctx: AppContext,
14
+ ): MethodAuthVerifier<AccessOutput | OAuthOutput> {
15
+ return ctx.authVerifier.authorization({
16
+ checkTakedown: true,
17
+ authorize: (permissions) => {
18
+ permissions.assertAccount({ attr: 'email', action: 'manage' })
19
+ },
20
+ })
21
+ }
22
+
23
+ const rateLimit: MethodRateLimit<AccessOutput | OAuthOutput> = [
24
+ {
25
+ durationMs: DAY,
26
+ points: 15,
27
+ calcKey: ({ auth }) => auth.credentials.did,
28
+ },
29
+ {
30
+ durationMs: HOUR,
31
+ points: 5,
32
+ calcKey: ({ auth }) => auth.credentials.did,
33
+ },
34
+ ]
35
+
6
36
  export default function (server: Server, ctx: AppContext) {
7
- server.add(com.atproto.server.requestEmailConfirmation, {
8
- rateLimit: [
9
- {
10
- durationMs: DAY,
11
- points: 15,
12
- calcKey: ({ auth }) => auth.credentials.did,
13
- },
14
- {
15
- durationMs: HOUR,
16
- points: 5,
17
- calcKey: ({ auth }) => auth.credentials.did,
18
- },
19
- ],
20
- auth: ctx.authVerifier.authorization({
21
- checkTakedown: true,
22
- authorize: (permissions) => {
23
- permissions.assertAccount({ attr: 'email', action: 'manage' })
24
- },
25
- }),
26
- handler: async ({ auth, req }) => {
27
- const did = auth.credentials.did
28
- const account = await ctx.accountManager.getAccount(did, {
29
- includeDeactivated: true,
30
- includeTakenDown: true,
31
- })
32
- if (!account) {
33
- throw new InvalidRequestError('account not found')
34
- }
37
+ const { entrywayClient } = ctx
35
38
 
36
- if (ctx.entrywayClient) {
39
+ const auth = requestEmailConfirmationAuth(ctx)
40
+
41
+ if (entrywayClient) {
42
+ server.add(com.atproto.server.requestEmailConfirmation, {
43
+ auth,
44
+ rateLimit,
45
+ handler: async ({ auth, req }) => {
37
46
  const { headers } = await ctx.entrywayAuthHeaders(
38
47
  req,
39
48
  auth.credentials.did,
40
49
  com.atproto.server.requestEmailConfirmation.$lxm,
41
50
  )
42
51
 
43
- await ctx.entrywayClient.xrpc(
44
- com.atproto.server.requestEmailConfirmation,
45
- { headers },
46
- )
47
-
48
- return
49
- }
50
-
51
- if (!account.email) {
52
- throw new InvalidRequestError('account does not have an email address')
53
- }
54
- const token = await ctx.accountManager.createEmailToken(
55
- did,
56
- 'confirm_email',
57
- )
58
- await ctx.mailer.sendConfirmEmail({ token }, { to: account.email })
59
- },
60
- })
52
+ await entrywayClient.xrpc(com.atproto.server.requestEmailConfirmation, {
53
+ headers,
54
+ })
55
+ },
56
+ })
57
+ } else {
58
+ server.add(com.atproto.server.requestEmailConfirmation, {
59
+ auth,
60
+ rateLimit,
61
+ handler: async ({ auth }) => {
62
+ const did = auth.credentials.did
63
+ const locale = undefined // @TODO get the locale somehow
64
+ await ctx.accountManager.requestEmailConfirmation(did, { locale })
65
+ },
66
+ })
67
+ }
61
68
  }
@@ -1,70 +1,86 @@
1
1
  import { DAY, HOUR } from '@atproto/common'
2
- import { InvalidRequestError, Server } from '@atproto/xrpc-server'
2
+ import {
3
+ ForbiddenError,
4
+ MethodAuthVerifier,
5
+ MethodRateLimit,
6
+ Server,
7
+ } from '@atproto/xrpc-server'
8
+ import { AccessOutput, OAuthOutput } from '../../../../auth-output.js'
9
+ import { ACCESS_FULL } from '../../../../auth-scope.js'
3
10
  import { AppContext } from '../../../../context.js'
4
11
  import { com } from '../../../../lexicons/index.js'
5
12
 
13
+ // Exposed as a utility to ensure auth in updateEmail and requestEmailUpdate
14
+ // stay consistent.
15
+ export function requestEmailUpdateAuth(
16
+ ctx: AppContext,
17
+ ): MethodAuthVerifier<AccessOutput | OAuthOutput> {
18
+ return ctx.authVerifier.authorization({
19
+ checkTakedown: true,
20
+ scopes: ACCESS_FULL,
21
+ authorize: () => {
22
+ throw new ForbiddenError(
23
+ 'Use the account manager interface to update email address associated with an account',
24
+ )
25
+ },
26
+ })
27
+ }
28
+
29
+ const rateLimit: MethodRateLimit<AccessOutput | OAuthOutput> = [
30
+ {
31
+ durationMs: DAY,
32
+ points: 15,
33
+ calcKey: ({ auth }) => auth.credentials.did,
34
+ },
35
+ {
36
+ durationMs: HOUR,
37
+ points: 5,
38
+ calcKey: ({ auth }) => auth.credentials.did,
39
+ },
40
+ ]
41
+
6
42
  export default function (server: Server, ctx: AppContext) {
7
43
  const { entrywayClient } = ctx
8
44
 
9
- server.add(com.atproto.server.requestEmailUpdate, {
10
- rateLimit: [
11
- {
12
- durationMs: DAY,
13
- points: 15,
14
- calcKey: ({ auth }) => auth.credentials.did,
15
- },
16
- {
17
- durationMs: HOUR,
18
- points: 5,
19
- calcKey: ({ auth }) => auth.credentials.did,
20
- },
21
- ],
22
- auth: ctx.authVerifier.authorization({
23
- checkTakedown: true,
24
- authorize: (permissions) => {
25
- permissions.assertAccount({ attr: 'email', action: 'manage' })
26
- },
27
- }),
28
- handler: async ({ auth, req }) => {
29
- const did = auth.credentials.did
30
- const account = await ctx.accountManager.getAccount(did, {
31
- includeDeactivated: true,
32
- includeTakenDown: true,
33
- })
34
- if (!account) {
35
- throw new InvalidRequestError('account not found')
36
- }
45
+ const auth = requestEmailUpdateAuth(ctx)
37
46
 
38
- if (entrywayClient) {
47
+ if (entrywayClient) {
48
+ server.add(com.atproto.server.requestEmailUpdate, {
49
+ rateLimit,
50
+ auth,
51
+ handler: async ({ auth, req }) => {
39
52
  const { headers } = await ctx.entrywayAuthHeaders(
40
53
  req,
41
54
  auth.credentials.did,
42
55
  com.atproto.server.requestEmailUpdate.$lxm,
43
56
  )
57
+
44
58
  return entrywayClient.xrpc(com.atproto.server.requestEmailUpdate, {
45
59
  headers,
46
60
  })
47
- }
61
+ },
62
+ })
63
+ } else {
64
+ server.add(com.atproto.server.requestEmailUpdate, {
65
+ rateLimit,
66
+ auth,
67
+ handler: async ({ auth }) => {
68
+ const did = auth.credentials.did
48
69
 
49
- if (!account.email) {
50
- throw new InvalidRequestError('account does not have an email address')
51
- }
70
+ // @TODO get the locale somehow (either by adding a field in the request
71
+ // body, or by using the `Accept-Language` header).
72
+ const locale = undefined
52
73
 
53
- const tokenRequired = !!account.emailConfirmedAt
54
- if (tokenRequired) {
55
- const token = await ctx.accountManager.createEmailToken(
74
+ const { tokenRequired } = await ctx.accountManager.requestEmailUpdate(
56
75
  did,
57
- 'update_email',
76
+ { locale },
58
77
  )
59
- await ctx.mailer.sendUpdateEmail({ token }, { to: account.email })
60
- }
61
78
 
62
- return {
63
- encoding: 'application/json' as const,
64
- body: {
65
- tokenRequired,
66
- },
67
- }
68
- },
69
- })
79
+ return {
80
+ encoding: 'application/json' as const,
81
+ body: { tokenRequired },
82
+ }
83
+ },
84
+ })
85
+ }
70
86
  }
@@ -1,82 +1,52 @@
1
- import { isEmailValid } from '@hapi/address'
2
- import { isDisposableEmail } from 'disposable-email-domains-js'
3
- import {
4
- ForbiddenError,
5
- InvalidRequestError,
6
- Server,
7
- } from '@atproto/xrpc-server'
1
+ import { InvalidRequestError, Server } from '@atproto/xrpc-server'
8
2
  import { UserAlreadyExistsError } from '../../../../account-manager/helpers/account.js'
9
- import { ACCESS_FULL } from '../../../../auth-scope.js'
10
3
  import { AppContext } from '../../../../context.js'
11
4
  import { com } from '../../../../lexicons/index.js'
5
+ import { requestEmailUpdateAuth } from './requestEmailUpdate.js'
12
6
 
13
7
  export default function (server: Server, ctx: AppContext) {
14
- server.add(com.atproto.server.updateEmail, {
15
- auth: ctx.authVerifier.authorization({
16
- checkTakedown: true,
17
- scopes: ACCESS_FULL,
18
- authorize: () => {
19
- throw new ForbiddenError(
20
- 'OAuth credentials are not supported for this endpoint',
21
- )
22
- },
23
- }),
24
- handler: async ({ auth, input: { body }, req }) => {
25
- const did = auth.credentials.did
26
- const { token, email } = body
27
- if (!isEmailValid(email) || isDisposableEmail(email)) {
28
- throw new InvalidRequestError(
29
- 'This email address is not supported, please use a different email.',
30
- )
31
- }
32
- const account = await ctx.accountManager.getAccount(did, {
33
- includeDeactivated: true,
34
- })
35
- if (!account) {
36
- throw new InvalidRequestError('account not found')
37
- }
8
+ const { entrywayClient } = ctx
9
+
10
+ // @NOTE Ensure that both endpoints use the same authentication logic
11
+ const auth = requestEmailUpdateAuth(ctx)
38
12
 
39
- if (ctx.entrywayClient) {
13
+ if (entrywayClient) {
14
+ server.add(com.atproto.server.updateEmail, {
15
+ auth,
16
+ handler: async ({ auth, input: { body }, req }) => {
40
17
  const { headers } = await ctx.entrywayAuthHeaders(
41
18
  req,
42
19
  auth.credentials.did,
43
20
  com.atproto.server.updateEmail.$lxm,
44
21
  )
45
22
 
46
- await ctx.entrywayClient.xrpc(com.atproto.server.updateEmail, {
23
+ await entrywayClient.xrpc(com.atproto.server.updateEmail, {
47
24
  headers,
48
25
  body,
49
26
  })
27
+ },
28
+ })
29
+ } else {
30
+ server.add(com.atproto.server.updateEmail, {
31
+ auth,
32
+ handler: async ({ auth, input: { body } }) => {
33
+ const did = auth.credentials.did
34
+ const { token, email } = body
50
35
 
51
- return
52
- }
36
+ // @TODO get the locale somehow (either by adding a field in the request
37
+ // body, or by using the `Accept-Language` header).
38
+ const locale = undefined
53
39
 
54
- // require valid token if account email is confirmed
55
- if (account.emailConfirmedAt) {
56
- if (!token) {
57
- throw new InvalidRequestError(
58
- 'confirmation token required',
59
- 'TokenRequired',
60
- )
61
- }
62
- await ctx.accountManager.assertValidEmailToken(
63
- did,
64
- 'update_email',
65
- token,
66
- )
67
- }
40
+ try {
41
+ await ctx.accountManager.updateEmail(did, email, token, { locale })
42
+ } catch (cause) {
43
+ if (cause instanceof UserAlreadyExistsError) {
44
+ throw new InvalidRequestError(cause.message, undefined, { cause })
45
+ }
68
46
 
69
- try {
70
- await ctx.accountManager.updateEmail({ did, email })
71
- } catch (err) {
72
- if (err instanceof UserAlreadyExistsError) {
73
- throw new InvalidRequestError(
74
- 'This email address is already in use, please use a different email.',
75
- )
76
- } else {
77
- throw err
47
+ throw cause
78
48
  }
79
- }
80
- },
81
- })
49
+ },
50
+ })
51
+ }
82
52
  }
@@ -1,10 +1,15 @@
1
1
  import assert from 'node:assert'
2
2
  import path from 'node:path'
3
3
  import { DAY, HOUR, SECOND } from '@atproto/common'
4
- import { BrandingInput, HcaptchaConfig } from '@atproto/oauth-provider'
4
+ import {
5
+ BrandingInput as BrandingConfig,
6
+ HcaptchaConfig,
7
+ } from '@atproto/oauth-provider'
5
8
  import { ensureValidDid } from '@atproto/syntax'
6
9
  import { ServerEnvironment } from './env.js'
7
10
 
11
+ export type { BrandingConfig }
12
+
8
13
  // off-config but still from env:
9
14
  // logging: LOG_LEVEL, LOG_SYSTEMS, LOG_ENABLED, LOG_DESTINATION
10
15
 
@@ -152,6 +157,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => {
152
157
  emailCfg = {
153
158
  smtpUrl: env.emailSmtpUrl,
154
159
  fromAddress: env.emailFromAddress,
160
+ disableConfirmationLink: env.emailDisableConfirmationLink ?? false,
155
161
  }
156
162
  }
157
163
 
@@ -167,6 +173,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => {
167
173
  moderationEmailCfg = {
168
174
  smtpUrl: env.moderationEmailSmtpUrl,
169
175
  fromAddress: env.moderationEmailAddress,
176
+ disableConfirmationLink: false,
170
177
  }
171
178
  }
172
179
 
@@ -254,6 +261,62 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => {
254
261
  preferCompressed: env.proxyPreferCompressed ?? false,
255
262
  }
256
263
 
264
+ const brandingCfg = {
265
+ name: env.serviceName ?? `${hostname} PDS`,
266
+ logo: env.logoUrl,
267
+ colors: {
268
+ light: env.lightColor,
269
+ dark: env.darkColor,
270
+
271
+ contrastSaturation: env.contrastSaturation,
272
+
273
+ primary: env.primaryColor,
274
+ primaryContrast: env.primaryColorContrast,
275
+ primaryHue: env.primaryColorHue,
276
+
277
+ error: env.errorColor,
278
+ errorContrast: env.errorColorContrast,
279
+ errorHue: env.errorColorHue,
280
+
281
+ warning: env.warningColor,
282
+ warningContrast: env.warningColorContrast,
283
+ warningHue: env.warningColorHue,
284
+
285
+ info: env.infoColor,
286
+ infoContrast: env.infoColorContrast,
287
+ infoHue: env.infoColorHue,
288
+
289
+ success: env.successColor,
290
+ successContrast: env.successColorContrast,
291
+ successHue: env.successColorHue,
292
+ },
293
+ links: [
294
+ {
295
+ title: { en: 'Home', fr: 'Accueil' },
296
+ href: env.homeUrl,
297
+ rel: 'canonical' as const, // Prevents login page from being indexed
298
+ },
299
+ {
300
+ title: { en: 'Terms of Service' },
301
+ href: env.termsOfServiceUrl,
302
+ rel: 'terms-of-service' as const,
303
+ },
304
+ {
305
+ title: { en: 'Privacy Policy' },
306
+ href: env.privacyPolicyUrl,
307
+ rel: 'privacy-policy' as const,
308
+ },
309
+ {
310
+ title: { en: 'Support' },
311
+ href: env.supportUrl,
312
+ rel: 'help' as const,
313
+ },
314
+ ].filter(
315
+ <T extends { href?: string }>(f: T): f is T & { href: string } =>
316
+ f.href != null && f.href !== '',
317
+ ),
318
+ }
319
+
257
320
  const oauthCfg: ServerConfig['oauth'] = entrywayCfg
258
321
  ? {
259
322
  issuer: entrywayCfg.url,
@@ -272,61 +335,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => {
272
335
  tokenSalt: env.hcaptchaTokenSalt,
273
336
  }
274
337
  : undefined,
275
- branding: {
276
- name: env.serviceName ?? `${hostname} PDS`,
277
- logo: env.logoUrl,
278
- colors: {
279
- light: env.lightColor,
280
- dark: env.darkColor,
281
-
282
- contrastSaturation: env.contrastSaturation,
283
-
284
- primary: env.primaryColor,
285
- primaryContrast: env.primaryColorContrast,
286
- primaryHue: env.primaryColorHue,
287
-
288
- error: env.errorColor,
289
- errorContrast: env.errorColorContrast,
290
- errorHue: env.errorColorHue,
291
-
292
- warning: env.warningColor,
293
- warningContrast: env.warningColorContrast,
294
- warningHue: env.warningColorHue,
295
-
296
- info: env.infoColor,
297
- infoContrast: env.infoColorContrast,
298
- infoHue: env.infoColorHue,
299
-
300
- success: env.successColor,
301
- successContrast: env.successColorContrast,
302
- successHue: env.successColorHue,
303
- },
304
- links: [
305
- {
306
- title: { en: 'Home', fr: 'Accueil' },
307
- href: env.homeUrl,
308
- rel: 'canonical' as const, // Prevents login page from being indexed
309
- },
310
- {
311
- title: { en: 'Terms of Service' },
312
- href: env.termsOfServiceUrl,
313
- rel: 'terms-of-service' as const,
314
- },
315
- {
316
- title: { en: 'Privacy Policy' },
317
- href: env.privacyPolicyUrl,
318
- rel: 'privacy-policy' as const,
319
- },
320
- {
321
- title: { en: 'Support' },
322
- href: env.supportUrl,
323
- rel: 'help' as const,
324
- },
325
- ].filter(
326
- <T extends { href?: string }>(f: T): f is T & { href: string } =>
327
- f.href != null && f.href !== '',
328
- ),
329
- },
338
+ branding: brandingCfg,
330
339
  trustedClients: env.trustedOAuthClients,
331
340
  },
332
341
  }
@@ -358,6 +367,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => {
358
367
  fetch: fetchCfg,
359
368
  lexicon: lexiconCfg,
360
369
  proxy: proxyCfg,
370
+ branding: brandingCfg,
361
371
  oauth: oauthCfg,
362
372
  }
363
373
  }
@@ -381,6 +391,7 @@ export type ServerConfig = {
381
391
  crawlers: string[]
382
392
  fetch: FetchConfig
383
393
  proxy: ProxyConfig
394
+ branding: BrandingConfig
384
395
  oauth: OAuthConfig
385
396
  lexicon: LexiconResolverConfig
386
397
  }
@@ -477,7 +488,7 @@ export type OAuthConfig = {
477
488
  issuer: string
478
489
  provider?: {
479
490
  hcaptcha?: HcaptchaConfig
480
- branding: BrandingInput
491
+ branding: BrandingConfig
481
492
  trustedClients?: string[]
482
493
  }
483
494
  }
@@ -499,6 +510,7 @@ export type InvitesConfig =
499
510
  export type EmailConfig = {
500
511
  smtpUrl: string
501
512
  fromAddress: string
513
+ disableConfirmationLink: boolean
502
514
  }
503
515
 
504
516
  export type SubscriptionConfig = {
package/src/config/env.ts CHANGED
@@ -97,6 +97,9 @@ export function readEnv() {
97
97
  // email
98
98
  emailSmtpUrl: envStr('PDS_EMAIL_SMTP_URL'),
99
99
  emailFromAddress: envStr('PDS_EMAIL_FROM_ADDRESS'),
100
+ emailDisableConfirmationLink: envBool(
101
+ 'PDS_EMAIL_DISABLE_CONFIRMATION_LINK',
102
+ ),
100
103
  moderationEmailSmtpUrl: envStr('PDS_MODERATION_EMAIL_SMTP_URL'),
101
104
  moderationEmailAddress: envStr('PDS_MODERATION_EMAIL_ADDRESS'),
102
105
 
package/src/context.ts CHANGED
@@ -156,7 +156,7 @@ export class AppContext {
156
156
  ? nodemailer.createTransport(cfg.email.smtpUrl)
157
157
  : nodemailer.createTransport({ jsonTransport: true })
158
158
 
159
- const mailer = new ServerMailer(mailTransport, cfg)
159
+ const mailer = new ServerMailer(mailTransport, cfg.email, cfg.branding)
160
160
 
161
161
  const modMailTransport =
162
162
  cfg.moderationEmail !== null
@@ -270,6 +270,7 @@ export class AppContext {
270
270
  const accountManager = new AccountManager(
271
271
  idResolver,
272
272
  jwtSecretKey,
273
+ mailer,
273
274
  cfg.service.did,
274
275
  cfg.identity.serviceHandleDomains,
275
276
  cfg.db,