@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.
Files changed (90) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/antivirus/antivirus.interface.d.ts +12 -0
  3. package/dist/antivirus/antivirus.interface.d.ts.map +1 -0
  4. package/dist/antivirus/antivirus.interface.js +3 -0
  5. package/dist/antivirus/noop-antivirus.provider.d.ts +6 -0
  6. package/dist/antivirus/noop-antivirus.provider.d.ts.map +1 -0
  7. package/dist/antivirus/noop-antivirus.provider.js +8 -0
  8. package/dist/antivirus/virustotal-antivirus.provider.d.ts +8 -0
  9. package/dist/antivirus/virustotal-antivirus.provider.d.ts.map +1 -0
  10. package/dist/antivirus/virustotal-antivirus.provider.js +48 -0
  11. package/dist/common/hooks.d.ts +3 -0
  12. package/dist/common/hooks.d.ts.map +1 -1
  13. package/dist/content/richtext/richtext-render.d.ts.map +1 -1
  14. package/dist/content/richtext/richtext-render.js +14 -1
  15. package/dist/content/seed.repository.d.ts +22 -0
  16. package/dist/content/seed.repository.d.ts.map +1 -1
  17. package/dist/dashboard-layout/seed-layout.d.ts.map +1 -1
  18. package/dist/dashboard-layout/seed-layout.js +13 -6
  19. package/dist/engine/ddl.d.ts +15 -0
  20. package/dist/engine/ddl.d.ts.map +1 -1
  21. package/dist/engine/ddl.js +30 -1
  22. package/dist/engine/policies.d.ts.map +1 -1
  23. package/dist/engine/policies.js +5 -0
  24. package/dist/engine/seed-registry.d.ts.map +1 -1
  25. package/dist/engine/seed-registry.js +23 -4
  26. package/dist/engine/seed-types-generator.d.ts.map +1 -1
  27. package/dist/engine/seed-types-generator.js +8 -5
  28. package/dist/engine/seed-validation.d.ts +7 -0
  29. package/dist/engine/seed-validation.d.ts.map +1 -1
  30. package/dist/engine/seed-validation.js +59 -1
  31. package/dist/engine/types.d.ts +7 -0
  32. package/dist/engine/types.d.ts.map +1 -1
  33. package/dist/engine/validation/richtext-sanitizer.d.ts.map +1 -1
  34. package/dist/engine/validation/richtext-sanitizer.js +2 -1
  35. package/dist/index.d.ts +16 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +16 -1
  38. package/dist/media/file-types.d.ts +103 -9
  39. package/dist/media/file-types.d.ts.map +1 -1
  40. package/dist/media/file-types.js +336 -52
  41. package/dist/media/magic-bytes.d.ts +21 -0
  42. package/dist/media/magic-bytes.d.ts.map +1 -0
  43. package/dist/media/magic-bytes.js +66 -0
  44. package/dist/oauth/authorization-code.repository.d.ts +33 -0
  45. package/dist/oauth/authorization-code.repository.d.ts.map +1 -0
  46. package/dist/oauth/authorization-code.repository.js +3 -0
  47. package/dist/oauth/client.repository.d.ts +17 -0
  48. package/dist/oauth/client.repository.d.ts.map +1 -0
  49. package/dist/oauth/client.repository.js +3 -0
  50. package/dist/oauth/consent.repository.d.ts +25 -0
  51. package/dist/oauth/consent.repository.d.ts.map +1 -0
  52. package/dist/oauth/consent.repository.js +3 -0
  53. package/dist/oauth/pkce.d.ts +21 -0
  54. package/dist/oauth/pkce.d.ts.map +1 -0
  55. package/dist/oauth/pkce.js +47 -0
  56. package/dist/oauth/role-guard.d.ts +32 -0
  57. package/dist/oauth/role-guard.d.ts.map +1 -0
  58. package/dist/oauth/role-guard.js +14 -0
  59. package/dist/oauth/scopes.d.ts +27 -0
  60. package/dist/oauth/scopes.d.ts.map +1 -0
  61. package/dist/oauth/scopes.js +41 -0
  62. package/dist/oauth/token.repository.d.ts +46 -0
  63. package/dist/oauth/token.repository.d.ts.map +1 -0
  64. package/dist/oauth/token.repository.js +3 -0
  65. package/dist/observability/activity-logger.d.ts +1 -1
  66. package/dist/observability/activity-logger.d.ts.map +1 -1
  67. package/dist/rate-limit/rate-limiter.d.ts +13 -5
  68. package/dist/rate-limit/rate-limiter.d.ts.map +1 -1
  69. package/dist/rate-limit/token-bucket-rate-limiter.d.ts +20 -0
  70. package/dist/rate-limit/token-bucket-rate-limiter.d.ts.map +1 -0
  71. package/dist/rate-limit/token-bucket-rate-limiter.js +63 -0
  72. package/dist/search/vector-extractor.d.ts +11 -0
  73. package/dist/search/vector-extractor.d.ts.map +1 -0
  74. package/dist/search/vector-extractor.js +25 -0
  75. package/dist/search/vector.repository.d.ts +13 -0
  76. package/dist/search/vector.repository.d.ts.map +1 -0
  77. package/dist/search/vector.repository.js +3 -0
  78. package/dist/security/time-trap-token.repository.d.ts +9 -0
  79. package/dist/security/time-trap-token.repository.d.ts.map +1 -0
  80. package/dist/security/time-trap-token.repository.js +3 -0
  81. package/dist/security/time-trap.d.ts +7 -0
  82. package/dist/security/time-trap.d.ts.map +1 -0
  83. package/dist/security/time-trap.js +49 -0
  84. package/dist/webhook-crypto.d.ts +2 -0
  85. package/dist/webhook-crypto.d.ts.map +1 -1
  86. package/dist/webhook-crypto.js +2 -2
  87. package/dist/webhooks/webhook-crypto.d.ts +2 -0
  88. package/dist/webhooks/webhook-crypto.d.ts.map +1 -1
  89. package/dist/webhooks/webhook-crypto.js +2 -2
  90. package/package.json +20 -12
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Result of a file magic bytes signature validation.
3
+ */
4
+ export interface MagicBytesValidationResult {
5
+ /** Whether the byte buffer matches the declared format signature. */
6
+ valid: boolean;
7
+ /** Detected canonical primary MIME type if valid or identified. */
8
+ detectedMime?: string;
9
+ /** Human-readable error description when validation fails. */
10
+ error?: string;
11
+ }
12
+ /**
13
+ * Validates that an uploaded file's binary content matches its declared MIME type,
14
+ * protecting against MIME-spoofing, disguised executables, and blocked formats (e.g. SVG).
15
+ *
16
+ * @param buffer - The raw binary buffer (ArrayBuffer or Uint8Array).
17
+ * @param declaredMime - The client-declared MIME type string.
18
+ * @returns A MagicBytesValidationResult indicating validity and details.
19
+ */
20
+ export declare function verifyMagicBytes(buffer: ArrayBuffer | Uint8Array, declaredMime: string): MagicBytesValidationResult;
21
+ //# sourceMappingURL=magic-bytes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"magic-bytes.d.ts","sourceRoot":"","sources":["../../src/media/magic-bytes.ts"],"names":[],"mappings":"AAWA;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;IACd,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAiBD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,WAAW,GAAG,UAAU,EAChC,YAAY,EAAE,MAAM,GACnB,0BAA0B,CAgD5B"}
@@ -0,0 +1,66 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ import { SUPPORTED_FILE_TYPES, getFileTypeByMime, BLOCKED_IMAGE_MIME_TYPES, matchesFileSignature, } from './file-types.js';
4
+ /**
5
+ * Iterates across supported binary definitions to find any matching file signature.
6
+ *
7
+ * @param bytes - The raw byte buffer to analyze.
8
+ * @returns The matching FileTypeDefinition if recognized, or undefined.
9
+ */
10
+ function detectBinaryType(bytes) {
11
+ for (const def of Object.values(SUPPORTED_FILE_TYPES)) {
12
+ if (def.isBinary && matchesFileSignature(def, bytes)) {
13
+ return def;
14
+ }
15
+ }
16
+ return undefined;
17
+ }
18
+ /**
19
+ * Validates that an uploaded file's binary content matches its declared MIME type,
20
+ * protecting against MIME-spoofing, disguised executables, and blocked formats (e.g. SVG).
21
+ *
22
+ * @param buffer - The raw binary buffer (ArrayBuffer or Uint8Array).
23
+ * @param declaredMime - The client-declared MIME type string.
24
+ * @returns A MagicBytesValidationResult indicating validity and details.
25
+ */
26
+ export function verifyMagicBytes(buffer, declaredMime) {
27
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
28
+ if (bytes.length < 2) {
29
+ return { valid: false, error: 'File buffer too small for signature inspection' };
30
+ }
31
+ const normalizedDeclared = declaredMime.split(';')[0].trim().toLowerCase();
32
+ // Unconditionally reject blocked MIME types (e.g. SVG)
33
+ if (BLOCKED_IMAGE_MIME_TYPES.includes(normalizedDeclared)) {
34
+ return {
35
+ valid: false,
36
+ error: `File type '${declaredMime}' is blocked for security reasons`,
37
+ };
38
+ }
39
+ const declaredDef = getFileTypeByMime(normalizedDeclared);
40
+ if (!declaredDef) {
41
+ return { valid: false, error: `Unrecognized file signature for declared MIME ${declaredMime}` };
42
+ }
43
+ // Non-binary types (e.g. text/plain, text/csv, text/markdown, application/json)
44
+ if (!declaredDef.isBinary) {
45
+ return { valid: true, detectedMime: declaredDef.primaryMime };
46
+ }
47
+ // If buffer is smaller than the minimum bytes needed for initial signature inspection (typically 4, or 2-3 for short signatures)
48
+ const minInspectionLength = Math.min(4, declaredDef.magicBytes?.length ?? 4);
49
+ if (bytes.length < minInspectionLength) {
50
+ return { valid: false, error: 'File buffer too small for signature inspection' };
51
+ }
52
+ // Binary types: verify signature against declared definition
53
+ if (matchesFileSignature(declaredDef, bytes)) {
54
+ return { valid: true, detectedMime: declaredDef.primaryMime };
55
+ }
56
+ // Signature mismatch: check if it matches another known binary signature
57
+ const detectedDef = detectBinaryType(bytes);
58
+ if (detectedDef) {
59
+ return {
60
+ valid: false,
61
+ detectedMime: detectedDef.primaryMime,
62
+ error: `Signature mismatch: file is ${detectedDef.extension.toUpperCase()} but declared as ${declaredMime}`,
63
+ };
64
+ }
65
+ return { valid: false, error: `Unrecognized file signature for declared MIME ${declaredMime}` };
66
+ }
@@ -0,0 +1,33 @@
1
+ import type { OAuthScope } from './scopes.js';
2
+ export interface NewAuthorizationCode {
3
+ /** SHA-256 hex hash of the code. The plaintext code is never persisted. */
4
+ codeHash: string;
5
+ clientId: string;
6
+ userId: string;
7
+ scope: OAuthScope[];
8
+ redirectUri: string;
9
+ codeChallenge: string;
10
+ /** Always 'S256'; the column CHECK constraint rejects anything else. */
11
+ codeChallengeMethod: 'S256';
12
+ expiresAt: number;
13
+ }
14
+ export interface AuthorizationCodeRecord extends NewAuthorizationCode {
15
+ createdAt: number;
16
+ consumedAt: number | null;
17
+ }
18
+ export interface IOAuthAuthorizationCodeRepository {
19
+ /** Persists a new single-use authorization code. Hash only, never plaintext. */
20
+ save(record: NewAuthorizationCode): Promise<void>;
21
+ /**
22
+ * Returns the code regardless of its consumed state, provided it is unexpired.
23
+ * Callers MUST inspect `consumedAt`: a non-null value means replay, which
24
+ * requires cascade revocation via IOAuthTokenRepository.revokeByAuthorizationCode.
25
+ */
26
+ findByHash(codeHash: string, nowTimestamp: number): Promise<AuthorizationCodeRecord | null>;
27
+ /**
28
+ * Atomically marks the code consumed. Returns true only for the first caller;
29
+ * every subsequent call returns false, which is the replay signal.
30
+ */
31
+ consumeByHash(codeHash: string, nowTimestamp: number): Promise<boolean>;
32
+ }
33
+ //# sourceMappingURL=authorization-code.repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorization-code.repository.d.ts","sourceRoot":"","sources":["../../src/oauth/authorization-code.repository.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C,MAAM,WAAW,oBAAoB;IACnC,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,MAAM,CAAA;IACrB,wEAAwE;IACxE,mBAAmB,EAAE,MAAM,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,uBAAwB,SAAQ,oBAAoB;IACnE,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,MAAM,WAAW,iCAAiC;IAChD,gFAAgF;IAChF,IAAI,CAAC,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEjD;;;;OAIG;IACH,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAA;IAE3F;;;OAGG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACxE"}
@@ -0,0 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ export {};
@@ -0,0 +1,17 @@
1
+ import type { OAuthScope } from './scopes.js';
2
+ export interface OAuthClientRecord {
3
+ clientId: string;
4
+ name: string;
5
+ /** Registered redirect URIs. Loopback entries are matched ignoring the port. */
6
+ redirectUris: string[];
7
+ allowedScopes: OAuthScope[];
8
+ /** True for clients that cannot hold a secret (native/CLI). PKCE is required regardless. */
9
+ isPublic: boolean;
10
+ createdAt: number;
11
+ disabledAt: number | null;
12
+ }
13
+ export interface IOAuthClientRepository {
14
+ /** Returns the client, or null when unknown or disabled. */
15
+ findActiveById(clientId: string): Promise<OAuthClientRecord | null>;
16
+ }
17
+ //# sourceMappingURL=client.repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.repository.d.ts","sourceRoot":"","sources":["../../src/oauth/client.repository.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,aAAa,EAAE,UAAU,EAAE,CAAA;IAC3B,4FAA4F;IAC5F,QAAQ,EAAE,OAAO,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACrC,4DAA4D;IAC5D,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAA;CACpE"}
@@ -0,0 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ export {};
@@ -0,0 +1,25 @@
1
+ import type { OAuthScope } from './scopes.js';
2
+ export interface ConsentRecord {
3
+ id: string;
4
+ clientId: string;
5
+ userId: string;
6
+ /** Cumulative set of scopes the user has approved for this client. */
7
+ scopes: OAuthScope[];
8
+ createdAt: number;
9
+ updatedAt: number;
10
+ revokedAt: number | null;
11
+ }
12
+ export interface IOAuthConsentRepository {
13
+ /** Returns the live consent for the pair, or null when absent or revoked. */
14
+ findActive(clientId: string, userId: string): Promise<ConsentRecord | null>;
15
+ /**
16
+ * Records consent for the pair, unioning `scopes` into any existing live grant
17
+ * and reviving a previously revoked row. Idempotent for an unchanged scope set.
18
+ */
19
+ grant(id: string, clientId: string, userId: string, scopes: readonly OAuthScope[], nowTimestamp: number): Promise<void>;
20
+ /** Revokes the consent. False when there was nothing live to revoke. */
21
+ revoke(clientId: string, userId: string, nowTimestamp: number): Promise<boolean>;
22
+ /** Lists all live consents for a user, newest first. */
23
+ listForUser(userId: string): Promise<ConsentRecord[]>;
24
+ }
25
+ //# sourceMappingURL=consent.repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consent.repository.d.ts","sourceRoot":"","sources":["../../src/oauth/consent.repository.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,sEAAsE;IACtE,MAAM,EAAE,UAAU,EAAE,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,6EAA6E;IAC7E,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAA;IAE3E;;;OAGG;IACH,KAAK,CACH,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,SAAS,UAAU,EAAE,EAC7B,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAAA;IAEhB,wEAAwE;IACxE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAEhF,wDAAwD;IACxD,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;CACtD"}
@@ -0,0 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ export {};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * PKCE (RFC 7636) verification. `S256` only — `plain` is rejected unconditionally,
3
+ * per OAuth 2.1, so no weak fallback configuration can ever exist.
4
+ */
5
+ /** The only accepted code challenge method. */
6
+ export declare const PKCE_CHALLENGE_METHOD: "S256";
7
+ export type PkceChallengeMethod = typeof PKCE_CHALLENGE_METHOD;
8
+ /** Minimum / maximum code_verifier length, RFC 7636 §4.1. */
9
+ export declare const PKCE_VERIFIER_MIN_LENGTH = 43;
10
+ export declare const PKCE_VERIFIER_MAX_LENGTH = 128;
11
+ /** Validates the code_verifier character set and length. */
12
+ export declare function isValidCodeVerifier(verifier: string): boolean;
13
+ /** Computes BASE64URL(SHA256(ASCII(verifier))). */
14
+ export declare function deriveCodeChallenge(verifier: string): Promise<string>;
15
+ /**
16
+ * Verifies a code_verifier against a stored code_challenge.
17
+ * Returns false — never throws — on any malformed input, unsupported method, or
18
+ * mismatch, so callers cannot distinguish failure modes from the return value.
19
+ */
20
+ export declare function verifyPkceChallenge(verifier: string, challenge: string, method: string): Promise<boolean>;
21
+ //# sourceMappingURL=pkce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../src/oauth/pkce.ts"],"names":[],"mappings":"AAGA;;;GAGG;AAEH,+CAA+C;AAC/C,eAAO,MAAM,qBAAqB,EAAG,MAAe,CAAA;AAEpD,MAAM,MAAM,mBAAmB,GAAG,OAAO,qBAAqB,CAAA;AAE9D,6DAA6D;AAC7D,eAAO,MAAM,wBAAwB,KAAK,CAAA;AAC1C,eAAO,MAAM,wBAAwB,MAAM,CAAA;AAI3C,4DAA4D;AAC5D,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAM7D;AAQD,mDAAmD;AACnD,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG3E;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAUlB"}
@@ -0,0 +1,47 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ /**
4
+ * PKCE (RFC 7636) verification. `S256` only — `plain` is rejected unconditionally,
5
+ * per OAuth 2.1, so no weak fallback configuration can ever exist.
6
+ */
7
+ /** The only accepted code challenge method. */
8
+ export const PKCE_CHALLENGE_METHOD = 'S256';
9
+ /** Minimum / maximum code_verifier length, RFC 7636 §4.1. */
10
+ export const PKCE_VERIFIER_MIN_LENGTH = 43;
11
+ export const PKCE_VERIFIER_MAX_LENGTH = 128;
12
+ const VERIFIER_PATTERN = /^[A-Za-z0-9\-._~]+$/;
13
+ /** Validates the code_verifier character set and length. */
14
+ export function isValidCodeVerifier(verifier) {
15
+ return (verifier.length >= PKCE_VERIFIER_MIN_LENGTH &&
16
+ verifier.length <= PKCE_VERIFIER_MAX_LENGTH &&
17
+ VERIFIER_PATTERN.test(verifier));
18
+ }
19
+ /** Base64url-encodes bytes without padding, per RFC 7636 §A. */
20
+ function base64UrlEncode(bytes) {
21
+ const binary = String.fromCharCode(...new Uint8Array(bytes));
22
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
23
+ }
24
+ /** Computes BASE64URL(SHA256(ASCII(verifier))). */
25
+ export async function deriveCodeChallenge(verifier) {
26
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
27
+ return base64UrlEncode(digest);
28
+ }
29
+ /**
30
+ * Verifies a code_verifier against a stored code_challenge.
31
+ * Returns false — never throws — on any malformed input, unsupported method, or
32
+ * mismatch, so callers cannot distinguish failure modes from the return value.
33
+ */
34
+ export async function verifyPkceChallenge(verifier, challenge, method) {
35
+ if (method !== PKCE_CHALLENGE_METHOD)
36
+ return false;
37
+ if (!isValidCodeVerifier(verifier))
38
+ return false;
39
+ const derived = await deriveCodeChallenge(verifier);
40
+ if (derived.length !== challenge.length)
41
+ return false;
42
+ let mismatch = 0;
43
+ for (let index = 0; index < derived.length; index += 1) {
44
+ mismatch |= derived.charCodeAt(index) ^ challenge.charCodeAt(index);
45
+ }
46
+ return mismatch === 0;
47
+ }
@@ -0,0 +1,32 @@
1
+ import type { OAuthScope } from './scopes.js';
2
+ /** Outcome of a role arbitration. `deniedScopes` is empty when fully granted. */
3
+ export interface ScopeGrantDecision {
4
+ grantedScopes: OAuthScope[];
5
+ deniedScopes: OAuthScope[];
6
+ }
7
+ /**
8
+ * Arbitrates which of the requested scopes a given user role may grant.
9
+ *
10
+ * This is the ONLY place role-based authorization may live in the OAuth flow.
11
+ * `/oauth/authorize` and `/oauth/token` must never branch on `role` themselves,
12
+ * so introducing a real role system later requires swapping the implementation
13
+ * bound in repositoryMiddleware and nothing else.
14
+ */
15
+ export interface IRoleGuard {
16
+ /**
17
+ * @param role - The resource owner's role claim, or undefined when absent.
18
+ * @param requestedScopes - Scopes the client asked for, already validated.
19
+ */
20
+ arbitrate(role: string | undefined, requestedScopes: readonly OAuthScope[]): Promise<ScopeGrantDecision>;
21
+ }
22
+ /**
23
+ * Stub guard for the pre-roles world: grants every requested scope to every role.
24
+ *
25
+ * This permissiveness is EXPLICIT and directly tested, not an accidental default.
26
+ * When the roles feature lands, replace this binding with a real adapter; the
27
+ * behaviour change will then be visible as a failing test here, by design.
28
+ */
29
+ export declare class AllowAllRoleGuard implements IRoleGuard {
30
+ arbitrate(_role: string | undefined, requestedScopes: readonly OAuthScope[]): Promise<ScopeGrantDecision>;
31
+ }
32
+ //# sourceMappingURL=role-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-guard.d.ts","sourceRoot":"","sources":["../../src/oauth/role-guard.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,UAAU,EAAE,CAAA;IAC3B,YAAY,EAAE,UAAU,EAAE,CAAA;CAC3B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,eAAe,EAAE,SAAS,UAAU,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;CACzG;AAED;;;;;;GAMG;AACH,qBAAa,iBAAkB,YAAW,UAAU;IAC5C,SAAS,CACb,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,eAAe,EAAE,SAAS,UAAU,EAAE,GACrC,OAAO,CAAC,kBAAkB,CAAC;CAG/B"}
@@ -0,0 +1,14 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ /**
4
+ * Stub guard for the pre-roles world: grants every requested scope to every role.
5
+ *
6
+ * This permissiveness is EXPLICIT and directly tested, not an accidental default.
7
+ * When the roles feature lands, replace this binding with a real adapter; the
8
+ * behaviour change will then be visible as a failing test here, by design.
9
+ */
10
+ export class AllowAllRoleGuard {
11
+ async arbitrate(_role, requestedScopes) {
12
+ return { grantedScopes: [...requestedScopes], deniedScopes: [] };
13
+ }
14
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * OAuth scope vocabulary. Scopes map 1:1 onto the MCP tools exposed today; no
3
+ * speculative scopes are defined.
4
+ *
5
+ * - `schema:read` -> beech_list_seeds, beech_get_seed, beech_schema_export,
6
+ * beech_schema_validate, beech_schema_plan
7
+ * - `schema:write` -> beech_schema_apply
8
+ *
9
+ * `beech_schema_plan` is deliberately classified as READ: despite the imperative
10
+ * name it is a dry-run that computes a migration plan and mutates nothing.
11
+ */
12
+ export declare const OAUTH_SCOPES: readonly ["schema:read", "schema:write"];
13
+ export type OAuthScope = (typeof OAUTH_SCOPES)[number];
14
+ /** Narrows an arbitrary string to a known scope. */
15
+ export declare function isOAuthScope(value: string): value is OAuthScope;
16
+ /**
17
+ * Parses an RFC 6749 §3.3 space-delimited scope string.
18
+ * Returns null if the string is empty or contains any unknown scope — callers
19
+ * must reject the request with `invalid_scope` rather than silently dropping it.
20
+ * Duplicates are collapsed; order is not significant.
21
+ */
22
+ export declare function parseScopeString(raw: string): OAuthScope[] | null;
23
+ /** Serializes scopes back to the space-delimited wire/storage format. */
24
+ export declare function formatScopes(scopes: readonly OAuthScope[]): string;
25
+ /** True when every scope in `requested` is present in `granted`. */
26
+ export declare function isScopeSubset(requested: readonly OAuthScope[], granted: readonly OAuthScope[]): boolean;
27
+ //# sourceMappingURL=scopes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../../src/oauth/scopes.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,0CAA2C,CAAA;AAEpE,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAA;AAEtD,oDAAoD;AACpD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,UAAU,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,GAAG,IAAI,CAMjE;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAElE;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CAC3B,SAAS,EAAE,SAAS,UAAU,EAAE,EAChC,OAAO,EAAE,SAAS,UAAU,EAAE,GAC7B,OAAO,CAET"}
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ /**
4
+ * OAuth scope vocabulary. Scopes map 1:1 onto the MCP tools exposed today; no
5
+ * speculative scopes are defined.
6
+ *
7
+ * - `schema:read` -> beech_list_seeds, beech_get_seed, beech_schema_export,
8
+ * beech_schema_validate, beech_schema_plan
9
+ * - `schema:write` -> beech_schema_apply
10
+ *
11
+ * `beech_schema_plan` is deliberately classified as READ: despite the imperative
12
+ * name it is a dry-run that computes a migration plan and mutates nothing.
13
+ */
14
+ export const OAUTH_SCOPES = ['schema:read', 'schema:write'];
15
+ /** Narrows an arbitrary string to a known scope. */
16
+ export function isOAuthScope(value) {
17
+ return OAUTH_SCOPES.includes(value);
18
+ }
19
+ /**
20
+ * Parses an RFC 6749 §3.3 space-delimited scope string.
21
+ * Returns null if the string is empty or contains any unknown scope — callers
22
+ * must reject the request with `invalid_scope` rather than silently dropping it.
23
+ * Duplicates are collapsed; order is not significant.
24
+ */
25
+ export function parseScopeString(raw) {
26
+ const parts = raw.trim().split(/\s+/).filter(Boolean);
27
+ if (parts.length === 0)
28
+ return null;
29
+ const unique = [...new Set(parts)];
30
+ if (!unique.every(isOAuthScope))
31
+ return null;
32
+ return unique;
33
+ }
34
+ /** Serializes scopes back to the space-delimited wire/storage format. */
35
+ export function formatScopes(scopes) {
36
+ return [...new Set(scopes)].join(' ');
37
+ }
38
+ /** True when every scope in `requested` is present in `granted`. */
39
+ export function isScopeSubset(requested, granted) {
40
+ return requested.every(scope => granted.includes(scope));
41
+ }
@@ -0,0 +1,46 @@
1
+ import type { OAuthScope } from './scopes.js';
2
+ export type OAuthTokenType = 'access' | 'refresh';
3
+ export interface NewOAuthToken {
4
+ id: string;
5
+ /** SHA-256 hex hash of the token. The plaintext token is never persisted. */
6
+ tokenHash: string;
7
+ tokenType: OAuthTokenType;
8
+ clientId: string;
9
+ userId: string;
10
+ scope: OAuthScope[];
11
+ /** Hash of the authorization code this token descends from, for cascade revocation. */
12
+ authorizationCodeHash: string;
13
+ expiresAt: number;
14
+ }
15
+ export interface OAuthTokenRecord extends NewOAuthToken {
16
+ createdAt: number;
17
+ revokedAt: number | null;
18
+ }
19
+ export interface AuthorizedClientSummary {
20
+ clientId: string;
21
+ clientName: string;
22
+ scope: OAuthScope[];
23
+ /** Creation timestamp of the most recent live token for this client. */
24
+ lastIssuedAt: number;
25
+ }
26
+ export interface IOAuthTokenRepository {
27
+ save(record: NewOAuthToken): Promise<void>;
28
+ /** Finds an unexpired, unrevoked token by hash and type. */
29
+ findActiveByHash(tokenHash: string, tokenType: OAuthTokenType, nowTimestamp: number): Promise<OAuthTokenRecord | null>;
30
+ /** Revokes a single token. False when already revoked or absent. */
31
+ revokeByHash(tokenHash: string, nowTimestamp: number): Promise<boolean>;
32
+ /**
33
+ * Revokes EVERY token descending from one authorization code, both access and
34
+ * refresh. Called on authorization-code replay (OAuth 2.1 §4.1.3).
35
+ * Returns the number of tokens revoked.
36
+ */
37
+ revokeByAuthorizationCode(authorizationCodeHash: string, nowTimestamp: number): Promise<number>;
38
+ /**
39
+ * Revokes every live token for one (client, user) pair — access AND refresh,
40
+ * never one without the other. Backs "revoke this app" in the dashboard.
41
+ */
42
+ revokeAllForClientAndUser(clientId: string, userId: string, nowTimestamp: number): Promise<number>;
43
+ /** Lists the clients holding at least one live token for this user, newest first. */
44
+ listAuthorizedClientsForUser(userId: string, nowTimestamp: number): Promise<AuthorizedClientSummary[]>;
45
+ }
46
+ //# sourceMappingURL=token.repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.repository.d.ts","sourceRoot":"","sources":["../../src/oauth/token.repository.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,CAAA;AAEjD,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,cAAc,CAAA;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,uFAAuF;IACvF,qBAAqB,EAAE,MAAM,CAAA;IAC7B,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACrD,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,wEAAwE;IACxE,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE1C,4DAA4D;IAC5D,gBAAgB,CACd,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,cAAc,EACzB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IAEnC,oEAAoE;IACpE,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAEvE;;;;OAIG;IACH,yBAAyB,CAAC,qBAAqB,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAE/F;;;OAGG;IACH,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAElG,qFAAqF;IACrF,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAAA;CACvG"}
@@ -0,0 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ export {};
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @module @beechcms/core/observability/activity-logger
11
11
  */
12
- export type ActivityAction = 'create' | 'update' | 'delete' | 'upload' | 'bulk_update';
12
+ export type ActivityAction = 'create' | 'update' | 'delete' | 'upload' | 'bulk_update' | 'security_alert';
13
13
  export type EntityType = 'content' | 'media' | 'seed';
14
14
  /**
15
15
  * Identifies the human (or machine) actor that triggered the event.
@@ -1 +1 @@
1
- {"version":3,"file":"activity-logger.d.ts","sourceRoot":"","sources":["../../src/observability/activity-logger.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,aAAa,CAAA;AACtF,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAA;AAErD;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,UAAU,EAAE,UAAU,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,KAAK,EAAE,aAAa,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;;;;;OASG;IACH,GAAG,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CACnD"}
1
+ {"version":3,"file":"activity-logger.d.ts","sourceRoot":"","sources":["../../src/observability/activity-logger.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,aAAa,GAAG,gBAAgB,CAAA;AACzG,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAA;AAErD;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,UAAU,EAAE,UAAU,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,KAAK,EAAE,aAAa,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;;;;;OASG;IACH,GAAG,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CACnD"}
@@ -1,18 +1,26 @@
1
1
  export interface RateLimitResult {
2
2
  isAllowed: boolean;
3
3
  /**
4
- * The number of seconds after which the client may retry the request.
5
- * Note: This field is optional and may not be supported by all implementations
6
- * (e.g. Cloudflare Rate Limit binding does not return retry-after information).
4
+ * The number of whole seconds after which the client may retry the request.
7
5
  */
8
6
  retryAfterSeconds?: number;
7
+ /**
8
+ * The maximum token capacity configured for this limiter.
9
+ */
10
+ limit?: number;
11
+ /**
12
+ * The number of whole tokens remaining in the bucket.
13
+ */
14
+ remaining?: number;
9
15
  }
