@routstr/cocod 0.0.16

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,82 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import { StructuredLogger } from "./logger";
7
+
8
+ const tempDirs: string[] = [];
9
+
10
+ afterEach(async () => {
11
+ while (tempDirs.length > 0) {
12
+ const dir = tempDirs.pop();
13
+ if (!dir) {
14
+ continue;
15
+ }
16
+
17
+ await rm(dir, { force: true, recursive: true });
18
+ }
19
+ });
20
+
21
+ describe("StructuredLogger", () => {
22
+ test("writes structured log entries and redacts sensitive fields", async () => {
23
+ const dir = await mkdtemp(join(tmpdir(), "cocod-logger-"));
24
+ tempDirs.push(dir);
25
+
26
+ const logFile = join(dir, "daemon.log");
27
+ const logger = new StructuredLogger({
28
+ logFile,
29
+ mirrorToConsole: false,
30
+ bindings: { component: "daemon" },
31
+ });
32
+
33
+ logger.info("wallet.unlock_requested", {
34
+ mintUrl: "https://mint.example.com/Bitcoin",
35
+ passphrase: "secret-passphrase",
36
+ });
37
+ await logger.flush();
38
+
39
+ const content = await readFile(logFile, "utf8");
40
+ const entry = JSON.parse(content.trim()) as Record<string, unknown>;
41
+
42
+ expect(entry.event).toBe("wallet.unlock_requested");
43
+ expect(entry.level).toBe("info");
44
+ expect(entry.component).toBe("daemon");
45
+ expect(entry.passphrase).toBe("[REDACTED]");
46
+ expect(entry.mintUrl).toBe("https://mint.example.com/Bitcoin");
47
+ });
48
+
49
+ test("rotates files and respects retention count", async () => {
50
+ const dir = await mkdtemp(join(tmpdir(), "cocod-logger-"));
51
+ tempDirs.push(dir);
52
+
53
+ const logFile = join(dir, "daemon.log");
54
+ const logger = new StructuredLogger({
55
+ logFile,
56
+ maxBytes: 250,
57
+ maxFiles: 2,
58
+ mirrorToConsole: false,
59
+ });
60
+
61
+ for (let index = 0; index < 12; index += 1) {
62
+ logger.info("rotation.test", {
63
+ index,
64
+ payload: "x".repeat(80),
65
+ });
66
+ }
67
+ await logger.flush();
68
+
69
+ const current = await stat(logFile);
70
+ const rotatedOne = await stat(`${logFile}.1`);
71
+ const rotatedTwo = await stat(`${logFile}.2`);
72
+
73
+ expect(current.size).toBeGreaterThan(0);
74
+ expect(rotatedOne.size).toBeGreaterThan(0);
75
+ expect(rotatedTwo.size).toBeGreaterThan(0);
76
+
77
+ const currentContent = await readFile(logFile, "utf8");
78
+ expect(currentContent).toContain('"index":11');
79
+
80
+ await expect(stat(`${logFile}.3`)).rejects.toThrow();
81
+ });
82
+ });
@@ -0,0 +1,359 @@
1
+ import { appendFile, mkdir, rename, stat, unlink } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+
4
+ import type { Logger } from "coco-cashu-core";
5
+
6
+ import { LOG_FILE } from "./config.js";
7
+
8
+ export type LogLevel = "error" | "warn" | "info" | "debug";
9
+
10
+ export interface AppLogger extends Logger {
11
+ flush(): Promise<void>;
12
+ }
13
+
14
+ export interface StructuredLoggerOptions {
15
+ service?: string;
16
+ logFile?: string;
17
+ level?: LogLevel;
18
+ maxBytes?: number;
19
+ maxFiles?: number;
20
+ mirrorToConsole?: boolean;
21
+ bindings?: Record<string, unknown>;
22
+ sharedState?: LoggerSharedState;
23
+ }
24
+
25
+ interface LoggerSharedState {
26
+ queue: Promise<void>;
27
+ initialization?: Promise<void>;
28
+ }
29
+
30
+ const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
31
+ error: 0,
32
+ warn: 1,
33
+ info: 2,
34
+ debug: 3,
35
+ };
36
+
37
+ const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
38
+ const DEFAULT_MAX_FILES = 5;
39
+ const DEFAULT_SERVICE = "cocod-daemon";
40
+ const REDACTED_KEYS = new Set([
41
+ "authorization",
42
+ "encryptedMnemonic",
43
+ "invoice",
44
+ "mnemonic",
45
+ "passphrase",
46
+ "request",
47
+ "seed",
48
+ "token",
49
+ "xCashuHeader",
50
+ ]);
51
+
52
+ const textEncoder = new TextEncoder();
53
+
54
+ export class StructuredLogger implements AppLogger {
55
+ private readonly service: string;
56
+ private readonly logFile: string;
57
+ private readonly level: LogLevel;
58
+ private readonly maxBytes: number;
59
+ private readonly maxFiles: number;
60
+ private readonly mirrorToConsole: boolean;
61
+ private readonly bindings: Record<string, unknown>;
62
+ private readonly sharedState: LoggerSharedState;
63
+
64
+ constructor(options: StructuredLoggerOptions = {}) {
65
+ this.service = options.service ?? DEFAULT_SERVICE;
66
+ this.logFile = options.logFile ?? LOG_FILE;
67
+ this.level = options.level ?? "info";
68
+ this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
69
+ this.maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
70
+ this.mirrorToConsole = options.mirrorToConsole ?? process.stdout.isTTY === true;
71
+ this.bindings = options.bindings ?? {};
72
+ this.sharedState = options.sharedState ?? { queue: Promise.resolve() };
73
+ }
74
+
75
+ error(message: string, ...meta: unknown[]): void {
76
+ this.enqueue("error", message, meta);
77
+ }
78
+
79
+ warn(message: string, ...meta: unknown[]): void {
80
+ this.enqueue("warn", message, meta);
81
+ }
82
+
83
+ info(message: string, ...meta: unknown[]): void {
84
+ this.enqueue("info", message, meta);
85
+ }
86
+
87
+ debug(message: string, ...meta: unknown[]): void {
88
+ this.enqueue("debug", message, meta);
89
+ }
90
+
91
+ log(level: LogLevel, message: string, ...meta: unknown[]): void {
92
+ this.enqueue(level, message, meta);
93
+ }
94
+
95
+ child(bindings: Record<string, unknown>): AppLogger {
96
+ return new StructuredLogger({
97
+ service: this.service,
98
+ logFile: this.logFile,
99
+ level: this.level,
100
+ maxBytes: this.maxBytes,
101
+ maxFiles: this.maxFiles,
102
+ mirrorToConsole: this.mirrorToConsole,
103
+ bindings: { ...this.bindings, ...bindings },
104
+ sharedState: this.sharedState,
105
+ });
106
+ }
107
+
108
+ async flush(): Promise<void> {
109
+ await this.sharedState.queue;
110
+ }
111
+
112
+ private shouldLog(level: LogLevel): boolean {
113
+ return LOG_LEVEL_PRIORITY[level] <= LOG_LEVEL_PRIORITY[this.level];
114
+ }
115
+
116
+ private enqueue(level: LogLevel, message: string, meta: unknown[]): void {
117
+ if (!this.shouldLog(level)) {
118
+ return;
119
+ }
120
+
121
+ const line = `${JSON.stringify(this.createEntry(level, message, meta))}\n`;
122
+ this.sharedState.queue = this.sharedState.queue
123
+ .then(async () => {
124
+ await this.ensureInitialized();
125
+ await this.rotateIfNeeded(line);
126
+ await appendFile(this.logFile, line, "utf8");
127
+
128
+ if (this.mirrorToConsole) {
129
+ this.writeToConsole(level, line);
130
+ }
131
+ })
132
+ .catch((error) => {
133
+ this.writeToConsole(
134
+ "error",
135
+ `${JSON.stringify({
136
+ ts: new Date().toISOString(),
137
+ level: "error",
138
+ service: this.service,
139
+ pid: process.pid,
140
+ event: "logger.write_failed",
141
+ error: serializeError(error),
142
+ })}\n`,
143
+ );
144
+ });
145
+ }
146
+
147
+ private async ensureInitialized(): Promise<void> {
148
+ if (!this.sharedState.initialization) {
149
+ this.sharedState.initialization = mkdir(dirname(this.logFile), { recursive: true }).then(
150
+ () => undefined,
151
+ );
152
+ }
153
+
154
+ await this.sharedState.initialization;
155
+ }
156
+
157
+ private async rotateIfNeeded(nextLine: string): Promise<void> {
158
+ const nextSize = textEncoder.encode(nextLine).byteLength;
159
+ const currentSize = await getFileSize(this.logFile);
160
+
161
+ if (currentSize === null || currentSize + nextSize <= this.maxBytes) {
162
+ return;
163
+ }
164
+
165
+ if (this.maxFiles < 1) {
166
+ await safeDelete(this.logFile);
167
+ return;
168
+ }
169
+
170
+ await safeDelete(`${this.logFile}.${this.maxFiles}`);
171
+
172
+ for (let index = this.maxFiles - 1; index >= 1; index -= 1) {
173
+ await safeRename(`${this.logFile}.${index}`, `${this.logFile}.${index + 1}`);
174
+ }
175
+
176
+ await safeRename(this.logFile, `${this.logFile}.1`);
177
+ }
178
+
179
+ private createEntry(level: LogLevel, message: string, meta: unknown[]): Record<string, unknown> {
180
+ const entry: Record<string, unknown> = {
181
+ ts: new Date().toISOString(),
182
+ level,
183
+ service: this.service,
184
+ pid: process.pid,
185
+ event: message,
186
+ ...sanitizeRecord(this.bindings),
187
+ };
188
+
189
+ const [fields, remainingMeta] = extractFields(meta);
190
+ Object.assign(entry, sanitizeRecord(fields));
191
+
192
+ if (remainingMeta.length > 0) {
193
+ entry.meta = remainingMeta.map((value) => sanitizeValue(value));
194
+ }
195
+
196
+ return entry;
197
+ }
198
+
199
+ private writeToConsole(level: LogLevel, line: string): void {
200
+ if (level === "error" || level === "warn") {
201
+ process.stderr.write(line);
202
+ return;
203
+ }
204
+
205
+ process.stdout.write(line);
206
+ }
207
+ }
208
+
209
+ export function createDaemonLogger(
210
+ options: Partial<StructuredLoggerOptions> = {},
211
+ ): StructuredLogger {
212
+ return new StructuredLogger({
213
+ service: DEFAULT_SERVICE,
214
+ logFile: options.logFile ?? LOG_FILE,
215
+ level: options.level ?? parseLogLevel(process.env.COCOD_LOG_LEVEL),
216
+ maxBytes:
217
+ options.maxBytes ?? parsePositiveInteger(process.env.COCOD_LOG_MAX_BYTES, DEFAULT_MAX_BYTES),
218
+ maxFiles:
219
+ options.maxFiles ?? parsePositiveInteger(process.env.COCOD_LOG_MAX_FILES, DEFAULT_MAX_FILES),
220
+ mirrorToConsole: options.mirrorToConsole,
221
+ bindings: options.bindings,
222
+ });
223
+ }
224
+
225
+ export function serializeError(error: unknown): Record<string, unknown> {
226
+ if (error instanceof Error) {
227
+ return {
228
+ name: error.name,
229
+ message: error.message,
230
+ stack: error.stack,
231
+ cause: error.cause === undefined ? undefined : sanitizeValue(error.cause),
232
+ };
233
+ }
234
+
235
+ return { message: String(error) };
236
+ }
237
+
238
+ function extractFields(meta: unknown[]): [Record<string, unknown>, unknown[]] {
239
+ if (meta.length === 1 && isPlainObject(meta[0])) {
240
+ return [meta[0], []];
241
+ }
242
+
243
+ return [{}, meta];
244
+ }
245
+
246
+ function sanitizeRecord(record: Record<string, unknown>): Record<string, unknown> {
247
+ const sanitized: Record<string, unknown> = {};
248
+
249
+ for (const [key, value] of Object.entries(record)) {
250
+ sanitized[key] = sanitizeValue(value, key);
251
+ }
252
+
253
+ return sanitized;
254
+ }
255
+
256
+ function sanitizeValue(value: unknown, key?: string, depth = 0): unknown {
257
+ if (key && REDACTED_KEYS.has(key)) {
258
+ return "[REDACTED]";
259
+ }
260
+
261
+ if (value instanceof Error) {
262
+ return serializeError(value);
263
+ }
264
+
265
+ if (depth >= 4) {
266
+ return "[Truncated]";
267
+ }
268
+
269
+ if (Array.isArray(value)) {
270
+ return value.map((item) => sanitizeValue(item, undefined, depth + 1));
271
+ }
272
+
273
+ if (isPlainObject(value)) {
274
+ const sanitized: Record<string, unknown> = {};
275
+
276
+ for (const [nestedKey, nestedValue] of Object.entries(value)) {
277
+ sanitized[nestedKey] = sanitizeValue(nestedValue, nestedKey, depth + 1);
278
+ }
279
+
280
+ return sanitized;
281
+ }
282
+
283
+ if (typeof value === "bigint") {
284
+ return value.toString();
285
+ }
286
+
287
+ if (value instanceof Uint8Array) {
288
+ return `[Uint8Array:${value.byteLength}]`;
289
+ }
290
+
291
+ return value;
292
+ }
293
+
294
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
295
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
296
+ return false;
297
+ }
298
+
299
+ const prototype = Object.getPrototypeOf(value);
300
+ return prototype === Object.prototype || prototype === null;
301
+ }
302
+
303
+ function parseLogLevel(value: string | undefined): LogLevel {
304
+ switch (value) {
305
+ case "error":
306
+ case "warn":
307
+ case "info":
308
+ case "debug":
309
+ return value;
310
+ default:
311
+ return "info";
312
+ }
313
+ }
314
+
315
+ function parsePositiveInteger(value: string | undefined, fallback: number): number {
316
+ if (!value) {
317
+ return fallback;
318
+ }
319
+
320
+ const parsed = Number.parseInt(value, 10);
321
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
322
+ }
323
+
324
+ async function getFileSize(filePath: string): Promise<number | null> {
325
+ try {
326
+ const fileStat = await stat(filePath);
327
+ return fileStat.size;
328
+ } catch (error) {
329
+ if (isMissingFileError(error)) {
330
+ return null;
331
+ }
332
+
333
+ throw error;
334
+ }
335
+ }
336
+
337
+ async function safeRename(from: string, to: string): Promise<void> {
338
+ try {
339
+ await rename(from, to);
340
+ } catch (error) {
341
+ if (!isMissingFileError(error)) {
342
+ throw error;
343
+ }
344
+ }
345
+ }
346
+
347
+ async function safeDelete(filePath: string): Promise<void> {
348
+ try {
349
+ await unlink(filePath);
350
+ } catch (error) {
351
+ if (!isMissingFileError(error)) {
352
+ throw error;
353
+ }
354
+ }
355
+ }
356
+
357
+ function isMissingFileError(error: unknown): boolean {
358
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
359
+ }
@@ -0,0 +1,55 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { DaemonStateManager } from "./state";
4
+
5
+ describe("DaemonStateManager", () => {
6
+ test("transitions through states", () => {
7
+ const stateManager = new DaemonStateManager();
8
+
9
+ expect(stateManager.getState().status).toBe("UNINITIALIZED");
10
+
11
+ stateManager.setLocked("encrypted", "https://mint.example.com");
12
+ expect(stateManager.getState().status).toBe("LOCKED");
13
+
14
+ const fakeManager = {} as unknown as import("coco-cashu-core").Manager;
15
+ stateManager.setUnlocked(fakeManager, "https://mint.example.com", new Uint8Array([1, 2, 3]));
16
+ expect(stateManager.getState().status).toBe("UNLOCKED");
17
+
18
+ stateManager.setError("boom");
19
+ expect(stateManager.getState().status).toBe("ERROR");
20
+ });
21
+
22
+ test("requireUnlocked returns 403 when locked", async () => {
23
+ const stateManager = new DaemonStateManager();
24
+ stateManager.setLocked("encrypted", "https://mint.example.com");
25
+
26
+ const handler = stateManager.requireUnlocked(async () => {
27
+ return Response.json({ output: "ok" });
28
+ });
29
+
30
+ const response = await handler(
31
+ new Request("http://localhost/balance"),
32
+ stateManager.getState(),
33
+ );
34
+ const body = (await response.json()) as { error?: string };
35
+
36
+ expect(response.status).toBe(403);
37
+ expect(body.error).toContain("locked");
38
+ });
39
+
40
+ test("requireLocked returns 409 when already unlocked", async () => {
41
+ const stateManager = new DaemonStateManager();
42
+ const fakeManager = {} as unknown as import("coco-cashu-core").Manager;
43
+ stateManager.setUnlocked(fakeManager, "https://mint.example.com", new Uint8Array([1]));
44
+
45
+ const handler = stateManager.requireLocked(async () => {
46
+ return Response.json({ output: "ok" });
47
+ });
48
+
49
+ const response = await handler(new Request("http://localhost/unlock"), stateManager.getState());
50
+ const body = (await response.json()) as { error?: string };
51
+
52
+ expect(response.status).toBe(409);
53
+ expect(body.error).toContain("already unlocked");
54
+ });
55
+ });
@@ -0,0 +1,128 @@
1
+ import type { Manager } from "coco-cashu-core";
2
+
3
+ export interface UninitializedState {
4
+ status: "UNINITIALIZED";
5
+ }
6
+
7
+ export interface LockedState {
8
+ status: "LOCKED";
9
+ encryptedMnemonic: string;
10
+ mintUrl: string;
11
+ }
12
+
13
+ export interface UnlockedState {
14
+ status: "UNLOCKED";
15
+ manager: Manager;
16
+ mintUrl: string;
17
+ seed: Uint8Array;
18
+ }
19
+
20
+ export interface ErrorState {
21
+ status: "ERROR";
22
+ message: string;
23
+ }
24
+
25
+ export type DaemonState = UninitializedState | LockedState | UnlockedState | ErrorState;
26
+
27
+ export type RouteHandler = (req: Request, state: DaemonState) => Promise<Response>;
28
+
29
+ export class DaemonStateManager {
30
+ private state: DaemonState;
31
+
32
+ constructor(initialState: DaemonState = { status: "UNINITIALIZED" }) {
33
+ this.state = initialState;
34
+ }
35
+
36
+ getState(): DaemonState {
37
+ return this.state;
38
+ }
39
+
40
+ isUnlocked(): this is { getState: () => UnlockedState } {
41
+ return this.state.status === "UNLOCKED";
42
+ }
43
+
44
+ isLocked(): this is { getState: () => LockedState } {
45
+ return this.state.status === "LOCKED";
46
+ }
47
+
48
+ isUninitialized(): boolean {
49
+ return this.state.status === "UNINITIALIZED";
50
+ }
51
+
52
+ setLocked(encryptedMnemonic: string, mintUrl: string): void {
53
+ this.state = { status: "LOCKED", encryptedMnemonic, mintUrl };
54
+ }
55
+
56
+ setUnlocked(manager: Manager, mintUrl: string, seed: Uint8Array): void {
57
+ this.state = { status: "UNLOCKED", manager, mintUrl, seed };
58
+ }
59
+
60
+ setUninitialized(): void {
61
+ this.state = { status: "UNINITIALIZED" };
62
+ }
63
+
64
+ setError(message: string): void {
65
+ this.state = { status: "ERROR", message };
66
+ }
67
+
68
+ requireUnlocked(
69
+ handler: (req: Request, state: UnlockedState) => Promise<Response>,
70
+ ): RouteHandler {
71
+ return async (req: Request, state: DaemonState) => {
72
+ if (state.status !== "UNLOCKED") {
73
+ if (state.status === "LOCKED") {
74
+ return Response.json(
75
+ {
76
+ error: "Wallet is locked. Run 'cocod unlock <passphrase>' to decrypt.",
77
+ },
78
+ { status: 403 },
79
+ );
80
+ }
81
+ if (state.status === "UNINITIALIZED") {
82
+ return Response.json(
83
+ {
84
+ error: "Wallet not initialized. Run 'cocod init [mnemonic]' first.",
85
+ },
86
+ { status: 503 },
87
+ );
88
+ }
89
+ return Response.json({ error: "Wallet error" }, { status: 500 });
90
+ }
91
+ return handler(req, state as UnlockedState);
92
+ };
93
+ }
94
+
95
+ requireUninitialized(handler: (req: Request) => Promise<Response>): RouteHandler {
96
+ return async (req: Request, state: DaemonState) => {
97
+ if (state.status !== "UNINITIALIZED") {
98
+ return Response.json(
99
+ {
100
+ error: "Wallet already initialized. Delete ~/.cocod/config.json to reset.",
101
+ },
102
+ { status: 409 },
103
+ );
104
+ }
105
+ return handler(req);
106
+ };
107
+ }
108
+
109
+ requireLocked(handler: (req: Request, state: LockedState) => Promise<Response>): RouteHandler {
110
+ return async (req: Request, state: DaemonState) => {
111
+ if (state.status !== "LOCKED") {
112
+ if (state.status === "UNINITIALIZED") {
113
+ return Response.json(
114
+ {
115
+ error: "Wallet not initialized. Run 'cocod init [mnemonic]' first.",
116
+ },
117
+ { status: 503 },
118
+ );
119
+ }
120
+ if (state.status === "UNLOCKED") {
121
+ return Response.json({ error: "Wallet is already unlocked" }, { status: 409 });
122
+ }
123
+ return Response.json({ error: "Wallet error" }, { status: 500 });
124
+ }
125
+ return handler(req, state as LockedState);
126
+ };
127
+ }
128
+ }
@@ -0,0 +1,51 @@
1
+ import { initializeCoco, ConsoleLogger, type Logger, type Manager } from "coco-cashu-core";
2
+ import { SqliteRepositories } from "coco-cashu-sqlite-bun";
3
+ import { Database } from "bun:sqlite";
4
+ import { mnemonicToSeedSync } from "@scure/bip39";
5
+ import { NPCPlugin } from "coco-cashu-plugin-npc";
6
+ import { privateKeyFromSeedWords } from "nostr-tools/nip06";
7
+ import { finalizeEvent, type EventTemplate } from "nostr-tools";
8
+ import { decryptMnemonic } from "./crypto.js";
9
+ import { SALT_FILE, DB_FILE } from "./config.js";
10
+ import type { WalletConfig } from "./config.js";
11
+
12
+ export async function initializeWallet(
13
+ config: WalletConfig,
14
+ passphrase?: string,
15
+ logger?: Logger,
16
+ ): Promise<Manager> {
17
+ let mnemonic: string;
18
+
19
+ if (config.encrypted) {
20
+ if (!passphrase) {
21
+ throw new Error("Passphrase required for encrypted wallet");
22
+ }
23
+ const salt = await Bun.file(SALT_FILE).text();
24
+ mnemonic = await decryptMnemonic(config.mnemonic, passphrase, salt);
25
+ } else {
26
+ mnemonic = config.mnemonic;
27
+ }
28
+
29
+ const seed = mnemonicToSeedSync(mnemonic);
30
+
31
+ const repo = new SqliteRepositories({ database: new Database(DB_FILE) });
32
+ const walletLogger = logger?.child?.({ component: "coco" }) ?? logger;
33
+ const cocoLogger = walletLogger ?? new ConsoleLogger("Coco", { level: "info" });
34
+ const sk = privateKeyFromSeedWords(mnemonic);
35
+ const signer = async (t: EventTemplate) => finalizeEvent(t, sk);
36
+ const npcPlugin = new NPCPlugin("https://npubx.cash", signer, {
37
+ useWebsocket: true,
38
+ logger: cocoLogger,
39
+ });
40
+ const coco = await initializeCoco({
41
+ repo,
42
+ seedGetter: async () => seed,
43
+ logger: cocoLogger,
44
+ });
45
+
46
+ coco.use(npcPlugin);
47
+
48
+ await coco.mint.addMint(config.mintUrl, { trusted: true });
49
+
50
+ return coco;
51
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }