@brianbuie/node-kit 0.0.0 → 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.
package/.dist/Jwt.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type JwtPayload, type SignOptions } from 'jsonwebtoken';
2
+ export declare class Jwt {
3
+ #private;
4
+ payload: JwtPayload;
5
+ options: SignOptions;
6
+ seconds: number;
7
+ key: string;
8
+ constructor({ payload, options, seconds, key, }: {
9
+ payload: JwtPayload;
10
+ options: SignOptions;
11
+ seconds: number;
12
+ key: string;
13
+ });
14
+ get now(): number;
15
+ get token(): string;
16
+ }
package/.dist/Jwt.js ADDED
@@ -0,0 +1,35 @@
1
+ import { default as jsonwebtoken } from 'jsonwebtoken';
2
+ import { merge } from 'lodash-es';
3
+ export class Jwt {
4
+ payload;
5
+ options;
6
+ seconds;
7
+ key;
8
+ #saved;
9
+ constructor({ payload, options, seconds, key, }) {
10
+ this.payload = payload;
11
+ this.options = options;
12
+ this.seconds = seconds;
13
+ this.key = key;
14
+ this.#createToken();
15
+ }
16
+ get now() {
17
+ return Math.floor(Date.now() / 1000);
18
+ }
19
+ get token() {
20
+ if (this.#saved && this.#saved.exp > this.now) {
21
+ return this.#saved.token;
22
+ }
23
+ return this.#createToken();
24
+ }
25
+ #createToken() {
26
+ const exp = this.now + this.seconds;
27
+ const payload = merge({
28
+ iat: this.now,
29
+ exp,
30
+ }, this.payload);
31
+ const token = jsonwebtoken.sign(payload, this.key, this.options);
32
+ this.#saved = { token, exp };
33
+ return token;
34
+ }
35
+ }
package/.dist/Log.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { type ChalkInstance } from 'chalk';
2
+ type Severity = 'DEFAULT' | 'DEBUG' | 'INFO' | 'NOTICE' | 'WARNING' | 'ERROR' | 'CRITICAL' | 'ALERT' | 'EMERGENCY';
3
+ type Options = {
4
+ severity: Severity;
5
+ color: ChalkInstance;
6
+ };
7
+ export declare class Log {
8
+ #private;
9
+ static isTest: boolean;
10
+ /**
11
+ * Handle first argument being a string or an object with a 'message' prop
12
+ * Also snapshots special objects (eg Error, Response) to keep props in later JSON.stringify output
13
+ */
14
+ static prepare(...input: unknown[]): {
15
+ message?: string;
16
+ details: unknown[];
17
+ };
18
+ /**
19
+ * Logs error details before throwing
20
+ */
21
+ static error(...input: unknown[]): void;
22
+ static warn(...input: unknown[]): {
23
+ message: string | undefined;
24
+ details: unknown[];
25
+ options: Options;
26
+ };
27
+ static notice(...input: unknown[]): {
28
+ message: string | undefined;
29
+ details: unknown[];
30
+ options: Options;
31
+ };
32
+ static info(...input: unknown[]): {
33
+ message: string | undefined;
34
+ details: unknown[];
35
+ options: Options;
36
+ };
37
+ static debug(...input: unknown[]): {
38
+ message: string | undefined;
39
+ details: unknown[];
40
+ options: Options;
41
+ } | undefined;
42
+ }
43
+ export {};
package/.dist/Log.js ADDED
@@ -0,0 +1,81 @@
1
+ import fs from 'fs';
2
+ import { inspect } from 'util';
3
+ import { isObjectLike } from 'lodash-es';
4
+ import chalk from 'chalk';
5
+ import { snapshot } from './snapshot.js';
6
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
7
+ export class Log {
8
+ static isTest = process.env.npm_package_name === packageJson.name && process.env.npm_lifecycle_event === 'test';
9
+ /**
10
+ * Gcloud parses JSON in stdout
11
+ */
12
+ static #toGcloud(entry) {
13
+ if (entry.details?.length === 1) {
14
+ console.log(JSON.stringify({ ...entry, details: entry.details[0] }));
15
+ }
16
+ else {
17
+ console.log(JSON.stringify(entry));
18
+ }
19
+ }
20
+ /**
21
+ * Includes colors and better inspection for logging during dev
22
+ */
23
+ static #toConsole(entry, color) {
24
+ if (entry.message)
25
+ console.log(color(`[${entry.severity}] ${entry.message}`));
26
+ entry.details?.forEach(detail => {
27
+ console.log(inspect(detail, { depth: 10, breakLength: 100, compact: true, colors: true }));
28
+ });
29
+ }
30
+ static #log(options, ...input) {
31
+ const { message, details } = this.prepare(...input);
32
+ // https://cloud.google.com/run/docs/container-contract#env-vars
33
+ const isGcloud = process.env.K_SERVICE !== undefined || process.env.CLOUD_RUN_JOB !== undefined;
34
+ if (isGcloud) {
35
+ this.#toGcloud({ message, severity: options.severity, details });
36
+ return { message, details, options };
37
+ }
38
+ // Hide output while testing this package
39
+ if (!this.isTest) {
40
+ this.#toConsole({ message, severity: options.severity, details }, options.color);
41
+ }
42
+ return { message, details, options };
43
+ }
44
+ /**
45
+ * Handle first argument being a string or an object with a 'message' prop
46
+ * Also snapshots special objects (eg Error, Response) to keep props in later JSON.stringify output
47
+ */
48
+ static prepare(...input) {
49
+ let [first, ...rest] = input.map(snapshot);
50
+ if (typeof first === 'string')
51
+ return { message: first, details: rest };
52
+ // @ts-ignore
53
+ if (isObjectLike(first) && typeof first['message'] === 'string') {
54
+ const { message, ...firstDetails } = first;
55
+ return { message, details: [firstDetails, ...rest] };
56
+ }
57
+ return { details: input };
58
+ }
59
+ /**
60
+ * Logs error details before throwing
61
+ */
62
+ static error(...input) {
63
+ const { message } = this.#log({ severity: 'ERROR', color: chalk.red }, ...input);
64
+ throw new Error(message);
65
+ }
66
+ static warn(...input) {
67
+ return this.#log({ severity: 'WARNING', color: chalk.yellow }, ...input);
68
+ }
69
+ static notice(...input) {
70
+ return this.#log({ severity: 'NOTICE', color: chalk.cyan }, ...input);
71
+ }
72
+ static info(...input) {
73
+ return this.#log({ severity: 'INFO', color: chalk.white }, ...input);
74
+ }
75
+ static debug(...input) {
76
+ const debugging = process.argv.some(arg => arg.includes('--debug')) || process.env.DEBUG !== undefined;
77
+ if (debugging || process.env.NODE_ENV !== 'production') {
78
+ return this.#log({ severity: 'DEBUG', color: chalk.gray }, ...input);
79
+ }
80
+ }
81
+ }
package/.dist/_index.d.ts CHANGED
@@ -1 +1,4 @@
1
1
  export { Fetcher, type Route, Query, FetchOptions } from './Fetcher.js';
