@beechcms/api 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 (111) hide show
  1. package/assets/dashboard/0auth_mcp.svg +1 -0
  2. package/assets/dashboard/assets/{chart-B1qG9c0-.js → chart-CYgKoF6U.js} +6 -6
  3. package/assets/dashboard/assets/dist-DHabpZqA.js +140 -0
  4. package/assets/dashboard/assets/index-BZMSpYrl.js +3613 -0
  5. package/assets/dashboard/assets/index-Dex2YZcC.css +1 -0
  6. package/assets/dashboard/assets/json-code-editor-DtZpKDXm.js +12 -0
  7. package/assets/dashboard/assets/pie-chart-recharts-C7WAhcCk.js +1 -0
  8. package/assets/dashboard/assets/richtext-render-x0TT0116.js +258 -0
  9. package/assets/dashboard/assets/timeseries-chart-recharts-B9RquiY6.js +1 -0
  10. package/assets/dashboard/assets/w3c-keyname-BOAvb0qz.js +1 -0
  11. package/assets/dashboard/index.html +4 -2
  12. package/dist/auth/auth.app.d.ts +13 -0
  13. package/dist/auth/index.d.ts +6 -0
  14. package/dist/factory.d.ts +14 -1
  15. package/dist/features/oauth/authorization-request.d.ts +127 -0
  16. package/dist/features/oauth/authorize.d.ts +97 -0
  17. package/dist/features/oauth/consents.d.ts +75 -0
  18. package/dist/features/oauth/constants.d.ts +48 -0
  19. package/dist/features/oauth/index.d.ts +31 -0
  20. package/dist/features/oauth/revoke.d.ts +29 -0
  21. package/dist/features/oauth/token-issuance.d.ts +49 -0
  22. package/dist/features/oauth/token.d.ts +38 -0
  23. package/dist/features/search/constants.d.ts +43 -0
  24. package/dist/features/search/handlers/embed.d.ts +31 -0
  25. package/dist/features/search/handlers/full-text-search.d.ts +28 -0
  26. package/dist/features/search/index.d.ts +20 -0
  27. package/dist/features/search/jobs/semantic-search.hooks.d.ts +21 -0
  28. package/dist/features/search/jobs/semantic-search.worker.d.ts +101 -0
  29. package/dist/features/search/public-search.router.d.ts +15 -0
  30. package/dist/features/search/search.d.ts +14 -6
  31. package/dist/features/search/semantic-search.hooks.d.ts +2 -0
  32. package/dist/features/search/semantic-search.worker.d.ts +21 -0
  33. package/dist/features/search/types.d.ts +100 -0
  34. package/dist/features/search/utils/search-utils.d.ts +56 -0
  35. package/dist/features/seeds/seeds.destructive.d.ts +6 -0
  36. package/dist/features/seeds/seeds.handler.d.ts +14 -0
  37. package/dist/features/seeds/seeds.helpers.d.ts +118 -0
  38. package/dist/features/seeds/seeds.mcp.d.ts +37 -0
  39. package/dist/features/upload/index.d.ts +2 -2
  40. package/dist/index.js +3501 -1338
  41. package/dist/middleware/auth.middleware.d.ts +34 -4
  42. package/dist/middleware/oauth-scope.middleware.d.ts +60 -0
  43. package/dist/middleware/rate-limit.middleware.d.ts +4 -2
  44. package/dist/middleware/repository.middleware.d.ts +9 -1
  45. package/dist/public/api-key-middleware.d.ts +0 -4
  46. package/dist/public/public-add.d.ts +1 -1
  47. package/dist/rate-limit/in-memory-rate-limiter.d.ts +1 -8
  48. package/dist/shared/db/repositories/content.repository.d1.d.ts +4 -2
  49. package/dist/shared/db/repositories/d1-oauth-authorization-code.repository.d.ts +9 -0
  50. package/dist/shared/db/repositories/d1-oauth-client.repository.d.ts +6 -0
  51. package/dist/shared/db/repositories/d1-oauth-consent.repository.d.ts +10 -0
  52. package/dist/shared/db/repositories/d1-oauth-token.repository.d.ts +12 -0
  53. package/dist/shared/db/repositories/d1-search.query.d.ts +54 -0
  54. package/dist/shared/db/repositories/d1-vector.repository.d.ts +11 -0
  55. package/dist/shared/db/repositories/in-memory-seed.repository.d.ts +2 -1
  56. package/dist/shared/db/repositories/seed.repository.d1.d.ts +3 -1
  57. package/dist/shared/db/repositories/time-trap-token.repository.d1.d.ts +7 -0
  58. package/dist/shared/storage/factory.d.ts +84 -4
  59. package/dist/shared/storage/r2-bucket.d.ts +127 -0
  60. package/dist/shared/storage/s3-bucket.d.ts +104 -10
  61. package/dist/shared/storage/upload.d.ts +33 -5
  62. package/dist/shared/utils/dual-key-rate-limiter.d.ts +21 -0
  63. package/dist/shared/utils/media-utils.d.ts +3 -3
  64. package/dist/shared/utils/opaque-token.d.ts +13 -0
  65. package/dist/types.d.ts +49 -1
  66. package/migrations/0000_v040_base.sql +170 -0
  67. package/migrations/_archive/0000_schema_init.sql +257 -0
  68. package/migrations/_archive/0001_init.sql +25 -0
  69. package/migrations/_archive/0002_seed_admin.sql +10 -0
  70. package/migrations/_archive/0003_refresh_tokens.sql +17 -0
  71. package/migrations/_archive/0004_seed_projects.sql +18 -0
  72. package/migrations/_archive/0005_update_projects.sql +56 -0
  73. package/migrations/_archive/0006_add_slug_and_unique_index.sql +7 -0
  74. package/migrations/_archive/0007_cleanup_test_data.sql +4 -0
  75. package/migrations/_archive/0008_seed_content_entries.sql +104 -0
  76. package/migrations/_archive/0009_public_idempotency.sql +11 -0
  77. package/migrations/_archive/0010_seed_real_data.sql +31 -0
  78. package/migrations/_archive/0010_testsite_data.sql +31 -0
  79. package/migrations/_archive/0011_analytics.sql +10 -0
  80. package/migrations/_archive/0012_seed_analytics.sql +17 -0
  81. package/migrations/_archive/0013_system_stats.sql +8 -0
  82. package/migrations/_archive/0014_activity_logs.sql +15 -0
  83. package/migrations/_archive/0015_notifications.sql +12 -0
  84. package/migrations/_archive/0016_fts5_global_search.sql +81 -0
  85. package/migrations/_archive/0017_seed_clienti.sql +90 -0
  86. package/migrations/_archive/0018_draft_data.sql +3 -0
  87. package/migrations/_archive/0019_drop_fts_triggers.sql +7 -0
  88. package/migrations/_archive/0020_composite_indexes.sql +15 -0
  89. package/migrations/_archive/0021_media_objects.sql +15 -0
  90. package/migrations/_archive/0022_analytics_seed.sql +23 -0
  91. package/migrations/_archive/0023_user_profile.sql +4 -0
  92. package/migrations/_archive/0024_activity_user_name.sql +3 -0
  93. package/migrations/_archive/0025_password_reset_tokens.sql +11 -0
  94. package/migrations/_archive/0026_remove_dev_admin.sql +4 -0
  95. package/migrations/_archive/0027_v040_doctype_architecture.sql +51 -0
  96. package/migrations/_archive/0029_fts_all_text_branches.sql +281 -0
  97. package/migrations/_archive/0031_site_settings.sql +9 -0
  98. package/migrations/_archive/0032_seeds.sql +28 -0
  99. package/migrations/_archive/0033_dashboard_layouts.sql +14 -0
  100. package/migrations/_archive/0034_kanban_foundation.sql +24 -0
  101. package/migrations/_archive/0035_setup_lock.sql +10 -0
  102. package/migrations/_archive/0036_notification_type_success.sql +23 -0
  103. package/migrations/_archive/0037_time_trap_tokens.sql +13 -0
  104. package/migrations/_archive/0038_oauth_authorization.sql +95 -0
  105. package/migrations/_archive/0039_oauth_client_beech_mcp.sql +20 -0
  106. package/package.json +3 -5
  107. package/assets/dashboard/assets/index-0fKOe8Wq.js +0 -745
  108. package/assets/dashboard/assets/index-Bj2upDZk.css +0 -1
  109. package/assets/dashboard/assets/pie-chart-recharts-DSf6kc6u.js +0 -1
  110. package/assets/dashboard/assets/timeseries-chart-recharts-CqM5c6MV.js +0 -1
  111. /package/migrations/{0029_automations.sql → _archive/0029_automations.sql} +0 -0
