ignotum 0.0.4 → 0.0.6

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.
@@ -0,0 +1,186 @@
1
+ import {
2
+ ArtifactPath,
3
+ ClientPath,
4
+ clientPublicFileExtensions,
5
+ deploymentArtifactLimits,
6
+ type ClientRoute,
7
+ } from "@ignotum/contracts/deployment";
8
+ import { artifactReference } from "@ignotum/deployment";
9
+ import { Effect, FileSystem, Option, Path, Schema } from "effect";
10
+
11
+ export class InvalidPublicFile extends Schema.TaggedError<InvalidPublicFile>()(
12
+ "InvalidPublicFile",
13
+ {
14
+ message: Schema.String,
15
+ path: Schema.String,
16
+ },
17
+ ) {}
18
+
19
+ export interface PublicBuildResult {
20
+ readonly files: number;
21
+ readonly routes: ReadonlyArray<ClientRoute>;
22
+ }
23
+
24
+ interface PublicFile {
25
+ readonly bytes: Uint8Array;
26
+ readonly destination: string;
27
+ readonly route: ClientRoute;
28
+ readonly source: string;
29
+ }
30
+
31
+ const pathSegmentPattern = /^[A-Za-z0-9._~-]+$/;
32
+
33
+ const startsWith = (bytes: Uint8Array, signature: ReadonlyArray<number>, offset = 0): boolean =>
34
+ signature.every((byte, index) => bytes[offset + index] === byte);
35
+
36
+ const asciiAt = (bytes: Uint8Array, value: string, offset = 0): boolean =>
37
+ startsWith(
38
+ bytes,
39
+ globalThis.Array.from(value, (character) => character.charCodeAt(0)),
40
+ offset,
41
+ );
42
+
43
+ const hasValidSignature = (extension: string, bytes: Uint8Array): boolean => {
44
+ switch (extension) {
45
+ case ".avif": {
46
+ if (!asciiAt(bytes, "ftyp", 4)) return false;
47
+ const headerLength = Math.min(bytes.length, 64);
48
+ for (let offset = 8; offset + 4 <= headerLength; offset += 4) {
49
+ if (asciiAt(bytes, "avif", offset) || asciiAt(bytes, "avis", offset)) return true;
50
+ }
51
+ return false;
52
+ }
53
+ case ".gif":
54
+ return asciiAt(bytes, "GIF87a") || asciiAt(bytes, "GIF89a");
55
+ case ".ico":
56
+ return startsWith(bytes, [0x00, 0x00, 0x01, 0x00]);
57
+ case ".jpeg":
58
+ case ".jpg":
59
+ return startsWith(bytes, [0xff, 0xd8, 0xff]);
60
+ case ".pdf":
61
+ return asciiAt(bytes, "%PDF-");
62
+ case ".png":
63
+ return startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
64
+ case ".webp":
65
+ return asciiAt(bytes, "RIFF") && asciiAt(bytes, "WEBP", 8);
66
+ default:
67
+ return false;
68
+ }
69
+ };
70
+
71
+ const invalid = (path: string, message: string) => InvalidPublicFile.make({ message, path });
72
+
73
+ export const copyPublicFiles = Effect.fn("Deploy.copyPublicFiles")(function* (
74
+ appDirectory: string,
75
+ outputDirectory: string,
76
+ ) {
77
+ const fileSystem = yield* FileSystem.FileSystem;
78
+ const path = yield* Path.Path;
79
+ const publicDirectory = path.join(appDirectory, "public");
80
+ if (!(yield* fileSystem.exists(publicDirectory))) {
81
+ return { files: 0, routes: [] } satisfies PublicBuildResult;
82
+ }
83
+
84
+ if (Option.isSome(yield* Effect.option(fileSystem.readLink(publicDirectory)))) {
85
+ return yield* invalid(
86
+ publicDirectory,
87
+ "The top-level public directory cannot be a symbolic link.",
88
+ );
89
+ }
90
+ const publicInfo = yield* fileSystem.stat(publicDirectory);
91
+ if (publicInfo.type !== "Directory") {
92
+ return yield* invalid(publicDirectory, "The top-level public path must be a directory.");
93
+ }
94
+
95
+ const entries = (yield* fileSystem.readDirectory(publicDirectory, {
96
+ recursive: true,
97
+ })).toSorted();
98
+ const publicFiles = yield* Effect.forEach(entries, (entry) =>
99
+ Effect.gen(function* () {
100
+ const relativePath = entry.replaceAll("\\", "/");
101
+ const source = path.join(publicDirectory, entry);
102
+ if (Option.isSome(yield* Effect.option(fileSystem.readLink(source)))) {
103
+ return yield* invalid(source, "Public entries cannot be symbolic links.");
104
+ }
105
+ const info = yield* fileSystem.stat(source);
106
+ if (info.type === "Directory") return undefined;
107
+ if (info.type !== "File") {
108
+ return yield* invalid(source, "Public entries must be regular files or directories.");
109
+ }
110
+
111
+ const segments = relativePath.split("/");
112
+ if (segments.some((segment) => !pathSegmentPattern.test(segment))) {
113
+ return yield* invalid(
114
+ source,
115
+ "Public file paths may only contain letters, numbers, dots, underscores, tildes, hyphens, and directory separators.",
116
+ );
117
+ }
118
+ if (segments[0] === "_ignotum") {
119
+ return yield* invalid(source, "The /_ignotum namespace is reserved by the platform.");
120
+ }
121
+
122
+ const extension = path.extname(relativePath).toLowerCase();
123
+ if (!clientPublicFileExtensions.some((allowedExtension) => allowedExtension === extension)) {
124
+ return yield* invalid(
125
+ source,
126
+ `Unsupported public file type '${extension || "(none)"}'. Allowed types: ${clientPublicFileExtensions.join(", ")}.`,
127
+ );
128
+ }
129
+ if (info.size > BigInt(deploymentArtifactLimits.fileBytes)) {
130
+ return yield* invalid(
131
+ source,
132
+ `Public files cannot exceed ${deploymentArtifactLimits.fileBytes} bytes.`,
133
+ );
134
+ }
135
+
136
+ const bytes = yield* fileSystem.readFile(source);
137
+ if (!hasValidSignature(extension, bytes)) {
138
+ return yield* invalid(
139
+ source,
140
+ `The contents do not match the '${extension}' file extension.`,
141
+ );
142
+ }
143
+
144
+ const pathname = `/${relativePath}`;
145
+ return {
146
+ bytes,
147
+ destination: path.join(outputDirectory, "routes", entry),
148
+ route: {
149
+ pathname: ClientPath.make(pathname),
150
+ artifact: yield* artifactReference(
151
+ ArtifactPath.make(`client/routes/${relativePath}`),
152
+ bytes,
153
+ ),
154
+ },
155
+ source,
156
+ } satisfies PublicFile;
157
+ }),
158
+ );
159
+ const files = publicFiles.filter((file): file is PublicFile => file !== undefined);
160
+
161
+ yield* Effect.forEach(files, (file) =>
162
+ Effect.gen(function* () {
163
+ if (yield* fileSystem.exists(file.destination)) {
164
+ return yield* invalid(
165
+ file.source,
166
+ `Public route '${file.route.pathname}' overlaps generated client output.`,
167
+ );
168
+ }
169
+ }),
170
+ );
171
+
172
+ yield* Effect.forEach(
173
+ files,
174
+ (file) =>
175
+ Effect.gen(function* () {
176
+ yield* fileSystem.makeDirectory(path.dirname(file.destination), { recursive: true });
177
+ yield* fileSystem.writeFile(file.destination, file.bytes);
178
+ }),
179
+ { discard: true },
180
+ );
181
+
182
+ return {
183
+ files: files.length,
184
+ routes: files.map((file) => file.route),
185
+ } satisfies PublicBuildResult;
186
+ });
@@ -429,6 +429,7 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (
429
429
  logLevel: "warn",
430
430
  mode: "production",
431
431
  plugins: [boundaries, serverFunctionEntryPlugin(definition)],
432
+ publicDir: false,
432
433
  resolve: {
433
434
  conditions: [...conditions],
434
435
  noExternal: true,
@@ -58,6 +58,13 @@ export const validateClientFiles = Effect.fn("ClientConfig.validateFiles")(funct
58
58
  const fileSystem = yield* FileSystem.FileSystem;
59
59
  const path = yield* Path.Path;
60
60
  const clientDirectory = path.join(appDirectory, "client");
61
+ const nestedPublicDirectory = path.join(clientDirectory, "public");
62
+ if (yield* fileSystem.exists(nestedPublicDirectory)) {
63
+ return yield* InvalidClientFile.make({
64
+ message: `Client public files must be placed in the top-level public directory, not ${nestedPublicDirectory}.`,
65
+ path: nestedPublicDirectory,
66
+ });
67
+ }
61
68
  const entryPath = path.join(clientDirectory, clientEntryFileName);
62
69
  if (!(yield* fileSystem.exists(entryPath))) {
63
70
  return yield* ClientFileNotFound.make({