refactor: 리펙토링
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { contractSetSchema } from "./contract-set.ts";
|
||||
|
||||
const versionSchema = z.string().regex(/^\d+(?:\.\d+){0,2}$/);
|
||||
|
||||
function assertEndpointUrl(
|
||||
value: string,
|
||||
local: boolean,
|
||||
options: Readonly<{ trailingSlashPath?: boolean }> = {},
|
||||
): void {
|
||||
const parsed = new URL(value);
|
||||
if (
|
||||
(parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
||||
parsed.username ||
|
||||
parsed.password ||
|
||||
parsed.hash ||
|
||||
parsed.search ||
|
||||
(!local && parsed.protocol !== "https:")
|
||||
) {
|
||||
throw new TypeError("invalid");
|
||||
}
|
||||
if (options.trailingSlashPath && !parsed.pathname.endsWith("/")) {
|
||||
throw new TypeError("invalid");
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidReleaseManifestUrl(value: string): boolean {
|
||||
if (!value.startsWith("/") || value.startsWith("//")) return false;
|
||||
if (new TextEncoder().encode(value).byteLength > 256) return false;
|
||||
if (value.includes("?") || value.includes("#") || value.includes("\\")) {
|
||||
return false;
|
||||
}
|
||||
if (/%2f|%5c/i.test(value)) return false;
|
||||
return !value
|
||||
.split("/")
|
||||
.some((segment) => segment === "." || segment === "..");
|
||||
}
|
||||
|
||||
export const capabilityOverrideArtifactSchema = z
|
||||
.object({
|
||||
REALTIME: z.enum(["DEFAULT", "DISABLED"]).default("DEFAULT"),
|
||||
WEB_WORKER: z.enum(["DEFAULT", "DISABLED"]).default("DEFAULT"),
|
||||
SERVICE_WORKER: z.enum(["DEFAULT", "DISABLED"]).default("DEFAULT"),
|
||||
OFFLINE_COMMANDS: z.enum(["DEFAULT", "DISABLED"]).default("DEFAULT"),
|
||||
})
|
||||
.strict()
|
||||
.default({
|
||||
REALTIME: "DEFAULT",
|
||||
WEB_WORKER: "DEFAULT",
|
||||
SERVICE_WORKER: "DEFAULT",
|
||||
OFFLINE_COMMANDS: "DEFAULT",
|
||||
});
|
||||
|
||||
type RuntimeConfigArtifactDraft = Readonly<{
|
||||
APP_ENV: "local" | "development" | "staging" | "production";
|
||||
API_BASE_URL: string;
|
||||
TELEMETRY_ENABLED: boolean;
|
||||
TELEMETRY_ENDPOINT?: string;
|
||||
AUTH_MODE: "external" | "demo";
|
||||
RELEASE_MANIFEST_URL: string;
|
||||
}>;
|
||||
|
||||
function runtimeConfigArtifactInvariants(
|
||||
config: RuntimeConfigArtifactDraft,
|
||||
context: z.RefinementCtx,
|
||||
): void {
|
||||
const local = config.APP_ENV === "local" || config.APP_ENV === "development";
|
||||
if (config.TELEMETRY_ENABLED && !config.TELEMETRY_ENDPOINT) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["TELEMETRY_ENDPOINT"],
|
||||
message: "required when telemetry is enabled",
|
||||
});
|
||||
}
|
||||
if (!local && config.AUTH_MODE === "demo") {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["AUTH_MODE"],
|
||||
message: "demo authentication is limited to local environments",
|
||||
});
|
||||
}
|
||||
try {
|
||||
assertEndpointUrl(config.API_BASE_URL, local, { trailingSlashPath: true });
|
||||
} catch {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["API_BASE_URL"],
|
||||
message:
|
||||
"absolute credential-free URL ending in / is required; HTTPS outside local",
|
||||
});
|
||||
}
|
||||
if (config.TELEMETRY_ENDPOINT) {
|
||||
try {
|
||||
assertEndpointUrl(config.TELEMETRY_ENDPOINT, local);
|
||||
} catch {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["TELEMETRY_ENDPOINT"],
|
||||
message:
|
||||
"absolute credential-free URL is required; HTTPS outside local",
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!isValidReleaseManifestUrl(config.RELEASE_MANIFEST_URL)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["RELEASE_MANIFEST_URL"],
|
||||
message: "same-origin absolute path without query, hash or traversal",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const runtimeConfigArtifactFields = {
|
||||
APP_ENV: z.enum(["local", "development", "staging", "production"]),
|
||||
API_BASE_URL: z.url(),
|
||||
REQUEST_TIMEOUT_MS: z.int().min(100).max(60_000).default(10_000),
|
||||
MAX_RETRY_ATTEMPTS: z.int().min(0).max(2).default(2),
|
||||
TELEMETRY_ENABLED: z.boolean(),
|
||||
TELEMETRY_ENDPOINT: z.url().optional(),
|
||||
AUTH_MODE: z.enum(["external", "demo"]),
|
||||
RELEASE_MANIFEST_URL: z.string().min(1).default("/release-manifest.json"),
|
||||
RELEASE_ID: z.string().min(1).optional(),
|
||||
BUILD_ID: z.string().min(1).optional(),
|
||||
} as const;
|
||||
|
||||
export const runtimeConfigV1ArtifactSchema = z
|
||||
.object({
|
||||
...runtimeConfigArtifactFields,
|
||||
CONFIG_SCHEMA_VERSION: z.literal("1"),
|
||||
API_CONTRACT_VERSION: versionSchema,
|
||||
})
|
||||
.strict()
|
||||
.superRefine(runtimeConfigArtifactInvariants);
|
||||
|
||||
export const runtimeConfigV2ArtifactSchema = z
|
||||
.object({
|
||||
...runtimeConfigArtifactFields,
|
||||
CONFIG_SCHEMA_VERSION: z.literal("2.0"),
|
||||
CAPABILITY_OVERRIDES: capabilityOverrideArtifactSchema,
|
||||
})
|
||||
.strict()
|
||||
.superRefine(runtimeConfigArtifactInvariants);
|
||||
|
||||
export const runtimeConfigArtifactSchema = z.discriminatedUnion(
|
||||
"CONFIG_SCHEMA_VERSION",
|
||||
[
|
||||
runtimeConfigV1ArtifactSchema,
|
||||
runtimeConfigV2ArtifactSchema,
|
||||
],
|
||||
);
|
||||
|
||||
const releaseManifestArtifactFields = {
|
||||
appVersion: z.string().min(1),
|
||||
buildId: z.string().min(1),
|
||||
commitSha: z.string().min(1),
|
||||
assetManifestHash: z.string().min(1),
|
||||
releaseId: z.string().min(1),
|
||||
builtAt: z.string().min(1),
|
||||
routeChunks: z.record(z.string().min(1), z.string().min(1)),
|
||||
} as const;
|
||||
|
||||
export const releaseManifestV1ArtifactSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(1),
|
||||
...releaseManifestArtifactFields,
|
||||
configSchemaVersion: versionSchema,
|
||||
apiContractVersion: versionSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const releaseManifestV2ArtifactSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(2),
|
||||
...releaseManifestArtifactFields,
|
||||
configSchemaVersion: z.literal("2.0"),
|
||||
contractSet: contractSetSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const releaseManifestArtifactSchema = z.discriminatedUnion(
|
||||
"schemaVersion",
|
||||
[releaseManifestV1ArtifactSchema, releaseManifestV2ArtifactSchema],
|
||||
);
|
||||
|
||||
export const buildManifestArtifactSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(1),
|
||||
buildId: z.string().min(1),
|
||||
commitSha: z.string().min(1),
|
||||
releaseId: z.string().min(1),
|
||||
moduleInventoryHash: z.string().min(1),
|
||||
generatedAt: z.string().min(1),
|
||||
buildContext: z
|
||||
.object({
|
||||
nodeVersion: z.string().min(1),
|
||||
packageManagerVersion: z.string().min(1),
|
||||
runnerImage: z.string().min(1),
|
||||
sourceDateEpoch: z.string().min(1).nullable(),
|
||||
})
|
||||
.strict(),
|
||||
outputs: z
|
||||
.object({
|
||||
directory: z.string().min(1),
|
||||
viteManifest: z.string().min(1),
|
||||
moduleInventory: z.string().min(1),
|
||||
routeChunks: z.record(z.string().min(1), z.string().min(1)),
|
||||
runtimeConfigSchema: z.string().min(1),
|
||||
})
|
||||
.strict(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type RuntimeConfigV1Artifact = z.output<
|
||||
typeof runtimeConfigV1ArtifactSchema
|
||||
>;
|
||||
export type RuntimeConfigV2Artifact = z.output<
|
||||
typeof runtimeConfigV2ArtifactSchema
|
||||
>;
|
||||
export type CapabilityOverrideArtifact = z.output<
|
||||
typeof capabilityOverrideArtifactSchema
|
||||
>;
|
||||
export type RuntimeConfigArtifact = z.output<typeof runtimeConfigArtifactSchema>;
|
||||
export type ReleaseManifestV1Artifact = z.output<
|
||||
typeof releaseManifestV1ArtifactSchema
|
||||
>;
|
||||
export type ReleaseManifestV2Artifact = z.output<
|
||||
typeof releaseManifestV2ArtifactSchema
|
||||
>;
|
||||
export type ReleaseArtifact = z.output<typeof releaseManifestArtifactSchema>;
|
||||
export type BuildManifestArtifact = z.output<typeof buildManifestArtifactSchema>;
|
||||
|
||||
export function parseReleaseArtifact(value: unknown): ReleaseArtifact {
|
||||
return releaseManifestArtifactSchema.parse(value);
|
||||
}
|
||||
|
||||
export function parseRuntimeConfigArtifact(value: unknown): RuntimeConfigArtifact {
|
||||
return runtimeConfigArtifactSchema.parse(value);
|
||||
}
|
||||
|
||||
export function parseBuildManifestArtifact(value: unknown): BuildManifestArtifact {
|
||||
return buildManifestArtifactSchema.parse(value);
|
||||
}
|
||||
|
||||
export function projectReleaseTokens(release: ReleaseArtifact) {
|
||||
const common = {
|
||||
schemaVersion: release.schemaVersion,
|
||||
appVersion: release.appVersion,
|
||||
buildId: release.buildId,
|
||||
commitSha: release.commitSha,
|
||||
configSchemaVersion: release.configSchemaVersion,
|
||||
assetManifestHash: release.assetManifestHash,
|
||||
releaseId: release.releaseId,
|
||||
builtAt: release.builtAt,
|
||||
} as const;
|
||||
|
||||
return release.schemaVersion === 1
|
||||
? Object.freeze({
|
||||
...common,
|
||||
schemaVersion: 1 as const,
|
||||
apiContractVersion: release.apiContractVersion,
|
||||
})
|
||||
: Object.freeze({
|
||||
...common,
|
||||
schemaVersion: 2 as const,
|
||||
contractSetDigest: release.contractSet.setDigest,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user