@arkstack/common 0.5.2 → 0.5.3

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/dist/index.js CHANGED
@@ -1,138 +1,54 @@
1
- import { Obj, str } from "@h3ravel/support";
2
- import { createJiti } from "jiti";
3
- import { createRequire } from "module";
4
- import path, { resolve } from "node:path";
5
- import { pathToFileURL } from "node:url";
6
- import { readdirSync } from "fs";
1
+ import { _ as ConfigLoader, a as env, c as nodeEnv, d as resolveRuntimeDir, f as resolveRuntimeModule, g as CONFIG_KEY, h as envLoader, i as discoverCommands, l as outputDir, m as EnvLoader, n as appUrl, o as importFile, p as toOutputPath, r as config, s as interopDefault, t as appKey, u as rebuildOutput, v as configLoader } from "./system-DUaI4u99.js";
2
+ import { _ as Hash, c as abortIf, d as initializeGlobalContext, f as isClass, g as Exception, h as AppException, l as assertFound, m as RequestException, p as perPage, s as abort, u as getModel, v as Encryption } from "./utils-DLifcZWV.js";
3
+ import { Hook as Hook$1 } from "@arkstack/foundry";
4
+ import { Arkstack } from "@arkstack/contract";
5
+ import { str } from "@h3ravel/support";
6
+ import path from "node:path";
7
+ import { ModelNotFoundException } from "arkormx";
7
8
  import { detect } from "detect-port";
8
9
  import pino from "pino";
9
10
  import chalk from "chalk";
10
- import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
11
- import { Secret, TOTP } from "otpauth";
12
- import { compare, genSalt, hash } from "bcryptjs";
13
- import { getUserConfig } from "arkormx";
11
+ import { networkInterfaces } from "node:os";
14
12
  //#region src/lifecycle.ts
