@beechcms/api 0.6.7 → 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/assets/dashboard/0auth_mcp.svg +1 -0
- package/assets/dashboard/assets/{chart-D_uST30B.js → chart-CYgKoF6U.js} +6 -6
- package/assets/dashboard/assets/dist-DHabpZqA.js +140 -0
- package/assets/dashboard/assets/index-BZMSpYrl.js +3613 -0
- package/assets/dashboard/assets/index-Dex2YZcC.css +1 -0
- package/assets/dashboard/assets/json-code-editor-DtZpKDXm.js +12 -0
- package/assets/dashboard/assets/pie-chart-recharts-C7WAhcCk.js +1 -0
- package/assets/dashboard/assets/richtext-render-x0TT0116.js +258 -0
- package/assets/dashboard/assets/timeseries-chart-recharts-B9RquiY6.js +1 -0
- package/assets/dashboard/assets/w3c-keyname-BOAvb0qz.js +1 -0
- package/assets/dashboard/index.html +4 -2
- package/dist/auth/auth.app.d.ts +13 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/factory.d.ts +14 -1
- package/dist/features/oauth/authorization-request.d.ts +127 -0
- package/dist/features/oauth/authorize.d.ts +97 -0
- package/dist/features/oauth/consents.d.ts +75 -0
- package/dist/features/oauth/constants.d.ts +48 -0
- package/dist/features/oauth/index.d.ts +31 -0
- package/dist/features/oauth/revoke.d.ts +29 -0
- package/dist/features/oauth/token-issuance.d.ts +49 -0
- package/dist/features/oauth/token.d.ts +38 -0
- package/dist/features/search/constants.d.ts +43 -0
- package/dist/features/search/handlers/embed.d.ts +31 -0
- package/dist/features/search/handlers/full-text-search.d.ts +28 -0
- package/dist/features/search/index.d.ts +20 -0
- package/dist/features/search/jobs/semantic-search.hooks.d.ts +21 -0
- package/dist/features/search/jobs/semantic-search.worker.d.ts +101 -0
- package/dist/features/search/public-search.router.d.ts +15 -0
- package/dist/features/search/search.d.ts +14 -6
- package/dist/features/search/semantic-search.hooks.d.ts +2 -0
- package/dist/features/search/semantic-search.worker.d.ts +21 -0
- package/dist/features/search/types.d.ts +100 -0
- package/dist/features/search/utils/search-utils.d.ts +56 -0
- package/dist/features/seeds/seeds.destructive.d.ts +6 -0
- package/dist/features/seeds/seeds.handler.d.ts +14 -0
- package/dist/features/seeds/seeds.helpers.d.ts +118 -0
- package/dist/features/seeds/seeds.mcp.d.ts +37 -0
- package/dist/features/upload/index.d.ts +2 -2
- package/dist/index.js +3544 -1368
- package/dist/middleware/auth.middleware.d.ts +34 -4
- package/dist/middleware/oauth-scope.middleware.d.ts +60 -0
- package/dist/middleware/rate-limit.middleware.d.ts +4 -2
- package/dist/middleware/repository.middleware.d.ts +9 -1
- package/dist/public/api-key-middleware.d.ts +0 -4
- package/dist/public/public-add.d.ts +1 -1
- package/dist/rate-limit/in-memory-rate-limiter.d.ts +1 -8
- package/dist/shared/db/repositories/content.repository.d1.d.ts +4 -2
- package/dist/shared/db/repositories/d1-oauth-authorization-code.repository.d.ts +9 -0
- package/dist/shared/db/repositories/d1-oauth-client.repository.d.ts +6 -0
- package/dist/shared/db/repositories/d1-oauth-consent.repository.d.ts +10 -0
- package/dist/shared/db/repositories/d1-oauth-token.repository.d.ts +12 -0
- package/dist/shared/db/repositories/d1-search.query.d.ts +54 -0
- package/dist/shared/db/repositories/d1-vector.repository.d.ts +11 -0
- package/dist/shared/db/repositories/in-memory-seed.repository.d.ts +2 -1
- package/dist/shared/db/repositories/seed.repository.d1.d.ts +3 -1
- package/dist/shared/db/repositories/time-trap-token.repository.d1.d.ts +7 -0
- package/dist/shared/storage/factory.d.ts +84 -4
- package/dist/shared/storage/r2-bucket.d.ts +127 -0
- package/dist/shared/storage/s3-bucket.d.ts +104 -10
- package/dist/shared/storage/upload.d.ts +33 -5
- package/dist/shared/utils/dual-key-rate-limiter.d.ts +21 -0
- package/dist/shared/utils/media-utils.d.ts +3 -3
- package/dist/shared/utils/opaque-token.d.ts +13 -0
- package/dist/types.d.ts +49 -1
- package/migrations/0000_v040_base.sql +184 -1
- package/migrations/_archive/0000_schema_init.sql +257 -0
- package/migrations/_archive/0001_init.sql +25 -0
- package/migrations/_archive/0002_seed_admin.sql +10 -0
- package/migrations/_archive/0003_refresh_tokens.sql +17 -0
- package/migrations/_archive/0004_seed_projects.sql +18 -0
- package/migrations/_archive/0005_update_projects.sql +56 -0
- package/migrations/_archive/0006_add_slug_and_unique_index.sql +7 -0
- package/migrations/_archive/0007_cleanup_test_data.sql +4 -0
- package/migrations/_archive/0008_seed_content_entries.sql +104 -0
- package/migrations/_archive/0009_public_idempotency.sql +11 -0
- package/migrations/_archive/0010_seed_real_data.sql +31 -0
- package/migrations/_archive/0010_testsite_data.sql +31 -0
- package/migrations/_archive/0011_analytics.sql +10 -0
- package/migrations/_archive/0012_seed_analytics.sql +17 -0
- package/migrations/_archive/0013_system_stats.sql +8 -0
- package/migrations/_archive/0014_activity_logs.sql +15 -0
- package/migrations/_archive/0015_notifications.sql +12 -0
- package/migrations/_archive/0016_fts5_global_search.sql +81 -0
- package/migrations/_archive/0017_seed_clienti.sql +90 -0
- package/migrations/_archive/0018_draft_data.sql +3 -0
- package/migrations/_archive/0019_drop_fts_triggers.sql +7 -0
- package/migrations/_archive/0020_composite_indexes.sql +15 -0
- package/migrations/_archive/0021_media_objects.sql +15 -0
- package/migrations/_archive/0022_analytics_seed.sql +23 -0
- package/migrations/_archive/0023_user_profile.sql +4 -0
- package/migrations/_archive/0024_activity_user_name.sql +3 -0
- package/migrations/_archive/0025_password_reset_tokens.sql +11 -0
- package/migrations/_archive/0026_remove_dev_admin.sql +4 -0
- package/migrations/_archive/0027_v040_doctype_architecture.sql +51 -0
- package/migrations/_archive/0029_fts_all_text_branches.sql +281 -0
- package/migrations/_archive/0031_site_settings.sql +9 -0
- package/migrations/_archive/0032_seeds.sql +28 -0
- package/migrations/_archive/0033_dashboard_layouts.sql +14 -0
- package/migrations/_archive/0034_kanban_foundation.sql +24 -0
- package/migrations/_archive/0035_setup_lock.sql +10 -0
- package/migrations/_archive/0036_notification_type_success.sql +23 -0
- package/migrations/_archive/0037_time_trap_tokens.sql +13 -0
- package/migrations/_archive/0038_oauth_authorization.sql +95 -0
- package/migrations/_archive/0039_oauth_client_beech_mcp.sql +20 -0
- package/package.json +3 -5
- package/assets/dashboard/assets/index-B58kBLrh.css +0 -1
- package/assets/dashboard/assets/index-DjniT5Z9.js +0 -745
- package/assets/dashboard/assets/pie-chart-recharts-CnVomiSi.js +0 -1
- package/assets/dashboard/assets/timeseries-chart-recharts-CTqi8qnV.js +0 -1
- /package/migrations/{0029_automations.sql → _archive/0029_automations.sql} +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { Env, Variables } from '../../types';
|
|
3
|
+
/** Context type alias for OAuth route handlers. */
|
|
4
|
+
type OAuthContext = Context<{
|
|
5
|
+
Bindings: Env;
|
|
6
|
+
Variables: Variables;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* `POST /oauth/revoke`
|
|
10
|
+
*
|
|
11
|
+
* Token revocation endpoint complying with RFC 7009.
|
|
12
|
+
* Allows a client to invalidate an active access or refresh token.
|
|
13
|
+
*
|
|
14
|
+
* Behavior and RFC 7009 conformance:
|
|
15
|
+
* 1. Rate limiting is applied per client IP using the `oauthToken` limiter.
|
|
16
|
+
* 2. Requires `token` and `client_id` in url-encoded form body (returns HTTP 400 if missing).
|
|
17
|
+
* 3. Supports optional `token_type_hint` ('access_token' or 'refresh_token') to optimize lookup order.
|
|
18
|
+
* 4. Hash lookup: Searches for an active token matching `sha256hex(token)`.
|
|
19
|
+
* 5. **Token Family Revocation**: If found and the token's `clientId` matches the caller,
|
|
20
|
+
* revokes the entire token family via `authorizationCodeHash` (revoking an access token
|
|
21
|
+
* revokes its sibling refresh token, and vice-versa).
|
|
22
|
+
* 6. **Always returns HTTP 200 `{}`** unless the request syntax is malformed (RFC 7009 §2.2),
|
|
23
|
+
* preventing token enumeration and leaking token existence or state to unauthorized parties.
|
|
24
|
+
*
|
|
25
|
+
* @param context - Hono request context.
|
|
26
|
+
* @returns HTTP 200 `{}` on success, 400 on malformed syntax, or 429 when rate limited.
|
|
27
|
+
*/
|
|
28
|
+
export declare function revokeHandler(context: OAuthContext): Promise<Response>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type IIdGenerator, type IOAuthTokenRepository, type OAuthScope } from '@beechcms/core';
|
|
2
|
+
/**
|
|
3
|
+
* Input arguments for generating and storing an access/refresh token pair.
|
|
4
|
+
*/
|
|
5
|
+
export interface IssueTokenPairInput {
|
|
6
|
+
/** Repository for persisting OAuth token records. */
|
|
7
|
+
tokenRepository: IOAuthTokenRepository;
|
|
8
|
+
/** Service for generating unique UUIDs for token records. */
|
|
9
|
+
idGenerator: IIdGenerator;
|
|
10
|
+
/** The client identifier receiving the token pair. */
|
|
11
|
+
clientId: string;
|
|
12
|
+
/** The user/resource owner identifier who authorized the grant. */
|
|
13
|
+
userId: string;
|
|
14
|
+
/** The granted OAuth scopes bound to these tokens. */
|
|
15
|
+
scope: OAuthScope[];
|
|
16
|
+
/** SHA-256 hash of the root authorization code from which this token family descends. */
|
|
17
|
+
authorizationCodeHash: string;
|
|
18
|
+
/** Current Unix epoch timestamp in seconds. */
|
|
19
|
+
nowSeconds: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Result of issuing an access/refresh token pair.
|
|
23
|
+
*/
|
|
24
|
+
export interface IssuedTokenPair {
|
|
25
|
+
/** Plaintext opaque access token returned to the client. */
|
|
26
|
+
accessToken: string;
|
|
27
|
+
/** SHA-256 digest of the access token stored in the database. */
|
|
28
|
+
accessTokenHash: string;
|
|
29
|
+
/** Plaintext opaque refresh token returned to the client. */
|
|
30
|
+
refreshToken: string;
|
|
31
|
+
/** SHA-256 digest of the refresh token stored in the database. */
|
|
32
|
+
refreshTokenHash: string;
|
|
33
|
+
/** Access token lifetime in seconds. */
|
|
34
|
+
expiresIn: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Generates and persists an access/refresh token pair for an authorized client.
|
|
38
|
+
*
|
|
39
|
+
* Security guarantees:
|
|
40
|
+
* - Plaintext tokens are generated via CSPRNG (`generateOpaqueToken`).
|
|
41
|
+
* - Only SHA-256 hashes (`tokenHash`) reach D1 persistence; plaintext tokens are
|
|
42
|
+
* never stored in the database.
|
|
43
|
+
* - Both tokens share the same `authorizationCodeHash` to link the token family
|
|
44
|
+
* for atomic revocation and replay attack mitigation.
|
|
45
|
+
*
|
|
46
|
+
* @param input - The token issuance dependencies and subject parameters.
|
|
47
|
+
* @returns The issued token pair, hashes, and access token expiration.
|
|
48
|
+
*/
|
|
49
|
+
export declare function issueTokenPair(input: IssueTokenPairInput): Promise<IssuedTokenPair>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { Env, Variables } from '../../types';
|
|
3
|
+
/** Context type alias for OAuth route handlers. */
|
|
4
|
+
type OAuthContext = Context<{
|
|
5
|
+
Bindings: Env;
|
|
6
|
+
Variables: Variables;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* Successful token response body adhering to RFC 6749 §5.1.
|
|
10
|
+
*/
|
|
11
|
+
export interface TokenResponseBody {
|
|
12
|
+
/** The issued access token string. */
|
|
13
|
+
access_token: string;
|
|
14
|
+
/** Fixed Bearer token type identifier. */
|
|
15
|
+
token_type: 'Bearer';
|
|
16
|
+
/** The lifetime in seconds of the access token (e.g. 900). */
|
|
17
|
+
expires_in: number;
|
|
18
|
+
/** The issued refresh token string used to obtain new access tokens. */
|
|
19
|
+
refresh_token: string;
|
|
20
|
+
/** Space-separated list of scopes granted to this token. */
|
|
21
|
+
scope: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `POST /oauth/token`
|
|
25
|
+
*
|
|
26
|
+
* Client-facing token endpoint (RFC 6749 §3.2). Unauthenticated by user session;
|
|
27
|
+
* authenticated by the grant parameters themselves (authorization code + PKCE verifier, or refresh token).
|
|
28
|
+
*
|
|
29
|
+
* Protections:
|
|
30
|
+
* - Dual-key rate limiting (per client IP and per `client_id` account key).
|
|
31
|
+
* - Supported grants: `authorization_code` and `refresh_token`.
|
|
32
|
+
* - Strictly sets `Cache-Control: no-store, Pragma: no-cache`.
|
|
33
|
+
*
|
|
34
|
+
* @param context - Hono request context.
|
|
35
|
+
* @returns HTTP response with token payload, RFC error object, or rate limit notification.
|
|
36
|
+
*/
|
|
37
|
+
export declare function tokenHandler(context: OAuthContext): Promise<Response>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module search/constants
|
|
3
|
+
* Error messages and validation limits for the search feature slice.
|
|
4
|
+
* All user-facing strings are centralised here to keep handlers free of literals.
|
|
5
|
+
*/
|
|
6
|
+
/** Validation and error messages returned by search route handlers. */
|
|
7
|
+
export declare const SEARCH_ERRORS: {
|
|
8
|
+
/** The `q` query parameter is absent or empty. */
|
|
9
|
+
readonly QUERY_REQUIRED: "Parametro 'q' obbligatorio.";
|
|
10
|
+
/** The `q` query parameter is shorter than the minimum allowed length. */
|
|
11
|
+
readonly QUERY_TOO_SHORT: "Il parametro 'q' deve avere almeno 2 caratteri.";
|
|
12
|
+
/** The `q` query parameter exceeds the maximum allowed length. */
|
|
13
|
+
readonly QUERY_TOO_LONG: "Il parametro 'q' supera la lunghezza massima di 150 caratteri.";
|
|
14
|
+
/** The request was blocked by the rate limiter. */
|
|
15
|
+
readonly RATE_LIMIT_EXCEEDED: "Too Many Requests";
|
|
16
|
+
/** The Cloudflare Workers AI binding is not configured. */
|
|
17
|
+
readonly AI_BINDING_UNAVAILABLE: "AI binding not available.";
|
|
18
|
+
/** The AI model returned an unrecognisable response shape. */
|
|
19
|
+
readonly EMBEDDING_GENERATION_FAILED: "Failed to generate embedding.";
|
|
20
|
+
};
|
|
21
|
+
/** Validation limits applied to search query parameters. */
|
|
22
|
+
export declare const SEARCH_LIMITS: {
|
|
23
|
+
/** Minimum number of characters required in the `q` parameter for FTS search. */
|
|
24
|
+
readonly QUERY_MIN_LENGTH: 2;
|
|
25
|
+
/** Maximum number of characters allowed in the `q` parameter for embed requests. */
|
|
26
|
+
readonly EMBED_QUERY_MAX_LENGTH: 150;
|
|
27
|
+
/** Default number of results returned per page when no `limit` parameter is provided. */
|
|
28
|
+
readonly DEFAULT_PAGE_SIZE: 20;
|
|
29
|
+
/** Maximum number of results that can be requested in a single page. */
|
|
30
|
+
readonly MAX_PAGE_SIZE: 50;
|
|
31
|
+
/** Minimum number of results that can be requested in a single page. */
|
|
32
|
+
readonly MIN_PAGE_SIZE: 1;
|
|
33
|
+
/** Minimum number of characters a single FTS term must have to be included in the match expression. */
|
|
34
|
+
readonly FTS_TERM_MIN_LENGTH: 2;
|
|
35
|
+
/** Minimum prefix length used when expanding a term into prefix tokens. */
|
|
36
|
+
readonly FTS_PREFIX_MIN_LENGTH: 3;
|
|
37
|
+
/** Maximum number of seconds the embed response is cached on the CDN edge. */
|
|
38
|
+
readonly EMBED_CACHE_MAX_AGE_SECONDS: 604800;
|
|
39
|
+
};
|
|
40
|
+
/** Worker AI model identifier used to generate text embeddings. */
|
|
41
|
+
export declare const EMBEDDING_MODEL: "@cf/baai/bge-small-en-v1.5";
|
|
42
|
+
/** Rate limiter key used for the public embed endpoint. */
|
|
43
|
+
export declare const EMBED_RATE_LIMITER_KEY: "publicApiRead";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module search/handlers/embed
|
|
3
|
+
* Thin route handler for `GET /api/v1/public/search/embed`.
|
|
4
|
+
*
|
|
5
|
+
* Generates a text embedding vector for the provided query string using
|
|
6
|
+
* the Cloudflare Workers AI binding. The vector is returned as a flat
|
|
7
|
+
* array of 32-bit floats and is aggressively cached at the CDN edge.
|
|
8
|
+
*
|
|
9
|
+
* Responsibilities:
|
|
10
|
+
* - Validate the `q` query parameter.
|
|
11
|
+
* - Apply per-IP rate limiting via the injected `rateLimiters` registry.
|
|
12
|
+
* - Call the Workers AI model and normalise its response into a Float32Array.
|
|
13
|
+
* - Set cache headers and return the embedding.
|
|
14
|
+
*/
|
|
15
|
+
import type { Context } from 'hono';
|
|
16
|
+
import type { AppEnv } from '../../../types';
|
|
17
|
+
/**
|
|
18
|
+
* Handles `GET /api/v1/public/search/embed?q=…`.
|
|
19
|
+
*
|
|
20
|
+
* Query parameters:
|
|
21
|
+
* - `q` / `query` / `text` – Query string to embed (required, ≤ 150 characters).
|
|
22
|
+
* The first non-empty value among the three aliases is used.
|
|
23
|
+
*
|
|
24
|
+
* Response headers:
|
|
25
|
+
* - `Cache-Control: public, max-age=604800`
|
|
26
|
+
* - `Edge-Control: s-maxage=604800`
|
|
27
|
+
*
|
|
28
|
+
* @param c - Hono context with typed `AppEnv` bindings and variables.
|
|
29
|
+
* @returns A JSON {@link EmbedResponse} containing the embedding vector.
|
|
30
|
+
*/
|
|
31
|
+
export declare function embedHandler(c: Context<AppEnv>): Promise<Response>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module search/handlers/full-text-search
|
|
3
|
+
* Thin route handler for `GET /api/search`.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* - Parse and validate query parameters.
|
|
7
|
+
* - Delegate the data fetch and count to the injected `searchRepository`.
|
|
8
|
+
* - Assemble and return the paginated {@link SearchResponse}.
|
|
9
|
+
*
|
|
10
|
+
* The handler intentionally contains no SQL — all data access is performed
|
|
11
|
+
* through the `searchRepository` injected by `repositoryMiddleware`.
|
|
12
|
+
*/
|
|
13
|
+
import type { Context } from 'hono';
|
|
14
|
+
import type { AppEnv } from '../../../types';
|
|
15
|
+
/**
|
|
16
|
+
* Handles `GET /api/search?q=…&schema_slug=…&status=…&limit=20&cursor=…`.
|
|
17
|
+
*
|
|
18
|
+
* Query parameters:
|
|
19
|
+
* - `q` – Full-text search term (required, ≥ 2 characters).
|
|
20
|
+
* - `schema_slug` – Restrict results to a single seed slug (optional).
|
|
21
|
+
* - `status` – Restrict results by publication status (optional).
|
|
22
|
+
* - `limit` – Number of results per page, clamped to `[1, 50]` (default: 20).
|
|
23
|
+
* - `cursor` – Opaque pagination cursor from a previous response (optional).
|
|
24
|
+
*
|
|
25
|
+
* @param c - Hono context with typed `AppEnv` bindings and variables.
|
|
26
|
+
* @returns A JSON {@link SearchResponse} with items, nextCursor, and total.
|
|
27
|
+
*/
|
|
28
|
+
export declare function fullTextSearchHandler(c: Context<AppEnv>): Promise<Response>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module search
|
|
3
|
+
* Public barrel for the search vertical slice.
|
|
4
|
+
*
|
|
5
|
+
* External code (e.g. `factory.ts`) **must** import exclusively from this file.
|
|
6
|
+
* Internal directories (`handlers/`, `constants.ts`, `types.ts`, `search-utils.ts`)
|
|
7
|
+
* are slice-private and must not be imported from outside this feature.
|
|
8
|
+
*
|
|
9
|
+
* Exports:
|
|
10
|
+
* - {@link searchRouter} – Authenticated FTS Hono sub-app (`/api/search`).
|
|
11
|
+
* - {@link publicSearchRouter} – Public embedding Hono sub-app (`/api/v1/public/search`).
|
|
12
|
+
* - {@link semanticSearchHooks} – Content lifecycle hooks for vector index maintenance.
|
|
13
|
+
* - {@link semanticSearchJobs} – Background job registry for vector computation.
|
|
14
|
+
* - {@link compileR2Manifest} – Utility to manually trigger R2 manifest recompilation.
|
|
15
|
+
*/
|
|
16
|
+
export { searchRouter } from './search';
|
|
17
|
+
export { publicSearchRouter } from './public-search.router';
|
|
18
|
+
export { semanticSearchHooks } from './jobs/semantic-search.hooks';
|
|
19
|
+
export { semanticSearchJobs, compileR2Manifest, computeVectorJob, deleteVectorJob, updateR2ManifestJob, type ComputeVectorPayload, type UpdateR2ManifestPayload, } from './jobs/semantic-search.worker';
|
|
20
|
+
export type { SearchResultItem, SearchResponse, FtsQueryParams, EmbedResponse, } from './types';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module search/semantic-search.hooks
|
|
3
|
+
* Lifecycle hooks that keep the semantic search index in sync with content changes.
|
|
4
|
+
*
|
|
5
|
+
* Each hook is registered on the `BeechHooks` object consumed by the content
|
|
6
|
+
* repository. Hooks use the queue service to dispatch async jobs so they
|
|
7
|
+
* never block the HTTP response.
|
|
8
|
+
*
|
|
9
|
+
* Job names match the keys registered in {@link semanticSearchJobs}:
|
|
10
|
+
* - `compute_vector` – generate/refresh the embedding for a published entry.
|
|
11
|
+
* - `update_r2_manifest` – recompile the R2 binary manifest after a deletion.
|
|
12
|
+
*/
|
|
13
|
+
import type { BeechHooks } from '@beechcms/core';
|
|
14
|
+
/**
|
|
15
|
+
* Lifecycle hooks that maintain the semantic search vector index.
|
|
16
|
+
*
|
|
17
|
+
* Registered via `BeechConfig.hooks` in the application factory. All async
|
|
18
|
+
* side-effects are dispatched through the queue so they never block the
|
|
19
|
+
* HTTP response path.
|
|
20
|
+
*/
|
|
21
|
+
export declare const semanticSearchHooks: BeechHooks;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module search/semantic-search.worker
|
|
3
|
+
* Background job handlers for semantic (vector) search indexing.
|
|
4
|
+
*
|
|
5
|
+
* Jobs are dispatched by {@link semanticSearchHooks} via the queue service
|
|
6
|
+
* and consumed by the Cloudflare Queue worker export.
|
|
7
|
+
*
|
|
8
|
+
* Exported jobs:
|
|
9
|
+
* - {@link computeVectorJob} – Generate an embedding and persist it to D1, then compile R2.
|
|
10
|
+
* - {@link updateR2ManifestJob} – Recompile R2 binary manifest without touching embeddings.
|
|
11
|
+
*
|
|
12
|
+
* Exported registry:
|
|
13
|
+
* - {@link semanticSearchJobs} – `JobRegistry` map passed to `BeechConfig.jobs`.
|
|
14
|
+
*/
|
|
15
|
+
import type { JobHandler, JobRegistry, Seed } from '@beechcms/core';
|
|
16
|
+
/**
|
|
17
|
+
* Payload for the `compute_vector` job.
|
|
18
|
+
* Enqueued by {@link semanticSearchHooks.afterCreate} and {@link semanticSearchHooks.afterUpdate}.
|
|
19
|
+
*/
|
|
20
|
+
export interface ComputeVectorPayload {
|
|
21
|
+
/** Slug of the seed (content type) the entry belongs to. */
|
|
22
|
+
seedSlug: string;
|
|
23
|
+
/** Unique identifier of the entry whose embedding should be computed. */
|
|
24
|
+
entryId: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Payload for the `update_r2_manifest` job.
|
|
28
|
+
* Enqueued by {@link semanticSearchHooks.afterUpdate} and {@link semanticSearchHooks.afterDelete}.
|
|
29
|
+
*/
|
|
30
|
+
export interface UpdateR2ManifestPayload {
|
|
31
|
+
/** Slug of the seed whose R2 manifest files should be recompiled. */
|
|
32
|
+
seedSlug: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Compiles all stored vectors for a seed into binary and JSON manifest files
|
|
36
|
+
* and writes them to the `SEARCH_R2` bucket.
|
|
37
|
+
*
|
|
38
|
+
* The two files produced per seed are:
|
|
39
|
+
* - `{slug}.bin` – Concatenated `Float32Array` buffers in entry-index order.
|
|
40
|
+
* - `{slug}.json` – JSON array of entry IDs in the same order as the binary file.
|
|
41
|
+
*
|
|
42
|
+
* These files are consumed by the client-side semantic search runtime to perform
|
|
43
|
+
* in-memory cosine-similarity ranking without a Vectorize index.
|
|
44
|
+
*
|
|
45
|
+
* When `searchR2` is `undefined` (e.g. in local development without an R2
|
|
46
|
+
* binding), the function returns immediately without writing anything.
|
|
47
|
+
*
|
|
48
|
+
* @param seed - Seed whose vectors should be compiled.
|
|
49
|
+
* @param db - D1 database instance used to load stored vectors.
|
|
50
|
+
* @param searchR2 - R2 bucket to write the manifest files to, or `undefined` to skip.
|
|
51
|
+
*/
|
|
52
|
+
export declare function compileR2Manifest(seed: Seed, db: D1Database, searchR2?: R2Bucket): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Worker job that generates the embedding vector for a content entry,
|
|
55
|
+
* persists it to D1, and recompiles the R2 manifest files.
|
|
56
|
+
*
|
|
57
|
+
* The job is a no-op (with a warning log) when:
|
|
58
|
+
* - The `DB` binding is missing.
|
|
59
|
+
* - The seed cannot be found.
|
|
60
|
+
* - The seed has no indexable branches.
|
|
61
|
+
* - The entry does not exist or is not published — in which case any
|
|
62
|
+
* existing vector is deleted and the manifest is recompiled.
|
|
63
|
+
* - The entry has no indexable text content.
|
|
64
|
+
* - The `AI` binding is missing.
|
|
65
|
+
*
|
|
66
|
+
* @param payload - `{ seedSlug, entryId }` identifying the entry to vectorise.
|
|
67
|
+
* @param context - Job execution context providing repository and env bindings.
|
|
68
|
+
*/
|
|
69
|
+
export declare const computeVectorJob: JobHandler<ComputeVectorPayload>;
|
|
70
|
+
/**
|
|
71
|
+
* Worker job that removes an embedding vector from D1 for a specific entry
|
|
72
|
+
* and recompiles the R2 binary and JSON manifest files.
|
|
73
|
+
*
|
|
74
|
+
* Enqueued when an entry is unpublished or deleted.
|
|
75
|
+
*
|
|
76
|
+
* @param payload - `{ seedSlug, entryId }` identifying the entry whose vector should be removed.
|
|
77
|
+
* @param context - Job execution context providing env bindings.
|
|
78
|
+
*/
|
|
79
|
+
export declare const deleteVectorJob: JobHandler<ComputeVectorPayload>;
|
|
80
|
+
/**
|
|
81
|
+
* Worker job that recompiles the R2 binary and JSON manifest files for a seed
|
|
82
|
+
* without touching the stored embedding vectors.
|
|
83
|
+
*
|
|
84
|
+
* Enqueued after a vector is deleted (entry unpublished or deleted) to keep
|
|
85
|
+
* the R2 manifests consistent with the D1 vector store.
|
|
86
|
+
*
|
|
87
|
+
* @param payload - `{ seedSlug }` identifying the seed to recompile.
|
|
88
|
+
* @param context - Job execution context providing env bindings.
|
|
89
|
+
*/
|
|
90
|
+
export declare const updateR2ManifestJob: JobHandler<UpdateR2ManifestPayload>;
|
|
91
|
+
/**
|
|
92
|
+
* Job registry for semantic search background jobs.
|
|
93
|
+
* Pass this to `BeechConfig.jobs` (or merge it with other job registries)
|
|
94
|
+
* to enable semantic indexing in your deployment.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* createBeechApp({ jobs: semanticSearchJobs, ... })
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export declare const semanticSearchJobs: JobRegistry;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module search/public-search.router
|
|
3
|
+
* Public (unauthenticated) search Hono sub-app.
|
|
4
|
+
*
|
|
5
|
+
* Mounted under the public API prefix in `factory.ts`.
|
|
6
|
+
* Authentication is handled upstream via API-key middleware; individual
|
|
7
|
+
* endpoints apply per-IP rate limiting through the injected `rateLimiters`.
|
|
8
|
+
*
|
|
9
|
+
* Routes:
|
|
10
|
+
* - `GET /embed` → {@link embedHandler}
|
|
11
|
+
*/
|
|
12
|
+
import { Hono } from 'hono';
|
|
13
|
+
import type { AppEnv } from '../../types';
|
|
14
|
+
/** Hono sub-app for the public search endpoints (embedding generation). */
|
|
15
|
+
export declare const publicSearchRouter: Hono<AppEnv, import("hono/types").BlankSchema, "/">;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @module search/search
|
|
3
|
+
* Authenticated full-text search Hono sub-app.
|
|
4
|
+
*
|
|
5
|
+
* Mounted at `/api/search` in `factory.ts`.
|
|
6
|
+
* Requires a valid JWT — auth is enforced via `authMiddleware`.
|
|
7
|
+
*
|
|
8
|
+
* Routes:
|
|
9
|
+
* - `GET /` → {@link fullTextSearchHandler}
|
|
10
|
+
*/
|
|
11
|
+
import { Hono } from 'hono';
|
|
12
|
+
import type { AppEnv } from '../../types';
|
|
13
|
+
/** Hono sub-app for the authenticated full-text search endpoint. */
|
|
14
|
+
export declare const searchRouter: Hono<AppEnv, import("hono/types").BlankSchema, "/">;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { JobHandler, JobRegistry, Seed } from '@beechcms/core';
|
|
2
|
+
export interface ComputeVectorPayload {
|
|
3
|
+
seedSlug: string;
|
|
4
|
+
entryId: string;
|
|
5
|
+
}
|
|
6
|
+
export interface UpdateR2ManifestPayload {
|
|
7
|
+
seedSlug: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Compiles all vectors for a seed into binary Float32Array and JSON manifest on R2.
|
|
11
|
+
*/
|
|
12
|
+
export declare function compileManifest(seed: Seed, db: D1Database, searchR2?: R2Bucket): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Worker job to compute the embedding vector for an entry, save it in D1, and compile R2 manifests.
|
|
15
|
+
*/
|
|
16
|
+
export declare const computeVectorJob: JobHandler<ComputeVectorPayload>;
|
|
17
|
+
/**
|
|
18
|
+
* Worker job to recompile the R2 manifest files for a seed.
|
|
19
|
+
*/
|
|
20
|
+
export declare const updateR2ManifestJob: JobHandler<UpdateR2ManifestPayload>;
|
|
21
|
+
export declare const semanticSearchJobs: JobRegistry;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module search/types
|
|
3
|
+
* Feature-scoped TypeScript types for the search vertical slice.
|
|
4
|
+
* Kept separate from `search-utils.ts` so utilities stay importable without
|
|
5
|
+
* dragging in HTTP-layer concerns, and to align with the VSA type file convention.
|
|
6
|
+
*/
|
|
7
|
+
import type { SearchResultRow } from '@beechcms/core';
|
|
8
|
+
/**
|
|
9
|
+
* Raw database row returned by the UNION ALL FTS query.
|
|
10
|
+
* Field names use snake_case to match SQLite column aliases verbatim.
|
|
11
|
+
*/
|
|
12
|
+
export interface FtsRow {
|
|
13
|
+
/** Unique identifier of the matching content entry. */
|
|
14
|
+
entry_id: string;
|
|
15
|
+
/** Slug of the seed (content type) the entry belongs to. */
|
|
16
|
+
schema_slug: string;
|
|
17
|
+
/** URL-friendly slug of the entry, if set. */
|
|
18
|
+
slug: string | null;
|
|
19
|
+
/** Publication status of the entry (e.g. `published`, `draft`). */
|
|
20
|
+
status: string;
|
|
21
|
+
/** Display title of the entry, sourced from the seed's `displayNameAlias` column. */
|
|
22
|
+
title: string | null;
|
|
23
|
+
/** HTML snippet produced by SQLite's `snippet()` function highlighting matched terms. */
|
|
24
|
+
excerpt: string;
|
|
25
|
+
/** BM25 relevance score; lower (more negative) means more relevant. */
|
|
26
|
+
rank: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A single search result item returned to the API consumer.
|
|
30
|
+
* HTML tags are stripped from the excerpt except for `<mark>` highlighting wrappers.
|
|
31
|
+
*/
|
|
32
|
+
export interface SearchResultItem {
|
|
33
|
+
/** Unique identifier of the matching content entry. */
|
|
34
|
+
id: string;
|
|
35
|
+
/** Slug of the seed (content type) the entry belongs to. */
|
|
36
|
+
schema_slug: string;
|
|
37
|
+
/** URL-friendly slug of the entry, if set. */
|
|
38
|
+
slug: string | null;
|
|
39
|
+
/** Publication status of the entry. */
|
|
40
|
+
status: string;
|
|
41
|
+
/** Display title of the entry; empty string when the entry has no title. */
|
|
42
|
+
title: string;
|
|
43
|
+
/** Plain-text excerpt with `<mark>…</mark>` wrappers around matched terms. */
|
|
44
|
+
excerpt: string;
|
|
45
|
+
/**
|
|
46
|
+
* Additional entry fields.
|
|
47
|
+
* Currently always `{}` — reserved for future field projection support.
|
|
48
|
+
*/
|
|
49
|
+
data: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Paginated response envelope returned by `GET /api/search`.
|
|
53
|
+
*/
|
|
54
|
+
export interface SearchResponse {
|
|
55
|
+
/** Matched entries for the current page. */
|
|
56
|
+
items: SearchResultItem[];
|
|
57
|
+
/**
|
|
58
|
+
* Opaque cursor for the next page, or `null` when there are no more results.
|
|
59
|
+
* Pass as the `cursor` query parameter on the next request.
|
|
60
|
+
*/
|
|
61
|
+
nextCursor: string | null;
|
|
62
|
+
/** Total number of matching entries across all pages. */
|
|
63
|
+
total: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Normalised, validated query parameters consumed by {@link buildFtsQuery}.
|
|
67
|
+
* The handler clamps `pageSize` before passing it here; this type assumes
|
|
68
|
+
* all values are already within their valid ranges.
|
|
69
|
+
*/
|
|
70
|
+
export interface FtsQueryParams {
|
|
71
|
+
/** Full-text search term entered by the user; must be ≥ 2 characters. */
|
|
72
|
+
queryText: string;
|
|
73
|
+
/**
|
|
74
|
+
* When set, restricts the search to entries belonging to this seed slug.
|
|
75
|
+
* `null` searches across all searchable seeds.
|
|
76
|
+
*/
|
|
77
|
+
schemaSlug: string | null;
|
|
78
|
+
/**
|
|
79
|
+
* When set, adds a `status = ?` filter to each per-seed sub-query.
|
|
80
|
+
* `null` returns entries in any status.
|
|
81
|
+
*/
|
|
82
|
+
statusFilter: string | null;
|
|
83
|
+
/** Maximum entries to return, already clamped to `[1, 50]` by the handler. */
|
|
84
|
+
pageSize: number;
|
|
85
|
+
/**
|
|
86
|
+
* Opaque pagination cursor produced by {@link encodeCursor}.
|
|
87
|
+
* `null` starts from the first page.
|
|
88
|
+
*/
|
|
89
|
+
cursor: string | null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Response body returned by `GET /api/v1/public/search/embed`.
|
|
93
|
+
*/
|
|
94
|
+
export interface EmbedResponse {
|
|
95
|
+
/** Flat array of 32-bit floats representing the embedding vector. */
|
|
96
|
+
data: number[];
|
|
97
|
+
/** Tuple describing the shape of the vector, e.g. `[384]`. */
|
|
98
|
+
shape: [number];
|
|
99
|
+
}
|
|
100
|
+
export type { SearchResultRow };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module search/search-utils
|
|
3
|
+
* Pure utility functions for the search feature slice.
|
|
4
|
+
*
|
|
5
|
+
* Zero Hono dependencies — fully importable from Vitest without a running worker.
|
|
6
|
+
* FTS is per-seed (`fts_{slug}`), joined with `content_{slug}` for metadata.
|
|
7
|
+
*/
|
|
8
|
+
import type { FtsRow, FtsQueryParams, SearchResultItem } from '../types';
|
|
9
|
+
export type { FtsRow, FtsQueryParams, SearchResultItem };
|
|
10
|
+
/**
|
|
11
|
+
* Encodes a BM25 rank and an entry ID into an opaque, URL-safe pagination cursor.
|
|
12
|
+
*
|
|
13
|
+
* The cursor is base64-encoded so it can be passed safely as a query parameter.
|
|
14
|
+
* Use {@link decodeCursor} to reverse the operation.
|
|
15
|
+
*
|
|
16
|
+
* @param rank - BM25 relevance score of the last row on the current page.
|
|
17
|
+
* @param entryId - Unique identifier of the last row on the current page.
|
|
18
|
+
* @returns An opaque base64 string suitable for the `cursor` query parameter.
|
|
19
|
+
*/
|
|
20
|
+
export declare function encodeCursor(rank: number, entryId: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Decodes a pagination cursor produced by {@link encodeCursor}.
|
|
23
|
+
*
|
|
24
|
+
* Returns `null` when the input is not valid base64 or does not contain
|
|
25
|
+
* the expected `rank:entryId` separator, so callers can safely ignore
|
|
26
|
+
* malformed cursors without throwing.
|
|
27
|
+
*
|
|
28
|
+
* @param cursor - Opaque cursor string from a previous search response.
|
|
29
|
+
* @returns Parsed `{ rank, entryId }` or `null` if the cursor is invalid.
|
|
30
|
+
*/
|
|
31
|
+
export declare function decodeCursor(cursor: string): {
|
|
32
|
+
rank: number;
|
|
33
|
+
entryId: string;
|
|
34
|
+
} | null;
|
|
35
|
+
export { buildFtsQuery, type FtsQueryResult, type FtsQueryParams as RepositoryFtsQueryParams, } from '../../../shared/db/repositories/d1-search.query';
|
|
36
|
+
/**
|
|
37
|
+
* Maps a raw database {@link FtsRow} (snake_case) to the wire-format
|
|
38
|
+
* {@link SearchResultItem} returned to API consumers.
|
|
39
|
+
*
|
|
40
|
+
* HTML tags in the excerpt are stripped via {@link stripHtmlPreservingMarkTags}.
|
|
41
|
+
*
|
|
42
|
+
* @param row - Raw row from the UNION ALL FTS query.
|
|
43
|
+
* @returns A sanitised {@link SearchResultItem} ready for JSON serialisation.
|
|
44
|
+
*/
|
|
45
|
+
export declare function mapFtsRow(row: FtsRow): SearchResultItem;
|
|
46
|
+
/**
|
|
47
|
+
* Maps a repository-shaped {@link SearchResultRow} (camelCase) to the
|
|
48
|
+
* wire-format {@link SearchResultItem} returned by the `GET /api/search` route.
|
|
49
|
+
*
|
|
50
|
+
* This is the counterpart of {@link mapFtsRow} for rows that have already been
|
|
51
|
+
* normalised by the search repository layer.
|
|
52
|
+
*
|
|
53
|
+
* @param row - Repository result row with camelCase field names.
|
|
54
|
+
* @returns A sanitised {@link SearchResultItem} ready for JSON serialisation.
|
|
55
|
+
*/
|
|
56
|
+
export declare function mapSearchResultRow(row: import('@beechcms/core').SearchResultRow): SearchResultItem;
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module features/seeds
|
|
3
|
+
*
|
|
4
|
+
* HTTP route handlers for Seed and Schema Management in BeechCMS.
|
|
5
|
+
*
|
|
6
|
+
* Provides RESTful administration endpoints for Content Types ("Seeds"),
|
|
7
|
+
* executing additive DDL migrations against SQLite/D1, managing branches (fields),
|
|
8
|
+
* rebuilding full-text search (FTS5) virtual tables, and composing sub-routers
|
|
9
|
+
* for destructive operations and MCP agent workflows.
|
|
10
|
+
*/
|
|
1
11
|
import { Hono } from 'hono';
|
|
2
12
|
import type { Env, Variables } from '../../types';
|
|
3
13
|
export declare const seedsApp: Hono<{
|
|
4
14
|
Bindings: Env;
|
|
5
15
|
Variables: Variables;
|
|
6
16
|
}, import("hono/types").BlankSchema, "/">;
|
|
17
|
+
export { McpClassification, classifyCandidate } from './seeds.mcp';
|
|
18
|
+
export * from './seeds.helpers';
|
|
19
|
+
export { destructiveApp } from './seeds.destructive';
|
|
20
|
+
export { mcpApp } from './seeds.mcp';
|