ajo-kit 0.1.0

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,168 @@
1
+ //#region packages/ajo-kit/src/constants.ts
2
+ /** HTTP-aware error serialized by loaders, actions, and API handlers. */
3
+ var Failure = class extends Error {
4
+ status;
5
+ constructor(status, message) {
6
+ super(message);
7
+ this.status = status;
8
+ }
9
+ toJSON() {
10
+ return {
11
+ message: mask(this.status, this.message),
12
+ status: this.status,
13
+ ...!production() && false
14
+ };
15
+ }
16
+ };
17
+ /** 404 error for missing routes or resources. */
18
+ var Missing = class extends Failure {
19
+ constructor(message = "Page not found") {
20
+ super(404, message);
21
+ }
22
+ };
23
+ /** 403 error for authenticated users without enough access. */
24
+ var Forbidden = class extends Failure {
25
+ constructor(message = "Access denied") {
26
+ super(403, message);
27
+ }
28
+ };
29
+ /** 401 error for requests that need authentication. */
30
+ var Denied = class extends Failure {
31
+ constructor(message = "Authentication required") {
32
+ super(401, message);
33
+ }
34
+ };
35
+ /** 400 validation error carrying field-level messages. */
36
+ var Invalid = class extends Failure {
37
+ fields;
38
+ constructor(fields, message = "Validation failed") {
39
+ super(400, message);
40
+ this.fields = fields;
41
+ }
42
+ toJSON() {
43
+ return {
44
+ message: mask(this.status, this.message),
45
+ status: this.status,
46
+ fields: this.fields,
47
+ ...!production() && false
48
+ };
49
+ }
50
+ };
51
+ var production = () => process.env.NODE_ENV === "production";
52
+ var mask = (status, message) => production() && status >= 500 ? "Internal Server Error" : message;
53
+ var config = (message) => {
54
+ if (production()) console.error(`[security] ${message}`);
55
+ return new Failure(500, message);
56
+ };
57
+ var code = (error) => {
58
+ if (!error || typeof error !== "object") return 500;
59
+ const value = error.status ?? error.statusCode;
60
+ return Number.isInteger(value) && value >= 400 && value <= 599 ? value : 500;
61
+ };
62
+ var message = (error, status) => {
63
+ if (status === 413) return "Content Too Large";
64
+ if (error instanceof Error && error.message) return error.message;
65
+ return status < 500 ? "Request failed" : "Unknown error";
66
+ };
67
+ /** Converts any thrown value into a framework Failure. */
68
+ function normalize(error) {
69
+ if (error instanceof Failure) return error;
70
+ const status = code(error);
71
+ return new Failure(status, message(error, status));
72
+ }
73
+ /** Returns route ancestor keys from outermost to innermost. */
74
+ var ancestors = (segments) => segments.map((_, i) => segments.slice(0, i + 1).join("/"));
75
+ /** Pushes a browser URL and asks the client router to navigate. */
76
+ var navigate = (to) => {
77
+ globalThis.history?.pushState({}, "", to);
78
+ globalThis.dispatchEvent?.(new CustomEvent("ajo:navigate"));
79
+ };
80
+ /** Returns true when a request expects JSON route/action data. */
81
+ var ajax = (req) => !!req.headers.accept?.includes("application/json");
82
+ /** Returns true when a request targets the /api route namespace. */
83
+ var api = (req) => req.path.startsWith("/api/");
84
+ var enabled = (value) => value === "1" || value?.toLowerCase() === "true";
85
+ var proxy = () => enabled(process.env.TRUST_PROXY);
86
+ var first = (value) => Array.isArray(value) ? value[0] : value;
87
+ var address = (value) => {
88
+ const raw = value.trim().replace(/^\[/, "").replace(/\]$/, "").replace(/^::ffff:/, "");
89
+ return raw === "::1" || raw === "127.0.0.1" ? "localhost" : raw;
90
+ };
91
+ var ipv4 = (value) => {
92
+ const parts = value.split(".");
93
+ return parts.length === 4 && parts.every((part) => {
94
+ if (!/^\d{1,3}$/.test(part)) return false;
95
+ const number = Number(part);
96
+ return number >= 0 && number <= 255;
97
+ });
98
+ };
99
+ var ipv6 = (value) => value.includes(":") && /^[0-9a-f:.]+$/i.test(value);
100
+ var local = (host) => {
101
+ try {
102
+ return address(new URL(`http://${host}`).hostname) === "localhost";
103
+ } catch {
104
+ return false;
105
+ }
106
+ };
107
+ var forwarded = (header) => {
108
+ const value = first(header);
109
+ if (!value) return;
110
+ const addr = address(value.split(",")[0]);
111
+ if (addr === "localhost" || ipv4(addr) || ipv6(addr)) return addr;
112
+ };
113
+ /** Resolves the client IP, honoring TRUST_PROXY for forwarded headers. */
114
+ var ip = (req) => {
115
+ const raw = proxy() ? forwarded(req.headers["x-forwarded-for"]) ?? req.socket?.remoteAddress : req.socket?.remoteAddress;
116
+ return raw ? address(raw) : "unknown";
117
+ };
118
+ /** Resolves the trusted app origin from APP_URL or the request host. */
119
+ var origin = (req) => {
120
+ const configured = process.env.APP_URL;
121
+ if (configured) try {
122
+ const url = new URL(configured);
123
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error();
124
+ return url.origin;
125
+ } catch {
126
+ throw config("Invalid APP_URL");
127
+ }
128
+ const host = req.headers.host;
129
+ if (!host) throw new Failure(400, "Missing Host header");
130
+ if (production() && !local(host)) throw config("APP_URL is required in production");
131
+ const forwarded = first(req.headers["x-forwarded-proto"])?.split(",")[0]?.trim();
132
+ const protocol = proxy() && (forwarded === "http" || forwarded === "https") ? forwarded : "http";
133
+ try {
134
+ return new URL(`${protocol}://${host}`).origin;
135
+ } catch {
136
+ throw new Failure(400, "Invalid Host header");
137
+ }
138
+ };
139
+ /** Builds parent/deferred links for a loader chain. */
140
+ function links(count) {
141
+ const chain = [];
142
+ for (let depth = 0; depth < count; depth++) {
143
+ let resolve;
144
+ let reject;
145
+ const promise = new Promise((res, rej) => {
146
+ resolve = res;
147
+ reject = rej;
148
+ });
149
+ const parent = async () => Object.assign({}, ...await Promise.all(chain.slice(0, depth).map((link) => link.deferred.promise)));
150
+ chain.push({
151
+ parent,
152
+ deferred: {
153
+ promise,
154
+ resolve,
155
+ reject
156
+ }
157
+ });
158
+ }
159
+ return chain;
160
+ }
161
+ /** Formats an ISO date for the current locale with compact defaults. */
162
+ var date = (iso, options) => new Date(iso).toLocaleDateString(void 0, options ?? {
163
+ month: "short",
164
+ day: "numeric",
165
+ year: "numeric"
166
+ });
167
+ //#endregion
168
+ export { Missing as a, api as c, links as d, navigate as f, Invalid as i, date as l, origin as m, Failure as n, ajax as o, normalize as p, Forbidden as r, ancestors as s, Denied as t, ip as u };
@@ -0,0 +1,34 @@
1
+ import { join } from "node:path";
2
+ import { readFileSync, readdirSync } from "node:fs";
3
+ //#region packages/ajo-kit/src/discover.ts
4
+ function discover(root = process.cwd()) {
5
+ const modules = join(root, "node_modules");
6
+ const plugins = [];
7
+ let entries;
8
+ try {
9
+ entries = readdirSync(modules);
10
+ } catch {
11
+ return plugins;
12
+ }
13
+ for (const entry of entries) {
14
+ if (!entry.startsWith("ajo-") || entry === "ajo-kit") continue;
15
+ const dir = join(modules, entry);
16
+ let pkg;
17
+ try {
18
+ pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
19
+ } catch {
20
+ continue;
21
+ }
22
+ if (!pkg.kit) continue;
23
+ plugins.push({
24
+ name: pkg.name,
25
+ path: dir,
26
+ ...pkg.kit,
27
+ ...pkg.kit.migrations && { migrations: join(dir, pkg.kit.migrations) },
28
+ ...pkg.kit.commands && { commands: join(dir, pkg.kit.commands) }
29
+ });
30
+ }
31
+ return plugins;
32
+ }
33
+ //#endregion
34
+ export { discover as t };
@@ -0,0 +1,23 @@
1
+ //#region packages/ajo-kit/src/headers.ts
2
+ var https = () => {
3
+ if (process.env.NODE_ENV !== "production" || !process.env.APP_URL) return false;
4
+ try {
5
+ return new URL(process.env.APP_URL).protocol === "https:";
6
+ } catch {
7
+ return false;
8
+ }
9
+ };
10
+ /** Security headers shared by dynamic SSR responses and static assets. */
11
+ var security = () => ({
12
+ "X-Content-Type-Options": "nosniff",
13
+ "Referrer-Policy": "strict-origin-when-cross-origin",
14
+ "Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=(), usb=()",
15
+ "Content-Security-Policy": "frame-ancestors 'none'",
16
+ ...https() && { "Strict-Transport-Security": "max-age=31536000; includeSubDomains" }
17
+ });
18
+ /** Writes headers, optionally preserving values already set downstream. */
19
+ var set = (res, values, missing = false) => {
20
+ for (const [key, value] of Object.entries(values)) if (!missing || !res.hasHeader(key)) res.setHeader(key, value);
21
+ };
22
+ //#endregion
23
+ export { set as n, security as t };
@@ -0,0 +1,89 @@
1
+ import { t as discover } from "./discover-D-b8Pqm8.js";
2
+ import { pathToFileURL } from "node:url";
3
+ import * as path from "node:path";
4
+ import { existsSync, promises } from "node:fs";
5
+ import { FileMigrationProvider, Migrator } from "kysely/migration";
6
+ //#region packages/ajo-kit/src/migrate.ts
7
+ var extensions = [
8
+ ".js",
9
+ ".ts",
10
+ ".mjs",
11
+ ".mts",
12
+ ".cjs",
13
+ ".cts"
14
+ ];
15
+ var pattern = /^(\d{4})_[a-z0-9]+(?:_[a-z0-9]+)*$/;
16
+ var file = (name) => !name.includes(".d.") && extensions.some((extension) => name.endsWith(extension));
17
+ function validate(id, names) {
18
+ for (const [index, name] of names.entries()) {
19
+ const sequence = pattern.exec(name)?.[1];
20
+ const expected = String(index + 1).padStart(4, "0");
21
+ if (sequence !== expected) throw new Error(`${id} migrations must be a contiguous sequence starting at 0001; expected ${expected}_*, found ${name}`);
22
+ }
23
+ }
24
+ function local(id, files) {
25
+ const names = files.filter(file).map((name) => name.slice(0, name.lastIndexOf("."))).sort();
26
+ if (new Set(names).size !== names.length) throw new Error(`${id} has duplicate migration filenames`);
27
+ validate(id, names);
28
+ return names;
29
+ }
30
+ function migrationFile(files, name) {
31
+ const safe = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
32
+ if (!safe) throw new Error("Migration name must contain a letter or number");
33
+ const number = local("project", files).length + 1;
34
+ if (number > 9999) throw new Error("Migration sequence exhausted");
35
+ return `${String(number).padStart(4, "0")}_${safe}.ts`;
36
+ }
37
+ async function load(source) {
38
+ const names = local(source.id, await promises.readdir(source.folder));
39
+ const migrations = await new FileMigrationProvider({
40
+ fs: promises,
41
+ path,
42
+ migrationFolder: source.folder,
43
+ import: (file) => import(pathToFileURL(file).href)
44
+ }).getMigrations();
45
+ const incomplete = names.filter((name) => typeof migrations[name]?.up !== "function" || typeof migrations[name]?.down !== "function");
46
+ if (incomplete.length) throw new Error(`${source.id} migrations must export up() and down(): ${incomplete.join(", ")}`);
47
+ return migrations;
48
+ }
49
+ async function merge(sources) {
50
+ const merged = {};
51
+ const ids = /* @__PURE__ */ new Set();
52
+ for (const source of sources) {
53
+ if (ids.has(source.id)) throw new Error(`Duplicate migration source "${source.id}"`);
54
+ ids.add(source.id);
55
+ for (const [name, migration] of Object.entries(await load(source))) {
56
+ const qualified = `${source.id}/${name}`;
57
+ merged[qualified] = migration;
58
+ }
59
+ }
60
+ return merged;
61
+ }
62
+ function migrator(instance, root = process.cwd()) {
63
+ const sources = discover(root).filter((plugin) => plugin.migrations).sort((left, right) => left.name.localeCompare(right.name)).map((plugin) => ({
64
+ folder: plugin.migrations,
65
+ id: `plugin/${plugin.name}`
66
+ }));
67
+ const project = path.join(root, "db/migrations");
68
+ if (existsSync(project)) sources.push({
69
+ folder: project,
70
+ id: "project"
71
+ });
72
+ return new Migrator({
73
+ db: instance,
74
+ allowUnorderedMigrations: true,
75
+ provider: { async getMigrations() {
76
+ return merge(sources);
77
+ } }
78
+ });
79
+ }
80
+ async function migrationStatus(instance, root = process.cwd()) {
81
+ const migrations = await migrator(instance, root).getMigrations();
82
+ if (!await instance.selectFrom("sqlite_master").select("name").where("type", "=", "table").where("name", "=", "kysely_migration").executeTakeFirst()) return migrations;
83
+ const available = new Set(migrations.map((migration) => migration.name));
84
+ const missing = (await instance.selectFrom("kysely_migration").select("name").execute()).map((migration) => migration.name).filter((name) => !available.has(name)).sort();
85
+ if (missing.length) throw new Error(`Migration history references missing migrations: ${missing.join(", ")}`);
86
+ return migrations;
87
+ }
88
+ //#endregion
89
+ export { migrationFile, migrationStatus, migrator };