@atproto/oauth-provider 0.19.6 → 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.
- package/CHANGELOG.md +33 -0
- package/dist/errors/error-parser.js +5 -5
- package/dist/errors/error-parser.js.map +1 -1
- package/package.json +22 -18
- package/src/access-token/access-token-mode.ts +0 -4
- package/src/account/account-manager.ts +0 -591
- package/src/account/account-store.ts +0 -305
- package/src/account/sign-in-data.ts +0 -15
- package/src/account/sign-up-input.ts +0 -20
- package/src/client/client-auth.ts +0 -64
- package/src/client/client-data.ts +0 -9
- package/src/client/client-id.ts +0 -4
- package/src/client/client-info.ts +0 -13
- package/src/client/client-manager.ts +0 -772
- package/src/client/client-store.ts +0 -35
- package/src/client/client-utils.ts +0 -37
- package/src/client/client.ts +0 -394
- package/src/constants.ts +0 -80
- package/src/customization/branding.ts +0 -12
- package/src/customization/build-customization-css.ts +0 -55
- package/src/customization/build-customization-data.ts +0 -24
- package/src/customization/colors.ts +0 -48
- package/src/customization/customization.ts +0 -29
- package/src/customization/links.ts +0 -10
- package/src/device/device-data.ts +0 -11
- package/src/device/device-id.ts +0 -27
- package/src/device/device-manager.ts +0 -292
- package/src/device/device-store.ts +0 -31
- package/src/device/session-id.ts +0 -21
- package/src/dpop/dpop-manager.ts +0 -214
- package/src/dpop/dpop-nonce.ts +0 -121
- package/src/dpop/dpop-proof.ts +0 -6
- package/src/errors/access-denied-error.ts +0 -12
- package/src/errors/account-selection-required-error.ts +0 -12
- package/src/errors/authorization-error.ts +0 -45
- package/src/errors/consent-required-error.ts +0 -12
- package/src/errors/error-parser.ts +0 -147
- package/src/errors/handle-unavailable-error.ts +0 -23
- package/src/errors/invalid-authorization-details-error.ts +0 -27
- package/src/errors/invalid-client-error.ts +0 -20
- package/src/errors/invalid-client-id-error.ts +0 -31
- package/src/errors/invalid-client-metadata-error.ts +0 -68
- package/src/errors/invalid-credentials-error.ts +0 -29
- package/src/errors/invalid-dpop-key-binding-error.ts +0 -21
- package/src/errors/invalid-dpop-proof-error.ts +0 -13
- package/src/errors/invalid-grant-error.ts +0 -21
- package/src/errors/invalid-invite-code-error.ts +0 -10
- package/src/errors/invalid-redirect-uri-error.ts +0 -17
- package/src/errors/invalid-request-error.ts +0 -32
- package/src/errors/invalid-scope-error.ts +0 -15
- package/src/errors/invalid-token-error.ts +0 -60
- package/src/errors/login-required-error.ts +0 -12
- package/src/errors/oauth-error.ts +0 -28
- package/src/errors/second-authentication-factor-required-error.ts +0 -25
- package/src/errors/unauthorized-client-error.ts +0 -20
- package/src/errors/use-dpop-nonce-error.ts +0 -32
- package/src/errors/www-authenticate-error.ts +0 -64
- package/src/index.ts +0 -16
- package/src/lexicon/lexicon-data.ts +0 -11
- package/src/lexicon/lexicon-getter.ts +0 -55
- package/src/lexicon/lexicon-manager.ts +0 -116
- package/src/lexicon/lexicon-store.ts +0 -35
- package/src/lib/csp/index.ts +0 -101
- package/src/lib/hcaptcha.ts +0 -228
- package/src/lib/html/README.md +0 -9
- package/src/lib/html/build-document.ts +0 -151
- package/src/lib/html/escapers.ts +0 -66
- package/src/lib/html/html.ts +0 -56
- package/src/lib/html/hydration-data.ts +0 -19
- package/src/lib/html/index.ts +0 -5
- package/src/lib/html/tags.ts +0 -67
- package/src/lib/html/util.ts +0 -17
- package/src/lib/http/README.md +0 -11
- package/src/lib/http/accept.ts +0 -90
- package/src/lib/http/context.ts +0 -42
- package/src/lib/http/headers.ts +0 -15
- package/src/lib/http/index.ts +0 -10
- package/src/lib/http/method.ts +0 -18
- package/src/lib/http/middleware.ts +0 -169
- package/src/lib/http/parser.ts +0 -101
- package/src/lib/http/path.ts +0 -82
- package/src/lib/http/request.ts +0 -256
- package/src/lib/http/response.ts +0 -122
- package/src/lib/http/route.ts +0 -60
- package/src/lib/http/router.ts +0 -117
- package/src/lib/http/security-headers.ts +0 -100
- package/src/lib/http/stream.ts +0 -57
- package/src/lib/http/types.ts +0 -16
- package/src/lib/http/url.ts +0 -23
- package/src/lib/nsid.ts +0 -10
- package/src/lib/redis.ts +0 -25
- package/src/lib/util/authorization-header.ts +0 -28
- package/src/lib/util/cast.ts +0 -18
- package/src/lib/util/color.ts +0 -168
- package/src/lib/util/crypto.ts +0 -32
- package/src/lib/util/date.ts +0 -7
- package/src/lib/util/error.ts +0 -7
- package/src/lib/util/function.ts +0 -39
- package/src/lib/util/locale.ts +0 -12
- package/src/lib/util/object.ts +0 -23
- package/src/lib/util/redirect-uri.ts +0 -46
- package/src/lib/util/time.ts +0 -49
- package/src/lib/util/type.ts +0 -182
- package/src/lib/util/ui8.ts +0 -14
- package/src/lib/util/well-known.ts +0 -8
- package/src/lib/util/zod-error.ts +0 -26
- package/src/lib/write-form-redirect.ts +0 -58
- package/src/lib/write-html.ts +0 -70
- package/src/metadata/build-metadata.ts +0 -141
- package/src/oauth-client.ts +0 -3
- package/src/oauth-dpop.ts +0 -2
- package/src/oauth-errors.ts +0 -26
- package/src/oauth-hooks.ts +0 -519
- package/src/oauth-middleware.ts +0 -53
- package/src/oauth-provider.ts +0 -1110
- package/src/oauth-store.ts +0 -12
- package/src/oauth-verifier.ts +0 -260
- package/src/replay/replay-manager.ts +0 -51
- package/src/replay/replay-store-memory.ts +0 -36
- package/src/replay/replay-store-redis.ts +0 -30
- package/src/replay/replay-store.ts +0 -44
- package/src/request/code.ts +0 -23
- package/src/request/request-data.ts +0 -36
- package/src/request/request-id.ts +0 -22
- package/src/request/request-manager.ts +0 -521
- package/src/request/request-store.ts +0 -60
- package/src/request/request-uri.ts +0 -42
- package/src/result/authorization-redirect-parameters.ts +0 -24
- package/src/result/authorization-result-authorize-page.ts +0 -16
- package/src/result/authorization-result-redirect.ts +0 -8
- package/src/router/assets/assets-manifest.ts +0 -117
- package/src/router/assets/assets.ts +0 -124
- package/src/router/assets/csrf.ts +0 -79
- package/src/router/assets/send-account-page.ts +0 -23
- package/src/router/assets/send-authorization-page.ts +0 -42
- package/src/router/assets/send-cookie-error-page.ts +0 -21
- package/src/router/assets/send-error-page.ts +0 -25
- package/src/router/assets/send-redirect.ts +0 -140
- package/src/router/create-account-page-middleware.ts +0 -88
- package/src/router/create-api-middleware.ts +0 -1051
- package/src/router/create-authorization-page-middleware.ts +0 -281
- package/src/router/create-oauth-middleware.ts +0 -257
- package/src/router/error-handler.ts +0 -6
- package/src/router/middleware-options.ts +0 -9
- package/src/signer/access-token-payload.ts +0 -25
- package/src/signer/api-token-payload.ts +0 -18
- package/src/signer/signer.ts +0 -121
- package/src/token/refresh-token.ts +0 -30
- package/src/token/token-claims.ts +0 -22
- package/src/token/token-data.ts +0 -40
- package/src/token/token-id.ts +0 -25
- package/src/token/token-manager.ts +0 -427
- package/src/token/token-store.ts +0 -88
- package/src/types/authorization-response-error.ts +0 -27
- package/src/types/color-hue.ts +0 -3
- package/src/types/email-otp.ts +0 -3
- package/src/types/email.ts +0 -26
- package/src/types/handle.ts +0 -23
- package/src/types/invite-code.ts +0 -4
- package/src/types/par-response-error.ts +0 -25
- package/src/types/password.ts +0 -4
- package/src/types/rgb-color.ts +0 -21
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -4
package/src/lib/hcaptcha.ts
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
|
-
import { z } from 'zod'
|
|
3
|
-
import {
|
|
4
|
-
Fetch,
|
|
5
|
-
FetchBound,
|
|
6
|
-
bindFetch,
|
|
7
|
-
fetchJsonProcessor,
|
|
8
|
-
fetchJsonZodProcessor,
|
|
9
|
-
fetchOkProcessor,
|
|
10
|
-
} from '@atproto-labs/fetch'
|
|
11
|
-
import { pipe } from '@atproto-labs/pipe'
|
|
12
|
-
|
|
13
|
-
export const hcaptchaTokenSchema = z.string().min(1)
|
|
14
|
-
export type HcaptchaToken = z.infer<typeof hcaptchaTokenSchema>
|
|
15
|
-
|
|
16
|
-
export const hcaptchaConfigSchema = z.object({
|
|
17
|
-
/**
|
|
18
|
-
* The hCaptcha site key to use for the sign-up form.
|
|
19
|
-
*/
|
|
20
|
-
siteKey: z.string().min(1),
|
|
21
|
-
/**
|
|
22
|
-
* The hCaptcha secret key to use for the sign-up form.
|
|
23
|
-
*/
|
|
24
|
-
secretKey: z.string().min(1),
|
|
25
|
-
/**
|
|
26
|
-
* A salt to use when hashing client tokens.
|
|
27
|
-
*/
|
|
28
|
-
tokenSalt: z.string().min(1),
|
|
29
|
-
/**
|
|
30
|
-
* The risk score above which the user is considered a threat and will be
|
|
31
|
-
* denied access. This will be ignored if the enterprise features are not
|
|
32
|
-
* available.
|
|
33
|
-
*
|
|
34
|
-
* Note: Score values ranges from 0.0 (no risk) to 1.0 (confirmed threat).
|
|
35
|
-
*/
|
|
36
|
-
scoreThreshold: z.number().optional(),
|
|
37
|
-
})
|
|
38
|
-
export type HcaptchaConfig = z.infer<typeof hcaptchaConfigSchema>
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @see {@link https://docs.hcaptcha.com/#verify-the-user-response-server-side hCaptcha API}
|
|
42
|
-
*/
|
|
43
|
-
export const hcaptchaVerifyResultSchema = z.object({
|
|
44
|
-
/**
|
|
45
|
-
* is the passcode valid, and does it meet security criteria you specified, e.g. sitekey?
|
|
46
|
-
*/
|
|
47
|
-
success: z.boolean(),
|
|
48
|
-
/**
|
|
49
|
-
* timestamp of the challenge (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
|
|
50
|
-
*/
|
|
51
|
-
challenge_ts: z.string(),
|
|
52
|
-
/**
|
|
53
|
-
* the hostname of the site where the challenge was passed
|
|
54
|
-
*/
|
|
55
|
-
hostname: z.string().nullable(),
|
|
56
|
-
/**
|
|
57
|
-
* optional: any error codes returned by the hCaptcha API.
|
|
58
|
-
* @see {@link https://docs.hcaptcha.com/#siteverify-error-codes-table}
|
|
59
|
-
*/
|
|
60
|
-
'error-codes': z.array(z.string()).optional(),
|
|
61
|
-
/**
|
|
62
|
-
* ENTERPRISE feature: a score denoting malicious activity. Value ranges from
|
|
63
|
-
* 0.0 (no risk) to 1.0 (confirmed threat).
|
|
64
|
-
*/
|
|
65
|
-
score: z.number().optional(),
|
|
66
|
-
/**
|
|
67
|
-
* ENTERPRISE feature: reason(s) for score.
|
|
68
|
-
*/
|
|
69
|
-
score_reason: z.array(z.string()).optional(),
|
|
70
|
-
/**
|
|
71
|
-
* sitekey of the request
|
|
72
|
-
*/
|
|
73
|
-
sitekey: z.string().optional(),
|
|
74
|
-
/**
|
|
75
|
-
* obj of form: {'ip_device': 1, .. etc}
|
|
76
|
-
*/
|
|
77
|
-
behavior_counts: z.record(z.unknown()).optional(),
|
|
78
|
-
/**
|
|
79
|
-
* how similar is this? (0.0 - 1.0, -1 on err)
|
|
80
|
-
*/
|
|
81
|
-
similarity: z.number().optional(),
|
|
82
|
-
/**
|
|
83
|
-
* count of similar_tokens not processed
|
|
84
|
-
*/
|
|
85
|
-
similarity_failures: z.number().optional(),
|
|
86
|
-
/**
|
|
87
|
-
* array of strings for any similarity errors
|
|
88
|
-
*/
|
|
89
|
-
similarity_error_details: z.array(z.string()).optional(),
|
|
90
|
-
/**
|
|
91
|
-
* encoded clientID
|
|
92
|
-
*/
|
|
93
|
-
scoped_uid_0: z.string().optional(),
|
|
94
|
-
/**
|
|
95
|
-
* encoded IP
|
|
96
|
-
*/
|
|
97
|
-
scoped_uid_1: z.string().optional(),
|
|
98
|
-
/**
|
|
99
|
-
* encoded IP (APT)
|
|
100
|
-
*/
|
|
101
|
-
scoped_uid_2: z.string().optional(),
|
|
102
|
-
/**
|
|
103
|
-
* Risk Insights (APT + RI)
|
|
104
|
-
*/
|
|
105
|
-
risk_insights: z.record(z.unknown()).optional(),
|
|
106
|
-
/**
|
|
107
|
-
* Advanced Threat Signatures (APT)
|
|
108
|
-
*/
|
|
109
|
-
sigs: z.record(z.unknown()).optional(),
|
|
110
|
-
/**
|
|
111
|
-
* tags added via Rules
|
|
112
|
-
*/
|
|
113
|
-
tags: z.array(z.string()).optional(),
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
export type HcaptchaVerifyResult = z.infer<typeof hcaptchaVerifyResultSchema>
|
|
117
|
-
|
|
118
|
-
export type HcaptchaClientTokens = {
|
|
119
|
-
hashedIp: string
|
|
120
|
-
hashedHandle: string
|
|
121
|
-
hashedUserAgent?: string
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const fetchSuccessHandler = pipe(
|
|
125
|
-
fetchOkProcessor(),
|
|
126
|
-
fetchJsonProcessor(),
|
|
127
|
-
fetchJsonZodProcessor(hcaptchaVerifyResultSchema),
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
export class HCaptchaClient {
|
|
131
|
-
protected readonly fetch: FetchBound
|
|
132
|
-
constructor(
|
|
133
|
-
readonly hostname: string,
|
|
134
|
-
readonly config: HcaptchaConfig,
|
|
135
|
-
fetch: Fetch = globalThis.fetch,
|
|
136
|
-
) {
|
|
137
|
-
this.fetch = bindFetch(fetch)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
public async verify(
|
|
141
|
-
behaviorType: 'login' | 'signup',
|
|
142
|
-
response: string,
|
|
143
|
-
remoteip: string,
|
|
144
|
-
clientTokens: HcaptchaClientTokens,
|
|
145
|
-
) {
|
|
146
|
-
return this.fetch('https://api.hcaptcha.com/siteverify', {
|
|
147
|
-
method: 'POST',
|
|
148
|
-
headers: {
|
|
149
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
150
|
-
},
|
|
151
|
-
body: new URLSearchParams({
|
|
152
|
-
secret: this.config.secretKey,
|
|
153
|
-
sitekey: this.config.siteKey,
|
|
154
|
-
behavior_type: behaviorType,
|
|
155
|
-
response,
|
|
156
|
-
remoteip,
|
|
157
|
-
client_tokens: JSON.stringify(clientTokens),
|
|
158
|
-
}).toString(),
|
|
159
|
-
}).then(fetchSuccessHandler)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
public checkVerifyResult(
|
|
163
|
-
result: HcaptchaVerifyResult,
|
|
164
|
-
tokens: HcaptchaClientTokens,
|
|
165
|
-
): void {
|
|
166
|
-
const { success, score } = result
|
|
167
|
-
|
|
168
|
-
if (success !== true) {
|
|
169
|
-
throw new HCaptchaVerifyError(
|
|
170
|
-
result,
|
|
171
|
-
tokens,
|
|
172
|
-
'Expected success to be true',
|
|
173
|
-
)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// https://docs.hcaptcha.com/#verify-the-user-response-server-side
|
|
177
|
-
|
|
178
|
-
// Please [...] note that the hostname field is derived from the user's
|
|
179
|
-
// browser, and should not be used for authentication of any kind; it is
|
|
180
|
-
// primarily useful as a statistical metric. Additionally, in the event that
|
|
181
|
-
// your site experiences unusually high challenge traffic, the hostname
|
|
182
|
-
// field may be returned as "not-provided" rather than the usual value; all
|
|
183
|
-
// other fields will return their normal values.
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
// Ignore if enterprise feature is not enabled
|
|
187
|
-
score != null &&
|
|
188
|
-
// Ignore if disabled through config
|
|
189
|
-
this.config.scoreThreshold != null &&
|
|
190
|
-
score >= this.config.scoreThreshold
|
|
191
|
-
) {
|
|
192
|
-
throw new HCaptchaVerifyError(
|
|
193
|
-
result,
|
|
194
|
-
tokens,
|
|
195
|
-
`Score ${score} is above the threshold ${this.config.scoreThreshold}`,
|
|
196
|
-
)
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
public buildClientTokens(
|
|
201
|
-
remoteip: string,
|
|
202
|
-
handle: string,
|
|
203
|
-
userAgent?: string,
|
|
204
|
-
): HcaptchaClientTokens {
|
|
205
|
-
return {
|
|
206
|
-
hashedIp: this.hashToken(remoteip),
|
|
207
|
-
hashedHandle: this.hashToken(handle),
|
|
208
|
-
hashedUserAgent: userAgent ? this.hashToken(userAgent) : undefined,
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
protected hashToken(value: string) {
|
|
213
|
-
const hash = createHash('sha256')
|
|
214
|
-
hash.update(this.config.tokenSalt)
|
|
215
|
-
hash.update(value)
|
|
216
|
-
return hash.digest().toString('base64')
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export class HCaptchaVerifyError extends Error {
|
|
221
|
-
constructor(
|
|
222
|
-
readonly result: HcaptchaVerifyResult,
|
|
223
|
-
readonly tokens: HcaptchaClientTokens,
|
|
224
|
-
message?: string,
|
|
225
|
-
) {
|
|
226
|
-
super(message)
|
|
227
|
-
}
|
|
228
|
-
}
|
package/src/lib/html/README.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Safe HTML generation and concatenation utility
|
|
2
|
-
|
|
3
|
-
This library provides a safe way to generate and concatenate HTML strings.
|
|
4
|
-
|
|
5
|
-
This code _could_ be used as a standalone library, but the Bluesky dev team does
|
|
6
|
-
not want to maintain it as such. As it is currently only used by the
|
|
7
|
-
`@atproto/oauth-provider` package, it is included here. Future development
|
|
8
|
-
should aim to keep this library independent of the rest of the
|
|
9
|
-
`@atproto/oauth-provider` package, so that it can be extracted and published.
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { HtmlValue } from './escapers.js'
|
|
2
|
-
import { Html } from './html.js'
|
|
3
|
-
import { html } from './tags.js'
|
|
4
|
-
|
|
5
|
-
export type AssetRef = {
|
|
6
|
-
url: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export type Attrs = Record<string, boolean | string | undefined>
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @see {@link https://developer.mozilla.org/fr/docs/Web/HTML/Attributes/rel}
|
|
13
|
-
*/
|
|
14
|
-
const ALLOWED_LINK_REL_VALUES = Object.freeze([
|
|
15
|
-
'alternate',
|
|
16
|
-
'author',
|
|
17
|
-
'canonical',
|
|
18
|
-
'dns-prefetch',
|
|
19
|
-
'external',
|
|
20
|
-
'expect',
|
|
21
|
-
'help',
|
|
22
|
-
'icon',
|
|
23
|
-
'license',
|
|
24
|
-
'manifest',
|
|
25
|
-
'me',
|
|
26
|
-
'modulepreload',
|
|
27
|
-
'next',
|
|
28
|
-
'pingback',
|
|
29
|
-
'preconnect',
|
|
30
|
-
'prefetch',
|
|
31
|
-
'preload',
|
|
32
|
-
'prerender',
|
|
33
|
-
'prev',
|
|
34
|
-
'privacy-policy',
|
|
35
|
-
'search',
|
|
36
|
-
'stylesheet',
|
|
37
|
-
'terms-of-service',
|
|
38
|
-
] as const)
|
|
39
|
-
export type LinkRel = (typeof ALLOWED_LINK_REL_VALUES)[number]
|
|
40
|
-
export const isLinkRel = (rel: unknown): rel is LinkRel =>
|
|
41
|
-
(ALLOWED_LINK_REL_VALUES as readonly unknown[]).includes(rel)
|
|
42
|
-
|
|
43
|
-
export type LinkAttrs = Attrs & {
|
|
44
|
-
href: string
|
|
45
|
-
rel: LinkRel
|
|
46
|
-
}
|
|
47
|
-
export type MetaAttrs =
|
|
48
|
-
| { name: string; content: string }
|
|
49
|
-
| { 'http-equiv': string; content: string }
|
|
50
|
-
|
|
51
|
-
const defaultViewport = html`<meta
|
|
52
|
-
name="viewport"
|
|
53
|
-
content="width=device-width, initial-scale=1.0"
|
|
54
|
-
/>`
|
|
55
|
-
|
|
56
|
-
export type BuildDocumentOptions = {
|
|
57
|
-
htmlAttrs?: Attrs
|
|
58
|
-
base?: URL
|
|
59
|
-
meta?: readonly MetaAttrs[]
|
|
60
|
-
links?: readonly LinkAttrs[]
|
|
61
|
-
preloads?: readonly AssetRef[]
|
|
62
|
-
head?: HtmlValue
|
|
63
|
-
title?: HtmlValue
|
|
64
|
-
scripts?: readonly (Html | AssetRef | undefined)[]
|
|
65
|
-
styles?: readonly (Html | AssetRef | undefined)[]
|
|
66
|
-
body?: HtmlValue
|
|
67
|
-
bodyAttrs?: Attrs
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export const buildDocument = ({
|
|
71
|
-
htmlAttrs,
|
|
72
|
-
head,
|
|
73
|
-
title,
|
|
74
|
-
body,
|
|
75
|
-
bodyAttrs,
|
|
76
|
-
base,
|
|
77
|
-
meta,
|
|
78
|
-
links,
|
|
79
|
-
preloads,
|
|
80
|
-
scripts,
|
|
81
|
-
styles,
|
|
82
|
-
}: BuildDocumentOptions) => html`<!doctype html>
|
|
83
|
-
<html${attrsToHtml(htmlAttrs)}>
|
|
84
|
-
<head>
|
|
85
|
-
<meta charset="UTF-8" />
|
|
86
|
-
${title && html`<title>${title}</title>`}
|
|
87
|
-
${base && html`<base href="${base.href}" />`}
|
|
88
|
-
${meta?.some(isViewportMeta) ? null : defaultViewport}
|
|
89
|
-
${meta?.map(metaToHtml)}
|
|
90
|
-
${preloads?.map(linkPreload)}
|
|
91
|
-
${links?.map(linkToHtml)}
|
|
92
|
-
${head}
|
|
93
|
-
${styles?.map(styleToHtml)}
|
|
94
|
-
</head>
|
|
95
|
-
<body${attrsToHtml(bodyAttrs)}>${body}${scripts?.map(scriptToHtml)}</body>
|
|
96
|
-
</html>`
|
|
97
|
-
|
|
98
|
-
function isViewportMeta<T extends MetaAttrs>(
|
|
99
|
-
attrs: T,
|
|
100
|
-
): attrs is T & { name: 'viewport' } {
|
|
101
|
-
return 'name' in attrs && attrs.name === 'viewport'
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function linkToHtml(attrs: LinkAttrs) {
|
|
105
|
-
return html`<link${attrsToHtml(attrs)} />`
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function metaToHtml(attrs: MetaAttrs) {
|
|
109
|
-
return html`<meta${attrsToHtml(attrs)} />`
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function* attrsToHtml(attrs?: Attrs) {
|
|
113
|
-
if (attrs) {
|
|
114
|
-
for (const [name, value] of Object.entries(attrs)) {
|
|
115
|
-
if (value == null) continue
|
|
116
|
-
else if (value === false) continue
|
|
117
|
-
else if (value === true) yield html` ${name}`
|
|
118
|
-
else yield html` ${name}="${value}"`
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function linkPreload(asset: AssetRef) {
|
|
124
|
-
const [path] = asset.url.split('?', 2)
|
|
125
|
-
|
|
126
|
-
if (path.endsWith('.js')) {
|
|
127
|
-
return html`<link rel="modulepreload" href="${asset.url}" />`
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (path.endsWith('.css')) {
|
|
131
|
-
return html`<link rel="preload" href="${asset.url}" as="style" />`
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return undefined
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function scriptToHtml(script?: Html | AssetRef): Html | undefined {
|
|
138
|
-
if (script == null) return undefined
|
|
139
|
-
return script instanceof Html
|
|
140
|
-
? // prettier-ignore
|
|
141
|
-
html`<script>${script}</script>` // hash validity requires no space around the content
|
|
142
|
-
: html`<script type="module" src="${script.url}"></script>`
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function styleToHtml(style?: Html | AssetRef): Html | undefined {
|
|
146
|
-
if (style == null) return undefined
|
|
147
|
-
return style instanceof Html
|
|
148
|
-
? // prettier-ignore
|
|
149
|
-
html`<style>${style}</style>` // hash validity requires no space around the content
|
|
150
|
-
: html`<link rel="stylesheet" href="${style.url}" />`
|
|
151
|
-
}
|
package/src/lib/html/escapers.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { Html } from './html.js'
|
|
2
|
-
import { NestedIterable, stringReplacer } from './util.js'
|
|
3
|
-
|
|
4
|
-
export function* javascriptEscaper(code: string) {
|
|
5
|
-
// "</script>" can only appear in javascript strings, so we can safely escape
|
|
6
|
-
// the "<" without breaking the javascript.
|
|
7
|
-
yield* stringReplacer(code, '</script>', '\\u003c/script>')
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function* jsonEscaper(value: unknown) {
|
|
11
|
-
// https://redux.js.org/usage/server-rendering#security-considerations
|
|
12
|
-
const json = JSON.stringify(value)
|
|
13
|
-
if (json === undefined) throw new TypeError('Cannot serialize to JSON')
|
|
14
|
-
// "<" can only appear in JSON strings, so we can safely escape it without
|
|
15
|
-
// breaking the JSON.
|
|
16
|
-
yield* stringReplacer(json, '<', '\\u003c')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function* cssEscaper(css: string) {
|
|
20
|
-
yield* stringReplacer(css, '</style>', '\\u003c/style>')
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type HtmlVariable = Html | string | number | null | undefined
|
|
24
|
-
export type HtmlValue = NestedIterable<HtmlVariable>
|
|
25
|
-
|
|
26
|
-
export function* htmlEscaper(
|
|
27
|
-
htmlFragments: TemplateStringsArray,
|
|
28
|
-
values: readonly HtmlValue[],
|
|
29
|
-
): Generator<string | Html, void, undefined> {
|
|
30
|
-
for (let i = 0; i < htmlFragments.length; i++) {
|
|
31
|
-
yield htmlFragments[i]!
|
|
32
|
-
|
|
33
|
-
const value = values[i]
|
|
34
|
-
if (value != null) yield* htmlVariableToFragments(value)
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function* htmlVariableToFragments(
|
|
39
|
-
value: HtmlValue,
|
|
40
|
-
): Generator<string | Html, void, undefined> {
|
|
41
|
-
if (value == null) {
|
|
42
|
-
return
|
|
43
|
-
} else if (typeof value === 'number') {
|
|
44
|
-
yield String(value)
|
|
45
|
-
} else if (typeof value === 'string') {
|
|
46
|
-
yield encode(value)
|
|
47
|
-
} else if (value instanceof Html) {
|
|
48
|
-
yield value
|
|
49
|
-
} else {
|
|
50
|
-
// Will throw if the value is not an iterable
|
|
51
|
-
for (const v of value) yield* htmlVariableToFragments(v)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const specialCharRegExp = /[<>"'&]/g
|
|
56
|
-
const specialCharMap = new Map([
|
|
57
|
-
['<', '<'],
|
|
58
|
-
['>', '>'],
|
|
59
|
-
['"', '"'],
|
|
60
|
-
["'", '''],
|
|
61
|
-
['&', '&'],
|
|
62
|
-
])
|
|
63
|
-
const specialCharMapGet = (c: string) => specialCharMap.get(c)!
|
|
64
|
-
function encode(value: string): string {
|
|
65
|
-
return value.replace(specialCharRegExp, specialCharMapGet)
|
|
66
|
-
}
|
package/src/lib/html/html.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
const symbol = Symbol('Html.dangerouslyCreate')
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* This class represents trusted HTML that can be safely embedded in a web page,
|
|
5
|
-
* or used as fragments to build a larger HTML document.
|
|
6
|
-
*/
|
|
7
|
-
export class Html implements Iterable<string> {
|
|
8
|
-
readonly #fragments: readonly (Html | string)[]
|
|
9
|
-
|
|
10
|
-
private constructor(fragments: Iterable<Html | string>, guard: symbol) {
|
|
11
|
-
if (guard !== symbol) {
|
|
12
|
-
// Forces developers to use `Html.dangerouslyCreate` to create an Html
|
|
13
|
-
// instance, to make it clear that the content needs to be trusted.
|
|
14
|
-
throw new TypeError(
|
|
15
|
-
'Use Html.dangerouslyCreate() to create an Html instance',
|
|
16
|
-
)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Transform into an array in case iterable can be consumed only once
|
|
20
|
-
// (e.g. a generator function).
|
|
21
|
-
this.#fragments = Array.from(fragments)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
toString(): string {
|
|
25
|
-
// More efficient than `return this.#fragments.join('')` because it avoids
|
|
26
|
-
// creating intermediate strings when items of this.#fragments are Html
|
|
27
|
-
// instances (as all their toString() would end-up being called, creating
|
|
28
|
-
// lots of intermediary strings). The approach here allows to do a full scan
|
|
29
|
-
// of all the child nodes and concatenate them in a single pass.
|
|
30
|
-
return Array.from(this).join('')
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
[Symbol.toPrimitive](hint): string {
|
|
34
|
-
switch (hint) {
|
|
35
|
-
case 'string':
|
|
36
|
-
case 'default':
|
|
37
|
-
return this.toString()
|
|
38
|
-
default:
|
|
39
|
-
throw new TypeError(`Cannot convert Html to a ${hint}`)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
*[Symbol.iterator](): IterableIterator<string> {
|
|
44
|
-
for (const fragment of this.#fragments) {
|
|
45
|
-
if (typeof fragment === 'string') {
|
|
46
|
-
yield fragment
|
|
47
|
-
} else {
|
|
48
|
-
yield* fragment
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
static dangerouslyCreate(fragments: Iterable<Html | string>): Html {
|
|
54
|
-
return new Html(fragments, symbol)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Html, js } from './index.js'
|
|
2
|
-
|
|
3
|
-
export function declareHydrationData<T extends Record<string, unknown>>(
|
|
4
|
-
values: T,
|
|
5
|
-
): Html {
|
|
6
|
-
return Html.dangerouslyCreate(hydrationDataGenerator(values))
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function* hydrationDataGenerator(
|
|
10
|
-
values: Record<string, unknown>,
|
|
11
|
-
): Generator<Html> {
|
|
12
|
-
for (const [key, val] of Object.entries(values)) {
|
|
13
|
-
yield js`window[${key}]=JSON.parse(${JSON.stringify(val)});`
|
|
14
|
-
}
|
|
15
|
-
// The script tag is removed after the data is assigned to the global
|
|
16
|
-
// variables to prevent other scripts from reading the values. The "app"
|
|
17
|
-
// script will read the global variable and then unset it.
|
|
18
|
-
yield js`document.currentScript.remove();`
|
|
19
|
-
}
|
package/src/lib/html/index.ts
DELETED
package/src/lib/html/tags.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
HtmlValue,
|
|
3
|
-
cssEscaper,
|
|
4
|
-
htmlEscaper,
|
|
5
|
-
javascriptEscaper,
|
|
6
|
-
jsonEscaper,
|
|
7
|
-
} from './escapers.js'
|
|
8
|
-
import { Html } from './html.js'
|
|
9
|
-
|
|
10
|
-
export { type HtmlValue }
|
|
11
|
-
export const html = (
|
|
12
|
-
tpl: TemplateStringsArray,
|
|
13
|
-
...val: readonly HtmlValue[]
|
|
14
|
-
) =>
|
|
15
|
-
tpl.length === 1 && val.length === 0
|
|
16
|
-
? // Optimization for static HTML, avoid creating an iterable
|
|
17
|
-
Html.dangerouslyCreate(tpl)
|
|
18
|
-
: Html.dangerouslyCreate(htmlEscaper(tpl, val))
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Escapes code to use as a JavaScript string inside a `<script>` tag.
|
|
22
|
-
*/
|
|
23
|
-
export const javascriptCode = (code: string) =>
|
|
24
|
-
Html.dangerouslyCreate(javascriptEscaper(code))
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Creates an HTML safe JavaScript code block, with JSON serialization of the
|
|
28
|
-
* injected variables.
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* ```js
|
|
32
|
-
* const dataOnTheServer = { foo: 'bar' };
|
|
33
|
-
* const clientScript = js`
|
|
34
|
-
* const data = ${dataOnTheServer};
|
|
35
|
-
* console.log(data);
|
|
36
|
-
* `
|
|
37
|
-
* console.log(clientScript.toString()); // Output: 'const data = {"foo":"bar"};console.log(data);'
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
export const js = (tpl: TemplateStringsArray, ...val: readonly unknown[]) =>
|
|
41
|
-
tpl.length === 1 && val.length === 0
|
|
42
|
-
? // Optimization for static JavaScript, avoid un-necessary serialization
|
|
43
|
-
javascriptCode(tpl[0])
|
|
44
|
-
: javascriptCode(String.raw({ raw: tpl }, ...val.map(jsonCode)))
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Escapes a value to be used as a JSON string inside a `<script>` tag.
|
|
48
|
-
*
|
|
49
|
-
* @see {@link https://redux.js.org/usage/server-rendering#security-considerations}
|
|
50
|
-
*/
|
|
51
|
-
export const jsonCode = (value: unknown) =>
|
|
52
|
-
Html.dangerouslyCreate(jsonEscaper(value))
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Creates an HTML safe CSS code block.
|
|
56
|
-
*/
|
|
57
|
-
export const css = (tpl: TemplateStringsArray, ...val: readonly number[]) =>
|
|
58
|
-
tpl.length === 1 && val.length === 0
|
|
59
|
-
? // Optimization for static CSS, avoid creating an iterable
|
|
60
|
-
cssCode(tpl[0])
|
|
61
|
-
: cssCode(String.raw({ raw: tpl }, ...val.map(jsonEscaper)))
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Escapes a value to be uses as CSS styles inside a `<style>` tag.
|
|
65
|
-
*/
|
|
66
|
-
export const cssCode = (code?: string) =>
|
|
67
|
-
code ? Html.dangerouslyCreate(cssEscaper(code)) : undefined
|
package/src/lib/html/util.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export type NestedIterable<V> = V | Iterable<NestedIterable<V>>
|
|
2
|
-
|
|
3
|
-
export function* stringReplacer(
|
|
4
|
-
source: string,
|
|
5
|
-
searchValue: string,
|
|
6
|
-
replaceValue: string,
|
|
7
|
-
): Generator<string, void, undefined> {
|
|
8
|
-
let previousIndex = 0
|
|
9
|
-
let index = source.indexOf(searchValue)
|
|
10
|
-
while (index !== -1) {
|
|
11
|
-
yield source.slice(previousIndex, index)
|
|
12
|
-
yield replaceValue
|
|
13
|
-
previousIndex = index + searchValue.length
|
|
14
|
-
index = source.indexOf(searchValue, previousIndex)
|
|
15
|
-
}
|
|
16
|
-
yield source.slice(previousIndex)
|
|
17
|
-
}
|
package/src/lib/http/README.md
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# utilities for generating middlewares to work with Node's http module or Express / Connect frameworks
|
|
2
|
-
|
|
3
|
-
This library uses a functional programming style to generate middleware
|
|
4
|
-
functions that can be used with Node's http module or Express / Connect
|
|
5
|
-
frameworks.
|
|
6
|
-
|
|
7
|
-
This code _could_ be used as a standalone library, but the Bluesky dev team does
|
|
8
|
-
not want to maintain it as such. As it is currently only used by the
|
|
9
|
-
`@atproto/oauth-provider` package, it is included here. Future development
|
|
10
|
-
should aim to keep this library independent of the rest of the
|
|
11
|
-
`@atproto/oauth-provider` package, so that it can be extracted and published.
|