2
+ export { Jwt } from './Jwt.js';
3
+ export { snapshot } from './snapshot.js';
4
+ export { Log } from './Log.js';
package/.dist/_index.js CHANGED
@@ -1 +1,4 @@
1
1
  export { Fetcher } from './Fetcher.js';
2
+ export { Jwt } from './Jwt.js';
3
+ export { snapshot } from './snapshot.js';
4
+ export { Log } from './Log.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Allows special objects (Error, Headers, Set) to be included in JSON.stringify output
3
+ * functions are removed
4
+ */
5
+ export declare function snapshot(i: unknown): any;
@@ -0,0 +1,33 @@
1
+ import { isObjectLike } from 'lodash-es';
2
+ /**
3
+ * Allows special objects (Error, Headers, Set) to be included in JSON.stringify output
4
+ * functions are removed
5
+ */
6
+ export function snapshot(i) {
7
+ if (Array.isArray(i))
8
+ return i.map(snapshot);
9
+ if (typeof i === 'function')
10
+ return undefined;
11
+ if (!isObjectLike(i))
12
+ return i;
13
+ let output = {};
14
+ // @ts-ignore If it has an 'entries' function, use that for looping (eg. Set, Map, Headers)
15
+ if (typeof i.entries === 'function') {
16
+ // @ts-ignore
17
+ for (let [k, v] of i.entries()) {
18
+ output[k] = snapshot(v);
19
+ }
20
+ return output;
21
+ }
22
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Enumerability_and_ownership_of_properties
23
+ // Get Enumerable, inherited properties
24
+ const obj = i;
25
+ for (let key in obj) {
26
+ output[key] = snapshot(obj[key]);
27
+ }
28
+ // Get Non-enumberable, own properties
29
+ Object.getOwnPropertyNames(obj).forEach(key => {
30
+ output[key] = snapshot(obj[key]);
31
+ });
32
+ return output;
33
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@brianbuie/node-kit",
3
- "version": "0.0.0",
3
+ "version": "0.1.0",
4
4
  "license": "ISC",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/brianbuie/node-kit.git"
8
8
  },
