lambder 7.3.1 → 8.1.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.
- package/CHANGELOG.md +1047 -3
- package/README.md +46 -21
- package/dist/api/LambderApiAnswer.d.ts +18 -22
- package/dist/api/LambderApiAnswer.js +6 -7
- package/dist/api/LambderApiCallContext.d.ts +21 -8
- package/dist/api/LambderApiCallContext.js +22 -4
- package/dist/api/LambderApiDefinition.d.ts +4 -3
- package/dist/api/LambderApiEnvelope.d.ts +14 -9
- package/dist/api/LambderApiEnvelope.js +33 -34
- package/dist/api/LambderApiGuards.d.ts +78 -51
- package/dist/api/LambderApiGuards.js +34 -36
- package/dist/api/LambderApiIdempotency.d.ts +68 -62
- package/dist/api/LambderApiIdempotency.js +214 -151
- package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
- package/dist/api/LambderApiOutputValidationError.js +50 -0
- package/dist/api/LambderApiPipeline.d.ts +47 -38
- package/dist/api/LambderApiPipeline.js +122 -63
- package/dist/api/LambderApiRateLimits.d.ts +201 -54
- package/dist/api/LambderApiRateLimits.js +185 -108
- package/dist/api/LambderApiRequest.d.ts +27 -21
- package/dist/api/LambderApiRequest.js +26 -19
- package/dist/api/LambderApiSignature.d.ts +12 -15
- package/dist/api/LambderApiSignature.js +28 -51
- package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
- package/dist/api/LambderApiValidationRefusal.js +10 -10
- package/dist/build/ContractTypePrinter.d.ts +85 -0
- package/dist/build/ContractTypePrinter.js +402 -0
- package/dist/build/freshProcessVerifier.d.ts +13 -0
- package/dist/build/freshProcessVerifier.js +19 -0
- package/dist/build/moduleLocation.d.ts +11 -0
- package/dist/build/moduleLocation.js +6 -0
- package/dist/build/writeApiContract.d.ts +78 -0
- package/dist/build/writeApiContract.js +302 -0
- package/dist/build/writeApiSignatures.d.ts +114 -0
- package/dist/build/writeApiSignatures.js +217 -0
- package/dist/build/writeFileAtomically.d.ts +8 -0
- package/dist/build/writeFileAtomically.js +22 -0
- package/dist/build.d.ts +14 -0
- package/dist/build.js +11 -0
- package/dist/client/LambderCaller.d.ts +13 -44
- package/dist/client/LambderCaller.js +77 -84
- package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
- package/dist/client/LambderReloadLoopBreaker.js +90 -46
- package/dist/client/LambderUploadRunner.d.ts +96 -0
- package/dist/client/LambderUploadRunner.js +234 -0
- package/dist/client/lambderFetchTransport.d.ts +4 -1
- package/dist/client/lambderFetchTransport.js +52 -28
- package/dist/client.d.ts +9 -3
- package/dist/client.js +6 -1
- package/dist/core/Lambder.d.ts +143 -79
- package/dist/core/Lambder.js +350 -231
- package/dist/core/LambderContext.d.ts +82 -15
- package/dist/core/LambderContext.js +107 -20
- package/dist/core/LambderCors.d.ts +21 -3
- package/dist/core/LambderCors.js +35 -16
- package/dist/core/LambderCrashHandling.d.ts +40 -0
- package/dist/core/LambderCrashHandling.js +97 -0
- package/dist/core/LambderCreateOptions.d.ts +151 -75
- package/dist/core/LambderCreateOptions.js +16 -23
- package/dist/core/LambderFiles.d.ts +21 -7
- package/dist/core/LambderFiles.js +62 -34
- package/dist/core/LambderIndexHtml.js +12 -11
- package/dist/core/LambderPolicyBuilders.d.ts +17 -5
- package/dist/core/LambderPolicyBuilders.js +17 -5
- package/dist/core/LambderPublicFiles.d.ts +11 -5
- package/dist/core/LambderPublicFiles.js +32 -4
- package/dist/core/LambderRequestPath.d.ts +43 -0
- package/dist/core/LambderRequestPath.js +63 -0
- package/dist/core/LambderResponse.d.ts +26 -5
- package/dist/core/LambderResponse.js +157 -70
- package/dist/core/LambderResponseBuilder.d.ts +49 -4
- package/dist/core/LambderResponseBuilder.js +64 -3
- package/dist/core/LambderRouting.d.ts +2 -3
- package/dist/core/LambderRouting.js +22 -7
- package/dist/core/LambderTemplatingEngine.js +211 -32
- package/dist/index.d.ts +25 -8
- package/dist/index.js +13 -4
- package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
- package/dist/invoke/LambderInvokeCaller.js +76 -66
- package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
- package/dist/invoke/LambderInvokeOutcome.js +9 -22
- package/dist/invoke/LambderLambdaEvent.d.ts +29 -9
- package/dist/invoke/LambderLambdaEvent.js +40 -22
- package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
- package/dist/invoke/lambderHandlerTransport.js +15 -18
- package/dist/mock/LambderMockApp.d.ts +67 -83
- package/dist/mock/LambderMockApp.js +167 -153
- package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
- package/dist/mock/LambderMockBrowserCookies.js +24 -28
- package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
- package/dist/mock/LambderMockCallRecorder.js +19 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
- package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
- package/dist/mock/LambderMockEntryRegistry.js +24 -29
- package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
- package/dist/mock/LambderMockFailureInjector.js +3 -6
- package/dist/mock/LambderMockTypes.d.ts +78 -108
- package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
- package/dist/mock/lambderMockInvokeTransport.js +11 -10
- package/dist/mock/lambderMockMswHandler.d.ts +43 -33
- package/dist/mock/lambderMockMswHandler.js +50 -39
- package/dist/mock/lambderMockUploadMswHandler.d.ts +26 -0
- package/dist/mock/lambderMockUploadMswHandler.js +28 -0
- package/dist/mock.d.ts +4 -1
- package/dist/mock.js +6 -3
- package/dist/session/LambderSessionController.d.ts +108 -89
- package/dist/session/LambderSessionController.js +187 -168
- package/dist/session/LambderSessionCrypto.d.ts +16 -7
- package/dist/session/LambderSessionCrypto.js +26 -12
- package/dist/session/LambderSessionManager.d.ts +124 -46
- package/dist/session/LambderSessionManager.js +262 -137
- package/dist/shared/LambderHtml.d.ts +42 -3
- package/dist/shared/LambderHtml.js +127 -7
- package/dist/shared/LambderHtmlPositions.d.ts +173 -0
- package/dist/shared/LambderHtmlPositions.js +652 -0
- package/dist/shared/LambderI18n.d.ts +10 -11
- package/dist/shared/LambderI18n.js +33 -21
- package/dist/shared/contracts/LambderCache.d.ts +66 -0
- package/dist/shared/contracts/LambderCache.js +11 -0
- package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
- package/dist/shared/contracts/LambderFileSource.js +5 -8
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
- package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
- package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
- package/dist/shared/contracts/LambderRateLimiter.js +4 -5
- package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
- package/dist/shared/contracts/LambderSessionStore.js +5 -6
- package/dist/shared/contracts/LambderUploadBucket.d.ts +154 -0
- package/dist/shared/contracts/LambderUploadBucket.js +74 -0
- package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
- package/dist/shared/transport/LambderApiTransport.js +7 -7
- package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
- package/dist/shared/transport/LambderCookieJar.js +54 -66
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
- package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
- package/dist/shared/util/LambderCallAbort.d.ts +5 -5
- package/dist/shared/util/LambderCallAbort.js +5 -5
- package/dist/shared/util/LambderClientIp.d.ts +27 -11
- package/dist/shared/util/LambderClientIp.js +96 -13
- package/dist/shared/util/LambderContentDisposition.d.ts +10 -0
- package/dist/shared/util/LambderContentDisposition.js +13 -0
- package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
- package/dist/shared/util/LambderExpiringMap.js +41 -57
- package/dist/shared/util/LambderNodeModules.js +6 -7
- package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
- package/dist/shared/util/LambderOptionChecks.js +4 -4
- package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
- package/dist/shared/util/LambderResponseBrand.js +5 -5
- package/dist/shared/util/LambderTextDigest.d.ts +7 -5
- package/dist/shared/util/LambderTextDigest.js +11 -5
- package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
- package/dist/shared/util/LambderTypeUtilities.js +3 -3
- package/dist/shared/util/boundKeyField.d.ts +20 -0
- package/dist/shared/util/boundKeyField.js +34 -0
- package/dist/shared/util/canonicalJson.d.ts +11 -0
- package/dist/shared/util/canonicalJson.js +28 -0
- package/dist/shared/util/joinKeyFields.d.ts +20 -0
- package/dist/shared/util/joinKeyFields.js +22 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
- package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
- package/dist/shared/wire/LambderApiContract.d.ts +98 -53
- package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
- package/dist/shared/wire/LambderApiOutcome.js +48 -23
- package/dist/shared/wire/LambderApiRefusal.d.ts +45 -27
- package/dist/shared/wire/LambderApiRefusal.js +42 -7
- package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
- package/dist/shared/wire/LambderApiSignature.js +16 -19
- package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
- package/dist/shared/wire/LambderCallOptions.js +9 -11
- package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
- package/dist/shared/wire/LambderCompressionCodec.js +31 -36
- package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
- package/dist/shared/wire/LambderCompressionOption.js +9 -9
- package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
- package/dist/shared/wire/LambderCrashDetail.js +12 -15
- package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
- package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
- package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
- package/dist/shared/wire/LambderInvokeApiId.js +27 -0
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
- package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
- package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
- package/dist/shared/wire/LambderRequestPayload.js +4 -6
- package/dist/shared/wire/LambderUploadObjectFields.d.ts +10 -0
- package/dist/shared/wire/LambderUploadObjectFields.js +24 -0
- package/dist/shared/wire/LambderUploadRefusal.d.ts +9 -0
- package/dist/shared/wire/LambderUploadRefusal.js +18 -0
- package/dist/shared/wire/LambderUploadSchemas.d.ts +12 -0
- package/dist/shared/wire/LambderUploadSchemas.js +30 -0
- package/dist/stores/LambderCacheFiller.d.ts +48 -0
- package/dist/stores/LambderCacheFiller.js +119 -0
- package/dist/stores/LambderCacheKeys.d.ts +26 -0
- package/dist/stores/LambderCacheKeys.js +54 -0
- package/dist/stores/LambderCacheValues.d.ts +45 -0
- package/dist/stores/LambderCacheValues.js +74 -0
- package/dist/stores/LambderDdbCache.d.ts +121 -56
- package/dist/stores/LambderDdbCache.js +528 -225
- package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
- package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
- package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
- package/dist/stores/LambderDdbRateLimiter.js +151 -39
- package/dist/stores/LambderDdbSdk.d.ts +43 -31
- package/dist/stores/LambderDdbSdk.js +80 -38
- package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
- package/dist/stores/LambderDdbSessionStore.js +119 -47
- package/dist/stores/LambderHttpFileSource.d.ts +15 -6
- package/dist/stores/LambderHttpFileSource.js +15 -13
- package/dist/stores/LambderMemoryCache.d.ts +49 -0
- package/dist/stores/LambderMemoryCache.js +113 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
- package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
- package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
- package/dist/stores/LambderMemoryRateLimiter.js +14 -13
- package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
- package/dist/stores/LambderMemorySessionStore.js +38 -19
- package/dist/stores/LambderMemoryUploadBucket.d.ts +99 -0
- package/dist/stores/LambderMemoryUploadBucket.js +219 -0
- package/dist/stores/LambderS3FileSource.d.ts +21 -6
- package/dist/stores/LambderS3FileSource.js +12 -7
- package/dist/stores/LambderS3UploadBucket.d.ts +73 -0
- package/dist/stores/LambderS3UploadBucket.js +144 -0
- package/dist/stores/LambderSdkInstallHint.d.ts +11 -0
- package/dist/stores/LambderSdkInstallHint.js +14 -0
- package/dist/testing/LambderTestApp.d.ts +23 -25
- package/dist/testing/LambderTestApp.js +22 -24
- package/dist/testing/LambderTestVisitor.d.ts +10 -12
- package/dist/testing/LambderTestVisitor.js +15 -15
- package/dist/testing.d.ts +3 -0
- package/dist/testing.js +2 -0
- package/package.json +26 -3
- package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
- package/dist/api/LambderApiPolicyEngine.js +0 -85
- package/dist/shared/util/LambderKeyFields.d.ts +0 -32
- package/dist/shared/util/LambderKeyFields.js +0 -34
|
@@ -6,10 +6,49 @@
|
|
|
6
6
|
* - strings/numbers are escaped
|
|
7
7
|
* - null/undefined/booleans render as "" (enables `${cond && html`...`}`)
|
|
8
8
|
* - arrays are flattened (`${items.map((i) => html`<li>${i}</li>`)}`)
|
|
9
|
-
* - nested html`...` fragments are inserted verbatim (no double escaping)
|
|
9
|
+
* - nested html`...` fragments are inserted verbatim (no double escaping),
|
|
10
|
+
* wherever the position rules below let a value go
|
|
10
11
|
* - raw(value) marks a trusted string as safe; never pass user input to it
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
+
* Escaping protects a value in text and inside a quoted attribute value, so
|
|
14
|
+
* each call reads its own static strings to find where every interpolation
|
|
15
|
+
* lands, and throws where escaping cannot protect it:
|
|
16
|
+
* - an unquoted attribute value (`class=${x}`, with or without a prefix
|
|
17
|
+
* before the interpolation), where a space in the value starts a new
|
|
18
|
+
* attribute;
|
|
19
|
+
* - inside a tag where an attribute name goes (`<input ${x}>`), where any
|
|
20
|
+
* value is a new attribute;
|
|
21
|
+
* - inside the quoted value of an event handler, `style` or `srcdoc`, which
|
|
22
|
+
* the browser reads as JavaScript, CSS or a whole document once it has
|
|
23
|
+
* decoded the escapes: `onclick="go('${x}')"` would run `');alert(1);//`;
|
|
24
|
+
* - inside a <script> or <style> element, where HTML escaping is the wrong
|
|
25
|
+
* grammar (embed data with jsonScript() outside it instead);
|
|
26
|
+
* - inside a comment and right before a `-`, `!` or `>` that, after a value
|
|
27
|
+
* ending in `-`, `--` or `--!`, would end the comment there;
|
|
28
|
+
* - after a tag inside a <title>, <textarea> or other text-only element, or
|
|
29
|
+
* after a CDATA section that runs past its first `>`, where inline SVG and
|
|
30
|
+
* MathML read the template apart from plain HTML.
|
|
31
|
+
* A call whose template ends anywhere but in plain text throws as well: a
|
|
32
|
+
* nested fragment is inserted without being read, so one that ends inside a
|
|
33
|
+
* tag, an attribute value, a comment or a script would move every value the
|
|
34
|
+
* template around it places after it.
|
|
35
|
+
* Quote the attribute, pass what a script needs in a data- attribute, or
|
|
36
|
+
* build the whole tag conditionally
|
|
37
|
+
* (`${checked ? html`<input checked>` : html`<input>`}`). And a quoted URL
|
|
38
|
+
* attribute value (href, src, action and the rest of URL_ATTRIBUTE_NAMES)
|
|
39
|
+
* holding an interpolation is checked whole once rendered: unless the
|
|
40
|
+
* template's own text before the first interpolation holds a `:`, `/`, `?`
|
|
41
|
+
* or `#`, a scheme other than http, https, mailto or tel renders the value
|
|
42
|
+
* as `about:invalid`. So `href="${user.website}"` stays usable for a link a
|
|
43
|
+
* user gave, and `href="/users/${id}"` renders as written.
|
|
44
|
+
*
|
|
45
|
+
* These rules follow the template, not the value: they hold whatever an
|
|
46
|
+
* interpolation carries (a string, a number, a nested html`...`, raw()), and
|
|
47
|
+
* for one that renders "" too, so a call site that breaks one throws on every
|
|
48
|
+
* call rather than on some data.
|
|
49
|
+
*
|
|
50
|
+
* The escaping is valid XML, and the rules hold for SVG, which runs script
|
|
51
|
+
* too, so `xml` is an alias for sitemaps, feeds and SVG.
|
|
13
52
|
*/
|
|
14
53
|
export declare class LambderSafeHtml {
|
|
15
54
|
readonly value: string;
|
|
@@ -21,7 +60,7 @@ export declare const escapeHtml: (value: string) => string;
|
|
|
21
60
|
/** Serialize any LambderHtmlValue to a string (escaped unless marked safe). */
|
|
22
61
|
export declare const renderHtmlValue: (value: LambderHtmlValue) => string;
|
|
23
62
|
export declare const html: (strings: TemplateStringsArray, ...values: LambderHtmlValue[]) => LambderSafeHtml;
|
|
24
|
-
/** Alias of html for XML documents (
|
|
63
|
+
/** Alias of html for XML documents (the same XML-valid escaping and position rules). */
|
|
25
64
|
export declare const xml: (strings: TemplateStringsArray, ...values: LambderHtmlValue[]) => LambderSafeHtml;
|
|
26
65
|
/** Mark a trusted string as safe (inserted without escaping). Never pass user input. */
|
|
27
66
|
export declare const raw: (value: string) => LambderSafeHtml;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HtmlPositionReader, RAW_TEXT_ELEMENTS, URL_ATTRIBUTE_NAMES, safeUrlValue, valueLanguageOf } from "./LambderHtmlPositions.js";
|
|
1
2
|
/**
|
|
2
3
|
* Type-safe templating via tagged template literals: interpolated values are
|
|
3
4
|
* HTML-escaped by default, so templates are XSS-safe and fully type-checked by
|
|
@@ -6,10 +7,49 @@
|
|
|
6
7
|
* - strings/numbers are escaped
|
|
7
8
|
* - null/undefined/booleans render as "" (enables `${cond && html`...`}`)
|
|
8
9
|
* - arrays are flattened (`${items.map((i) => html`<li>${i}</li>`)}`)
|
|
9
|
-
* - nested html`...` fragments are inserted verbatim (no double escaping)
|
|
10
|
+
* - nested html`...` fragments are inserted verbatim (no double escaping),
|
|
11
|
+
* wherever the position rules below let a value go
|
|
10
12
|
* - raw(value) marks a trusted string as safe; never pass user input to it
|
|
11
13
|
*
|
|
12
|
-
*
|
|
14
|
+
* Escaping protects a value in text and inside a quoted attribute value, so
|
|
15
|
+
* each call reads its own static strings to find where every interpolation
|
|
16
|
+
* lands, and throws where escaping cannot protect it:
|
|
17
|
+
* - an unquoted attribute value (`class=${x}`, with or without a prefix
|
|
18
|
+
* before the interpolation), where a space in the value starts a new
|
|
19
|
+
* attribute;
|
|
20
|
+
* - inside a tag where an attribute name goes (`<input ${x}>`), where any
|
|
21
|
+
* value is a new attribute;
|
|
22
|
+
* - inside the quoted value of an event handler, `style` or `srcdoc`, which
|
|
23
|
+
* the browser reads as JavaScript, CSS or a whole document once it has
|
|
24
|
+
* decoded the escapes: `onclick="go('${x}')"` would run `');alert(1);//`;
|
|
25
|
+
* - inside a <script> or <style> element, where HTML escaping is the wrong
|
|
26
|
+
* grammar (embed data with jsonScript() outside it instead);
|
|
27
|
+
* - inside a comment and right before a `-`, `!` or `>` that, after a value
|
|
28
|
+
* ending in `-`, `--` or `--!`, would end the comment there;
|
|
29
|
+
* - after a tag inside a <title>, <textarea> or other text-only element, or
|
|
30
|
+
* after a CDATA section that runs past its first `>`, where inline SVG and
|
|
31
|
+
* MathML read the template apart from plain HTML.
|
|
32
|
+
* A call whose template ends anywhere but in plain text throws as well: a
|
|
33
|
+
* nested fragment is inserted without being read, so one that ends inside a
|
|
34
|
+
* tag, an attribute value, a comment or a script would move every value the
|
|
35
|
+
* template around it places after it.
|
|
36
|
+
* Quote the attribute, pass what a script needs in a data- attribute, or
|
|
37
|
+
* build the whole tag conditionally
|
|
38
|
+
* (`${checked ? html`<input checked>` : html`<input>`}`). And a quoted URL
|
|
39
|
+
* attribute value (href, src, action and the rest of URL_ATTRIBUTE_NAMES)
|
|
40
|
+
* holding an interpolation is checked whole once rendered: unless the
|
|
41
|
+
* template's own text before the first interpolation holds a `:`, `/`, `?`
|
|
42
|
+
* or `#`, a scheme other than http, https, mailto or tel renders the value
|
|
43
|
+
* as `about:invalid`. So `href="${user.website}"` stays usable for a link a
|
|
44
|
+
* user gave, and `href="/users/${id}"` renders as written.
|
|
45
|
+
*
|
|
46
|
+
* These rules follow the template, not the value: they hold whatever an
|
|
47
|
+
* interpolation carries (a string, a number, a nested html`...`, raw()), and
|
|
48
|
+
* for one that renders "" too, so a call site that breaks one throws on every
|
|
49
|
+
* call rather than on some data.
|
|
50
|
+
*
|
|
51
|
+
* The escaping is valid XML, and the rules hold for SVG, which runs script
|
|
52
|
+
* too, so `xml` is an alias for sitemaps, feeds and SVG.
|
|
13
53
|
*/
|
|
14
54
|
export class LambderSafeHtml {
|
|
15
55
|
value;
|
|
@@ -35,16 +75,96 @@ export const renderHtmlValue = (value) => {
|
|
|
35
75
|
return String(value);
|
|
36
76
|
return escapeHtml(value);
|
|
37
77
|
};
|
|
78
|
+
/** Template text, quoted to name a place in an error message: its last 40 characters when it is longer. */
|
|
79
|
+
const quoteTemplateEnd = (text) => JSON.stringify(text.length > 40 ? `...${text.slice(-40)}` : text);
|
|
80
|
+
/** The interpolation at `index`, named in an error message by the template text just before it. */
|
|
81
|
+
const describeInterpolation = (strings, index) => `the interpolation after ${quoteTemplateEnd(strings.slice(0, index + 1).join("${...}"))}`;
|
|
38
82
|
export const html = (strings, ...values) => {
|
|
83
|
+
const positions = new HtmlPositionReader();
|
|
39
84
|
let out = "";
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
85
|
+
// A quoted URL attribute value holding an interpolation, collected from
|
|
86
|
+
// its opening quote to its closing one so safeUrlValue reads it whole.
|
|
87
|
+
let urlValue = null;
|
|
88
|
+
for (let index = 0; index < strings.length; index += 1) {
|
|
89
|
+
const piece = strings[index];
|
|
90
|
+
positions.read(piece);
|
|
91
|
+
// With no branches to join, only a value in a comment can leave the
|
|
92
|
+
// reader undecided: the one just before this piece.
|
|
93
|
+
if (positions.undecidable) {
|
|
94
|
+
throw new Error(`html\`...\`: ${describeInterpolation(strings, index - 1)} is inside a comment, right before text that ends the comment ` +
|
|
95
|
+
`or not depending on how the value ends ("-", "--" or "--!" before a ">"). Put a space after the interpolation.`);
|
|
96
|
+
}
|
|
97
|
+
let text = piece;
|
|
98
|
+
if (urlValue) {
|
|
99
|
+
const end = text.indexOf(urlValue.quote);
|
|
100
|
+
if (end === -1) {
|
|
101
|
+
urlValue.url += text;
|
|
102
|
+
text = "";
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
out += safeUrlValue(urlValue.url + text.slice(0, end), urlValue.firstValueIndex);
|
|
106
|
+
urlValue = null;
|
|
107
|
+
text = text.slice(end);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (index === values.length) {
|
|
111
|
+
out += text;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
const point = positions.point();
|
|
115
|
+
if (point.position === "afterEquals" || point.position === "unquoted") {
|
|
116
|
+
throw new Error(`html\`...\`: ${describeInterpolation(strings, index)} is in an unquoted attribute position. ` +
|
|
117
|
+
`Escaping cannot prevent injection there; quote the attribute: attr="\${value}".`);
|
|
118
|
+
}
|
|
119
|
+
if (point.position === "tag") {
|
|
120
|
+
throw new Error(`html\`...\`: ${describeInterpolation(strings, index)} is inside a tag where an attribute name goes. ` +
|
|
121
|
+
`Escaping cannot prevent injection there; put the value inside a quoted attribute value, or build the whole tag ` +
|
|
122
|
+
`conditionally: \${checked ? html\`<input checked>\` : html\`<input>\`}.`);
|
|
123
|
+
}
|
|
124
|
+
const valueLanguage = point.position === "quoted" ? valueLanguageOf(point.attributeName) : null;
|
|
125
|
+
if (valueLanguage) {
|
|
126
|
+
throw new Error(`html\`...\`: ${describeInterpolation(strings, index)} is inside the ${point.attributeName} attribute, whose value the browser ` +
|
|
127
|
+
`reads as ${valueLanguage} once it has decoded the HTML escapes, so escaping cannot prevent injection there. ` +
|
|
128
|
+
`Pass the value in a data- attribute a script reads, or build the whole tag conditionally.`);
|
|
129
|
+
}
|
|
130
|
+
if (point.position === "rawText" && RAW_TEXT_ELEMENTS.has(point.tagName)) {
|
|
131
|
+
throw new Error(`html\`...\`: ${describeInterpolation(strings, index)} is inside a <${point.tagName}> element where HTML ` +
|
|
132
|
+
`escaping does not apply. Pass data with the jsonScript() helper outside the element instead.`);
|
|
133
|
+
}
|
|
134
|
+
if (point.position === "rawText") {
|
|
135
|
+
throw new Error(`html\`...\`: ${describeInterpolation(strings, index)} comes after <${point.tagName}> content that inline SVG and ` +
|
|
136
|
+
`MathML read apart from plain HTML (a tag inside a text-only element, or a CDATA section), so where the value lands ` +
|
|
137
|
+
`is not known. Write that content as text, with its "<" escaped as <, and leave CDATA sections out: values are escaped anyway.`);
|
|
138
|
+
}
|
|
139
|
+
// A URL value no interpolation has opened yet opens in this piece,
|
|
140
|
+
// and holds no quote of its kind before this interpolation, so its
|
|
141
|
+
// opening quote is the last one in the text.
|
|
142
|
+
if (!urlValue && point.position === "quoted" && URL_ATTRIBUTE_NAMES.has(point.attributeName)) {
|
|
143
|
+
const start = text.lastIndexOf(point.quote) + 1;
|
|
144
|
+
out += text.slice(0, start);
|
|
145
|
+
text = text.slice(start);
|
|
146
|
+
urlValue = { url: "", firstValueIndex: text.length, quote: point.quote, attributeName: point.attributeName };
|
|
147
|
+
}
|
|
148
|
+
positions.skipValue();
|
|
149
|
+
const rendered = text + renderHtmlValue(values[index]);
|
|
150
|
+
if (urlValue)
|
|
151
|
+
urlValue.url += rendered;
|
|
152
|
+
else
|
|
153
|
+
out += rendered;
|
|
154
|
+
}
|
|
155
|
+
if (urlValue) {
|
|
156
|
+
throw new Error(`html\`...\`: ${describeInterpolation(strings, values.length - 1)} is inside a quoted ` +
|
|
157
|
+
`${urlValue.attributeName} value that never closes.`);
|
|
158
|
+
}
|
|
159
|
+
if (!positions.inPlainText) {
|
|
160
|
+
throw new Error(`html\`...\`: the template ${quoteTemplateEnd(strings.join("${...}"))} ends inside markup it opened (a tag, an attribute ` +
|
|
161
|
+
`value, a comment, or the content of a <script>, <title> or other text-only element), or after content inline SVG and ` +
|
|
162
|
+
`MathML read apart from plain HTML. A template it is inserted into places its own values without reading it, so it has ` +
|
|
163
|
+
`to be complete markup.`);
|
|
44
164
|
}
|
|
45
165
|
return new LambderSafeHtml(out);
|
|
46
166
|
};
|
|
47
|
-
/** Alias of html for XML documents (
|
|
167
|
+
/** Alias of html for XML documents (the same XML-valid escaping and position rules). */
|
|
48
168
|
export const xml = html;
|
|
49
169
|
/** Mark a trusted string as safe (inserted without escaping). Never pass user input. */
|
|
50
170
|
export const raw = (value) => new LambderSafeHtml(value);
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
export type HtmlPosition = "text" | "comment" | "tag" | "afterEquals" | "unquoted" | "quoted" | "rawText";
|
|
2
|
+
/** Where a point of a template sits in its HTML, and which attribute or element a value there belongs to. */
|
|
3
|
+
export type HtmlPoint = {
|
|
4
|
+
position: HtmlPosition;
|
|
5
|
+
/** For a value position (afterEquals, unquoted, quoted): the attribute it belongs to, lowercased. "" elsewhere. */
|
|
6
|
+
attributeName: string;
|
|
7
|
+
/** For a quoted point: the quote character its value opened with. "" elsewhere. */
|
|
8
|
+
quote: string;
|
|
9
|
+
/**
|
|
10
|
+
* For a rawText point: the element whose content it is, lowercased, or
|
|
11
|
+
* what left inline SVG and MathML reading the template apart from plain
|
|
12
|
+
* HTML (see foreignPartedBy). "" elsewhere.
|
|
13
|
+
*/
|
|
14
|
+
tagName: string;
|
|
15
|
+
};
|
|
16
|
+
/** Why an HtmlPositionReader cannot tell where the HTML read so far sits, and what a refusal names for it. */
|
|
17
|
+
export type HtmlUndecidable = {
|
|
18
|
+
/**
|
|
19
|
+
* "commentValue": a value inside a comment is followed by text that ends
|
|
20
|
+
* the comment or not depending on how the value ends. "attributeEquals":
|
|
21
|
+
* readings joined between attributes after different attribute names
|
|
22
|
+
* (joinWith) met an "=", which gives its value to a different attribute
|
|
23
|
+
* in each.
|
|
24
|
+
*/
|
|
25
|
+
reason: "commentValue" | "attributeEquals";
|
|
26
|
+
/** The label skipValue or joinWith was given for it. */
|
|
27
|
+
label: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Of those, the ones whose content is another language, where HTML escaping
|
|
31
|
+
* is the wrong grammar: a value inside a <script> or <style> is refused. In
|
|
32
|
+
* the rest an escaped value is text, since its "<" is escaped and cannot end
|
|
33
|
+
* the element, until their content holds a tag (see foreignPartedBy).
|
|
34
|
+
*/
|
|
35
|
+
export declare const RAW_TEXT_ELEMENTS: ReadonlySet<string>;
|
|
36
|
+
/**
|
|
37
|
+
* Says where the end of the HTML read so far sits, with the part of the HTML
|
|
38
|
+
* standard's tokenizer that decides it (ReaderState): an `=` or a `>` inside
|
|
39
|
+
* a quoted value, or in text and comments, counts for nothing, a comment ends
|
|
40
|
+
* where the browser ends it, and the content of a <script>, <style>, <title>
|
|
41
|
+
* and the other TEXT_CONTENT_ELEMENTS is skipped up to its own end tag.
|
|
42
|
+
*
|
|
43
|
+
* A template's static pieces are read one after another, and a piece may end
|
|
44
|
+
* anywhere, in the middle of "<!-" included: the reader asks where each value
|
|
45
|
+
* between them lands (point) and moves past it (skipValue). The values
|
|
46
|
+
* themselves are never read, since an escaped one holds no "<", ">" or
|
|
47
|
+
* quote, so the position a value lands in is a property of the template. A
|
|
48
|
+
* reading can be copied and joined with another reading of the same
|
|
49
|
+
* template, which is how the template engine reads the branches of an
|
|
50
|
+
* if/else.
|
|
51
|
+
*/
|
|
52
|
+
export declare class HtmlPositionReader {
|
|
53
|
+
private state;
|
|
54
|
+
/** The name of the tag being read, or of the element whose content is, lowercased. */
|
|
55
|
+
private tagName;
|
|
56
|
+
/** The tag being read is an end tag, whose ">" never opens text content. */
|
|
57
|
+
private endTag;
|
|
58
|
+
/** The attribute whose name or value is being read; between attributes, the one an "=" gives a value to ("" for none). */
|
|
59
|
+
private attributeName;
|
|
60
|
+
/** The quote character the value being read opened with. */
|
|
61
|
+
private quote;
|
|
62
|
+
/** Between attributes: this reading joins readings that give an "=" to different attributes. */
|
|
63
|
+
private equalsAmbiguous;
|
|
64
|
+
/** Inside a comment: the COMMENT_ states it may be in, as bits. */
|
|
65
|
+
private commentStates;
|
|
66
|
+
/** Inside a <script>: 0 in script data, 1 escaped (after "<!--"), 2 double escaped (after "<!--" and "<script"). */
|
|
67
|
+
private scriptLevel;
|
|
68
|
+
/** Inside text content: the end tag name read so far, or the script tag name that double escapes a script or undoes that. */
|
|
69
|
+
private nameBuffer;
|
|
70
|
+
/**
|
|
71
|
+
* What has left inline SVG and MathML reading the HTML read so far apart
|
|
72
|
+
* from plain HTML, or "" while the two read it alike. Inside them the
|
|
73
|
+
* content of the TEXT_CONTENT_ELEMENTS is markup, and "<![CDATA[" runs to
|
|
74
|
+
* "]]>" where plain HTML ends a bogus comment at the first ">". Once
|
|
75
|
+
* text-only content holds a tag, or a CDATA section may end elsewhere
|
|
76
|
+
* than plain HTML's comment, one reading can sit inside a tag where the
|
|
77
|
+
* other sits in text, past the element's end tag too, so every value
|
|
78
|
+
* from there on is a rawText point. A <script> or <style> is left out: a
|
|
79
|
+
* value inside one is refused anyway, and a "<" in them (`a<b`) is common
|
|
80
|
+
* enough that parting there would refuse every value after most inline
|
|
81
|
+
* scripts.
|
|
82
|
+
*/
|
|
83
|
+
private foreignPartedBy;
|
|
84
|
+
/** The label an undecidable reading is blamed on: the last value placed in a comment, or the join between attributes. */
|
|
85
|
+
private blame;
|
|
86
|
+
private undecidableReason;
|
|
87
|
+
/** Read `html` on from where the last read stopped. */
|
|
88
|
+
read(html: string): void;
|
|
89
|
+
/** Where a value inserted at the end of the HTML read so far lands. */
|
|
90
|
+
point(): HtmlPoint;
|
|
91
|
+
/**
|
|
92
|
+
* Move past a value inserted here. An escaped value holds no "<", ">" or
|
|
93
|
+
* quote, so only a comment reads on differently after it: the value may
|
|
94
|
+
* end in "-", "--" or "--!", and the text after it may then end the
|
|
95
|
+
* comment on a ">" that ends it after no other value. The reader keeps
|
|
96
|
+
* every comment state such an ending leaves, and loses track (blaming
|
|
97
|
+
* `label`) when the text after them ends the comment in some and not in
|
|
98
|
+
* others.
|
|
99
|
+
*/
|
|
100
|
+
skipValue(label?: string): void;
|
|
101
|
+
/** An independent copy of this reading, to read one branch of a template on from here. */
|
|
102
|
+
copy(): HtmlPositionReader;
|
|
103
|
+
/**
|
|
104
|
+
* Take in `other`, a reading of the same template along another branch,
|
|
105
|
+
* when both sit at the same position, and say whether they did: from
|
|
106
|
+
* then on this reading reads for both. Between attributes, readings
|
|
107
|
+
* whose last attribute names differ count as the same position, so a
|
|
108
|
+
* conditional boolean attribute (`<input <!--if:x-->checked<!--/if:x--> name="a">`)
|
|
109
|
+
* reads on as one reading. Only an "=" next reads them apart, and it
|
|
110
|
+
* makes the joined reading lose track, blaming `label`.
|
|
111
|
+
*/
|
|
112
|
+
joinWith(other: HtmlPositionReader, label?: string): boolean;
|
|
113
|
+
/** Why the reader has lost track of where the HTML read so far sits, or null while it has not. A caller refuses the template then. */
|
|
114
|
+
get undecidable(): HtmlUndecidable | null;
|
|
115
|
+
/**
|
|
116
|
+
* The HTML read so far ends in plain text, where complete markup ends:
|
|
117
|
+
* outside any tag, comment and text content, and read alike by inline
|
|
118
|
+
* SVG and MathML. A fragment inserted into another template has to end
|
|
119
|
+
* here, since the template around it places its values without reading it.
|
|
120
|
+
*/
|
|
121
|
+
get inPlainText(): boolean;
|
|
122
|
+
/** The state and whichever fields it reads on with, as one string: two readings with the same key read any text the same way. */
|
|
123
|
+
private positionKey;
|
|
124
|
+
private openTag;
|
|
125
|
+
private enterBetweenAttributes;
|
|
126
|
+
/**
|
|
127
|
+
* A tag's ">" leads into its content: text content for the
|
|
128
|
+
* TEXT_CONTENT_ELEMENTS even when written `<script />`, which HTML reads
|
|
129
|
+
* as an open element, and data after an end tag.
|
|
130
|
+
*/
|
|
131
|
+
private closeTag;
|
|
132
|
+
/**
|
|
133
|
+
* A "<" in text content starts a tag, a comment or a bogus comment in
|
|
134
|
+
* markup, and it is not the element's own end tag: inline SVG and MathML
|
|
135
|
+
* read it as markup (see foreignPartedBy).
|
|
136
|
+
*/
|
|
137
|
+
private noteMarkupInText;
|
|
138
|
+
private loseTrack;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The language the browser reads an attribute's value in once it has
|
|
142
|
+
* decoded the HTML escapes, for the attributes whose value is not text: an
|
|
143
|
+
* event handler (any `on` attribute) runs as JavaScript, `style` is read as
|
|
144
|
+
* CSS and `srcdoc` as a whole document. Escaping keeps a value inside the
|
|
145
|
+
* quotes and nothing more there: `onclick="go('${x}')"` runs
|
|
146
|
+
* `');alert(1);//`, since `'` is a quote again by the time the script
|
|
147
|
+
* sees it.
|
|
148
|
+
*/
|
|
149
|
+
export declare const valueLanguageOf: (attributeName: string) => string | null;
|
|
150
|
+
/**
|
|
151
|
+
* The attributes whose value is one URL. In several a `javascript:` value
|
|
152
|
+
* runs script (a link, a form's action, a frame's src), and in the rest a
|
|
153
|
+
* scheme an inserted value picked is still one the page never meant to point
|
|
154
|
+
* at. A quoted value of one of them holding an inserted value is rendered
|
|
155
|
+
* whole and checked (safeUrlValue). The list-valued ones (srcset, ping) are
|
|
156
|
+
* left out: their entries never run.
|
|
157
|
+
*/
|
|
158
|
+
export declare const URL_ATTRIBUTE_NAMES: ReadonlySet<string>;
|
|
159
|
+
/**
|
|
160
|
+
* A rendered URL value `url`, or NEUTRALIZED_URL_VALUE when an inserted value
|
|
161
|
+
* may have given it a scheme outside SAFE_URL_SCHEMES. `firstValueIndex` is
|
|
162
|
+
* where in `url` the first inserted value landed, -1 when none did. Once the
|
|
163
|
+
* template's own text before that point holds a `:`, `/`, `?` or `#`, the
|
|
164
|
+
* template fixed the scheme (`/users/${id}`, `sms:${number}`) and the value
|
|
165
|
+
* stands as rendered. Otherwise the whole value is read for its scheme the
|
|
166
|
+
* way a browser reads it: leading and trailing control characters and spaces
|
|
167
|
+
* dropped, tabs and line breaks removed wherever they are, letters in any
|
|
168
|
+
* case. A value with no scheme is relative and stands. A character reference
|
|
169
|
+
* (`&`) where the scheme or its colon would be is refused rather than
|
|
170
|
+
* decoded: `:` and `:` are colons to the browser. Whatever kind of
|
|
171
|
+
* value was inserted, html`...` and raw() included, the rule is the same.
|
|
172
|
+
*/
|
|
173
|
+
export declare const safeUrlValue: (url: string, firstValueIndex: number) => string;
|