@arkstack/common 0.14.18 → 0.14.20

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,437 @@
1
+ import { a as env, f as resolveRuntimeModule, o as importFile, t as appKey } from "./system-DUaI4u99.js";
2
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
3
+ import { Arkstack } from "@arkstack/contract";
4
+ import path from "node:path";
5
+ import { Secret, TOTP } from "otpauth";
6
+ import { compare, genSalt, hash } from "bcryptjs";
7
+ import { getUserConfig } from "arkormx";
8
+ //#region src/utils/encryption.ts
9
+ var Encryption = class {
10
+ static algorithm = "aes-256-gcm";
11
+ static getKey() {
12
+ const secret = appKey("TWO_FACTOR_ENCRYPTION_KEY");
13
+ if (!secret) throw new Error("APP_KEY is required to use two-factor authentication. Run `ark key:generate`.");
14
+ return createHash("sha256").update(secret).digest();
15
+ }
16
+ static encrypt(value) {
17
+ const iv = randomBytes(12);
18
+ const cipher = createCipheriv(this.algorithm, this.getKey(), iv);
19
+ const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
20
+ return [
21
+ iv,
22
+ cipher.getAuthTag(),
23
+ ciphertext
24
+ ].map((part) => part.toString("base64url")).join(":");
25
+ }
26
+ static decrypt(payload) {
27
+ const [iv, authTag, ciphertext] = payload.split(":");
28
+ if (!iv || !authTag || !ciphertext) throw new Error("Invalid encrypted payload format");
29
+ const decipher = createDecipheriv(this.algorithm, this.getKey(), Buffer.from(iv, "base64url"));
30
+ decipher.setAuthTag(Buffer.from(authTag, "base64url"));
31
+ return Buffer.concat([decipher.update(Buffer.from(ciphertext, "base64url")), decipher.final()]).toString("utf8");
32
+ }
33
+ };
34
+ //#endregion
35
+ //#region src/utils/hash.ts
36
+ var Hash = class {
37
+ /**
38
+ * Hash a value using bcrypt
39
+ *
40
+ * @param value
41
+ * @returns
42
+ */
43
+ static async make(value) {
44
+ return await hash(value, await genSalt(10));
45
+ }
46
+ /**
47
+ * Verify a value against a hashed value
48
+ *
49
+ * @param value
50
+ * @param hashedValue
51
+ * @returns
52
+ */
53
+ static async verify(value, hashedValue) {
54
+ return await compare(value, hashedValue);
55
+ }
56
+ /**
57
+ * Generate a one-time password (OTP) using TOTP algorithm
58
+ *
59
+ * @param digits The number of digits for the OTP, default is 6.
60
+ * @param label A label to identify the OTP, can be an email or phone number.
61
+ * @param period Interval of time for which a token is valid, in seconds.
62
+ * @returns
63
+ */
64
+ static otp(digits = 6, label = "Alice", period = 30) {
65
+ return new TOTP({
66
+ label,
67
+ digits,
68
+ issuer: env("APP_NAME", "Roseed"),
69
+ algorithm: "SHA1",
70
+ period,
71
+ secret: "US3WHSG7X5KAPV27VANWKQHF3SH3HULL"
72
+ });
73
+ }
74
+ static totp(secret, label, issuer = env("APP_NAME", "Roseed"), period = 30) {
75
+ return new TOTP({
76
+ issuer,
77
+ label,
78
+ algorithm: "SHA1",
79
+ digits: 6,
80
+ period,
81
+ secret: Secret.fromBase32(secret)
82
+ });
83
+ }
84
+ };
85
+ //#endregion
86
+ //#region src/Exceptions/Exception.ts
87
+ var Exception = class extends Error {
88
+ name;
89
+ constructor(message, options) {
90
+ super(message, options);
91
+ this.name = "Exception";
92
+ }
93
+ };
94
+ //#endregion
95
+ //#region src/Exceptions/AppException.ts
96
+ var AppException = class extends Exception {
97
+ errors = void 0;
98
+ statusCode;
99
+ constructor(message, statusCode = 400, options) {
100
+ super(message, options);
101
+ this.statusCode = statusCode;
102
+ }
103
+ };
104
+ //#endregion
105
+ //#region src/Exceptions/RequestException.ts
106
+ var RequestException = class RequestException extends AppException {
107
+ statusCode;
108
+ constructor(message, statusCode = 400, options) {
109
+ super(message, statusCode, options);
110
+ this.statusCode = statusCode;
111
+ }
112
+ /**
113
+ * Asserts that a value is not null or undefined.
114
+ *
115
+ * @param value
116
+ * @param message
117
+ * @param code
118
+ * @throws {RequestException} Throws if the value is null or undefined.
119
+ */
120
+ static assertFound(value, message, code = 404) {
121
+ if (!value) throw new RequestException(message, code);
122
+ }
123
+ /**
124
+ * Asserts that a value is not null or undefined.
125
+ *
126
+ * @param value
127
+ * @param message
128
+ * @param code
129
+ * @throws {RequestException} Throws if the value is null or undefined.
130
+ * @deprecated Use assertFound instead
131
+ */
132
+ static assertNotEmpty(value, message, code = 404) {
133
+ return this.assertFound(value, message, code);
134
+ }
135
+ /**
136
+ * Asserts that a boolean condition is true.
137
+ *
138
+ * @param boolean
139
+ * @param message
140
+ * @param code
141
+ * @throws {RequestException} Throws if the boolean condition is true.
142
+ */
143
+ static abortIf(boolean, message, code) {
144
+ if (boolean) throw new RequestException(message, code);
145
+ }
146
+ };
147
+ //#endregion
148
+ //#region src/utils/helpers.ts
149
+ /**
150
+ * Checks and asserts if target is a class
151
+ *
152
+ * @param target
153
+ * @returns
154
+ */
155
+ const isClass = (target) => {
156
+ return typeof target === "function" && /^class\s/.test(Function.prototype.toString.call(target));
157
+ };
158
+ /**
159
+ * Determine the number of items to return per page based on the provided query parameters.
160
+ *
161
+ * @param query
162
+ * @returns
163
+ */
164
+ const perPage = (query) => {
165
+ const requestedPerPage = Number(query.limit ?? query.perPage ?? 15);
166
+ return Number.isFinite(requestedPerPage) && requestedPerPage > 0 ? Math.min(requestedPerPage, 50) : 15;
167
+ };
168
+ async function getModel(modelName) {
169
+ const resolveModelExport = (module, modelName) => {
170
+ if (!isModelModule(module)) return module;
171
+ return module.default ?? module[modelName] ?? module;
172
+ };
173
+ const isModelModule = (value) => typeof value === "object" && value !== null;
174
+ const modelPath = getUserConfig().paths?.models || "./src/models";
175
+ const model = resolveModelExport(await importFile(resolveRuntimeModule(path.join(path.isAbsolute(modelPath) ? modelPath : path.join(Arkstack.rootDir(), modelPath), modelName))), path.basename(modelName, path.extname(modelName)));
176
+ if (typeof model !== "function") throw new Error(`Model "${modelName}" not found`);
177
+ return model;
178
+ }
179
+ const initializeGlobalContext = async ({ Request, Response, Session } = {}) => {
180
+ try {
181
+ const { Request: Req, Response: Res, Session: Ses } = await import("@arkstack/http");
182
+ Session ??= new Ses();
183
+ Request ??= new Req();
184
+ Response ??= new Res();
185
+ } catch {
186
+ Session ??= new class {}();
187
+ Request ??= new class {}();
188
+ Response ??= new class {}();
189
+ }
190
+ globalThis.session ??= () => Session;
191
+ globalThis.request ??= () => Request;
192
+ globalThis.response ??= () => Response;
193
+ };
194
+ /**
195
+ * Thows to abort the current request
196
+ *
197
+ * @param message
198
+ * @param code
199
+ * @throws {RequestException}
200
+ */
201
+ const abort = (message = "Request Aborted", code = 404) => {
202
+ RequestException.abortIf(true, message, code);
203
+ };
204
+ /**
205
+ * Asserts that a boolean condition is true.
206
+ *
207
+ * @param boolean
208
+ * @param message
209
+ * @param code
210
+ * @throws {RequestException} Throws if the boolean condition is true.
211
+ */
212
+ const abortIf = (boolean, message = "Request Aborted", code = 404) => {
213
+ RequestException.abortIf(boolean, message, code);
214
+ };
215
+ /**
216
+ * Asserts that a value is not null or undefined.
217
+ *
218
+ * @param value
219
+ * @param message
220
+ * @param code
221
+ * @throws {RequestException} Throws if the value is null or undefined.
222
+ */
223
+ const assertFound = (value, message, code = 404) => {
224
+ if (!value) throw new RequestException(message, code);
225
+ };
226
+ //#endregion
227
+ //#region src/utils/traits.ts
228
+ /**
229
+ * CRC32 implementation in TypeScript, adapted from https://stackoverflow.com/a/18639999
230
+ * Note: This implementation is not cryptographically secure and is only used for generating
231
+ * unique identifiers for traits based on their factory function's string representation.
232
+ */
233
+ const crcTable = [];
234
+ for (let n = 0; n < 256; n++) {
235
+ let c = n;
236
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
237
+ crcTable[n] = c;
238
+ }
239
+ const crc32 = (str) => {
240
+ let crc = -1;
241
+ for (let i = 0; i < str.length; i++) crc = crc >>> 8 ^ crcTable[(crc ^ str.charCodeAt(i)) & 255];
242
+ return (crc ^ -1) >>> 0;
243
+ };
244
+ const isCons = (fn) => typeof fn === "function" && !!fn.prototype && !!fn.prototype.constructor;
245
+ const isArkormModelInstance = (value) => typeof value === "object" && value !== null && typeof value.constructor === "function" && typeof value.getAttribute === "function" && typeof value.setAttribute === "function";
246
+ const isTypeFactory = (fn) => typeof fn === "function" && !fn.prototype && fn.length === 0;
247
+ /**
248
+ * API: generate trait (technical implementation)
249
+ *
250
+ * @param args
251
+ */
252
+ function trait(...args) {
253
+ const factory = args.length === 2 ? args[1] : args[0];
254
+ const superTraits = args.length === 2 ? args[0] : void 0;
255
+ return {
256
+ id: crc32(factory.toString()),
257
+ symbol: Symbol("trait"),
258
+ factory,
259
+ superTraits
260
+ };
261
+ }
262
+ /**
263
+ * utility function: add an additional invisible property to an object
264
+ *
265
+ * @param cons
266
+ * @param field
267
+ * @param value
268
+ * @returns
269
+ */
270
+ const extendProperties = (cons, field, value) => Object.defineProperty(cons, field, {
271
+ value,
272
+ enumerable: false,
273
+ writable: false
274
+ });
275
+ const traitMethodRegistry = Symbol("trait-method-registry");
276
+ const cloneMethodRegistry = (target) => {
277
+ const registry = target[traitMethodRegistry];
278
+ return new Map([...registry?.entries() ?? []].map(([name, methods]) => [name, [...methods]]));
279
+ };
280
+ const registerMethodScope = (target, base, ignored) => {
281
+ const registry = cloneMethodRegistry(base);
282
+ for (const name of Reflect.ownKeys(target)) {
283
+ if (ignored.has(name)) continue;
284
+ const method = Object.getOwnPropertyDescriptor(target, name)?.value;
285
+ if (typeof method !== "function") continue;
286
+ const methods = registry.get(name);
287
+ const previous = base?.[name];
288
+ if (methods) {
289
+ if (methods.at(-1) !== method) methods.push(method);
290
+ registry.set(name, methods);
291
+ } else if (typeof previous === "function" && previous !== method) registry.set(name, [previous, method]);
292
+ }
293
+ Object.defineProperty(target, traitMethodRegistry, {
294
+ configurable: false,
295
+ enumerable: false,
296
+ value: registry,
297
+ writable: false
298
+ });
299
+ };
300
+ /**
301
+ * Registers conflicting trait methods
302
+ *
303
+ * @param classInstance
304
+ * @param baseClass
305
+ */
306
+ const registerTraitMethods = (classInstance, baseClass) => {
307
+ registerMethodScope(classInstance.prototype, baseClass.prototype, new Set(["constructor"]));
308
+ registerMethodScope(classInstance, baseClass, new Set([
309
+ "length",
310
+ "name",
311
+ "prototype",
312
+ "arguments",
313
+ "caller"
314
+ ]));
315
+ };
316
+ /**
317
+ * Return every trait implementation for a method, bound to the supplied
318
+ * instance or class. Methods are ordered from the base implementation to the
319
+ * currently active trait implementation.
320
+ *
321
+ * @param target
322
+ * @param name
323
+ * @returns
324
+ */
325
+ const getTraitMethods = (target, name) => {
326
+ return (((typeof target === "function" ? target : Object.getPrototypeOf(target))?.[traitMethodRegistry])?.get(name) ?? []).map((method) => method.bind(target));
327
+ };
328
+ /**
329
+ * Invoke every trait implementation for a method in registration order.
330
+ *
331
+ * @param target
332
+ * @param name
333
+ * @param args
334
+ * @returns
335
+ */
336
+ const callTraitMethods = (target, name, ...args) => {
337
+ const methods = getTraitMethods(target, name);
338
+ if (methods.length === 0) {
339
+ console.warn(`No conflicting trait methods found for "${String(name)}".`);
340
+ return [];
341
+ }
342
+ return methods.map((method) => method(...args));
343
+ };
344
+ /**
345
+ * utility function: get raw trait
346
+ *
347
+ * @param x
348
+ * @returns
349
+ */
350
+ const rawTrait = (x) => isTypeFactory(x) ? x() : x;
351
+ /**
352
+ * utility function: derive a trait
353
+ *
354
+ * @param trait$
355
+ * @param baseClass
356
+ * @param derived
357
+ * @returns
358
+ */
359
+ const deriveTrait = (trait$, baseClass, derived) => {
360
+ const trait = rawTrait(trait$);
361
+ if (trait === void 0 || trait === null || typeof trait.id !== "number") throw new Error("use(): received an undefined or invalid trait. This usually means a circular import — the trait module had not finished initializing when use() ran. Avoid importing models at the top level of trait modules, or break the import cycle.");
362
+ let classInstance = baseClass;
363
+ if (!derived.has(trait.id)) {
364
+ derived.set(trait.id, true);
365
+ if (trait.superTraits !== void 0) for (const superTrait of reverseTraitList(trait.superTraits)) classInstance = deriveTrait(superTrait, classInstance, derived);
366
+ const base = classInstance;
367
+ classInstance = trait.factory(classInstance);
368
+ registerTraitMethods(classInstance, base);
369
+ extendProperties(classInstance, "id", crc32(trait.factory.toString()));
370
+ extendProperties(classInstance, trait.symbol, true);
371
+ }
372
+ return classInstance;
373
+ };
374
+ /**
375
+ * utility function: get reversed trait list
376
+ *
377
+ * @param traits
378
+ * @returns
379
+ */
380
+ const reverseTraitList = (traits) => traits.slice().reverse();
381
+ function use(...args) {
382
+ const withMethodHelpers = args[0] === true;
383
+ const traits = withMethodHelpers ? args.slice(1) : args;
384
+ if (traits.length === 0) throw new Error("invalid number of parameters (expected one or more traits)");
385
+ let classInstance;
386
+ let lot;
387
+ const last = traits[traits.length - 1];
388
+ if (isCons(last) && !isTypeFactory(last)) {
389
+ classInstance = last;
390
+ lot = traits.slice(0, -1);
391
+ } else if (isArkormModelInstance(last)) {
392
+ classInstance = last.constructor;
393
+ lot = traits.slice(0, -1);
394
+ } else {
395
+ classInstance = class ROOT {};
396
+ lot = traits;
397
+ }
398
+ const derived = /* @__PURE__ */ new Map();
399
+ for (const trait of reverseTraitList(lot)) classInstance = deriveTrait(trait, classInstance, derived);
400
+ if (withMethodHelpers) classInstance = class TraitMethodEnabled extends classInstance {
401
+ getTraitMethods(name) {
402
+ return getTraitMethods(this, name);
403
+ }
404
+ callTraitMethods(name, ...args) {
405
+ return callTraitMethods(this, name, ...args);
406
+ }
407
+ static getTraitMethods(name) {
408
+ return getTraitMethods(this, name);
409
+ }
410
+ static callTraitMethods(name, ...args) {
411
+ return callTraitMethods(this, name, ...args);
412
+ }
413
+ };
414
+ return classInstance;
415
+ }
416
+ /**
417
+ * API: type guard for checking whether class instance is derived from a trait
418
+ *
419
+ * @param instance
420
+ * @param trait
421
+ * @returns
422
+ */
423
+ function uses(instance, trait) {
424
+ if (typeof instance !== "object" || instance === null) return false;
425
+ let obj = instance;
426
+ if (isCons(trait) && !isTypeFactory(trait)) return instance instanceof trait;
427
+ const idTrait = (isTypeFactory(trait) ? trait() : trait)["id"];
428
+ while (obj) {
429
+ if (Object.hasOwn(obj, "constructor")) {
430
+ if ((obj.constructor["id"] ?? 0) === idTrait) return true;
431
+ }
432
+ obj = Object.getPrototypeOf(obj);
433
+ }
434
+ return false;
435
+ }
436
+ //#endregion
437
+ export { Hash as _, use as a, abortIf as c, initializeGlobalContext as d, isClass as f, Exception as g, AppException as h, trait as i, assertFound as l, RequestException as m, crc32 as n, uses as o, perPage as p, getTraitMethods as r, abort as s, callTraitMethods as t, getModel as u, Encryption as v };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkstack/common",
3
- "version": "0.14.18",
3
+ "version": "0.14.20",
4
4
  "type": "module",
