@beechcms/core 0.7.0 → 0.8.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/.tsbuildinfo +1 -1
- package/dist/antivirus/antivirus.interface.d.ts +12 -0
- package/dist/antivirus/antivirus.interface.d.ts.map +1 -0
- package/dist/antivirus/antivirus.interface.js +3 -0
- package/dist/antivirus/noop-antivirus.provider.d.ts +6 -0
- package/dist/antivirus/noop-antivirus.provider.d.ts.map +1 -0
- package/dist/antivirus/noop-antivirus.provider.js +8 -0
- package/dist/antivirus/virustotal-antivirus.provider.d.ts +8 -0
- package/dist/antivirus/virustotal-antivirus.provider.d.ts.map +1 -0
- package/dist/antivirus/virustotal-antivirus.provider.js +48 -0
- package/dist/common/hooks.d.ts +3 -0
- package/dist/common/hooks.d.ts.map +1 -1
- package/dist/content/richtext/richtext-render.d.ts.map +1 -1
- package/dist/content/richtext/richtext-render.js +14 -1
- package/dist/content/seed.repository.d.ts +22 -0
- package/dist/content/seed.repository.d.ts.map +1 -1
- package/dist/dashboard-layout/seed-layout.d.ts.map +1 -1
- package/dist/dashboard-layout/seed-layout.js +13 -6
- package/dist/engine/ddl.d.ts +15 -0
- package/dist/engine/ddl.d.ts.map +1 -1
- package/dist/engine/ddl.js +30 -1
- package/dist/engine/policies.d.ts.map +1 -1
- package/dist/engine/policies.js +5 -0
- package/dist/engine/seed-registry.d.ts.map +1 -1
- package/dist/engine/seed-registry.js +23 -4
- package/dist/engine/seed-types-generator.d.ts.map +1 -1
- package/dist/engine/seed-types-generator.js +8 -5
- package/dist/engine/seed-validation.d.ts +7 -0
- package/dist/engine/seed-validation.d.ts.map +1 -1
- package/dist/engine/seed-validation.js +59 -1
- package/dist/engine/types.d.ts +7 -0
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/engine/validation/richtext-sanitizer.d.ts.map +1 -1
- package/dist/engine/validation/richtext-sanitizer.js +2 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/media/file-types.d.ts +103 -9
- package/dist/media/file-types.d.ts.map +1 -1
- package/dist/media/file-types.js +336 -52
- package/dist/media/magic-bytes.d.ts +21 -0
- package/dist/media/magic-bytes.d.ts.map +1 -0
- package/dist/media/magic-bytes.js +66 -0
- package/dist/oauth/authorization-code.repository.d.ts +33 -0
- package/dist/oauth/authorization-code.repository.d.ts.map +1 -0
- package/dist/oauth/authorization-code.repository.js +3 -0
- package/dist/oauth/client.repository.d.ts +17 -0
- package/dist/oauth/client.repository.d.ts.map +1 -0
- package/dist/oauth/client.repository.js +3 -0
- package/dist/oauth/consent.repository.d.ts +25 -0
- package/dist/oauth/consent.repository.d.ts.map +1 -0
- package/dist/oauth/consent.repository.js +3 -0
- package/dist/oauth/pkce.d.ts +21 -0
- package/dist/oauth/pkce.d.ts.map +1 -0
- package/dist/oauth/pkce.js +47 -0
- package/dist/oauth/role-guard.d.ts +32 -0
- package/dist/oauth/role-guard.d.ts.map +1 -0
- package/dist/oauth/role-guard.js +14 -0
- package/dist/oauth/scopes.d.ts +27 -0
- package/dist/oauth/scopes.d.ts.map +1 -0
- package/dist/oauth/scopes.js +41 -0
- package/dist/oauth/token.repository.d.ts +46 -0
- package/dist/oauth/token.repository.d.ts.map +1 -0
- package/dist/oauth/token.repository.js +3 -0
- package/dist/observability/activity-logger.d.ts +1 -1
- package/dist/observability/activity-logger.d.ts.map +1 -1
- package/dist/rate-limit/rate-limiter.d.ts +13 -5
- package/dist/rate-limit/rate-limiter.d.ts.map +1 -1
- package/dist/rate-limit/token-bucket-rate-limiter.d.ts +20 -0
- package/dist/rate-limit/token-bucket-rate-limiter.d.ts.map +1 -0
- package/dist/rate-limit/token-bucket-rate-limiter.js +63 -0
- package/dist/search/vector-extractor.d.ts +11 -0
- package/dist/search/vector-extractor.d.ts.map +1 -0
- package/dist/search/vector-extractor.js +25 -0
- package/dist/search/vector.repository.d.ts +13 -0
- package/dist/search/vector.repository.d.ts.map +1 -0
- package/dist/search/vector.repository.js +3 -0
- package/dist/security/time-trap-token.repository.d.ts +9 -0
- package/dist/security/time-trap-token.repository.d.ts.map +1 -0
- package/dist/security/time-trap-token.repository.js +3 -0
- package/dist/security/time-trap.d.ts +7 -0
- package/dist/security/time-trap.d.ts.map +1 -0
- package/dist/security/time-trap.js +49 -0
- package/dist/webhook-crypto.d.ts +2 -0
- package/dist/webhook-crypto.d.ts.map +1 -1
- package/dist/webhook-crypto.js +2 -2
- package/dist/webhooks/webhook-crypto.d.ts +2 -0
- package/dist/webhooks/webhook-crypto.d.ts.map +1 -1
- package/dist/webhooks/webhook-crypto.js +2 -2
- package/package.json +20 -12
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
import { indexableSearchBranches } from '../engine/ddl.js';
|
|
4
|
+
/**
|
|
5
|
+
* Extracts and concatenates text from all public indexable text/richtext branches of a seed.
|
|
6
|
+
* Enforces privacy policies by utilizing indexableSearchBranches.
|
|
7
|
+
*
|
|
8
|
+
* @param seed The seed definition.
|
|
9
|
+
* @param entry The content entry record.
|
|
10
|
+
* @returns The combined text, or null if no indexable text exists.
|
|
11
|
+
*/
|
|
12
|
+
export function extractIndexableText(seed, entry) {
|
|
13
|
+
const branches = indexableSearchBranches(seed);
|
|
14
|
+
if (branches.length === 0)
|
|
15
|
+
return null;
|
|
16
|
+
const texts = [];
|
|
17
|
+
for (const branch of branches) {
|
|
18
|
+
const val = entry[branch.alias];
|
|
19
|
+
if (val && typeof val === 'string') {
|
|
20
|
+
texts.push(val);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const combined = texts.join(' ').trim();
|
|
24
|
+
return combined.length > 0 ? combined : null;
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Seed } from '../engine/types.js';
|
|
2
|
+
export interface IVectorRepository {
|
|
3
|
+
/** Saves or updates the embedding vector for an entry */
|
|
4
|
+
saveVector(seed: Seed, entryId: string, vector: Float32Array): Promise<void>;
|
|
5
|
+
/** Removes the embedding vector (used when unpublished/deleted) */
|
|
6
|
+
deleteVector(seed: Seed, entryId: string): Promise<void>;
|
|
7
|
+
/** Retrieves all vectors for a given seed to compile to R2 */
|
|
8
|
+
getAllVectors(seed: Seed): Promise<{
|
|
9
|
+
entryId: string;
|
|
10
|
+
vector: Float32Array;
|
|
11
|
+
}[]>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=vector.repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vector.repository.d.ts","sourceRoot":"","sources":["../../src/search/vector.repository.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAA;AAE9C,MAAM,WAAW,iBAAiB;IAChC,yDAAyD;IACzD,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE5E,mEAAmE;IACnE,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAExD,8DAA8D;IAC9D,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,YAAY,CAAA;KAAE,EAAE,CAAC,CAAA;CAChF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ITimeTrapTokenRepository {
|
|
2
|
+
/** Checks if a time-trap token hash has already been consumed. */
|
|
3
|
+
isTokenUsed(tokenHash: string): Promise<boolean>;
|
|
4
|
+
/** Marks a time-trap token hash as consumed with an expiration timestamp. */
|
|
5
|
+
markTokenUsed(tokenHash: string, usedAt: number, expiresAt: number): Promise<void>;
|
|
6
|
+
/** Cleans up expired token entries. */
|
|
7
|
+
cleanup(nowSeconds: number): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=time-trap-token.repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-trap-token.repository.d.ts","sourceRoot":"","sources":["../../src/security/time-trap-token.repository.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,wBAAwB;IACvC,kEAAkE;IAClE,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAChD,6EAA6E;IAC7E,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClF,uCAAuC;IACvC,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3C"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function generateTimeTrapToken(secret: string, timestampSeconds?: number): Promise<string>;
|
|
2
|
+
export declare function verifyTimeTrapToken(token: string, secret: string, minDeltaSeconds?: number, maxAgeSeconds?: number): Promise<{
|
|
3
|
+
valid: boolean;
|
|
4
|
+
reason?: string;
|
|
5
|
+
elapsedSeconds?: number;
|
|
6
|
+
}>;
|
|
7
|
+
//# sourceMappingURL=time-trap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-trap.d.ts","sourceRoot":"","sources":["../../src/security/time-trap.ts"],"names":[],"mappings":"AAaA,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAOtG;AAED,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,eAAe,GAAE,MAAY,EAC7B,aAAa,GAAE,MAAa,GAC3B,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAyCvE"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
async function getHmacKey(secret) {
|
|
4
|
+
return crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']);
|
|
5
|
+
}
|
|
6
|
+
export async function generateTimeTrapToken(secret, timestampSeconds) {
|
|
7
|
+
const t0 = timestampSeconds ?? Math.floor(Date.now() / 1000);
|
|
8
|
+
const payload = `t0_${t0}`;
|
|
9
|
+
const key = await getHmacKey(secret);
|
|
10
|
+
const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(payload));
|
|
11
|
+
const sigHex = Array.from(new Uint8Array(signature)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
12
|
+
return `${payload}.${sigHex}`;
|
|
13
|
+
}
|
|
14
|
+
export async function verifyTimeTrapToken(token, secret, minDeltaSeconds = 1.5, maxAgeSeconds = 3600) {
|
|
15
|
+
if (!token || typeof token !== 'string') {
|
|
16
|
+
return { valid: false, reason: 'Missing or invalid token format' };
|
|
17
|
+
}
|
|
18
|
+
const parts = token.split('.');
|
|
19
|
+
if (parts.length !== 2) {
|
|
20
|
+
return { valid: false, reason: 'Malformed token structure' };
|
|
21
|
+
}
|
|
22
|
+
const [payload, sigHex] = parts;
|
|
23
|
+
if (!payload.startsWith('t0_')) {
|
|
24
|
+
return { valid: false, reason: 'Invalid token prefix' };
|
|
25
|
+
}
|
|
26
|
+
const t0 = Number.parseInt(payload.slice(3), 10);
|
|
27
|
+
if (!Number.isFinite(t0)) {
|
|
28
|
+
return { valid: false, reason: 'Invalid timestamp value' };
|
|
29
|
+
}
|
|
30
|
+
const hexMatches = sigHex.match(/.{1,2}/g);
|
|
31
|
+
if (!hexMatches || hexMatches.length !== 32) {
|
|
32
|
+
return { valid: false, reason: 'Invalid signature encoding' };
|
|
33
|
+
}
|
|
34
|
+
const key = await getHmacKey(secret);
|
|
35
|
+
const sigBytes = new Uint8Array(hexMatches.map(byte => parseInt(byte, 16)));
|
|
36
|
+
const isValidSig = await crypto.subtle.verify('HMAC', key, sigBytes, new TextEncoder().encode(payload));
|
|
37
|
+
if (!isValidSig) {
|
|
38
|
+
return { valid: false, reason: 'Cryptographic signature mismatch' };
|
|
39
|
+
}
|
|
40
|
+
const now = Date.now() / 1000;
|
|
41
|
+
const elapsed = now - t0;
|
|
42
|
+
if (elapsed < minDeltaSeconds) {
|
|
43
|
+
return { valid: false, reason: `Submission too fast (${elapsed.toFixed(2)}s < ${minDeltaSeconds}s)`, elapsedSeconds: elapsed };
|
|
44
|
+
}
|
|
45
|
+
if (elapsed > maxAgeSeconds) {
|
|
46
|
+
return { valid: false, reason: `Token expired (${elapsed.toFixed(0)}s > ${maxAgeSeconds}s)`, elapsedSeconds: elapsed };
|
|
47
|
+
}
|
|
48
|
+
return { valid: true, elapsedSeconds: elapsed };
|
|
49
|
+
}
|
package/dist/webhook-crypto.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Signs a raw request body. Returns `sha256=<hex>`. */
|
|
2
2
|
export declare function signWebhookBody(body: string, secret: string): Promise<string>;
|
|
3
|
+
/** Constant-time comparison of two equal-or-unequal-length strings. */
|
|
4
|
+
export declare function timingSafeEqual(a: string, b: string): boolean;
|
|
3
5
|
/**
|
|
4
6
|
* Verifies an inbound signature against the recomputed HMAC.
|
|
5
7
|
* Accepts the `sha256=` prefix on the provided signature (optional).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook-crypto.d.ts","sourceRoot":"","sources":["../src/webhook-crypto.ts"],"names":[],"mappings":"AAqBA,wDAAwD;AACxD,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAInF;
|
|
1
|
+
{"version":3,"file":"webhook-crypto.d.ts","sourceRoot":"","sources":["../src/webhook-crypto.ts"],"names":[],"mappings":"AAqBA,wDAAwD;AACxD,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAInF;AAED,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAK7D;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACpC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAKlB"}
|
package/dist/webhook-crypto.js
CHANGED
|
@@ -15,8 +15,8 @@ export async function signWebhookBody(body, secret) {
|
|
|
15
15
|
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(body));
|
|
16
16
|
return SIG_PREFIX + toHex(sig);
|
|
17
17
|
}
|
|
18
|
-
/** Constant-time comparison of two equal-or-unequal-length
|
|
19
|
-
function timingSafeEqual(a, b) {
|
|
18
|
+
/** Constant-time comparison of two equal-or-unequal-length strings. */
|
|
19
|
+
export function timingSafeEqual(a, b) {
|
|
20
20
|
if (a.length !== b.length)
|
|
21
21
|
return false;
|
|
22
22
|
let mismatch = 0;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Signs a raw request body. Returns `sha256=<hex>`. */
|
|
2
2
|
export declare function signWebhookBody(body: string, secret: string): Promise<string>;
|
|
3
|
+
/** Constant-time comparison of two equal-or-unequal-length strings. */
|
|
4
|
+
export declare function timingSafeEqual(a: string, b: string): boolean;
|
|
3
5
|
/**
|
|
4
6
|
* Verifies an inbound signature against the recomputed HMAC.
|
|
5
7
|
* Accepts the `sha256=` prefix on the provided signature (optional).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook-crypto.d.ts","sourceRoot":"","sources":["../../src/webhooks/webhook-crypto.ts"],"names":[],"mappings":"AAqBA,wDAAwD;AACxD,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAInF;
|
|
1
|
+
{"version":3,"file":"webhook-crypto.d.ts","sourceRoot":"","sources":["../../src/webhooks/webhook-crypto.ts"],"names":[],"mappings":"AAqBA,wDAAwD;AACxD,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAInF;AAED,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAK7D;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACpC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAKlB"}
|
|
@@ -15,8 +15,8 @@ export async function signWebhookBody(body, secret) {
|
|
|
15
15
|
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(body));
|
|
16
16
|
return SIG_PREFIX + toHex(sig);
|
|
17
17
|
}
|
|
18
|
-
/** Constant-time comparison of two equal-or-unequal-length
|
|
19
|
-
function timingSafeEqual(a, b) {
|
|
18
|
+
/** Constant-time comparison of two equal-or-unequal-length strings. */
|
|
19
|
+
export function timingSafeEqual(a, b) {
|
|
20
20
|
if (a.length !== b.length)
|
|
21
21
|
return false;
|
|
22
22
|
let mismatch = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beechcms/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"import": "./dist/index.js",
|
|
14
14
|
"types": "./dist/index.d.ts"
|
|
15
15
|
},
|
|
16
|
+
"./richtext-render": {
|
|
17
|
+
"import": "./dist/content/richtext/richtext-render.js",
|
|
18
|
+
"types": "./dist/content/richtext/richtext-render.d.ts"
|
|
19
|
+
},
|
|
16
20
|
"./webhook-crypto": {
|
|
17
21
|
"import": "./dist/webhooks/webhook-crypto.js",
|
|
18
22
|
"types": "./dist/webhooks/webhook-crypto.d.ts"
|
|
@@ -24,17 +28,21 @@
|
|
|
24
28
|
"vitest": "^4.1.0"
|
|
25
29
|
},
|
|
26
30
|
"dependencies": {
|
|
27
|
-
"@tiptap/core": "^3.
|
|
28
|
-
"@tiptap/extension-
|
|
29
|
-
"@tiptap/extension-
|
|
30
|
-
"@tiptap/extension-
|
|
31
|
-
"@tiptap/extension-
|
|
32
|
-
"@tiptap/extension-
|
|
33
|
-
"@tiptap/extension-
|
|
34
|
-
"@tiptap/extension-
|
|
35
|
-
"@tiptap/extension-
|
|
36
|
-
"@tiptap/
|
|
37
|
-
"@tiptap/
|
|
31
|
+
"@tiptap/core": "^3.31.3",
|
|
32
|
+
"@tiptap/extension-color": "^3.31.3",
|
|
33
|
+
"@tiptap/extension-highlight": "^3.31.3",
|
|
34
|
+
"@tiptap/extension-image": "^3.31.3",
|
|
35
|
+
"@tiptap/extension-link": "^3.31.3",
|
|
36
|
+
"@tiptap/extension-list": "^3.31.3",
|
|
37
|
+
"@tiptap/extension-mathematics": "^3.31.3",
|
|
38
|
+
"@tiptap/extension-subscript": "^3.31.3",
|
|
39
|
+
"@tiptap/extension-superscript": "^3.31.3",
|
|
40
|
+
"@tiptap/extension-table": "^3.31.3",
|
|
41
|
+
"@tiptap/extension-text-align": "^3.31.3",
|
|
42
|
+
"@tiptap/extension-text-style": "^3.31.3",
|
|
43
|
+
"@tiptap/extension-underline": "^3.31.3",
|
|
44
|
+
"@tiptap/html": "^3.31.3",
|
|
45
|
+
"@tiptap/starter-kit": "^3.31.3",
|
|
38
46
|
"katex": "^0.16.11",
|
|
39
47
|
"zod": "^4.3.6"
|
|
40
48
|
},
|