chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import type {
|
||||
BrowserDataFailureCode,
|
||||
BrowserDataOperation,
|
||||
} from "../../application/ports/browser-file-storage/shared.ts";
|
||||
|
||||
export type PublicCacheRuntimePolicy = Readonly<{
|
||||
origin: string;
|
||||
ownedCachePrefix: string;
|
||||
mutationLockName: string;
|
||||
maxEntryBytes: number;
|
||||
maxReleaseBytes: number;
|
||||
maxEntriesPerRelease: number;
|
||||
retainedPreviousReleaseCount: number;
|
||||
allowedRequestHeaderNames: readonly string[];
|
||||
allowedVaryHeaderNames: readonly string[];
|
||||
allowedResponseHeaderNames: readonly string[];
|
||||
unknownResponseHeaderAction: "REJECT" | "STRIP";
|
||||
allowedQueryParameterNames: readonly string[];
|
||||
forbiddenQueryParameterNames: readonly string[];
|
||||
isQueryParameterValueAllowed: (name: string, value: string) => boolean;
|
||||
isContentTypeAllowed: (contentType: string) => boolean;
|
||||
isReleaseRegistryIdAllowed: (releaseRegistryId: string) => boolean;
|
||||
}>;
|
||||
|
||||
export type PublicCacheSafeObservation = Readonly<{
|
||||
operation: BrowserDataOperation;
|
||||
outcome: "STARTED" | "SUCCEEDED" | "FAILED";
|
||||
failureCode?: BrowserDataFailureCode;
|
||||
releaseRegistryId?: string;
|
||||
byteBucket?: "0" | "1B_1MiB" | "1MiB_16MiB" | "GT_16MiB";
|
||||
entryBucket?: "0" | "1_10" | "11_100" | "GT_100";
|
||||
}>;
|
||||
|
||||
export type PublicCacheSafeObserver = (
|
||||
observation: PublicCacheSafeObservation,
|
||||
) => void;
|
||||
|
||||
const RELEASE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
|
||||
const CACHE_PREFIX = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,63}:$/u;
|
||||
const DEFAULT_PUBLIC_CONTENT_TYPE =
|
||||
/^(?:application\/(?:javascript|json|manifest\+json|wasm)|font\/[a-z0-9.+-]+|image\/[a-z0-9.+-]+|text\/(?:css|javascript|plain))(?:\s*;.*)?$/iu;
|
||||
|
||||
export function createDefaultPublicCachePolicy(
|
||||
origin: string,
|
||||
): PublicCacheRuntimePolicy {
|
||||
return resolvePublicCachePolicy({
|
||||
origin,
|
||||
ownedCachePrefix: "ca-public-v1:",
|
||||
mutationLockName: "ca-public-v1:mutation",
|
||||
maxEntryBytes: 16 * 1024 * 1024,
|
||||
maxReleaseBytes: 128 * 1024 * 1024,
|
||||
maxEntriesPerRelease: 500,
|
||||
retainedPreviousReleaseCount: 1,
|
||||
allowedRequestHeaderNames: ["accept", "accept-language"],
|
||||
allowedVaryHeaderNames: [],
|
||||
allowedResponseHeaderNames: [
|
||||
"cache-control",
|
||||
"content-language",
|
||||
"content-type",
|
||||
"etag",
|
||||
"last-modified",
|
||||
"vary",
|
||||
],
|
||||
unknownResponseHeaderAction: "STRIP",
|
||||
allowedQueryParameterNames: [],
|
||||
forbiddenQueryParameterNames: [
|
||||
"access_token",
|
||||
"api_key",
|
||||
"auth",
|
||||
"email",
|
||||
"jwt",
|
||||
"session",
|
||||
"token",
|
||||
"user",
|
||||
],
|
||||
isQueryParameterValueAllowed: () => false,
|
||||
isContentTypeAllowed: (contentType) =>
|
||||
DEFAULT_PUBLIC_CONTENT_TYPE.test(contentType),
|
||||
isReleaseRegistryIdAllowed: (releaseRegistryId) =>
|
||||
RELEASE_ID.test(releaseRegistryId),
|
||||
});
|
||||
}
|
||||
|
||||
export function resolvePublicCachePolicy(
|
||||
policy: PublicCacheRuntimePolicy,
|
||||
): PublicCacheRuntimePolicy {
|
||||
const normalized: PublicCacheRuntimePolicy = Object.freeze({
|
||||
...policy,
|
||||
origin: new URL(policy.origin).origin,
|
||||
allowedRequestHeaderNames: Object.freeze(
|
||||
policy.allowedRequestHeaderNames.map((name) => name.toLowerCase()),
|
||||
),
|
||||
allowedVaryHeaderNames: Object.freeze(
|
||||
policy.allowedVaryHeaderNames.map((name) => name.toLowerCase()),
|
||||
),
|
||||
allowedResponseHeaderNames: Object.freeze(
|
||||
policy.allowedResponseHeaderNames.map((name) => name.toLowerCase()),
|
||||
),
|
||||
allowedQueryParameterNames: Object.freeze(
|
||||
policy.allowedQueryParameterNames.map((name) => name.toLowerCase()),
|
||||
),
|
||||
forbiddenQueryParameterNames: Object.freeze(
|
||||
policy.forbiddenQueryParameterNames.map((name) => name.toLowerCase()),
|
||||
),
|
||||
});
|
||||
assertPublicCachePolicy(normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function assertPublicCachePolicy(
|
||||
policy: PublicCacheRuntimePolicy,
|
||||
): void {
|
||||
const origin = new URL(policy.origin);
|
||||
if (
|
||||
origin.origin !== policy.origin ||
|
||||
!isAllowedPublicCacheOrigin(origin) ||
|
||||
!CACHE_PREFIX.test(policy.ownedCachePrefix) ||
|
||||
policy.mutationLockName.length === 0 ||
|
||||
!positiveSafeInteger(policy.maxEntryBytes) ||
|
||||
!positiveSafeInteger(policy.maxReleaseBytes) ||
|
||||
policy.maxEntryBytes > policy.maxReleaseBytes ||
|
||||
!positiveSafeInteger(policy.maxEntriesPerRelease) ||
|
||||
policy.maxEntriesPerRelease > 10_000 ||
|
||||
!Number.isSafeInteger(policy.retainedPreviousReleaseCount) ||
|
||||
policy.retainedPreviousReleaseCount < 1 ||
|
||||
policy.retainedPreviousReleaseCount > 5 ||
|
||||
!headerNameList(policy.allowedRequestHeaderNames) ||
|
||||
!headerNameList(policy.allowedVaryHeaderNames) ||
|
||||
!headerNameList(policy.allowedResponseHeaderNames) ||
|
||||
!["REJECT", "STRIP"].includes(policy.unknownResponseHeaderAction) ||
|
||||
!queryNameList(policy.allowedQueryParameterNames) ||
|
||||
policy.allowedVaryHeaderNames.some(
|
||||
(name) => !policy.allowedRequestHeaderNames.includes(name),
|
||||
) ||
|
||||
policy.forbiddenQueryParameterNames.some((name) => name.length === 0) ||
|
||||
policy.allowedQueryParameterNames.some((name) =>
|
||||
policy.forbiddenQueryParameterNames.includes(name),
|
||||
)
|
||||
) {
|
||||
throw new TypeError("Public Cache Storage policy is invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
export function isAllowedPublicCacheOrigin(url: URL): boolean {
|
||||
return (
|
||||
url.protocol === "https:" ||
|
||||
(url.protocol === "http:" &&
|
||||
(url.hostname === "localhost" ||
|
||||
url.hostname === "[::1]" ||
|
||||
/^127(?:\.\d{1,3}){3}$/u.test(url.hostname)))
|
||||
);
|
||||
}
|
||||
|
||||
export function cacheByteBucket(
|
||||
byteLength: number,
|
||||
): NonNullable<PublicCacheSafeObservation["byteBucket"]> {
|
||||
if (byteLength === 0) return "0";
|
||||
if (byteLength <= 1024 * 1024) return "1B_1MiB";
|
||||
if (byteLength <= 16 * 1024 * 1024) return "1MiB_16MiB";
|
||||
return "GT_16MiB";
|
||||
}
|
||||
|
||||
export function cacheEntryBucket(
|
||||
count: number,
|
||||
): NonNullable<PublicCacheSafeObservation["entryBucket"]> {
|
||||
if (count === 0) return "0";
|
||||
if (count <= 10) return "1_10";
|
||||
if (count <= 100) return "11_100";
|
||||
return "GT_100";
|
||||
}
|
||||
|
||||
export function observePublicCacheSafely(
|
||||
observer: PublicCacheSafeObserver | undefined,
|
||||
observation: PublicCacheSafeObservation,
|
||||
): void {
|
||||
try {
|
||||
observer?.(Object.freeze({ ...observation }));
|
||||
} catch {
|
||||
// Cache behavior never depends on observability.
|
||||
}
|
||||
}
|
||||
|
||||
function positiveSafeInteger(value: number): boolean {
|
||||
return Number.isSafeInteger(value) && value > 0;
|
||||
}
|
||||
|
||||
function headerNameList(names: readonly string[]): boolean {
|
||||
return (
|
||||
new Set(names).size === names.length &&
|
||||
names.every((name) => /^[a-z0-9!#$%&'*+.^_`|~-]+$/u.test(name))
|
||||
);
|
||||
}
|
||||
|
||||
function queryNameList(names: readonly string[]): boolean {
|
||||
return (
|
||||
new Set(names).size === names.length &&
|
||||
names.every((name) => /^[a-z0-9][a-z0-9._-]{0,63}$/u.test(name))
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user