5
5
  "description": "Core utilities, primitives, and shared infrastructure for the Arkstack ecosystem.",
6
6
  "homepage": "https://arkstack.toneflix.net",
@@ -28,22 +28,28 @@
28
28
  },
29
29
  "exports": {
30
30
  ".": "./dist/index.js",
31
+ "./faker": "./dist/faker.js",
31
32
  "./utils": "./dist/utils/index.js",
32
33
  "./package.json": "./package.json"
33
34
  },
34
35
  "dependencies": {
35
- "jiti": "^2.7.0",
36
+ "@pictwo/faker": "^1.1.0",
36
37
  "bcryptjs": "^3.0.3",
37
38
  "chalk": "^5.6.2",
38
39
  "detect-port": "^2.1.0",
40
+ "dotenv": "^17.4.2",
41
+ "jiti": "^2.7.0",
39
42
  "otpauth": "^9.5.1",
40
43
  "pino": "^10.3.1"
41
44
  },
42
45
  "peerDependencies": {
43
46
  "@h3ravel/support": "^2.2.0",
44
47
  "arkormx": "^2.10.1",
45
- "@arkstack/foundry": "^0.14.18",
46
- "@arkstack/contract": "^0.14.18"
48
+ "@arkstack/contract": "^0.14.20",
49
+ "@arkstack/foundry": "^0.14.20"
50
+ },
51
+ "optionalDependencies": {
52
+ "@faker-js/faker": "^10.4.0"
47
53
  },
48
54
  "scripts": {
49
55
  "build": "tsdown --config-loader unrun",