9
9
  "scripts": {
10
- "test": "tsc && node --test \".dist/**/*.test.js\"",
10
+ "test": "tsc && node --test \".dist/**/*.test.js\" --quiet",
11
11
  "preversion": "npm test",
12
12
  "postversion": "git push --follow-tags"
13
13
  },
@@ -25,8 +25,11 @@
25
25
  "node": ">=24"
26
26
  },
27
27
  "dependencies": {
28
+ "@types/jsonwebtoken": "^9.0.10",
28
29
  "@types/lodash-es": "^4.17.12",
29
30
  "@types/node": "^24.9.1",
31
+ "chalk": "^5.6.2",
32
+ "jsonwebtoken": "^9.0.2",
30
33
  "lodash-es": "^4.17.21"
31
34
  },
32
35
  "devDependencies": {
@@ -0,0 +1,22 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import jsonwebtoken from 'jsonwebtoken';
4
+ import { Jwt } from './Jwt.js';
5
+
6
+ describe('Jwt', () => {
7
+ it('should create a valid JWT', () => {
8
+ const key = 'test';
9
+ const jwt = new Jwt({
10
+ payload: {
11
+ example: 'value',
12
+ },
13
+ options: {
14
+ algorithm: 'HS256',
15
+ },
16
+ seconds: 60,
17
+ key,
18
+ });
19
+ const result = jsonwebtoken.verify(jwt.token, key);
20
+ assert(typeof result !== 'string' && result.example === 'value');
21
+ });
22
+ });
package/src/Jwt.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { default as jsonwebtoken, type JwtPayload, type SignOptions } from 'jsonwebtoken';
2
+ import { merge } from 'lodash-es';
3
+
4
+ export class Jwt {
5
+ payload: JwtPayload;
6
+ options: SignOptions;
7
+ seconds: number;
8
+ key: string;
9
+ #saved?: {
10
+ exp: number;
11
+ token: string;
12
+ };
13
+
14
+ constructor({
15
+ payload,
16
+ options,
17
+ seconds,
18
+ key,
19
+ }: {
20
+ payload: JwtPayload;
21
+ options: SignOptions;
22
+ seconds: number;
23
+ key: string;
24
+ }) {
25
+ this.payload = payload;
26
+ this.options = options;
27
+ this.seconds = seconds;
28
+ this.key = key;
29
+ this.#createToken();
30
+ }
31
+
32
+ get now() {
33
+ return Math.floor(Date.now() / 1000);
34
+ }
35
+
36
+ get token() {
37
+ if (this.#saved && this.#saved.exp > this.now) {
38
+ return this.#saved.token;
39
+ }
40
+ return this.#createToken();
41
+ }
42
+
43
+ #createToken() {
44
+ const exp = this.now + this.seconds;
45
+ const payload: JwtPayload = merge(
46
+ {
47
+ iat: this.now,
48
+ exp,
49
+ },
50
+ this.payload
51
+ );
52
+ const token = jsonwebtoken.sign(payload, this.key, this.options);
53
+ this.#saved = { token, exp };
54
+ return token;
55
+ }
56
+ }
@@ -0,0 +1,28 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { Log } from './Log.js';
4
+
5
+ describe('Log', () => {
6
+ it('should throw error', () => {
7
+ try {
8
+ Log.error('Test error');
9
+ } catch (e) {
10
+ return;
11
+ }
12
+ assert(false, 'Did not throw error');
13
+ });
14
+
15
+ it('should recognize this is a test', () => {
16
+ assert(Log.isTest);
17
+ });
18
+
19
+ it('should use first argument as message when string', () => {
20
+ const result = Log.prepare('test', { something: 'else' });
21
+ assert(result.message === 'test');
22
+ });
23
+
24
+ it('should use message prop when provided', () => {
25
+ const result = Log.prepare({ message: 'test', something: 'else' });
26
+ assert(result.message === 'test');
27
+ });
28
+ });
package/src/Log.ts ADDED
@@ -0,0 +1,102 @@
1
+ import fs from 'fs';
2
+ import { inspect } from 'util';
3
+ import { isObjectLike } from 'lodash-es';
4
+ import chalk, { type ChalkInstance } from 'chalk';
5
+ import { snapshot } from './snapshot.js';
6
+
7
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
8
+
9
+ type Severity = 'DEFAULT' | 'DEBUG' | 'INFO' | 'NOTICE' | 'WARNING' | 'ERROR' | 'CRITICAL' | 'ALERT' | 'EMERGENCY';
10
+
11
+ type Options = {
12
+ severity: Severity;
13
+ color: ChalkInstance;
14
+ };
15
+
16
+ type Entry = {
17
+ message?: string;
18
+ severity: Severity;
19
+ details?: unknown[];
20
+ };
21
+
22
+ export class Log {
23
+ static isTest = process.env.npm_package_name === packageJson.name && process.env.npm_lifecycle_event === 'test';
24
+
25
+ /**
26
+ * Gcloud parses JSON in stdout
27
+ */
28
+ static #toGcloud(entry: Entry) {
29
+ if (entry.details?.length === 1) {
30
+ console.log(JSON.stringify({ ...entry, details: entry.details[0] }));
31
+ } else {
32
+ console.log(JSON.stringify(entry));
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Includes colors and better inspection for logging during dev
38
+ */
39
+ static #toConsole(entry: Entry, color: ChalkInstance) {
40
+ if (entry.message) console.log(color(`[${entry.severity}] ${entry.message}`));
41
+ entry.details?.forEach(detail => {
42
+ console.log(inspect(detail, { depth: 10, breakLength: 100, compact: true, colors: true }));
43
+ });
44
+ }
45
+
46
+ static #log(options: Options, ...input: unknown[]) {
47
+ const { message, details } = this.prepare(...input);
48
+ // https://cloud.google.com/run/docs/container-contract#env-vars
49
+ const isGcloud = process.env.K_SERVICE !== undefined || process.env.CLOUD_RUN_JOB !== undefined;
50
+ if (isGcloud) {
51
+ this.#toGcloud({ message, severity: options.severity, details });
52
+ return { message, details, options };
53
+ }
54
+ // Hide output while testing this package
55
+ if (!this.isTest) {
56
+ this.#toConsole({ message, severity: options.severity, details }, options.color);
57
+ }
58
+ return { message, details, options };
59
+ }
60
+
61
+ /**
62
+ * Handle first argument being a string or an object with a 'message' prop
63
+ * Also snapshots special objects (eg Error, Response) to keep props in later JSON.stringify output
64
+ */
65
+ static prepare(...input: unknown[]): { message?: string; details: unknown[] } {
66
+ let [first, ...rest] = input.map(snapshot);
67
+ if (typeof first === 'string') return { message: first, details: rest };
68
+ // @ts-ignore
69
+ if (isObjectLike(first) && typeof first['message'] === 'string') {
70
+ const { message, ...firstDetails } = first as { message: string };
71
+ return { message, details: [firstDetails, ...rest] };
72
+ }
73
+ return { details: input };
74
+ }
75
+
76
+ /**
77
+ * Logs error details before throwing
78
+ */
79
+ static error(...input: unknown[]) {
80
+ const { message } = this.#log({ severity: 'ERROR', color: chalk.red }, ...input);
81
+ throw new Error(message);
82
+ }
83
+
84
+ static warn(...input: unknown[]) {
85
+ return this.#log({ severity: 'WARNING', color: chalk.yellow }, ...input);
86
+ }
87
+
88
+ static notice(...input: unknown[]) {
89
+ return this.#log({ severity: 'NOTICE', color: chalk.cyan }, ...input);
90
+ }
91
+
92
+ static info(...input: unknown[]) {
93
+ return this.#log({ severity: 'INFO', color: chalk.white }, ...input);
94
+ }
95
+
96
+ static debug(...input: unknown[]) {
97
+ const debugging = process.argv.some(arg => arg.includes('--debug')) || process.env.DEBUG !== undefined;
98
+ if (debugging || process.env.NODE_ENV !== 'production') {
99
+ return this.#log({ severity: 'DEBUG', color: chalk.gray }, ...input);
100
+ }
101
+ }
102
+ }
package/src/_index.ts CHANGED
@@ -1 +1,4 @@
1
1
  export { Fetcher, type Route, Query, FetchOptions } from './Fetcher.js';