10
16
  export interface IRateLimiter {
11
17
  /**
12
18
  * Checks whether the given key is within the rate limit.
13
- * The key should combine the client IP address and an endpoint-specific prefix
14
- * to prevent one endpoint's limit from being shared with another.
15
19
  */
16
20
  checkLimit(key: string): Promise<RateLimitResult>;
21
+ /**
22
+ * Clears cached bucket state (for testing and isolation).
23
+ */
24
+ reset?(): void;
17
25
  }
18
26
  //# sourceMappingURL=rate-limiter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/rate-limit/rate-limiter.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;CAClD"}
1
+ {"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/rate-limit/rate-limiter.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,OAAO,CAAA;IAClB;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IACjD;;OAEG;IACH,KAAK,CAAC,IAAI,IAAI,CAAA;CACf"}
@@ -0,0 +1,20 @@
1
+ import type { IRateLimiter, RateLimitResult } from './rate-limiter.js';
2
+ import type { IClock } from '../common/clock.js';
3
+ export interface TokenBucketOptions {
4
+ capacity?: number;
5
+ refillRatePerSecond?: number;
6
+ clock?: IClock;
7
+ maxIdleTimeSeconds?: number;
8
+ }
9
+ export declare class TokenBucketRateLimiter implements IRateLimiter {
10
+ private readonly buckets;
11
+ private readonly capacity;
12
+ private readonly refillRatePerSecond;
13
+ private readonly clock;
14
+ private readonly maxIdleTimeSeconds;
15
+ constructor(options?: TokenBucketOptions);
16
+ private pruneExpiredBuckets;
17
+ checkLimit(key: string): Promise<RateLimitResult>;
18
+ reset(): void;
19
+ }
20
+ //# sourceMappingURL=token-bucket-rate-limiter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-bucket-rate-limiter.d.ts","sourceRoot":"","sources":["../../src/rate-limit/token-bucket-rate-limiter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACtE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAQhD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,qBAAa,sBAAuB,YAAW,YAAY;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAQ;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAQ;gBAE/B,OAAO,CAAC,EAAE,kBAAkB;IAQxC,OAAO,CAAC,mBAAmB;IASrB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAsCvD,KAAK,IAAI,IAAI;CAGd"}
@@ -0,0 +1,63 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ import { SystemClock } from '../common/clock.js';
4
+ export class TokenBucketRateLimiter {
5
+ buckets = new Map();
6
+ capacity;
7
+ refillRatePerSecond;
8
+ clock;
9
+ maxIdleTimeSeconds;
10
+ constructor(options) {
11
+ this.capacity = options?.capacity ?? 17;
12
+ // Default: 1 token every 3.53 seconds (~0.283286 tokens/sec)
13
+ this.refillRatePerSecond = options?.refillRatePerSecond ?? (1 / 3.53);
14
+ this.clock = options?.clock ?? SystemClock;
15
+ this.maxIdleTimeSeconds = options?.maxIdleTimeSeconds ?? 3600; // 1 hour idle TTL
16
+ }
17
+ pruneExpiredBuckets(now) {
18
+ if (this.buckets.size < 500)
19
+ return;
20
+ for (const [key, state] of this.buckets.entries()) {
21
+ if (now - state.lastRefillTimestamp > this.maxIdleTimeSeconds) {
22
+ this.buckets.delete(key);
23
+ }
24
+ }
25
+ }
26
+ async checkLimit(key) {
27
+ const now = this.clock.now() / 1000; // fractional seconds for continuous refill
28
+ this.pruneExpiredBuckets(now);
29
+ let bucket = this.buckets.get(key);
30
+ if (!bucket) {
31
+ bucket = {
32
+ tokens: this.capacity,
33
+ lastRefillTimestamp: now,
34
+ };
35
+ }
36
+ else {
37
+ const elapsed = Math.max(0, now - bucket.lastRefillTimestamp);
38
+ bucket.tokens = Math.min(this.capacity, bucket.tokens + elapsed * this.refillRatePerSecond);
39
+ bucket.lastRefillTimestamp = now;
40
+ }
41
+ if (bucket.tokens >= 1) {
42
+ bucket.tokens -= 1;
43
+ this.buckets.set(key, bucket);
44
+ return {
45
+ isAllowed: true,
46
+ limit: this.capacity,
47
+ remaining: Math.floor(bucket.tokens),
48
+ };
49
+ }
50
+ this.buckets.set(key, bucket);
51
+ const tokensNeeded = 1 - bucket.tokens;
52
+ const retryAfterSeconds = Math.max(1, Math.ceil(tokensNeeded / this.refillRatePerSecond));
53
+ return {
54
+ isAllowed: false,
55
+ retryAfterSeconds,
56
+ limit: this.capacity,
57
+ remaining: Math.max(0, Math.floor(bucket.tokens)),
58
+ };
59
+ }
60
+ reset() {
61
+ this.buckets.clear();
62
+ }
63
+ }
@@ -0,0 +1,11 @@
1
+ import type { Seed } from '../engine/types.js';
2
+ /**
3
+ * Extracts and concatenates text from all public indexable text/richtext branches of a seed.
4
+ * Enforces privacy policies by utilizing indexableSearchBranches.
5
+ *
6
+ * @param seed The seed definition.
7
+ * @param entry The content entry record.
8
+ * @returns The combined text, or null if no indexable text exists.
9
+ */
10
+ export declare function extractIndexableText(seed: Seed, entry: Record<string, any>): string | null;
11
+ //# sourceMappingURL=vector-extractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vector-extractor.d.ts","sourceRoot":"","sources":["../../src/search/vector-extractor.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAA;AAG9C;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,GAAG,IAAI,CAc1F"}