@atproto/oauth-provider 0.19.7 → 0.19.8

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 (165) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/errors/error-parser.js +5 -5
  3. package/dist/errors/error-parser.js.map +1 -1
  4. package/package.json +22 -18
  5. package/src/access-token/access-token-mode.ts +0 -4
  6. package/src/account/account-manager.ts +0 -591
  7. package/src/account/account-store.ts +0 -305
  8. package/src/account/sign-in-data.ts +0 -15
  9. package/src/account/sign-up-input.ts +0 -20
  10. package/src/client/client-auth.ts +0 -64
  11. package/src/client/client-data.ts +0 -9
  12. package/src/client/client-id.ts +0 -4
  13. package/src/client/client-info.ts +0 -13
  14. package/src/client/client-manager.ts +0 -772
  15. package/src/client/client-store.ts +0 -35
  16. package/src/client/client-utils.ts +0 -37
  17. package/src/client/client.ts +0 -394
  18. package/src/constants.ts +0 -80
  19. package/src/customization/branding.ts +0 -12
  20. package/src/customization/build-customization-css.ts +0 -55
  21. package/src/customization/build-customization-data.ts +0 -24
  22. package/src/customization/colors.ts +0 -48
  23. package/src/customization/customization.ts +0 -29
  24. package/src/customization/links.ts +0 -10
  25. package/src/device/device-data.ts +0 -11
  26. package/src/device/device-id.ts +0 -27
  27. package/src/device/device-manager.ts +0 -292
  28. package/src/device/device-store.ts +0 -31
  29. package/src/device/session-id.ts +0 -21
  30. package/src/dpop/dpop-manager.ts +0 -214
  31. package/src/dpop/dpop-nonce.ts +0 -121
  32. package/src/dpop/dpop-proof.ts +0 -6
  33. package/src/errors/access-denied-error.ts +0 -12
  34. package/src/errors/account-selection-required-error.ts +0 -12
  35. package/src/errors/authorization-error.ts +0 -45
  36. package/src/errors/consent-required-error.ts +0 -12
  37. package/src/errors/error-parser.ts +0 -147
  38. package/src/errors/handle-unavailable-error.ts +0 -23
  39. package/src/errors/invalid-authorization-details-error.ts +0 -27
  40. package/src/errors/invalid-client-error.ts +0 -20
  41. package/src/errors/invalid-client-id-error.ts +0 -31
  42. package/src/errors/invalid-client-metadata-error.ts +0 -68
  43. package/src/errors/invalid-credentials-error.ts +0 -29
  44. package/src/errors/invalid-dpop-key-binding-error.ts +0 -21
  45. package/src/errors/invalid-dpop-proof-error.ts +0 -13
  46. package/src/errors/invalid-grant-error.ts +0 -21
  47. package/src/errors/invalid-invite-code-error.ts +0 -10
  48. package/src/errors/invalid-redirect-uri-error.ts +0 -17
  49. package/src/errors/invalid-request-error.ts +0 -32
  50. package/src/errors/invalid-scope-error.ts +0 -15
  51. package/src/errors/invalid-token-error.ts +0 -60
  52. package/src/errors/login-required-error.ts +0 -12
  53. package/src/errors/oauth-error.ts +0 -28
  54. package/src/errors/second-authentication-factor-required-error.ts +0 -25
  55. package/src/errors/unauthorized-client-error.ts +0 -20
  56. package/src/errors/use-dpop-nonce-error.ts +0 -32
  57. package/src/errors/www-authenticate-error.ts +0 -64
  58. package/src/index.ts +0 -16
  59. package/src/lexicon/lexicon-data.ts +0 -11
  60. package/src/lexicon/lexicon-getter.ts +0 -55
  61. package/src/lexicon/lexicon-manager.ts +0 -116
  62. package/src/lexicon/lexicon-store.ts +0 -35
  63. package/src/lib/csp/index.ts +0 -101
  64. package/src/lib/hcaptcha.ts +0 -228
  65. package/src/lib/html/README.md +0 -9
  66. package/src/lib/html/build-document.ts +0 -151
  67. package/src/lib/html/escapers.ts +0 -66
  68. package/src/lib/html/html.ts +0 -56
  69. package/src/lib/html/hydration-data.ts +0 -19
  70. package/src/lib/html/index.ts +0 -5
  71. package/src/lib/html/tags.ts +0 -67
  72. package/src/lib/html/util.ts +0 -17
  73. package/src/lib/http/README.md +0 -11
  74. package/src/lib/http/accept.ts +0 -90
  75. package/src/lib/http/context.ts +0 -42
  76. package/src/lib/http/headers.ts +0 -15
  77. package/src/lib/http/index.ts +0 -10
  78. package/src/lib/http/method.ts +0 -18
  79. package/src/lib/http/middleware.ts +0 -169
  80. package/src/lib/http/parser.ts +0 -101
  81. package/src/lib/http/path.ts +0 -82
  82. package/src/lib/http/request.ts +0 -256
  83. package/src/lib/http/response.ts +0 -122
  84. package/src/lib/http/route.ts +0 -60
  85. package/src/lib/http/router.ts +0 -117
  86. package/src/lib/http/security-headers.ts +0 -100
  87. package/src/lib/http/stream.ts +0 -57
  88. package/src/lib/http/types.ts +0 -16
  89. package/src/lib/http/url.ts +0 -23
  90. package/src/lib/nsid.ts +0 -10
  91. package/src/lib/redis.ts +0 -25
  92. package/src/lib/util/authorization-header.ts +0 -28
  93. package/src/lib/util/cast.ts +0 -18
  94. package/src/lib/util/color.ts +0 -168
  95. package/src/lib/util/crypto.ts +0 -32
  96. package/src/lib/util/date.ts +0 -7
  97. package/src/lib/util/error.ts +0 -7
  98. package/src/lib/util/function.ts +0 -39
  99. package/src/lib/util/locale.ts +0 -12
  100. package/src/lib/util/object.ts +0 -23
  101. package/src/lib/util/redirect-uri.ts +0 -46
  102. package/src/lib/util/time.ts +0 -49
  103. package/src/lib/util/type.ts +0 -182
  104. package/src/lib/util/ui8.ts +0 -14
  105. package/src/lib/util/well-known.ts +0 -8
  106. package/src/lib/util/zod-error.ts +0 -26
  107. package/src/lib/write-form-redirect.ts +0 -58
  108. package/src/lib/write-html.ts +0 -70
  109. package/src/metadata/build-metadata.ts +0 -141
  110. package/src/oauth-client.ts +0 -3
  111. package/src/oauth-dpop.ts +0 -2
  112. package/src/oauth-errors.ts +0 -26
  113. package/src/oauth-hooks.ts +0 -519
  114. package/src/oauth-middleware.ts +0 -53
  115. package/src/oauth-provider.ts +0 -1110
  116. package/src/oauth-store.ts +0 -12
  117. package/src/oauth-verifier.ts +0 -260
  118. package/src/replay/replay-manager.ts +0 -51
  119. package/src/replay/replay-store-memory.ts +0 -36
  120. package/src/replay/replay-store-redis.ts +0 -30
  121. package/src/replay/replay-store.ts +0 -44
  122. package/src/request/code.ts +0 -23
  123. package/src/request/request-data.ts +0 -36
  124. package/src/request/request-id.ts +0 -22
  125. package/src/request/request-manager.ts +0 -521
  126. package/src/request/request-store.ts +0 -60
  127. package/src/request/request-uri.ts +0 -42
  128. package/src/result/authorization-redirect-parameters.ts +0 -24
  129. package/src/result/authorization-result-authorize-page.ts +0 -16
  130. package/src/result/authorization-result-redirect.ts +0 -8
  131. package/src/router/assets/assets-manifest.ts +0 -117
  132. package/src/router/assets/assets.ts +0 -124
  133. package/src/router/assets/csrf.ts +0 -79
  134. package/src/router/assets/send-account-page.ts +0 -23
  135. package/src/router/assets/send-authorization-page.ts +0 -42
  136. package/src/router/assets/send-cookie-error-page.ts +0 -21
  137. package/src/router/assets/send-error-page.ts +0 -25
  138. package/src/router/assets/send-redirect.ts +0 -140
  139. package/src/router/create-account-page-middleware.ts +0 -88
  140. package/src/router/create-api-middleware.ts +0 -1051
  141. package/src/router/create-authorization-page-middleware.ts +0 -281
  142. package/src/router/create-oauth-middleware.ts +0 -257
  143. package/src/router/error-handler.ts +0 -6
  144. package/src/router/middleware-options.ts +0 -9
  145. package/src/signer/access-token-payload.ts +0 -25
  146. package/src/signer/api-token-payload.ts +0 -18
  147. package/src/signer/signer.ts +0 -121
  148. package/src/token/refresh-token.ts +0 -30
  149. package/src/token/token-claims.ts +0 -22
  150. package/src/token/token-data.ts +0 -40
  151. package/src/token/token-id.ts +0 -25
  152. package/src/token/token-manager.ts +0 -427
  153. package/src/token/token-store.ts +0 -88
  154. package/src/types/authorization-response-error.ts +0 -27
  155. package/src/types/color-hue.ts +0 -3
  156. package/src/types/email-otp.ts +0 -3
  157. package/src/types/email.ts +0 -26
  158. package/src/types/handle.ts +0 -23
  159. package/src/types/invite-code.ts +0 -4
  160. package/src/types/par-response-error.ts +0 -25
  161. package/src/types/password.ts +0 -4
  162. package/src/types/rgb-color.ts +0 -21
  163. package/tsconfig.build.json +0 -8
  164. package/tsconfig.build.tsbuildinfo +0 -1
  165. package/tsconfig.json +0 -4
