feat(cli): added view command (#71)

This commit is contained in:
Corentin THOMASSET 2024-09-01 14:33:43 +02:00 committed by GitHub
parent 3f536403db
commit b5c89a4de5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 607 additions and 32 deletions

View File

@ -139,6 +139,16 @@ cat file.txt | enclosed create
enclosed create --deleteAfterReading --password "password" --ttl 3600 "Hello, World!"
```
### View a note
```bash
# The password will be prompted if the note is password-protected
enclosed view <note-url>
# Or you can provide the password directly
enclosed view --password "password" <note-url>
```
### Configure the enclosed instance to use
```bash

View File

@ -45,6 +45,16 @@ cat file.txt | enclosed create
enclosed create --deleteAfterReading --password "password" --ttl 3600 "Hello, World!"
```
### View a note
```bash
# The password will be prompted if the note is password-protected
enclosed view <note-url>
# Or you can provide the password directly
enclosed view --password "password" <note-url>
```
### Configure the enclosed instance to use
```bash

View File

@ -1,7 +1,7 @@
{
"name": "@enclosed/cli",
"type": "module",
"version": "0.0.3",
"version": "0.0.4",
"packageManager": "pnpm@9.9.0",
"description": "Enclosed cli to create secure notes.",
"author": "Corentin Thomasset <corentinth@proton.me> (https://corentin.tech)",
@ -34,6 +34,7 @@
},
"dependencies": {
"@enclosed/lib": "workspace:*",
"@inquirer/prompts": "^5.3.8",
"citty": "^0.1.6",
"conf": "^13.0.1",
"lodash-es": "^4.17.21",

View File

@ -1,6 +1,7 @@
import { defineCommand, runMain } from 'citty';
import { createNoteCommand } from './create-note/create-note.command';
import { configCommand } from './config/config.command';
import { viewNoteCommand } from './view-note/view-note.command';
const main = defineCommand({
meta: {
@ -9,6 +10,7 @@ const main = defineCommand({
},
subCommands: {
create: createNoteCommand,
view: viewNoteCommand,
config: configCommand,
},
});

View File

@ -0,0 +1,58 @@
import { defineCommand } from 'citty';
import { decryptNote, fetchNote, isApiClientErrorWithStatusCode, parseNoteUrl } from '@enclosed/lib';
import picocolors from 'picocolors';
import { getInstanceUrl } from '../config/config.usecases';
import { promptForPassword } from './view-note.models';
export const viewNoteCommand = defineCommand({
meta: {
name: 'view',
description: 'View a note',
},
args: {
noteUrl: {
description: 'Note URL',
type: 'positional',
required: true,
},
password: {
description: 'Password to decrypt the note (will be prompted if needed and not provided)',
valueHint: 'password',
alias: 'p',
type: 'string',
required: false,
},
},
run: async ({ args }) => {
const { noteUrl, password } = args;
try {
const { noteId, encryptionKey } = parseNoteUrl({ noteUrl });
const { content: encryptedContent, isPasswordProtected } = await fetchNote({
noteId,
apiBaseUrl: getInstanceUrl(),
});
const { decryptedContent } = await decryptNote({
encryptedContent,
encryptionKey,
password: isPasswordProtected ? password ?? await promptForPassword() : undefined,
});
console.log(decryptedContent);
} catch (error) {
if (isApiClientErrorWithStatusCode({ error, statusCode: 404 })) {
console.error(picocolors.red('Note not found'));
return;
}
if (isApiClientErrorWithStatusCode({ error, statusCode: 429 })) {
console.error(picocolors.red('Api rate limit reached, please try again later'));
return;
}
console.error(picocolors.red('Failed to fetch or decrypt note'));
}
},
});

View File

@ -0,0 +1,13 @@
import { password as prompt } from '@inquirer/prompts';
export { promptForPassword };
async function promptForPassword(): Promise<string> {
const password = await prompt({
message: 'Enter the password',
});
console.log(''); // Add a new line after the password prompt
return password;
}

View File

@ -1,7 +1,7 @@
{
"name": "@enclosed/lib",
"type": "module",
"version": "0.0.2",
"version": "0.0.3",
"packageManager": "pnpm@9.9.0",
"description": "Enclosed lib to create secure notes.",
"author": "Corentin Thomasset <corentinth@proton.me> (https://corentin.tech)",
@ -58,7 +58,9 @@
"main": "./dist/index.node.cjs",
"module": "./dist/index.web.mjs",
"types": "./dist/index.web.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"engines": {
"node": ">=22.0.0"
},
@ -75,10 +77,12 @@
"prepublishOnly": "pnpm run build"
},
"dependencies": {
"lodash-es": "^4.17.21",
"ofetch": "^1.3.4"
},
"devDependencies": {
"@antfu/eslint-config": "^2.27.0",
"@types/lodash-es": "^4.17.12",
"@vitest/coverage-v8": "^2.0.5",
"dotenv": "^16.4.5",
"eslint": "^9.9.0",

View File

@ -0,0 +1,52 @@
import { ofetch } from 'ofetch';
import { DEFAULT_API_BASE_URL } from './api.constants';
export { apiClient };
async function tryToGetBody({ response }: { response: Response }): Promise<unknown> {
try {
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
return await response.json();
}
return await response.text();
} catch (_error) {
return {};
}
}
async function apiClient<T>({
path,
method,
body,
baseUrl = DEFAULT_API_BASE_URL,
}: {
path: string;
method: string;
body?: Record<string, unknown>;
baseUrl?: string;
}): Promise<T> {
const data = await ofetch<T>(
path,
{
method,
body,
baseURL: baseUrl,
onResponseError: async ({ response }) => {
throw Object.assign(
new Error('Failed to fetch note'),
{
response: {
status: response.status,
body: tryToGetBody({ response }),
},
},
);
},
},
);
return data;
}

View File

@ -0,0 +1 @@
export const DEFAULT_API_BASE_URL = 'https://enclosed.cc';

View File

@ -0,0 +1,46 @@
import { describe, expect, test } from 'vitest';
import { isApiClientErrorWithCode, isApiClientErrorWithStatusCode } from './api.models';
describe('api models', () => {
describe('isApiClientErrorWithStatusCode', () => {
test('permit to check if an error raised by the api client has a specific status code as response', () => {
const error = Object.assign(new Error('Failed to fetch note'), {
response: {
status: 404,
},
});
expect(isApiClientErrorWithStatusCode({ error, statusCode: 404 })).to.eql(true);
expect(isApiClientErrorWithStatusCode({ error, statusCode: 500 })).to.eql(false);
});
test('the error must be an instance of Error and have a response object', () => {
expect(isApiClientErrorWithStatusCode({ error: {}, statusCode: 404 })).to.eql(false);
expect(isApiClientErrorWithStatusCode({ error: new Error('Failed to fetch note'), statusCode: 404 })).to.eql(false);
});
});
describe('isApiClientErrorWithCode', () => {
test('permit to check if an error raised by the api client has a specific code in the error body', () => {
const error = Object.assign(new Error('Failed to fetch note'), {
response: {
body: {
error: {
code: 'NOT_FOUND',
},
},
},
});
expect(isApiClientErrorWithCode({ error, code: 'NOT_FOUND' })).to.eql(true);
expect(isApiClientErrorWithCode({ error, code: 'INTERNAL_ERROR' })).to.eql(false);
});
test('the error must be an instance of Error and have a response object with a body object', () => {
expect(isApiClientErrorWithCode({ error: {}, code: 'NOT_FOUND' })).to.eql(false);
expect(isApiClientErrorWithCode({ error: new Error('Failed to fetch note'), code: 'NOT_FOUND' })).to.eql(false);
expect(isApiClientErrorWithCode({ error: { response: {} }, code: 'NOT_FOUND' })).to.eql(false);
expect(isApiClientErrorWithCode({ error: { response: { body: {} } }, code: 'NOT_FOUND' })).to.eql(false);
});
});
});

View File

@ -0,0 +1,19 @@
import { get, isError } from 'lodash-es';
export { isApiClientErrorWithStatusCode, isApiClientErrorWithCode };
function isApiClientErrorWithStatusCode({ error, statusCode }: { error: unknown; statusCode: number }): boolean {
if (!isError(error)) {
return false;
}
return get(error, 'response.status') === statusCode;
}
function isApiClientErrorWithCode({ error, code }: { error: unknown; code: string }): boolean {
if (!isError(error)) {
return false;
}
return get(error, 'response.body.error.code') === code;
}

View File

@ -1,9 +1,12 @@
import { createEnclosedLib } from './notes/notes.usecases';
import { decryptNoteContent, deriveMasterKey, encryptNoteContent, generateBaseKey } from './crypto/node/crypto.node.usecases';
import { storeNote } from './notes/notes.services';
import { fetchNote, storeNote } from './notes/notes.services';
import { createDecryptUsecase, createEncryptUsecase } from './crypto/crypto.usecases';
import { isApiClientErrorWithCode, isApiClientErrorWithStatusCode } from './api/api.models';
export const { encryptNote } = createEncryptUsecase({ generateBaseKey, deriveMasterKey, encryptNoteContent });
export const { decryptNote } = createDecryptUsecase({ deriveMasterKey, decryptNoteContent });
export const { createNote } = createEnclosedLib({ encryptNote, storeNote });
export const { createNote, createNoteUrl, parseNoteUrl } = createEnclosedLib({ encryptNote, storeNote });
export { fetchNote, storeNote, isApiClientErrorWithStatusCode, isApiClientErrorWithCode };

View File

@ -0,0 +1,9 @@
import { describe, expect, test } from 'vitest';
import * as nodeLib from './index.node';
import * as webLib from './index.web';
describe('lib api', () => {
test('the web lib exports the same functions as the node lib', () => {
expect(Object.keys(nodeLib)).to.eql(Object.keys(webLib));
});
});

View File

@ -1,9 +1,12 @@
import { createEnclosedLib } from './notes/notes.usecases';
import { decryptNoteContent, deriveMasterKey, encryptNoteContent, generateBaseKey } from './crypto/web/crypto.web.usecases';
import { storeNote } from './notes/notes.services';
import { fetchNote, storeNote } from './notes/notes.services';
import { createDecryptUsecase, createEncryptUsecase } from './crypto/crypto.usecases';
import { isApiClientErrorWithCode, isApiClientErrorWithStatusCode } from './api/api.models';
export const { encryptNote } = createEncryptUsecase({ generateBaseKey, deriveMasterKey, encryptNoteContent });
export const { decryptNote } = createDecryptUsecase({ deriveMasterKey, decryptNoteContent });
export const { createNote } = createEnclosedLib({ encryptNote, storeNote });
export const { createNote, createNoteUrl, parseNoteUrl } = createEnclosedLib({ encryptNote, storeNote });
export { fetchNote, storeNote, isApiClientErrorWithStatusCode, isApiClientErrorWithCode };

View File

@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest';
import { createNoteUrl } from './notes.models';
import { createNoteUrl, parseNoteUrl } from './notes.models';
describe('note models', () => {
describe('createNoteUrl', () => {
@ -19,4 +19,51 @@ describe('note models', () => {
});
});
});
describe('parseNoteUrl', () => {
test('retreives the note id and encryption key from a sharable note url', () => {
expect(
parseNoteUrl({ noteUrl: 'https://example.com/123#abc' }),
).to.eql({
noteId: '123',
encryptionKey: 'abc',
});
});
test('trailing slash in the base url is handled', () => {
expect(
parseNoteUrl({ noteUrl: 'https://example.com/123/#abc' }),
).to.eql({
noteId: '123',
encryptionKey: 'abc',
});
});
test('in case of nested paths, the last path segment is considered the note id', () => {
expect(
parseNoteUrl({ noteUrl: 'https://example.com/123/456#abc' }),
).to.eql({
noteId: '456',
encryptionKey: 'abc',
});
});
test('throws an error if their is no note id or encryption key', () => {
expect(() => {
parseNoteUrl({ noteUrl: 'https://example.com/#abc' });
}).to.throw('Invalid note url');
expect(() => {
parseNoteUrl({ noteUrl: 'https://example.com/123#' });
}).to.throw('Invalid note url');
expect(() => {
parseNoteUrl({ noteUrl: 'https://example.com/123' });
}).to.throw('Invalid note url');
expect(() => {
parseNoteUrl({ noteUrl: 'https://example.com/' });
}).to.throw('Invalid note url');
});
});
});

View File

@ -1,4 +1,4 @@
export { createNoteUrl };
export { createNoteUrl, parseNoteUrl };
function createNoteUrl({ noteId, encryptionKey, clientBaseUrl }: { noteId: string; encryptionKey: string; clientBaseUrl: string }): { noteUrl: string } {
const url = new URL(`/${noteId}`, clientBaseUrl);
@ -8,3 +8,16 @@ function createNoteUrl({ noteId, encryptionKey, clientBaseUrl }: { noteId: strin
return { noteUrl };
}
function parseNoteUrl({ noteUrl }: { noteUrl: string }): { noteId: string; encryptionKey: string } {
const url = new URL(noteUrl);
const noteId = url.pathname.split('/').filter(Boolean).pop();
const encryptionKey = url.hash.replace(/^#/, '');
if (!noteId || !encryptionKey) {
throw new Error('Invalid note url');
}
return { noteId, encryptionKey };
}

View File

@ -1,35 +1,47 @@
import { ofetch } from 'ofetch';
import { apiClient } from '../api/api.client';
export { storeNote };
export { storeNote, fetchNote };
async function storeNote({
content,
isPasswordProtected,
ttlInSeconds,
deleteAfterReading,
noteCreationApiUrl,
apiBaseUrl,
}: {
content: string;
isPasswordProtected: boolean;
ttlInSeconds: number;
deleteAfterReading: boolean;
noteCreationApiUrl: string;
apiBaseUrl?: string;
}): Promise<{ noteId: string }> {
const { noteId } = await ofetch<{ noteId: string }>(
noteCreationApiUrl,
{
method: 'POST',
body: {
content,
isPasswordProtected,
ttlInSeconds,
deleteAfterReading,
},
onResponseError: async ({ response }) => {
throw Object.assign(new Error('Failed to create note'), { response });
},
const { noteId } = await apiClient<{ noteId: string }>({
path: 'api/notes',
baseUrl: apiBaseUrl,
method: 'POST',
body: {
content,
isPasswordProtected,
ttlInSeconds,
deleteAfterReading,
},
);
});
return { noteId };
}
async function fetchNote({
noteId,
apiBaseUrl,
}: {
noteId: string;
apiBaseUrl?: string;
}) {
const { note } = await apiClient<{ note: { content: string; isPasswordProtected: boolean } }>({
path: `api/notes/${noteId}`,
baseUrl: apiBaseUrl,
method: 'GET',
});
return note;
}

View File

@ -1,4 +1,4 @@
import { createNoteUrl as createNoteUrlImpl } from './notes.models';
import { createNoteUrl as createNoteUrlImpl, parseNoteUrl } from './notes.models';
export { createEnclosedLib };
@ -7,28 +7,35 @@ const BASE_URL = 'https://enclosed.cc';
function createEnclosedLib({
encryptNote,
// decryptNote,
storeNote: storeNoteImpl,
// fetchNote: fetchNoteImpl,
}: {
encryptNote: (args: { content: string; password?: string }) => Promise<{ encryptedContent: string; encryptionKey: string }>;
storeNote: (params: { content: string; isPasswordProtected: boolean; ttlInSeconds: number; deleteAfterReading: boolean; noteCreationApiUrl: string }) => Promise<{ noteId: string }>;
// decryptNote: (args: { encryptedContent: string; encryptionKey: string }) => Promise<{ content: string }>;
storeNote: (params: { content: string; isPasswordProtected: boolean; ttlInSeconds: number; deleteAfterReading: boolean; apiBaseUrl?: string }) => Promise<{ noteId: string }>;
// fetchNote: (params: { noteId: string; apiBaseUrl?: string }) => Promise<{ content: string; isPasswordProtected: boolean }>;
}) {
return {
parseNoteUrl,
createNoteUrl: createNoteUrlImpl,
createNote: async ({
content,
password,
ttlInSeconds = ONE_HOUR_IN_SECONDS,
deleteAfterReading = false,
clientBaseUrl = BASE_URL,
noteCreationApiUrl = new URL('/api/notes', clientBaseUrl).toString(),
apiBaseUrl = clientBaseUrl,
createNoteUrl = createNoteUrlImpl,
storeNote = params => storeNoteImpl({ ...params, noteCreationApiUrl }),
storeNote = params => storeNoteImpl({ ...params, apiBaseUrl }),
}: {
content: string;
password?: string;
ttlInSeconds?: number;
deleteAfterReading?: boolean;
clientBaseUrl?: string;
noteCreationApiUrl?: string;
apiBaseUrl?: string;
createNoteUrl?: (args: { noteId: string; encryptionKey: string; clientBaseUrl: string }) => { noteUrl: string };
storeNote?: (params: { content: string; isPasswordProtected: boolean; ttlInSeconds: number; deleteAfterReading: boolean }) => Promise<{ noteId: string }>;
}) => {

View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"esModuleInterop": true
},
"include": [
"src"
]
}

253
pnpm-lock.yaml generated
View File

@ -156,6 +156,9 @@ importers:
'@enclosed/lib':
specifier: workspace:*
version: link:../lib
'@inquirer/prompts':
specifier: ^5.3.8
version: 5.3.8
citty:
specifier: ^0.1.6
version: 0.1.6
@ -207,6 +210,9 @@ importers:
packages/lib:
dependencies:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
ofetch:
specifier: ^1.3.4
version: 1.3.4
@ -214,6 +220,9 @@ importers:
'@antfu/eslint-config':
specifier: ^2.27.0
version: 2.27.3(@typescript-eslint/utils@8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(@vue/compiler-sfc@3.4.38)(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4)(vitest@2.0.5(@types/node@22.5.1)(jsdom@25.0.0)(terser@5.31.6))
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
'@vitest/coverage-v8':
specifier: ^2.0.5
version: 2.0.5(vitest@2.0.5(@types/node@22.5.1)(jsdom@25.0.0)(terser@5.31.6))
@ -1140,6 +1149,62 @@ packages:
'@iconify/utils@2.1.32':
resolution: {integrity: sha512-LeifFZPPKu28O3AEDpYJNdEbvS4/ojAPyIW+pF/vUpJTYnbTiXUHkCh0bwgFRzKvdpb8H4Fbfd/742++MF4fPQ==}
'@inquirer/checkbox@2.4.7':
resolution: {integrity: sha512-5YwCySyV1UEgqzz34gNsC38eKxRBtlRDpJLlKcRtTjlYA/yDKuc1rfw+hjw+2WJxbAZtaDPsRl5Zk7J14SBoBw==}
engines: {node: '>=18'}
'@inquirer/confirm@3.1.22':
resolution: {integrity: sha512-gsAKIOWBm2Q87CDfs9fEo7wJT3fwWIJfnDGMn9Qy74gBnNFOACDNfhUzovubbJjWnKLGBln7/NcSmZwj5DuEXg==}
engines: {node: '>=18'}
'@inquirer/core@9.0.10':
resolution: {integrity: sha512-TdESOKSVwf6+YWDz8GhS6nKscwzkIyakEzCLJ5Vh6O3Co2ClhCJ0A4MG909MUWfaWdpJm7DE45ii51/2Kat9tA==}
engines: {node: '>=18'}
'@inquirer/editor@2.1.22':
resolution: {integrity: sha512-K1QwTu7GCK+nKOVRBp5HY9jt3DXOfPGPr6WRDrPImkcJRelG9UTx2cAtK1liXmibRrzJlTWOwqgWT3k2XnS62w==}
engines: {node: '>=18'}
'@inquirer/expand@2.1.22':
resolution: {integrity: sha512-wTZOBkzH+ItPuZ3ZPa9lynBsdMp6kQ9zbjVPYEtSBG7UulGjg2kQiAnUjgyG4SlntpTce5bOmXAPvE4sguXjpA==}
engines: {node: '>=18'}
'@inquirer/figures@1.0.5':
resolution: {integrity: sha512-79hP/VWdZ2UVc9bFGJnoQ/lQMpL74mGgzSYX1xUqCVk7/v73vJCMw1VuyWN1jGkZ9B3z7THAbySqGbCNefcjfA==}
engines: {node: '>=18'}
'@inquirer/input@2.2.9':
resolution: {integrity: sha512-7Z6N+uzkWM7+xsE+3rJdhdG/+mQgejOVqspoW+w0AbSZnL6nq5tGMEVASaYVWbkoSzecABWwmludO2evU3d31g==}
engines: {node: '>=18'}
'@inquirer/number@1.0.10':
resolution: {integrity: sha512-kWTxRF8zHjQOn2TJs+XttLioBih6bdc5CcosXIzZsrTY383PXI35DuhIllZKu7CdXFi2rz2BWPN9l0dPsvrQOA==}
engines: {node: '>=18'}
'@inquirer/password@2.1.22':
resolution: {integrity: sha512-5Fxt1L9vh3rAKqjYwqsjU4DZsEvY/2Gll+QkqR4yEpy6wvzLxdSgFhUcxfDAOtO4BEoTreWoznC0phagwLU5Kw==}
engines: {node: '>=18'}
'@inquirer/prompts@5.3.8':
resolution: {integrity: sha512-b2BudQY/Si4Y2a0PdZZL6BeJtl8llgeZa7U2j47aaJSCeAl1e4UI7y8a9bSkO3o/ZbZrgT5muy/34JbsjfIWxA==}
engines: {node: '>=18'}
'@inquirer/rawlist@2.2.4':
resolution: {integrity: sha512-pb6w9pWrm7EfnYDgQObOurh2d2YH07+eDo3xQBsNAM2GRhliz6wFXGi1thKQ4bN6B0xDd6C3tBsjdr3obsCl3Q==}
engines: {node: '>=18'}
'@inquirer/search@1.0.7':
resolution: {integrity: sha512-p1wpV+3gd1eST/o5N3yQpYEdFNCzSP0Klrl+5bfD3cTTz8BGG6nf4Z07aBW0xjlKIj1Rp0y3x/X4cZYi6TfcLw==}
engines: {node: '>=18'}
'@inquirer/select@2.4.7':
resolution: {integrity: sha512-JH7XqPEkBpNWp3gPCqWqY8ECbyMoFcCZANlL6pV9hf59qK6dGmkOlx1ydyhY+KZ0c5X74+W6Mtp+nm2QX0/MAQ==}
engines: {node: '>=18'}
'@inquirer/type@1.5.2':
resolution: {integrity: sha512-w9qFkumYDCNyDZmNQjf/n6qQuvQ4dMC3BJesY4oF+yr0CxR5vxujflAVeIcS6U336uzi9GM0kAfZlLrZ9UTkpA==}
engines: {node: '>=18'}
'@internationalized/date@3.5.5':
resolution: {integrity: sha512-H+CfYvOZ0LTJeeLOqm19E3uj/4YjrmOFtBufDHPfvtI80hFAMqtrp7oCACpe4Cil5l8S0Qu/9dYfZc/5lY8WQQ==}
@ -1565,6 +1630,9 @@ packages:
'@types/mdast@3.0.15':
resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==}
'@types/mute-stream@0.0.4':
resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==}
'@types/node-cron@3.0.11':
resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==}
@ -1583,6 +1651,9 @@ packages:
'@types/unist@2.0.11':
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
'@types/wrap-ansi@3.0.0':
resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==}
'@typescript-eslint/eslint-plugin@8.3.0':
resolution: {integrity: sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -1819,6 +1890,10 @@ packages:
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@ -1973,6 +2048,9 @@ packages:
character-reference-invalid@1.1.4:
resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==}
chardet@0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
check-error@2.1.1:
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
engines: {node: '>= 16'}
@ -2003,6 +2081,10 @@ packages:
resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
engines: {node: '>=6'}
cli-width@4.1.0:
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
engines: {node: '>= 12'}
clipboardy@4.0.0:
resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==}
engines: {node: '>=18'}
@ -2522,6 +2604,10 @@ packages:
resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
engines: {node: '>=6'}
external-editor@3.1.0:
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
engines: {node: '>=4'}
fast-copy@3.0.2:
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
@ -2743,6 +2829,10 @@ packages:
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
engines: {node: '>=16.17.0'}
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@ -3168,6 +3258,10 @@ packages:
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
hasBin: true
mute-stream@1.0.0:
resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -3246,6 +3340,10 @@ packages:
resolution: {integrity: sha512-GQEkNkH/GHOhPFXcqZs3IDahXEQcQxsSjEkK4KvEEST4t7eNzoMjxTzef+EZ+JluDEV+Raoi3WQ2CflnRdSVnQ==}
engines: {node: '>=18'}
os-tmpdir@1.0.2:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
@ -3962,6 +4060,10 @@ packages:
resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==}
engines: {node: '>=14.0.0'}
tmp@0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'}
to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}
@ -4008,6 +4110,10 @@ packages:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
type-fest@0.21.3:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
type-fest@0.6.0:
resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
engines: {node: '>=8'}
@ -4305,6 +4411,10 @@ packages:
'@cloudflare/workers-types':
optional: true
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@ -4370,6 +4480,10 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
yoctocolors-cjs@2.1.2:
resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==}
engines: {node: '>=18'}
youch@3.3.3:
resolution: {integrity: sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==}
@ -5080,6 +5194,103 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@inquirer/checkbox@2.4.7':
dependencies:
'@inquirer/core': 9.0.10
'@inquirer/figures': 1.0.5
'@inquirer/type': 1.5.2
ansi-escapes: 4.3.2
yoctocolors-cjs: 2.1.2
'@inquirer/confirm@3.1.22':
dependencies:
'@inquirer/core': 9.0.10
'@inquirer/type': 1.5.2
'@inquirer/core@9.0.10':
dependencies:
'@inquirer/figures': 1.0.5
'@inquirer/type': 1.5.2
'@types/mute-stream': 0.0.4
'@types/node': 22.5.1
'@types/wrap-ansi': 3.0.0
ansi-escapes: 4.3.2
cli-spinners: 2.9.2
cli-width: 4.1.0
mute-stream: 1.0.0
signal-exit: 4.1.0
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
yoctocolors-cjs: 2.1.2
'@inquirer/editor@2.1.22':
dependencies:
'@inquirer/core': 9.0.10
'@inquirer/type': 1.5.2
external-editor: 3.1.0
'@inquirer/expand@2.1.22':
dependencies:
'@inquirer/core': 9.0.10
'@inquirer/type': 1.5.2
yoctocolors-cjs: 2.1.2
'@inquirer/figures@1.0.5': {}
'@inquirer/input@2.2.9':
dependencies:
'@inquirer/core': 9.0.10
'@inquirer/type': 1.5.2
'@inquirer/number@1.0.10':
dependencies:
'@inquirer/core': 9.0.10
'@inquirer/type': 1.5.2
'@inquirer/password@2.1.22':
dependencies:
'@inquirer/core': 9.0.10
'@inquirer/type': 1.5.2
ansi-escapes: 4.3.2
'@inquirer/prompts@5.3.8':
dependencies:
'@inquirer/checkbox': 2.4.7
'@inquirer/confirm': 3.1.22
'@inquirer/editor': 2.1.22
'@inquirer/expand': 2.1.22
'@inquirer/input': 2.2.9
'@inquirer/number': 1.0.10
'@inquirer/password': 2.1.22
'@inquirer/rawlist': 2.2.4
'@inquirer/search': 1.0.7
'@inquirer/select': 2.4.7
'@inquirer/rawlist@2.2.4':
dependencies:
'@inquirer/core': 9.0.10
'@inquirer/type': 1.5.2
yoctocolors-cjs: 2.1.2
'@inquirer/search@1.0.7':
dependencies:
'@inquirer/core': 9.0.10
'@inquirer/figures': 1.0.5
'@inquirer/type': 1.5.2
yoctocolors-cjs: 2.1.2
'@inquirer/select@2.4.7':
dependencies:
'@inquirer/core': 9.0.10
'@inquirer/figures': 1.0.5
'@inquirer/type': 1.5.2
ansi-escapes: 4.3.2
yoctocolors-cjs: 2.1.2
'@inquirer/type@1.5.2':
dependencies:
mute-stream: 1.0.0
'@internationalized/date@3.5.5':
dependencies:
'@swc/helpers': 0.5.12
@ -5507,6 +5718,10 @@ snapshots:
dependencies:
'@types/unist': 2.0.11
'@types/mute-stream@0.0.4':
dependencies:
'@types/node': 22.5.1
'@types/node-cron@3.0.11': {}
'@types/node-forge@1.3.11':
@ -5523,6 +5738,8 @@ snapshots:
'@types/unist@2.0.11': {}
'@types/wrap-ansi@3.0.0': {}
'@typescript-eslint/eslint-plugin@8.3.0(@typescript-eslint/parser@8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4)':
dependencies:
'@eslint-community/regexpp': 4.11.0
@ -5895,6 +6112,10 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ansi-escapes@4.3.2:
dependencies:
type-fest: 0.21.3
ansi-regex@5.0.1: {}
ansi-regex@6.0.1: {}
@ -6049,6 +6270,8 @@ snapshots:
character-reference-invalid@1.1.4: {}
chardet@0.7.0: {}
check-error@2.1.1: {}
chokidar@3.6.0:
@ -6083,6 +6306,8 @@ snapshots:
cli-spinners@2.9.2: {}
cli-width@4.1.0: {}
clipboardy@4.0.0:
dependencies:
execa: 8.0.1
@ -6762,6 +6987,12 @@ snapshots:
exit-hook@2.2.1: {}
external-editor@3.1.0:
dependencies:
chardet: 0.7.0
iconv-lite: 0.4.24
tmp: 0.0.33
fast-copy@3.0.2: {}
fast-deep-equal@3.1.3: {}
@ -6974,6 +7205,10 @@ snapshots:
human-signals@5.0.0: {}
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
@ -7402,6 +7637,8 @@ snapshots:
mustache@4.2.0: {}
mute-stream@1.0.0: {}
nanoid@3.3.7: {}
natural-compare-lite@1.4.0: {}
@ -7484,6 +7721,8 @@ snapshots:
string-width: 7.2.0
strip-ansi: 7.1.0
os-tmpdir@1.0.2: {}
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
@ -8183,6 +8422,10 @@ snapshots:
tinyspy@3.0.0: {}
tmp@0.0.33:
dependencies:
os-tmpdir: 1.0.2
to-fast-properties@2.0.0: {}
to-regex-range@5.0.1:
@ -8225,6 +8468,8 @@ snapshots:
type-fest@0.20.2: {}
type-fest@0.21.3: {}
type-fest@0.6.0: {}
type-fest@0.8.1: {}
@ -8562,6 +8807,12 @@ snapshots:
- supports-color
- utf-8-validate
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
@ -8612,6 +8863,8 @@ snapshots:
yocto-queue@0.1.0: {}
yoctocolors-cjs@2.1.2: {}
youch@3.3.3:
dependencies:
cookie: 0.5.0