2
+ export { Jwt } from './Jwt.js';
3
+ export { snapshot } from './snapshot.js';
4
+ export { Log } from './Log.js';
@@ -0,0 +1,35 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { snapshot } from './snapshot.js';
4
+
5
+ describe('snapshot', () => {
6
+ it('should capture Error details', () => {
7
+ try {
8
+ throw new Error('Test Error');
9
+ } catch (e) {
10
+ const result = snapshot(e) as Error;
11
+ assert(result.message !== undefined);
12
+ assert(result.stack !== undefined);
13
+ }
14
+ });
15
+
16
+ it('should capture Map values', () => {
17
+ const test = new Map<string, string>();
18
+ test.set('key', 'value');
19
+ const shot = JSON.parse(JSON.stringify(snapshot(test))) as Record<string, string>;
20
+ assert(shot.key === 'value');
21
+ });
22
+
23
+ it('should capture Request values', () => {
24
+ const test = new Request('https://www.google.com', { headers: { example: 'value' } });
25
+ const shot = JSON.parse(JSON.stringify(snapshot(test))) as Record<string, any>;
26
+ assert(shot.url !== undefined);
27
+ assert(shot.headers.example === 'value');
28
+ });
29
+
30
+ it('should ignore functions', () => {
31
+ const test = { func: () => null };
32
+ const shot = snapshot(test) as Record<string, any>;
33
+ assert(shot.func === undefined);
34
+ });
35
+ });
@@ -0,0 +1,36 @@
1
+ import { isObjectLike } from 'lodash-es';
2
+
3
+ /**
4
+ * Allows special objects (Error, Headers, Set) to be included in JSON.stringify output
5
+ * functions are removed
6
+ */
7
+ export function snapshot(i: unknown): any {
8
+ if (Array.isArray(i)) return i.map(snapshot);
9
+ if (typeof i === 'function') return undefined;
10
+ if (!isObjectLike(i)) return i;
11
+
12
+ let output: Record<string, any> = {};
13
+ // @ts-ignore If it has an 'entries' function, use that for looping (eg. Set, Map, Headers)
14
+ if (typeof i.entries === 'function') {
15
+ // @ts-ignore
16
+ for (let [k, v] of i.entries()) {
17
+ output[k] = snapshot(v);
18
+ }
19
+ return output;
20
+ }
21
+
22
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Enumerability_and_ownership_of_properties
23
+
24
+ // Get Enumerable, inherited properties
25
+ const obj: Record<string, any> = i!;
26
+ for (let key in obj) {
27
+ output[key] = snapshot(obj[key]);
28
+ }
29
+
30
+ // Get Non-enumberable, own properties
31
+ Object.getOwnPropertyNames(obj).forEach(key => {
32
+ output[key] = snapshot(obj[key]);
33
+ });
34
+
35
+ return output;
36
+ }