@@ -1,33 +1,127 @@
1
1
  import { BeechBucket, PutBucketOptions, GetBucketResult, PresignOptions } from '@beechcms/core';
2
2
  /**
3
- * Implementation of BeechBucket using S3-compatible API (AWS SDK).
4
- * Used for production when connecting to R2 or any other S3 provider via HTTP.
3
+ * Configuration options for initializing an {@link S3Bucket} instance.
4
+ */
5
+ export interface S3BucketConfig {
6
+ /** S3 API endpoint URL (e.g. Cloudflare R2 endpoint or MinIO endpoint). */
7
+ endpoint: string;
8
+ /** Access Key ID for S3 authentication. */
9
+ accessKeyId: string;
10
+ /** Secret Access Key for S3 authentication. */
11
+ secretAccessKey: string;
12
+ /** Target S3 bucket name. */
13
+ bucketName: string;
14
+ /** Base URL of the API server used for constructing fallback media access URLs. */
15
+ baseUrl: string;
16
+ /** Optional CDN base URL prepended to media assets when served via CDN. */
17
+ cdnUrl?: string;
18
+ }
19
+ /**
20
+ * Implementation of {@link BeechBucket} using an S3-compatible API via the AWS SDK.
21
+ *
22
+ * @remarks
23
+ * Used in production and staging when connecting to Cloudflare R2 or other S3-compatible providers (such as MinIO) over HTTP.
24
+ * Supports standard bucket operations: upload (`put`), retrieval (`get`), metadata inspection (`head`),
25
+ * deletion (`delete`), pagination (`list`), total size calculation (`getTotalSize`), and presigned URL generation (`presignPut`, `presignGet`).
5
26
  */