@@ -1,88 +0,0 @@
1
- import type { Did } from '@atproto/did'
2
- import type { Account } from '@atproto/oauth-provider-api'
3
- import { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'
4
- import { Code } from '../request/code.js'
5
- import { RefreshToken } from './refresh-token.js'
6
- import { TokenData } from './token-data.js'
7
- import { TokenId } from './token-id.js'
8
-
9
- // Export all types needed to implement the TokenStore interface
10
- export * from './refresh-token.js'
11
- export * from './token-data.js'
12
- export * from './token-id.js'
13
- export type { Account, Awaitable, Did }
14
-
15
- export type TokenInfo = {
16
- id: TokenId
17
- data: TokenData
18
- account: Account
19
- currentRefreshToken: null | RefreshToken
20
- }
21
-
22
- export type NewTokenData = {
23
- clientAuth: TokenData['clientAuth']
24
- expiresAt: TokenData['expiresAt']
25
- updatedAt: TokenData['updatedAt']
26
- scope: NonNullable<TokenData['scope']>
27
- }
28
-
29
- export type CreateTokenData = TokenData & {
30
- scope: NonNullable<TokenData['scope']>
31
- }
32
-
33
- /**
34
- * @param data historically, {@link TokenData.scope} was not present in
35
- * {@link TokenData}, causing it to be "nullable" when returned from
36
- * {@link TokenStore.readToken}. We use {@link CreateTokenData} here to allow
37
- * the store implementation to expect its presence.
38
- */
39
- export interface TokenStore {
40
- createToken(
41
- tokenId: TokenId,
42
- data: CreateTokenData,
43
- refreshToken?: RefreshToken,
44
- ): Awaitable<void>
45
-
46
- readToken(tokenId: TokenId): Awaitable<null | TokenInfo>
47
-
48
- deleteToken(tokenId: TokenId): Awaitable<void>
49
-
50
- rotateToken(
51
- tokenId: TokenId,
52
- newTokenId: TokenId,
53
- newRefreshToken: RefreshToken,
54
- newData: NewTokenData,
55
- ): Awaitable<void>
56
-
57
- /**
58
- * Find a token by its refresh token. Note that previous refresh tokens
59
- * should also return the token. The data model is responsible for storing
60
- * old refresh tokens when a new one is issued.
61
- */
62
- findTokenByRefreshToken(
63
- refreshToken: RefreshToken,
64
- ): Awaitable<null | TokenInfo>
65
-
66
- findTokenByCode(code: Code): Awaitable<null | TokenInfo>
67
-
68
- listAccountTokens(did: Did): Awaitable<TokenInfo[]>
69
- }
70
-
71
- export const isTokenStore = buildInterfaceChecker<TokenStore>([
72
- 'createToken',
73
- 'deleteToken',
74
- 'findTokenByCode',
75
- 'findTokenByRefreshToken',
76
- 'listAccountTokens',
77
- 'readToken',
78
- 'rotateToken',
79
- ])
80
-
81
- export function asTokenStore<V extends Partial<TokenStore>>(
82
- implementation?: V,
83
- ): V & TokenStore {
84
- if (!implementation || !isTokenStore(implementation)) {
85
- throw new Error('Invalid TokenStore implementation')
86
- }
87
- return implementation
88
- }
@@ -1,27 +0,0 @@
1
- import { z } from 'zod'
2
- import {
3
- oauthAuthorizationResponseErrorSchema,
4
- oidcAuthorizationResponseErrorSchema,
5
- } from '@atproto/oauth-types'
6
-
7
- export const authorizationResponseErrorSchema = z.union([
8
- oauthAuthorizationResponseErrorSchema,
9
- // OIDC authentication error response are not part of the ATproto flavoured
10
- // OAuth but we allow them because they provide better feedback to the client
11
- // (in particular when SSO is used).
12
- oidcAuthorizationResponseErrorSchema,
13
- // This error is defined by rfc9396 (not part of the OAuth 2.1 or OIDC). But
14
- // since, in ATproto flavoured OAuth, client registration is a dynamic part of
15
- // the authorization process, we allow it.
16
- z.literal('invalid_authorization_details'),
17
- ])
18
-
19
- export type AuthorizationResponseError = z.infer<
20
- typeof authorizationResponseErrorSchema
21
- >
22
-
23
- export function isAuthorizationResponseError<T>(
24
- value: T,
25
- ): value is T & AuthorizationResponseError {
26
- return authorizationResponseErrorSchema.safeParse(value).success
27
- }
@@ -1,3 +0,0 @@
1
- import { z } from 'zod'
2
-
3
- export const colorHueSchema = z.number().min(0).max(360)
@@ -1,3 +0,0 @@
1
- import { z } from 'zod'
2
-
3
- export const emailOtpSchema = z.string().min(1)
@@ -1,26 +0,0 @@
1
- import { isEmailValid } from '@hapi/address'
2
- import { isDisposableEmail } from 'disposable-email-domains-js'
3
- import { z } from 'zod'
4
-
5
- export const emailSchema = z
6
- .string()
7
- .email()
8
- // @NOTE Internally, `zod` uses a regexp for validating emails.. This
9
- // validation strategy *could* be less permissive in some (edge) cases than
10
- // `@hapi/address` as the latter uses an algorithm based on the spec. Truth
11
- // is, it is kinda hard to know if the set of emails allowed by
12
- // `@hapi/address` is covered by the set of emails allowed by `zod`.
13
- // Additionally, this could change with future changes in either libraries.
14
- //
15
- // Because of this uncertainty, and because other part of the Bluesky/ATProto
16
- // codebases rely solely on `zod`, this code only allows emails that are valid
17
- // according to both libraries ensuring that we never encounter a case where
18
- // an email allowed here is in a format that would be rejected by other parts
19
- // of our systems.
20
- .refine(isEmailValid, {
21
- message: 'Invalid email address',
22
- })
23
- .refine((email) => !isDisposableEmail(email), {
24
- message: 'Disposable email addresses are not allowed',
25
- })
26
- .transform((value) => value.toLowerCase())
@@ -1,23 +0,0 @@
1
- import { z } from 'zod'
2
- import {
3
- HandleString,
4
- ensureValidHandle,
5
- normalizeHandle,
6
- } from '@atproto/syntax'
7
-
8
- /**
9
- * @note Only validates again AT Protocol's syntax. Additional rules (specific
10
- * domains, slurs, etc.) may be imposed through the store implementation.
11
- */
12
- export const handleSchema = z.string().transform((value, ctx): HandleString => {
13
- try {
14
- ensureValidHandle(value)
15
- return normalizeHandle(value)
16
- } catch (err) {
17
- ctx.addIssue({
18
- code: z.ZodIssueCode.custom,
19
- message: err instanceof Error ? err.message : 'Invalid handle',
20
- })
21
- return z.NEVER
22
- }
23
- })
@@ -1,4 +0,0 @@
1
- import { z } from 'zod'
2
-
3
- export const inviteCodeSchema = z.string().min(1)
4
- export type InviteCode = z.infer<typeof inviteCodeSchema>
@@ -1,25 +0,0 @@
1
- import { z } from 'zod'
2
- import { authorizationResponseErrorSchema } from './authorization-response-error.js'
3
-
4
- // https://datatracker.ietf.org/doc/html/rfc9126#section-2.3-1
5
- // > Since initial processing of the pushed authorization request does not
6
- // > involve resource owner interaction, error codes related to user
7
- // > interaction, such as "access_denied", are never returned.
8
-
9
- export const parResponseErrorSchema = z.intersection(
10
- authorizationResponseErrorSchema,
11
- z.enum([
12
- 'invalid_request',
13
- 'unauthorized_client',
14
- 'unsupported_response_type',
15
- 'invalid_scope',
16
- 'server_error',
17
- 'temporarily_unavailable',
18
- ]),
19
- )
20
-
21
- export type PARResponseError = z.infer<typeof parResponseErrorSchema>
22
-
23
- export function isPARResponseError<T>(value: T): value is T & PARResponseError {
24
- return parResponseErrorSchema.safeParse(value).success
25
- }
@@ -1,4 +0,0 @@
1
- import { z } from 'zod'
2
-
3
- export const oldPasswordSchema = z.string().min(1).max(512)
4
- export const newPasswordSchema = z.string().min(8).max(256)
@@ -1,21 +0,0 @@
1
- import { z } from 'zod'
2
- import { RgbColor, parseColor } from '../lib/util/color.js'
3
-
4
- export const rgbColorSchema = z.string().transform((value, ctx): RgbColor => {
5
- try {
6
- const parsed = parseColor(value)
7
- if ('a' in parsed && parsed.a !== undefined) {
8
- ctx.addIssue({
9
- code: z.ZodIssueCode.custom,
10
- message: 'Alpha values are not supported',
11
- })
12
- }
13
- return parsed
14
- } catch (e) {
15
- ctx.addIssue({
16
- code: z.ZodIssueCode.custom,
17
- message: e instanceof Error ? e.message : 'Invalid color value',
18
- })
19
- return z.NEVER
20
- }
21
- })
@@ -1,8 +0,0 @@
1
- {
2
- "extends": ["../../../tsconfig/nodenext.json"],
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src",
6
- },
7
- "include": ["src"],
8
- }
@@ -1 +0,0 @@
1
- {"version":"7.0.0-dev.20260614.1","root":["./src/constants.ts","./src/index.ts","./src/oauth-client.ts","./src/oauth-dpop.ts","./src/oauth-errors.ts","./src/oauth-hooks.ts","./src/oauth-middleware.ts","./src/oauth-provider.ts","./src/oauth-store.ts","./src/oauth-verifier.ts","./src/access-token/access-token-mode.ts","./src/account/account-manager.ts","./src/account/account-store.ts","./src/account/sign-in-data.ts","./src/account/sign-up-input.ts","./src/client/client-auth.ts","./src/client/client-data.ts","./src/client/client-id.ts","./src/client/client-info.ts","./src/client/client-manager.ts","./src/client/client-store.ts","./src/client/client-utils.ts","./src/client/client.ts","./src/customization/branding.ts","./src/customization/build-customization-css.ts","./src/customization/build-customization-data.ts","./src/customization/colors.ts","./src/customization/customization.ts","./src/customization/links.ts","./src/device/device-data.ts","./src/device/device-id.ts","./src/device/device-manager.ts","./src/device/device-store.ts","./src/device/session-id.ts","./src/dpop/dpop-manager.ts","./src/dpop/dpop-nonce.ts","./src/dpop/dpop-proof.ts","./src/errors/access-denied-error.ts","./src/errors/account-selection-required-error.ts","./src/errors/authorization-error.ts","./src/errors/consent-required-error.ts","./src/errors/error-parser.ts","./src/errors/handle-unavailable-error.ts","./src/errors/invalid-authorization-details-error.ts","./src/errors/invalid-client-error.ts","./src/errors/invalid-client-id-error.ts","./src/errors/invalid-client-metadata-error.ts","./src/errors/invalid-credentials-error.ts","./src/errors/invalid-dpop-key-binding-error.ts","./src/errors/invalid-dpop-proof-error.ts","./src/errors/invalid-grant-error.ts","./src/errors/invalid-invite-code-error.ts","./src/errors/invalid-redirect-uri-error.ts","./src/errors/invalid-request-error.ts","./src/errors/invalid-scope-error.ts","./src/errors/invalid-token-error.ts","./src/errors/login-required-error.ts","./src/errors/oauth-error.ts","./src/errors/second-authentication-factor-required-error.ts","./src/errors/unauthorized-client-error.ts","./src/errors/use-dpop-nonce-error.ts","./src/errors/www-authenticate-error.ts","./src/lexicon/lexicon-data.ts","./src/lexicon/lexicon-getter.ts","./src/lexicon/lexicon-manager.ts","./src/lexicon/lexicon-store.ts","./src/lib/hcaptcha.ts","./src/lib/nsid.ts","./src/lib/redis.ts","./src/lib/write-form-redirect.ts","./src/lib/write-html.ts","./src/lib/csp/index.ts","./src/lib/html/build-document.ts","./src/lib/html/escapers.ts","./src/lib/html/html.ts","./src/lib/html/hydration-data.ts","./src/lib/html/index.ts","./src/lib/html/tags.ts","./src/lib/html/util.ts","./src/lib/http/accept.ts","./src/lib/http/context.ts","./src/lib/http/headers.ts","./src/lib/http/index.ts","./src/lib/http/method.ts","./src/lib/http/middleware.ts","./src/lib/http/parser.ts","./src/lib/http/path.ts","./src/lib/http/request.ts","./src/lib/http/response.ts","./src/lib/http/route.ts","./src/lib/http/router.ts","./src/lib/http/security-headers.ts","./src/lib/http/stream.ts","./src/lib/http/types.ts","./src/lib/http/url.ts","./src/lib/util/authorization-header.ts","./src/lib/util/cast.ts","./src/lib/util/color.ts","./src/lib/util/crypto.ts","./src/lib/util/date.ts","./src/lib/util/error.ts","./src/lib/util/function.ts","./src/lib/util/locale.ts","./src/lib/util/object.ts","./src/lib/util/redirect-uri.ts","./src/lib/util/time.ts","./src/lib/util/type.ts","./src/lib/util/ui8.ts","./src/lib/util/well-known.ts","./src/lib/util/zod-error.ts","./src/metadata/build-metadata.ts","./src/replay/replay-manager.ts","./src/replay/replay-store-memory.ts","./src/replay/replay-store-redis.ts","./src/replay/replay-store.ts","./src/request/code.ts","./src/request/request-data.ts","./src/request/request-id.ts","./src/request/request-manager.ts","./src/request/request-store.ts","./src/request/request-uri.ts","./src/result/authorization-redirect-parameters.ts","./src/result/authorization-result-authorize-page.ts","./src/result/authorization-result-redirect.ts","./src/router/create-account-page-middleware.ts","./src/router/create-api-middleware.ts","./src/router/create-authorization-page-middleware.ts","./src/router/create-oauth-middleware.ts","./src/router/error-handler.ts","./src/router/middleware-options.ts","./src/router/assets/assets-manifest.ts","./src/router/assets/assets.ts","./src/router/assets/csrf.ts","./src/router/assets/send-account-page.ts","./src/router/assets/send-authorization-page.ts","./src/router/assets/send-cookie-error-page.ts","./src/router/assets/send-error-page.ts","./src/router/assets/send-redirect.ts","./src/signer/access-token-payload.ts","./src/signer/api-token-payload.ts","./src/signer/signer.ts","./src/token/refresh-token.ts","./src/token/token-claims.ts","./src/token/token-data.ts","./src/token/token-id.ts","./src/token/token-manager.ts","./src/token/token-store.ts","./src/types/authorization-response-error.ts","./src/types/color-hue.ts","./src/types/email-otp.ts","./src/types/email.ts","./src/types/handle.ts","./src/types/invite-code.ts","./src/types/par-response-error.ts","./src/types/password.ts","./src/types/rgb-color.ts"]}
package/tsconfig.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "include": [],
3
- "references": [{ "path": "./tsconfig.build.json" }],
4
- }