15
- const bindGracefulShutdown = (shutdown) => {
13
+ const bindGracefulShutdown = (shutdown, defer) => {
14
+ if (defer) return;
16
15
  [
17
16
  "SIGINT",
18
17
  "SIGTERM",
19
18
  "SIGQUIT"
20
19
  ].forEach((signal) => {
21
20
  process.on(signal, async () => {
21
+ if (Hook$1.has("shutdown", "before")) Hook$1.get("shutdown", "after", {});
22
22
  await shutdown();
23
23
  });
24
24
  });
25
25
  };
26
26
  //#endregion
27
- //#region src/system.ts
28
- /**
29
- * Read the .env file
30
- *
31
- * @param env
32
- * @param def
33
- * @returns
34
- */
35
- const env = (env, defaultValue) => {
36
- let val = process.env[env] ?? "";
37
- if ([
38
- true,
39
- "true",
40
- "on",
41
- false,
42
- "false",
43
- "off"
44
- ].includes(val)) val = [
45
- true,
46
- "true",
47
- "on"
48
- ].includes(val);
49
- if (!isNaN(Number(val)) && typeof val !== "boolean" && typeof val !== "undefined" && val !== "") val = Number(val);
50
- if (val === "") val = void 0;
51
- if (val === "null") val = null;
52
- val ??= defaultValue;
53
- return val;
54
- };
55
- /**
56
- * Build the app url
57
- *
58
- * @param link
59
- * @returns
60
- */
61
- const appUrl = (link) => {
62
- const port = env("PORT") || "3000";
63
- const defaultUrl = `http://localhost:${port}`;
64
- const appUrl = env("APP_URL") ?? defaultUrl;
65
- try {
66
- const url = new URL(appUrl);
67
- if (url.port || url.hostname === "localhost") url.port = port;
68
- const baseUrl = url.toString().replace(/\/$/, "");
69
- if (link) return `${baseUrl}${`/${link.replace(/^\/+/, "")}`}`;
70
- return baseUrl;
71
- } catch {
72
- return link ? `${defaultUrl}/${link.replace(/^\/+/, "")}` : defaultUrl;
73
- }
74
- };
75
- /**
76
- * Gets the application configuration.
77
- *
78
- * @param key The configuration key to retrieve.
79
- * @param defaultValue The default value to return if the key is not found.
80
- * @returns The configuration value.
81
- */
82
- const config = (key, defaultValue) => {
83
- const dist = path.relative(process.cwd(), outputDir());
84
- const require = createRequire(import.meta.url);
85
- const config = readdirSync(path.join(process.cwd(), `${dist}/config`), { withFileTypes: true }).filter((file) => {
86
- if (file.name.includes("middleware") && globalThis.arkctx.runtime === "CLI") return false;
87
- return file.isFile() && (file.name.endsWith(".js") || file.name.endsWith(".ts"));
88
- }).reduce((configs, file) => {
89
- const configName = path.basename(file.name, path.extname(file.name));
90
- configs[configName] = require(path.join(file.parentPath, file.name)).default(globalThis.app());
91
- return configs;
92
- }, {});
93
- if (key) return Obj.get(config, key, defaultValue);
94
- return config;
95
- };
96
- /**
97
- * Gets the current Node environment (development or production).
98
- *
99
- * @returns
100
- */
101
- const nodeEnv = () => {
102
- let envValue = env("NODE_ENV", "development");
103
- if (envValue !== "development" && envValue !== "production") envValue = "development";
104
- return envValue === "production" ? "prod" : "dev";
105
- };
27
+ //#region src/network.ts
106
28
  /**
107
- * Gets the output directory for the application based on the current environment.
29
+ * Boots the app using an available port close to the requested port
30
+ *when requested port is not available
108
31
  *
109
- * @param cwd The current working directory (optional, defaults to process.cwd()).
110
- * @returns
32
+ * @param boot
33
+ * @param preferredPort
34
+ * @param app
35
+ * @param defer
111
36
  */
112
- const outputDir = (cwd = process.cwd()) => {
113
- const NODE_ENV = nodeEnv();
114
- const output = {
115
- dev: env("OUTPUT_DIR_DEV", ".arkstack/build"),
116
- prod: env("OUTPUT_DIR", "dist")
117
- };
118
- return path.isAbsolute(output[NODE_ENV] ?? output.dev) ? output[NODE_ENV] ?? output.dev : path.join(cwd, output[NODE_ENV] ?? output.dev);
119
- };
120
- const importFile = async (filePath) => {
121
- const resolvedPath = resolve(filePath);
122
- return await createJiti(pathToFileURL(resolvedPath).href, {
123
- interopDefault: false,
124
- tsconfigPaths: true
125
- }).import(resolvedPath);
126
- };
127
- //#endregion
128
- //#region src/network.ts
129
- const bootWithDetectedPort = async (boot, preferredPort = 3e3, app) => {
37
+ const bootWithDetectedPort = async (boot, preferredPort = 3e3, app, defer) => {
38
+ const port = await detect(preferredPort);
39
+ if (Hook$1.has("boot", "before")) Hook$1.get("boot", "before", port, app);
130
40
  if (app && !globalThis.app) globalThis.app = () => app;
131
41
  globalThis.env = env;
132
42
  globalThis.config = config;
133
43
  globalThis.str = str;
44
+ globalThis.abort = abort;
45
+ globalThis.abortIf = abortIf;
46
+ globalThis.assertFound = assertFound;
134
47
  globalThis.arkctx = { runtime: "HTTP" };
135
- await boot(await detect(preferredPort));
48
+ await boot(port);
49
+ await initializeGlobalContext();
50
+ bindGracefulShutdown(async () => await app?.shutdown(), defer);
51
+ if (Hook$1.has("boot", "after")) Hook$1.get("boot", "after", port, app);
136
52
  };
137
53
  const renderError = ({ message = "An unexpected error occurred.", stack, title, code = 500 }) => {
138
54
  title = {
@@ -180,56 +96,6 @@ const loadPrototypes = () => {
180
96
  };
181
97
  };
182
98
  //#endregion
183
- //#region src/Exceptions/Exception.ts
184
- var Exception = class extends Error {
185
- name;
186
- constructor(message, options) {
187
- super(message, options);
188
- this.name = "Exception";
189
- }
190
- };
191
- //#endregion
192
- //#region src/Exceptions/AppException.ts
193
- var AppException = class extends Exception {
194
- errors = void 0;
195
- statusCode;
196
- constructor(message, statusCode = 400, options) {
197
- super(message, options);
198
- this.statusCode = statusCode;
199
- }
200
- };
201
- //#endregion
202
- //#region src/Exceptions/RequestException.ts
203
- var RequestException = class RequestException extends AppException {
204
- statusCode;
205
- constructor(message, statusCode = 400, options) {
206
- super(message, statusCode, options);
207
- this.statusCode = statusCode;
208
- }
209
- /**
210
- * Asserts that a value is not null or undefined.
211
- *
212
- * @param value
213
- * @param message
214
- * @param code
215
- * @throws {RequestException} Throws if the value is null or undefined.
216
- */
217
- static assertNotEmpty(value, message, code = 404) {
218
- if (!value) throw new RequestException(message, code);
219
- }
220
- /**
221
- * Asserts that a boolean condition is true.
222
- *
223
- * @param boolean
224
- * @param message
225
- * @param code
226
- * @throws {RequestException} Throws if the boolean condition is true.
227
- */
228
- static abortIf(boolean, message, code) {
229
- if (boolean) throw new RequestException(message, code);
230
- }
231
- };
232
- //#endregion
233
99
  //#region src/ErrorHandler.ts
234
100
  var ErrorHandler = class ErrorHandler {
235
101
  static loggerCache = /* @__PURE__ */ new Map();
@@ -244,7 +110,7 @@ var ErrorHandler = class ErrorHandler {
244
110
  return Number.isInteger(code) && code >= 100 && code < 600 ? code : fallback;
245
111
  }
246
112
  static getErrorLogger() {
247
- const destination = path.resolve(process.cwd(), "storage/logs/error.log");
113
+ const destination = path.resolve(Arkstack.rootDir(), "storage/logs/error.log");
248
114
  if (!ErrorHandler.loggerCache.has(destination)) ErrorHandler.loggerCache.set(destination, pino({ level: "error" }, pino.destination({
249
115
  dest: destination,
250
116
  mkdir: true,
@@ -283,7 +149,12 @@ var ErrorHandler = class ErrorHandler {
283
149
  return typeof ErrorHandler.toErrorShape(error)?.errors !== "undefined";
284
150
  }
285
151
  static isModelNotFoundError(error) {
286
- return typeof ErrorHandler.toErrorShape(error)?.getModelName === "function";
152
+ const candidate = ErrorHandler.toErrorShape(error);
153
+ if (error.cause && error.cause instanceof ModelNotFoundException) {
154
+ error.getModelName = error.cause.getModelName;
155
+ return true;
156
+ }
157
+ return typeof candidate?.getModelName === "function" || error instanceof ModelNotFoundException;
287
158
  }
288
159
  static shouldHideStack() {
289
160
  const value = process.env.HIDE_ERROR_STACK;
@@ -319,6 +190,7 @@ var ErrorHandler = class ErrorHandler {
319
190
  delete payload.errors;
320
191
  delete payload.stack;
321
192
  }
193
+ if (detailedError?.body && typeof detailedError.body === "object") Object.assign(payload, detailedError.body);
322
194
  return payload;
323
195
  }
324
196
  static logUnhandledError(err, request, message) {
@@ -341,6 +213,54 @@ const shouldLogError = ErrorHandler.shouldLogError;
341
213
  const createErrorPayload = ErrorHandler.createErrorPayload;
342
214
  const logUnhandledError = ErrorHandler.logUnhandledError;
343
215
  //#endregion
216
+ //#region src/Publisher.ts
217
+ const REGISTRY_KEY = Symbol.for("arkstack.publishables");
218
+ /**
219
+ * Registry of artifacts packages want to publish into the consuming application.
220
+ *
221
+ * Packages call {@link Publisher.publishes} from their `setup` module so that
222
+ * `ark publish` can copy the artifacts (migrations, config stubs, assets, …)
223
+ * into the app. The registry is backed by a global symbol so it stays a single
224
+ * shared instance even across duplicated module copies.
225
+ */
226
+ var Publisher = class {
227
+ /** The shared, global-symbol-backed registry of publishable groups. */
228
+ static get registry() {
229
+ return globalThis[REGISTRY_KEY] ??= [];
230
+ }
231
+ /**
232
+ * Register artifacts a package wants to publish into the application.
233
+ *
234
+ * @example
235
+ * ```ts
236
+ * Publisher.publishes({
237
+ * package: '@arkstack/cache',
238
+ * tag: 'cache-migrations',
239
+ * entries: [{ from: join(here, '../stubs/...'), to: 'src/database/migrations/...' }],
240
+ * })
241
+ * ```
242
+ *
243
+ * @param group The publishable group to register.
244
+ */
245
+ static publishes(group) {
246
+ this.registry.push(group);
247
+ }
248
+ /**
249
+ * Read the registered publishable groups, optionally filtered by package or
250
+ * tag.
251
+ *
252
+ * @param filter Restrict the result to a package and/or tag.
253
+ * @returns The matching publishable groups.
254
+ */
255
+ static publishables(filter = {}) {
256
+ return this.registry.filter((group) => (!filter.package || group.package === filter.package) && (!filter.tag || group.tag === filter.tag));
257
+ }
258
+ /** Remove every registered publishable group (primarily for tests). */
259
+ static clear() {
260
+ this.registry.length = 0;
261
+ }
262
+ };
263
+ //#endregion
344
264
  //#region src/Logger.ts
345
265
  var Console = class {
346
266
  static log = (...args) => Logger.log(args.map((e) => [e, "white"]));
@@ -553,30 +473,30 @@ var Hook = class {
553
473
  * @param name
554
474
  * @param value
555
475
  */
556
- static set = (name, hook) => {
476
+ static set(name, hook) {
557
477
  const oldhook = this.hooks.get(name) ?? {};
558
478
  this.hooks.set(name, {
559
479
  ...oldhook,
560
480
  ...hook
561
481
  });
562
- };
482
+ }
563
483
  /**
564
484
  * Check if a hook is defined by name
565
485
  *
566
486
  * @param name
567
487
  */
568
- static has = (name, pos) => {
488
+ static has(name, pos) {
569
489
  if (pos && this.hooks.has(name)) return Boolean(this.get(name, pos));
570
490
  return this.hooks.has(name);
571
- };
572
- static get(name, pos) {
491
+ }
492
+ static get(name, pos, ...args) {
573
493
  const hook = this.hooks.get(name);
574
494
  if (!hook) return void 0;
575
- if (pos !== void 0) {
576
- if (!hook[pos]) return void 0;
577
- return hook[pos];
578
- }
579
- return hook;
495
+ if (pos === void 0) return hook;
496
+ const fn = hook[pos];
497
+ if (!fn) return void 0;
498
+ if (args.length > 0) return fn(...args);
499
+ return fn;
580
500
  }
581
501
  /**
582
502
  * Retrieve all defined hooks
@@ -593,7 +513,7 @@ var Hook = class {
593
513
  *
594
514
  * @param name
595
515
  */
596
- static unset = (name, pos) => {
516
+ static unset(name, pos) {
597
517
  if (name && this.hooks.has(name)) {
598
518
  if (pos !== void 0) {
599
519
  const hook = this.get(name);
@@ -603,7 +523,7 @@ var Hook = class {
603
523
  }
604
524
  this.hooks.delete(name);
605
525
  } else this.clear();
606
- };
526
+ }
607
527
  /**
608
528
  * Clear all the defined hooks
609
529
  */
@@ -612,107 +532,41 @@ var Hook = class {
612
532
  };
613
533
  };
614
534
  //#endregion
615
- //#region src/utils/encryption.ts
616
- var Encryption = class {
617
- static algorithm = "aes-256-gcm";
618
- static getKey() {
619
- const secret = env("TWO_FACTOR_ENCRYPTION_KEY");
620
- if (!secret) throw new Error("TWO_FACTOR_ENCRYPTION_KEY is required to use two-factor authentication");
621
- return createHash("sha256").update(secret).digest();
622
- }
623
- static encrypt(value) {
624
- const iv = randomBytes(12);
625
- const cipher = createCipheriv(this.algorithm, this.getKey(), iv);
626
- const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
627
- return [
628
- iv,
629
- cipher.getAuthTag(),
630
- ciphertext
631
- ].map((part) => part.toString("base64url")).join(":");
632
- }
633
- static decrypt(payload) {
634
- const [iv, authTag, ciphertext] = payload.split(":");
635
- if (!iv || !authTag || !ciphertext) throw new Error("Invalid encrypted payload format");
636
- const decipher = createDecipheriv(this.algorithm, this.getKey(), Buffer.from(iv, "base64url"));
637
- decipher.setAuthTag(Buffer.from(authTag, "base64url"));
638
- return Buffer.concat([decipher.update(Buffer.from(ciphertext, "base64url")), decipher.final()]).toString("utf8");
639
- }
640
- };
641
- //#endregion
642
- //#region src/utils/hash.ts
643
- var Hash = class {
644
- /**
645
- * Hash a value using bcrypt
646
- *
647
- * @param value
648
- * @returns
649
- */
650
- static async make(value) {
651
- return await hash(value, await genSalt(10));
652
- }
653
- /**
654
- * Verify a value against a hashed value
655
- *
656
- * @param value
657
- * @param hashedValue
658
- * @returns
659
- */
660
- static async verify(value, hashedValue) {
661
- return await compare(value, hashedValue);
662
- }
663
- /**
664
- * Generate a one-time password (OTP) using TOTP algorithm
665
- *
666
- * @param digits The number of digits for the OTP, default is 6.
667
- * @param label A label to identify the OTP, can be an email or phone number.
668
- * @param period Interval of time for which a token is valid, in seconds.
669
- * @returns
670
- */
671
- static otp(digits = 6, label = "Alice", period = 30) {
672
- return new TOTP({
673
- label,
674
- digits,
675
- issuer: env("APP_NAME", "Roseed"),
676
- algorithm: "SHA1",
677
- period,
678
- secret: "US3WHSG7X5KAPV27VANWKQHF3SH3HULL"
679
- });
680
- }
681
- static totp(secret, label, issuer = env("APP_NAME", "Roseed"), period = 30) {
682
- return new TOTP({
683
- issuer,
684
- label,
685
- algorithm: "SHA1",
686
- digits: 6,
687
- period,
688
- secret: Secret.fromBase32(secret)
689
- });
690
- }
535
+ //#region src/tls.ts
536
+ let devCertCache;
537
+ /**
538
+ * Generate (and cache) an in-memory self-signed TLS certificate for local HTTPS
539
+ * development. `selfsigned` is imported lazily so it's only loaded when secure
540
+ * dev mode is actually used.
541
+ *
542
+ * The certificate is not trusted by browsers (expect the usual self-signed
543
+ * warning); it exists only so the dev server can speak HTTPS.
544
+ *
545
+ * @param host The common name for the certificate (defaults to `localhost`).
546
+ */
547
+ const devTlsCredentials = async (host = "localhost") => {
548
+ if (devCertCache) return devCertCache;
549
+ const mod = await import("selfsigned");
550
+ const pems = (mod.default ?? mod).generate([{
551
+ name: "commonName",
552
+ value: host
553
+ }], {
554
+ days: 365,
555
+ keySize: 2048,
556
+ algorithm: "sha256"
557
+ });
558
+ devCertCache = {
559
+ key: pems.private,
560
+ cert: pems.cert
561
+ };
562
+ return devCertCache;
691
563
  };
692
- //#endregion
693
- //#region src/utils/helpers.ts
694
564
  /**
695
- * Determine the number of items to return per page based on the provided query parameters.
696
- *
697
- * @param query
698
- * @returns
565
+ * The machine's first non-internal IPv4 address its address on the local
566
+ * network — or `undefined` when only loopback interfaces are available.
699
567
  */
700
- const perPage = (query) => {
701
- const requestedPerPage = Number(query.limit ?? query.perPage ?? 15);
702
- return Number.isFinite(requestedPerPage) && requestedPerPage > 0 ? Math.min(requestedPerPage, 50) : 15;
568
+ const localNetworkAddress = () => {
569
+ for (const list of Object.values(networkInterfaces())) for (const net of list ?? []) if (net.family === "IPv4" && !net.internal) return net.address;
703
570
  };
704
- async function getModel(modelName) {
705
- const resolveModelExport = (module, modelName) => {
706
- if (!isModelModule(module)) return module;
707
- return module.default ?? module[modelName] ?? module;
708
- };
709
- const isModelModule = (value) => typeof value === "object" && value !== null;
710
- const modelPath = getUserConfig().paths?.models || "./src/models";
711
- const model = resolveModelExport(await importFile(path.join(path.isAbsolute(modelPath) ? modelPath : path.join(process.cwd(), modelPath), modelName)), path.basename(modelName, path.extname(modelName)));
712
- if (typeof model !== "function") throw new Error(`Model "${modelName}" not found`);
713
- return model;
714
- }
715
571
  //#endregion
716
- export { AppException, Encryption, ErrorHandler, Exception, Hash, Hook, Logger, RequestException, appUrl, bindGracefulShutdown, bootWithDetectedPort, config, createErrorPayload, env, getErrorLogger, getModel, getPrimaryError, getValidationErrors, importFile, isModelNotFoundError, isValidationError, loadPrototypes, logUnhandledError, nodeEnv, normalizeStatusCode, outputDir, perPage, renderError, serializeError, shouldHideStack, shouldLogError, toErrorShape };
717
-
718
- //# sourceMappingURL=index.js.map
572
+ export { AppException, CONFIG_KEY, ConfigLoader, Encryption, EnvLoader, ErrorHandler, Exception, Hash, Hook, Logger, Publisher, RequestException, abort, abortIf, appKey, appUrl, assertFound, bindGracefulShutdown, bootWithDetectedPort, config, configLoader, createErrorPayload, devTlsCredentials, discoverCommands, env, envLoader, getErrorLogger, getModel, getPrimaryError, getValidationErrors, importFile, initializeGlobalContext, interopDefault, isClass, isModelNotFoundError, isValidationError, loadPrototypes, localNetworkAddress, logUnhandledError, nodeEnv, normalizeStatusCode, outputDir, perPage, rebuildOutput, renderError, resolveRuntimeDir, resolveRuntimeModule, serializeError, shouldHideStack, shouldLogError, toErrorShape, toOutputPath };