6
27
  export declare class S3Bucket implements BeechBucket {
28
+ /** AWS SDK S3 client configured with custom endpoint, credentials, and path-style addressing. */
7
29
  private client;
30
+ /** Name of the target S3/R2 bucket. */
8
31
  private bucketName;
32
+ /** Base URL of the API server used as the fallback origin for media asset URLs. */
9
33
  private baseUrl;
34
+ /** Optional custom CDN base URL used to serve media assets directly from a CDN. */
10
35
  private cdnUrl;
11
- constructor(config: {
12
- endpoint: string;
13
- accessKeyId: string;
14
- secretAccessKey: string;
15
- bucketName: string;
16
- baseUrl: string;
17
- cdnUrl?: string;
18
- });
36
+ /**
37
+ * Initializes a new instance of {@link S3Bucket}.
38
+ *
39
+ * @param config - The S3 client and bucket configuration options.
40
+ */
41
+ constructor(config: S3BucketConfig);
42
+ /**
43
+ * Uploads an object to the S3 bucket.
44
+ *
45
+ * @param key - The unique object key (path) within the bucket.
46
+ * @param body - The object content as an `ArrayBuffer`, `Uint8Array`, or `ReadableStream`.
47
+ * @param options - Optional upload settings such as `contentType` and custom `metadata`.
48
+ * @throws {Error} If the S3 upload operation fails.
49
+ * @returns A promise that resolves when the upload completes.
50
+ */
19
51
  put(key: string, body: ArrayBuffer | Uint8Array | ReadableStream, options?: PutBucketOptions): Promise<void>;
52
+ /**
53
+ * Retrieves an object from the S3 bucket as a stream.
54
+ *
55
+ * @param key - The unique object key (path) within the bucket.
56
+ * @returns A promise resolving to a {@link GetBucketResult} with the object body stream and metadata, or `null` if the object does not exist (`NoSuchKey` / `NotFound`).
57
+ * @throws {Error} If an S3 error other than `NoSuchKey` or `NotFound` occurs (e.g. `AccessDenied`, timeout).
58
+ */
20
59
  get(key: string): Promise<GetBucketResult | null>;
60
+ /**
61
+ * Deletes an object from the S3 bucket.
62
+ *
63
+ * @param key - The unique object key (path) to delete.
64
+ * @throws {Error} If the S3 delete operation fails.
65
+ * @returns A promise that resolves when the object has been deleted.
66
+ */
21
67
  delete(key: string): Promise<void>;
68
+ /**
69
+ * Retrieves metadata for an object without downloading its payload.
70
+ *
71
+ * @param key - The unique object key (path) within the bucket.
72
+ * @returns A promise resolving to the object's size, content type, and custom metadata, or `null` if the object does not exist.
73
+ * @throws {Error} If an S3 error other than `NoSuchKey` or `NotFound` occurs.
74
+ */
22
75
  head(key: string): Promise<{
23
76
  size: number;
24
77
  contentType?: string;
25
78
  metadata?: Record<string, string>;
26
79
  } | null>;
80
+ /**
81
+ * Constructs the public access URL for an object key.
82
+ *
83
+ * @remarks
84
+ * If a CDN URL is configured, returns the CDN-prefixed path (`${cdnUrl}/${encodedKey}`).
85
+ * Otherwise, returns the API fallback path (`${baseUrl}/api/media/${encodedKey}`).
86
+ *
87
+ * @param key - The storage object key.
88
+ * @returns The fully qualified URL to access the media object.
89
+ */
27
90
  getUrl(key: string): string;
91
+ /**
92
+ * Computes the cumulative size in bytes of all objects stored in the bucket.
93
+ *
94
+ * @remarks
95
+ * Iterates through all bucket objects using paginated S3 `ListObjectsV2` requests.
96
+ *
97
+ * @returns A promise resolving to the total storage size in bytes.
98
+ */
28
99
  getTotalSize(): Promise<number>;
100
+ /**
101
+ * Generates a presigned URL allowing clients to directly upload an object via HTTP PUT.
102
+ *
103
+ * @remarks
104
+ * Binds the expiration time, content type, and expected content length into the AWS SigV4 signature.
105
+ *
106
+ * @param key - The target object key (path) for upload.
107
+ * @param options - Presigning options including `expiresIn`, `contentType`, and `contentLength`.
108
+ * @returns A promise resolving to the presigned PUT URL.
109
+ */
29
110
  presignPut(key: string, options: PresignOptions): Promise<string>;
111
+ /**
112
+ * Generates a presigned URL allowing clients to directly download an object via HTTP GET.
113
+ *
114
+ * @param key - The target object key (path) to download.
115
+ * @param options - Presigning options including `expiresIn`.
116
+ * @returns A promise resolving to the presigned GET URL.
117
+ */
30
118
  presignGet(key: string, options: PresignOptions): Promise<string>;
119
+ /**
120
+ * Lists objects in the bucket matching an optional prefix, with pagination support.
121
+ *
122
+ * @param options - Optional listing parameters including `prefix`, `limit` (max keys), and `cursor` (continuation token).
123
+ * @returns A promise resolving to an array of object summaries (key and size) and an optional continuation cursor.
124
+ */
31
125
  list(options?: {
32
126
  prefix?: string;
33
127
  limit?: number;
@@ -1,7 +1,35 @@
1
- export declare function deleteR2Objects(c: {
1
+ import type { BeechBucket, MediaRepository, SystemStatsRepository } from '@beechcms/core';
2
+ /**
3
+ * Context dependencies required to execute media deletion operations.
4
+ */
5
+ export interface DeleteR2ObjectsContext {
6
+ /** Variables bound to the request context. */
2
7
  var: {
3
- bucket: any;
4
- mediaRepository: any;
5
- systemStatsRepository: any;
8
+ /** Storage bucket instance used to delete the physical object. */
9
+ bucket: BeechBucket;
10
+ /** Media repository used to look up and untrack media database records. */
11
+ mediaRepository: MediaRepository;
12
+ /** System statistics repository used to decrement cumulative storage usage. */
13
+ systemStatsRepository: SystemStatsRepository;
6
14
  };
7
- }, objectKeys: string[]): Promise<void>;
15
+ }
16
+ /**
17
+ * Deletes one or more media objects from the storage bucket and synchronizes database tracking and statistics.
18
+ *
19
+ * @remarks
20
+ * For each provided object key, the following sequence is performed:
21
+ * 1. Looks up the media record in `mediaRepository` to obtain file size and ensure the key is tracked.
22
+ * If the key is not found or the lookup fails, an error is thrown before bucket deletion.
23
+ * 2. Deletes the physical binary object from the storage `bucket`.
24
+ * 3. Untracks the media record from `mediaRepository` and decrements cumulative storage in `systemStatsRepository`.
25
+ * 4. If bucket deletion succeeds but untracking or statistics decrement fails, the key is recorded as out-of-sync
26
+ * and a composite error is thrown after all operations complete to surface data drift.
27
+ *
28
+ * @param context - Context object holding `bucket`, `mediaRepository`, and `systemStatsRepository` dependencies in `context.var`.
29
+ * @param objectKeys - Array of media storage object keys to delete.
30
+ * @throws {Error} If media record lookup fails or key is untracked in the database.
31
+ * @throws {Error} If the storage bucket deletion fails.
32
+ * @throws {Error} If one or more media records fail to untrack after bucket deletion (data drift).
33
+ * @returns A promise that resolves when all objects have been successfully deleted and untracked.
34
+ */
35
+ export declare function deleteR2Objects(context: DeleteR2ObjectsContext, objectKeys: string[]): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import type { IRateLimiter } from '@beechcms/core';
2
+ export interface DualKeyRateLimitOptions {
3
+ ipLimiter: IRateLimiter;
4
+ accountLimiter: IRateLimiter;
5
+ clientIp: string;
6
+ accountKey: string;
7
+ }
8
+ export interface DualKeyRateLimitResult {
9
+ isAllowed: boolean;
10
+ retryAfterSeconds?: number;
11
+ blockedBy?: 'ip' | 'account' | 'both';
12
+ }
13
+ /**
14
+ * Normalizes account identifiers (e.g. emails) by trimming whitespace and converting to lowercase.
15
+ */
16
+ export declare function normalizeAccountKey(rawKey: string): string;
17
+ /**
18
+ * Coordinates atomic evaluation of IP and Account rate limiters.
19
+ * If either bucket violates its limit, access is rejected (HTTP 429).
20
+ */
21
+ export declare function checkDualKeyRateLimit(options: DualKeyRateLimitOptions): Promise<DualKeyRateLimitResult>;
@@ -16,11 +16,11 @@ import type { Seed } from '@beechcms/core';
16
16
  export declare function extractMediaKey(mediaUrl: string, cdnUrl?: string): string | null;
17
17
  /**
18
18
  * Estrae tutte le chiavi R2 dal data di un'entry.
19
- * Cerca nei campi `file` (stringa URL) e `json` (array/oggetto con URL).
20
- * Il data è in formato DB: chiavi = branch.id (es. art_03, prd_05).
19
+ * Cerca nei campi `file` (stringa URL), `json` (array/oggetto con URL) e `repeater` (array di record).
20
+ * Il data è in formato DB o deserializzato (chiavi = branch alias).
21
21
  *
22
22
  * @param seed - Schema del tipo di contenuto
23
- * @param entryData - Payload in formato DB (chiavi = branch ID)
23
+ * @param entryData - Payload (chiavi = branch alias)
24
24
  * @param cdnUrl - URL CDN opzionale configurato
25
25
  * @returns Array di chiavi R2 uniche da eliminare
26
26
  */
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 256 bits of CSPRNG entropy, hex-encoded.
3
+ *
4
+ * The single entropy source for every bearer credential in the API: dashboard
5
+ * session refresh tokens, OAuth authorization codes, and OAuth access/refresh
6
+ * tokens. It lives in `shared/utils` — not inside `auth/` and not copied into a
7
+ * feature slice — so that changing the length or the encoding is a one-line
8
+ * change that cannot silently leave a second copy behind.
9
+ *
10
+ * Promoted from `auth/utils/refresh-token.ts` (`generateRefreshToken`) in the
11
+ * `oauth-authorization-server` sprint. Body is unchanged.
12
+ */
13
+ export declare function generateOpaqueToken(): string;
package/dist/types.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * per-request context values injected by middleware (`Variables`), and
5
5
  * the combined `AppEnv` used to type every Hono app/router in the API.
6
6
  */
7
- import type { Seed, ContentRepository, IdempotencyRepository, BeechBucket, MediaRepository, SystemStatsRepository, IHashProvider, ITokenService, IUserRepository, ISessionRepository, IPasswordResetTokenRepository, IActivityLogger, IActivityLogRepository, INotificationRepository, INotificationService, IWidgetRepository, ISearchRepository, IAnalyticsRepository, IContentScanRepository, ISeedRegistry, IClock, IIdGenerator, IAutomationRunner, IAutomationRepository, IScheduler, BackrefMap, ISiteSettingsRepository, IDemoDataRepository, JwtClaims, ISeedLayoutRepository, ISeedRepository, ISchemaMutator, IDashboardLayoutRepository, IQueueService, IKanbanPositionRepository, IPrivacyService, ActorContext } from '@beechcms/core';
7
+ import type { Seed, ContentRepository, IdempotencyRepository, BeechBucket, MediaRepository, SystemStatsRepository, IHashProvider, ITokenService, IUserRepository, ISessionRepository, IPasswordResetTokenRepository, IActivityLogger, IActivityLogRepository, INotificationRepository, INotificationService, IWidgetRepository, ISearchRepository, IAnalyticsRepository, IContentScanRepository, ISeedRegistry, IClock, IIdGenerator, IAutomationRunner, IAutomationRepository, IScheduler, BackrefMap, ISiteSettingsRepository, IDemoDataRepository, JwtClaims, ISeedLayoutRepository, ISeedRepository, ISchemaMutator, IDashboardLayoutRepository, IQueueService, IKanbanPositionRepository, IPrivacyService, ActorContext, IAntivirusProvider, ITimeTrapTokenRepository, IOAuthClientRepository, IOAuthAuthorizationCodeRepository, IOAuthTokenRepository, IOAuthConsentRepository, IRoleGuard, OAuthScope } from '@beechcms/core';
8
8
  import type { IRateLimiterRegistry } from './middleware/rate-limit.middleware';
9
9
  import type { ISetupChecklistRepository } from './shared/db/repositories/d1-setup-checklist.repository';
10
10
  /**
@@ -14,12 +14,18 @@ import type { ISetupChecklistRepository } from './shared/db/repositories/d1-setu
14
14
  export interface Env {
15
15
  /** D1 database binding, the source of truth for all content and system tables. */
16
16
  DB: D1Database;
17
+ /** Cloudflare Workers AI binding for embeddings / LLMs. */
18
+ AI?: any;
19
+ /** Cloudflare R2 bucket binding for compiled search vectors and manifests. */
20
+ SEARCH_R2?: R2Bucket;
17
21
  /** Secret used to sign and verify JWTs. */
18
22
  JWT_SECRET: string;
19
23
  /** Expected `iss` claim on issued/verified JWTs. */
20
24
  JWT_ISSUER?: string;
21
25
  /** Expected `aud` claim on issued/verified JWTs. */
22
26
  JWT_AUDIENCE?: string;
27
+ /** Native Cloudflare R2 bucket binding for media storage. */
28
+ MEDIA_BUCKET?: R2Bucket;
23
29
  /** R2-compatible access key ID for media storage. */
24
30
  R2_ACCESS_KEY_ID?: string;
25
31
  /** R2-compatible secret access key for media storage. */
@@ -92,6 +98,29 @@ export interface Env {
92
98
  QSTASH_NEXT_SIGNING_KEY?: string;
93
99
  /** Master key used for Application-Level Encryption (ALE). */
94
100
  PRIVACY_MASTER_KEY?: string;
101
+ /** API key for VirusTotal file/hash scanning. */
102
+ VIRUSTOTAL_API_KEY?: string;
103
+ /** Secret used to sign and verify public form time-trap tokens. */
104
+ PUBLIC_TIME_TRAP_SECRET?: string;
105
+ /** Comma-separated list of allowed origins for public submissions. */
106
+ ALLOWED_ORIGINS?: string;
107
+ }
108
+ /**
109
+ * Authentication material carried by a request that authenticated with an OAuth 2.1
110
+ * access token instead of the dashboard admin JWT.
111
+ *
112
+ * `null` means the request authenticated with the admin JWT (or, on routers where
113
+ * `authMiddleware` is not registered, did not authenticate at all). The scope gate
114
+ * treats `null` as "not an OAuth request" and lets it through unchanged, so admin
115
+ * dashboard behaviour is never affected by scope enforcement.
116
+ */
117
+ export interface OAuthGrantContext {
118
+ /** Client that presented the access token (`oauth_tokens.client_id`). */
119
+ clientId: string;
120
+ /** Resource owner the token acts for (`oauth_tokens.user_id`). */
121
+ userId: string;
122
+ /** Scopes frozen into the token record at issuance time. Never re-derived per request. */
123
+ scope: OAuthScope[];
95
124
  }
96
125
  /**
97
126
  * Per-request context values (`c.get(...)` / `c.set(...)`), injected by middleware
@@ -102,6 +131,11 @@ export interface Variables {
102
131
  actor?: ActorContext;
103
132
  /** Decoded JWT claims of the authenticated user. */
104
133
  jwtPayload: JwtClaims;
134
+ /**
135
+ * Set by `authMiddleware({ acceptOAuth: true })`. Non-null only when the request
136
+ * presented an OAuth access token. Read by `oauthScopeMiddleware()`.
137
+ */
138
+ oauthGrant: OAuthGrantContext | null;
105
139
  /** Application-level privacy and encryption service. */
106
140
  privacyService: IPrivacyService;
107
141
  /** Looks up a Seed definition by slug from the loaded schema. */
@@ -176,6 +210,20 @@ export interface Variables {
176
210
  dashboardLayoutRepository: IDashboardLayoutRepository;
177
211
  /** Repository for Kanban card position/ordering state. */
178
212
  kanbanPositionRepository: IKanbanPositionRepository;
213
+ /** Antivirus scanning provider. */
214
+ antivirusProvider: IAntivirusProvider;
215
+ /** Repository for single-use time-trap token deduplication. */
216
+ timeTrapTokenRepository: ITimeTrapTokenRepository;
217
+ /** Registry of OAuth clients authorized to use the authorization-code flow. */
218
+ oauthClientRepository: IOAuthClientRepository;
219
+ /** Repository for single-use OAuth authorization codes (hash-only). */
220
+ oauthAuthorizationCodeRepository: IOAuthAuthorizationCodeRepository;
221
+ /** Repository for OAuth access/refresh tokens (hash-only). */
222
+ oauthTokenRepository: IOAuthTokenRepository;
223
+ /** Repository for per-(client,user) OAuth scope consents. */
224
+ oauthConsentRepository: IOAuthConsentRepository;
225
+ /** Arbitrates which requested scopes a user's role may grant. Sole role-authorization seam in the OAuth flow. */
226
+ roleGuard: IRoleGuard;
179
227
  }
180
228
  /** Combined Hono generic type (`Bindings` + `Variables`) used to type every app/router in the API. */
181
229
  export type AppEnv = {
@@ -276,3 +276,173 @@ CREATE TABLE IF NOT EXISTS setup_completed (
276
276
  id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1)
277
277
  );
278
278
 
279
+
280
+ -- =============================================================================
281
+ -- 15. RUNTIME SEEDS
282
+ -- `seeds` : one row per content type. `definition` is the full Seed JSON.
283
+ -- `seed_meta` : single-row table holding the registry version token used for
284
+ -- multi-isolate cache invalidation.
285
+ -- =============================================================================
286
+
287
+ CREATE TABLE IF NOT EXISTS seeds (
288
+ slug TEXT NOT NULL PRIMARY KEY,
289
+ definition TEXT NOT NULL,
290
+ status TEXT NOT NULL DEFAULT 'active'
291
+ CHECK (status IN ('active', 'deleted')),
292
+ source TEXT NOT NULL DEFAULT 'runtime'
293
+ CHECK (source IN ('code', 'runtime')),
294
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
295
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
296
+ );
297
+
298
+ CREATE INDEX IF NOT EXISTS idx_seeds_status ON seeds(status);
299
+
300
+ CREATE TABLE IF NOT EXISTS seed_meta (
301
+ id TEXT NOT NULL PRIMARY KEY,
302
+ value TEXT NOT NULL
303
+ );
304
+
305
+ INSERT OR IGNORE INTO seed_meta (id, value) VALUES ('registry_version', '1');
306
+
307
+
308
+ -- =============================================================================
309
+ -- 16. DASHBOARD LAYOUTS
310
+ -- One row per scope: 'default' | 'role:admin' | 'role:editor' ...
311
+ -- =============================================================================
312
+
313
+ CREATE TABLE IF NOT EXISTS dashboard_layouts (
314
+ scope TEXT NOT NULL PRIMARY KEY,
315
+ layout TEXT NOT NULL,
316
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
317
+ updated_by TEXT NOT NULL
318
+ );
319
+
320
+
321
+ -- =============================================================================
322
+ -- 17. KANBAN FOUNDATION
323
+ -- kanban_positions : per-(seed, axis, entry) fractional-index ordering.
324
+ -- seed_layouts.view_config : additive JSON blob for per-seed dashboard view
325
+ -- preferences (kanban axis/sort/hidden columns).
326
+ -- =============================================================================
327
+
328
+ CREATE TABLE IF NOT EXISTS kanban_positions (
329
+ seed_slug TEXT NOT NULL,
330
+ entry_id TEXT NOT NULL,
331
+ axis_branch_id TEXT NOT NULL,
332
+ position TEXT NOT NULL,
333
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
334
+ PRIMARY KEY (seed_slug, entry_id, axis_branch_id)
335
+ );
336
+
337
+ CREATE INDEX IF NOT EXISTS idx_kanban_positions_column
338
+ ON kanban_positions (seed_slug, axis_branch_id, position);
339
+
340
+ ALTER TABLE seed_layouts ADD COLUMN view_config TEXT;
341
+
342
+
343
+ -- =============================================================================
344
+ -- 18. PUBLIC TIME-TRAP TOKENS
345
+ -- Single-use tracking for HMAC time-trap tokens to prevent replay attacks.
346
+ -- =============================================================================
347
+
348
+ CREATE TABLE IF NOT EXISTS public_time_trap_tokens (
349
+ token_hash TEXT NOT NULL PRIMARY KEY,
350
+ used_at INTEGER NOT NULL,
351
+ expires_at INTEGER NOT NULL
352
+ );
353
+
354
+ CREATE INDEX IF NOT EXISTS idx_time_trap_tokens_expires
355
+ ON public_time_trap_tokens (expires_at);
356
+
357
+
358
+ -- =============================================================================
359
+ -- 19. OAUTH 2.1 AUTHORIZATION SERVER
360
+ -- Authorization code + PKCE grant. Every credential (code, access token,
361
+ -- refresh token) is persisted as a SHA-256 hex hash only — never plaintext.
362
+ -- Expired rows are filtered at read time via expires_at; no pruning job.
363
+ -- =============================================================================
364
+
365
+ CREATE TABLE IF NOT EXISTS oauth_clients (
366
+ client_id TEXT NOT NULL PRIMARY KEY,
367
+ name TEXT NOT NULL,
368
+ redirect_uris TEXT NOT NULL,
369
+ allowed_scopes TEXT NOT NULL,
370
+ is_public INTEGER NOT NULL DEFAULT 1
371
+ CHECK (is_public IN (0, 1)),
372
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
373
+ disabled_at INTEGER DEFAULT NULL
374
+ );
375
+
376
+ CREATE TABLE IF NOT EXISTS oauth_authorization_codes (
377
+ code_hash TEXT NOT NULL PRIMARY KEY,
378
+ client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
379
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
380
+ scope TEXT NOT NULL,
381
+ redirect_uri TEXT NOT NULL,
382
+ code_challenge TEXT NOT NULL,
383
+ code_challenge_method TEXT NOT NULL DEFAULT 'S256'
384
+ CHECK (code_challenge_method = 'S256'),
385
+ expires_at INTEGER NOT NULL,
386
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
387
+ consumed_at INTEGER DEFAULT NULL
388
+ );
389
+
390
+ CREATE INDEX IF NOT EXISTS idx_oauth_codes_user ON oauth_authorization_codes(user_id);
391
+ CREATE INDEX IF NOT EXISTS idx_oauth_codes_expires ON oauth_authorization_codes(expires_at);
392
+
393
+ CREATE TABLE IF NOT EXISTS oauth_tokens (
394
+ id TEXT NOT NULL PRIMARY KEY,
395
+ token_hash TEXT NOT NULL,
396
+ token_type TEXT NOT NULL
397
+ CHECK (token_type IN ('access', 'refresh')),
398
+ client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
399
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
400
+ scope TEXT NOT NULL,
401
+ authorization_code_hash TEXT NOT NULL,
402
+ expires_at INTEGER NOT NULL,
403
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
404
+ revoked_at INTEGER DEFAULT NULL
405
+ );
406
+
407
+ CREATE INDEX IF NOT EXISTS idx_oauth_tokens_hash ON oauth_tokens(token_hash);
408
+ CREATE INDEX IF NOT EXISTS idx_oauth_tokens_code ON oauth_tokens(authorization_code_hash);
409
+ CREATE INDEX IF NOT EXISTS idx_oauth_tokens_client ON oauth_tokens(client_id, user_id);
410
+ CREATE INDEX IF NOT EXISTS idx_oauth_tokens_user ON oauth_tokens(user_id);
411
+ CREATE INDEX IF NOT EXISTS idx_oauth_tokens_expires ON oauth_tokens(expires_at);
412
+
413
+ CREATE TABLE IF NOT EXISTS oauth_consents (
414
+ id TEXT NOT NULL PRIMARY KEY,
415
+ client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
416
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
417
+ scopes TEXT NOT NULL,
418
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
419
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
420
+ revoked_at INTEGER DEFAULT NULL
421
+ );
422
+
423
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_oauth_consents_pair ON oauth_consents(client_id, user_id);
424
+ CREATE INDEX IF NOT EXISTS idx_oauth_consents_user ON oauth_consents(user_id);
425
+
426
+ -- Static registry, no dynamic client registration (RFC 7591). Seeded client
427
+ -- is @beechcms/mcp — public client (stdio CLI, no secret), PKCE S256 proof of
428
+ -- possession. Redirect URI registered WITHOUT a port: matchesRegisteredRedirectUri()
429
+ -- compares loopback URIs on protocol+hostname+pathname only (OAuth 2.1 §8.4.2),
430
+ -- because the CLI binds an ephemeral port at runtime.
431
+ INSERT OR IGNORE INTO oauth_clients (client_id, name, redirect_uris, allowed_scopes, is_public)
432
+ VALUES (
433
+ 'beech-mcp',
434
+ 'BeechCMS MCP Server',
435
+ '["http://127.0.0.1/oauth/callback"]',
436
+ 'schema:read schema:write',
437
+ 1
438
+ );
439
+
440
+ INSERT OR IGNORE INTO oauth_clients (client_id, name, redirect_uris, allowed_scopes, is_public)
441
+ VALUES (
442
+ 'beech-mcp-cli',
443
+ 'BeechCMS MCP Server',
444
+ '["http://127.0.0.1/callback"]',
445
+ 'schema:read schema:write',
446
+ 1
447
+ );
448
+