create-ortha-app 0.4.2 → 0.4.3

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.
@@ -1,98 +1,59 @@
1
1
  /**
2
2
  * Typed configuration for this app.
3
3
  *
4
- * **The single place that reads `process.env`.** Everything downstream — the
5
- * host, every plugin — receives typed values, so "where does this setting come
6
- * from" has exactly one answer. Deploy-specific values come from the
7
- * environment; stable product tuning lives here as literals.
4
+ * **`config/` is the single place that reads `process.env`.** Everything
5
+ * downstream — the host, every plugin — receives typed values, so "where does
6
+ * this setting come from" has exactly one answer. Deploy-specific values come
7
+ * from the environment; stable product tuning lives in the builders as literals.
8
+ *
9
+ * **Shape:** one module per plugin under `config/`, each exporting a builder,
10
+ * and this file assembling them. So the literal at the bottom is the table of
11
+ * contents — what this app runs, in one screen — and a reader who wants to know
12
+ * how one setting is derived opens the one module that owns it. Adding a setting
13
+ * means editing one builder; adding a plugin means one module and one line here.
14
+ *
15
+ * *How* a value is parsed is decided in neither place: the readers come from
16
+ * `@orthacms/utils-server`. Each refuses a value it cannot honour instead of
17
+ * guessing, and the guessing is what makes a misconfigured deployment look
18
+ * configured. Anything conditional is a *value* — a builder returns `undefined`
19
+ * for a backend you did not configure, and `defined(…)` drops the keys that were
20
+ * never set, so no config object here needs a `...(x ? { … } : {})` spread.
21
+ *
22
+ * This file stays the entry point rather than becoming another module in the
23
+ * folder: `ortha start` runs the compiled `dist/server/ortha.config.js`, and
24
+ * `src/plugins.ts` imports the types below.
8
25
  */
9
26
  import { join } from 'node:path';
10
27
  import type {
11
28
  ApiDocsOptions,
12
29
  TrustProxySetting
13
30
  } from '@orthacms/bootstrap-server';
14
- import type { IdentityPluginConfig } from '@orthacms/identity-server';
15
- // ortha:if sso-oidc
16
- import type { OidcProviderConfig } from '@orthacms/identity-provider-oidc';
17
- // ortha:end
18
31
  import type { I18nPluginConfig } from '@orthacms/i18n-server';
19
- import type { MediaPluginConfig } from '@orthacms/media-server';
20
- // ortha:if media-local
21
- import type { LocalStorageConfig } from '@orthacms/media-provider-local';
22
- // ortha:end
23
- // ortha:if media-s3
24
- import type { S3StorageConfig } from '@orthacms/media-provider-s3';
25
- // ortha:end
26
- // ortha:if media-azure
27
- import type { AzureStorageConfig } from '@orthacms/media-provider-azure';
28
- // ortha:end
29
- // ortha:if media-gcs
30
- import type { GcsStorageConfig } from '@orthacms/media-provider-gcs';
31
- // ortha:end
32
- // ortha:if media-vercel-blob
33
- import type { VercelBlobStorageConfig } from '@orthacms/media-provider-vercel-blob';
34
- // ortha:end
35
- import type { CopilotPluginConfig } from '@orthacms/copilot-server';
36
- // ortha:if copilot-anthropic
37
- import type { AnthropicProviderConfig } from '@orthacms/copilot-provider-anthropic';
38
- // ortha:end
39
- // ortha:if copilot-openai
40
- import type { OpenAiProviderConfig } from '@orthacms/copilot-provider-openai';
41
- // ortha:end
42
32
  // ortha:if mcp
43
33
  import type { McpPluginConfig } from '@orthacms/mcp-server';
44
34
  // ortha:end
35
+ import {
36
+ readEnv,
37
+ readPositiveInt,
38
+ readTrustProxy,
39
+ requireEnv
40
+ } from '@orthacms/utils-server';
41
+
42
+ import { docsConfig } from './config/docs';
43
+ import { identityConfig, type AppIdentityConfig } from './config/identity';
44
+ import { i18nConfig } from './config/i18n';
45
+ import { mediaConfig, type AppMediaConfig } from './config/media';
46
+ import { copilotConfig, type AppCopilotConfig } from './config/copilot';
47
+ // ortha:if mcp
48
+ import { mcpConfig } from './config/mcp';
49
+ // ortha:end
45
50
 
46
51
  /**
47
- * Copilot settings plus the backends this deployment can reach.
48
- *
49
- * The provider settings live **here**, not inside `CopilotPluginConfig`: the
50
- * plugin is adapter-agnostic by decision, so it names no provider kind. A key
51
- * is present only when the deployment configured that backend, and `plugins.ts`
52
- * registers exactly the ones that are — "configured" is a fact this file can
53
- * read, where a `defaultProvider` naming one of them could be misspelled or
54
- * point at a backend nobody registered.
55
- */
56
- /**
57
- * Identity settings, plus the identity providers this app can reach.
58
- *
59
- * The provider settings live here rather than inside `IdentityPluginConfig`,
60
- * for the same reason the copilot's backends do: the plugin names no protocol,
61
- * and this file is the one place that reads the environment. The constructed
62
- * adapters are registered in `src/plugins.ts`.
52
+ * Re-exported so `import type { AppCopilotConfig } from '../ortha.config'`
53
+ * keeps working. `src/plugins.ts` names these, and which file they are declared
54
+ * in is an implementation detail of this one.
63
55
  */
64
- export interface AppIdentityConfig extends IdentityPluginConfig {
65
- /**
66
- * Identity providers, keyed by the name they are registered under. That
67
- * name appears in the sign-in URL and in every `sso_identities` row, so
68
- * renaming one orphans the links that name it.
69
- *
70
- * Optional, and absent unless this app was generated with single sign-on.
71
- */
72
- ssoProviders?: {
73
- // ortha:if sso-oidc
74
- /** A generic OpenID Connect provider. Present when both env vars are set. */
75
- oidc?: OidcProviderConfig & { name: string };
76
- // ortha:end
77
- };
78
- }
79
-
80
- export interface AppCopilotConfig extends CopilotPluginConfig {
81
- providers: {
82
- // ortha:if copilot-anthropic
83
- /** Native Claude. Present when ANTHROPIC_API_KEY is set. */
84
- claude?: AnthropicProviderConfig;
85
- // ortha:end
86
- // ortha:if copilot-openai
87
- /**
88
- * An OpenAI-wire endpoint — Ollama, vLLM, LiteLLM, Azure or OpenAI.
89
- * Present when COPILOT_OPENAI_BASE_URL is set: an endpoint nobody
90
- * named is a backend that can only time out.
91
- */
92
- openai?: OpenAiProviderConfig;
93
- // ortha:end
94
- };
95
- }
56
+ export type { AppIdentityConfig, AppMediaConfig, AppCopilotConfig };
96
57
 
97
58
  /** Root configuration for this app. */
98
59
  export interface OrthaConfig {
@@ -106,27 +67,7 @@ export interface OrthaConfig {
106
67
  plugins: {
107
68
  identity: AppIdentityConfig;
108
69
  i18n: I18nPluginConfig;
109
- media: MediaPluginConfig & {
110
- /**
111
- * Settings for the storage backend `plugins.ts` constructs. Typed
112
- * by the factory it imports — the two move together.
113
- */
114
- // ortha:if media-local
115
- storage: LocalStorageConfig;
116
- // ortha:end
117
- // ortha:if media-s3
118
- storage: S3StorageConfig;
119
- // ortha:end
120
- // ortha:if media-azure
121
- storage: AzureStorageConfig;
122
- // ortha:end
123
- // ortha:if media-gcs
124
- storage: GcsStorageConfig;
125
- // ortha:end
126
- // ortha:if media-vercel-blob
127
- storage: VercelBlobStorageConfig;
128
- // ortha:end
129
- };
70
+ media: AppMediaConfig;
130
71
  copilot: AppCopilotConfig;
131
72
  // ortha:if mcp
132
73
  mcp: McpPluginConfig;
@@ -134,105 +75,14 @@ export interface OrthaConfig {
134
75
  };
135
76
  }
136
77
 
137
- /**
138
- * Reads a value the app cannot run without, failing at load rather than
139
- * several seconds into boot.
140
- *
141
- * Allowed to default to `''`, a missing `DATABASE_URL` reaches `pg` as "use
142
- * the libpq defaults" — so the first query fails with whatever the local
143
- * environment happens to produce, and nothing in the message names the
144
- * variable nobody set.
145
- */
146
- function requireEnv(name: string): string {
147
- const raw = process.env[name]?.trim();
148
- if (!raw) {
149
- throw new Error(
150
- `Missing required environment variable ${name}. ` +
151
- 'Set it in your .env before starting the app.'
152
- );
153
- }
154
- return raw;
155
- }
156
-
157
- /**
158
- * A numeric setting: the default when unset, the value when it is a plain
159
- * positive integer, and an error otherwise.
160
- *
161
- * Deliberately not `Number(process.env[x]) || fallback`, which is wrong in
162
- * three directions and silent in all of them: `0` is falsy so it becomes the
163
- * default, a negative is truthy so it is accepted (a negative session TTL
164
- * issues every session already expired), and `1e9` parses.
165
- */
166
- function readPositiveInt(name: string, fallback: number): number {
167
- const raw = process.env[name]?.trim();
168
- if (!raw) return fallback;
169
-
170
- if (!/^\d+$/.test(raw) || Number(raw) <= 0) {
171
- throw new Error(
172
- `Environment variable ${name} must be a positive whole number ` +
173
- `(got "${raw}").`
174
- );
175
- }
176
- return Number(raw);
177
- }
178
-
179
- /**
180
- * Express's `trust proxy` setting: a hop count (the recommended form, and the
181
- * only one a client cannot forge past), a boolean, or a subnet/preset string
182
- * passed through verbatim. Unset leaves forwarded headers ignored.
183
- */
184
- function readTrustProxy(): TrustProxySetting | undefined {
185
- const raw = process.env['TRUST_PROXY']?.trim();
186
- if (!raw) return undefined;
187
-
188
- const hops = Number(raw);
189
- if (Number.isInteger(hops) && hops >= 0) return hops;
190
- if (raw === 'true' || raw === 'false') return raw === 'true';
191
-
192
- return raw;
193
- }
194
-
195
- /**
196
- * True only in a deployment that said so, spelling checked.
197
- *
198
- * This one comparison gates two protections at once — whether the API
199
- * reference is published, and whether the session cookie carries `Secure` — so
200
- * a typo silently turns both off and is indistinguishable from correct
201
- * configuration until you read a `Set-Cookie` header.
202
- */
203
- const NODE_ENVS = ['development', 'test', 'production'] as const;
204
- const nodeEnv = process.env['NODE_ENV']?.trim();
205
-
206
- if (nodeEnv && !(NODE_ENVS as readonly string[]).includes(nodeEnv)) {
207
- throw new Error(
208
- `NODE_ENV is "${nodeEnv}", which this app does not recognise — expected ` +
209
- `one of ${NODE_ENVS.join(', ')}, or nothing at all for local ` +
210
- 'development. Anything else reads as "not production", which ' +
211
- 'publishes the API reference and drops `Secure` from the session cookie.'
212
- );
213
- }
214
-
215
- const isProduction = nodeEnv === 'production';
216
-
217
- /** A comma-separated list setting, trimmed and emptied of blanks. */
218
- function readList(name: string, fallback: string): string[] {
219
- return (process.env[name] ?? fallback)
220
- .split(',')
221
- .map((item) => item.trim())
222
- .filter(Boolean);
223
- }
224
- // ortha:if copilot-anthropic
225
- const anthropicApiKey = process.env['ANTHROPIC_API_KEY']?.trim();
226
- // ortha:end
227
- // ortha:if copilot-openai
228
- const openAiBaseUrl = process.env['COPILOT_OPENAI_BASE_URL']?.trim();
229
- // ortha:end
230
-
231
78
  const config: OrthaConfig = {
232
79
  port: readPositiveInt('PORT', 3000),
233
80
  globalPrefix: 'api',
81
+ // A hop count is the recommended form and the only one a client cannot
82
+ // forge past. Unset, forwarded headers are ignored entirely — right for an
83
+ // app exposed directly, wrong behind any proxy.
234
84
  trustProxy: readTrustProxy(),
235
- bodyLimit: process.env['MAX_REQUEST_BODY'] || '1mb',
85
+ bodyLimit: readEnv('MAX_REQUEST_BODY') ?? '1mb',
236
86
  // The built admin bundle, served by this same process so the API and the
237
87
  // UI share one origin — which is what identity's httpOnly, SameSite=lax
238
88
  // session cookie needs. `ortha dev` uses Vite's proxy for the same effect.
@@ -241,263 +91,19 @@ const config: OrthaConfig = {
241
91
  // must mean the same thing whether it is read from `dist/` or from source.
242
92
  staticDir: join(process.cwd(), 'dist/admin'),
243
93
  database: {
244
- url: requireEnv('DATABASE_URL')
245
- },
246
- docs: {
247
- // On outside production, where the reference is a development tool.
248
- // `API_DOCS=true` publishes it from a deployed instance.
249
- enabled: process.env['API_DOCS']
250
- ? process.env['API_DOCS'] === 'true'
251
- : !isProduction,
252
- title: '__APP_TITLE__ API',
253
- version: '1.0.0'
94
+ url: requireEnv(
95
+ 'DATABASE_URL',
96
+ 'Set it in your .env before starting the app.'
97
+ )
254
98
  },
99
+ docs: docsConfig(),
255
100
  plugins: {
256
- identity: {
257
- // Origins allowed to make state-changing calls (login-CSRF
258
- // defence). In development that is the Vite dev server; in
259
- // production the app is same-origin, so this list is what a
260
- // separately-hosted admin would need adding to.
261
- allowedOrigins: (
262
- process.env['ALLOWED_ORIGINS'] ??
263
- `http://localhost:${readPositiveInt('ADMIN_PORT', 4200)}`
264
- )
265
- .split(',')
266
- .map((origin) => origin.trim())
267
- .filter(Boolean),
268
- session: {
269
- ttlSeconds: readPositiveInt(
270
- 'SESSION_TTL_SECONDS',
271
- 60 * 60 * 24 * 7
272
- ),
273
- cookieSecure: isProduction,
274
- cookieSameSite: 'lax'
275
- },
276
- token: {
277
- inviteTtlSeconds: readPositiveInt(
278
- 'INVITE_TTL_SECONDS',
279
- 60 * 60 * 24 * 7
280
- ),
281
- resetTtlSeconds: readPositiveInt('RESET_TTL_SECONDS', 60 * 60)
282
- },
283
- rateLimit: {
284
- ttlSeconds: readPositiveInt('LOGIN_RATE_LIMIT_TTL_SECONDS', 60),
285
- limit: readPositiveInt('LOGIN_RATE_LIMIT', 10)
286
- },
287
- // Single sign-on. The providers themselves are constructed in
288
- // `src/plugins.ts`; these settings shape the handshake.
289
- sso: {
290
- // The origin browsers reach this API on. It builds the
291
- // redirect_uri you register with each provider, and it is
292
- // configured rather than read from the request's Host header,
293
- // which a client controls. Leave it unset when the admin and
294
- // the API share an origin — the usual case.
295
- ...(process.env['SSO_PUBLIC_BASE_URL']
296
- ? { publicBaseUrl: process.env['SSO_PUBLIC_BASE_URL'] }
297
- : {}),
298
- requestTtlSeconds: readPositiveInt(
299
- 'SSO_REQUEST_TTL_SECONDS',
300
- 600
301
- )
302
- },
303
- // ortha:if sso-oidc
304
- ssoProviders: {
305
- // Present only when both are set: an issuer with no client id
306
- // becomes a sign-in button that can only fail, and every SSO
307
- // failure looks the same, so whoever clicks it learns nothing.
308
- ...(process.env['SSO_OIDC_ISSUER'] &&
309
- process.env['SSO_OIDC_CLIENT_ID']
310
- ? {
311
- oidc: {
312
- name: process.env['SSO_OIDC_NAME'] ?? 'oidc',
313
- issuer: process.env['SSO_OIDC_ISSUER'],
314
- clientId: process.env['SSO_OIDC_CLIENT_ID'],
315
- ...(process.env['SSO_OIDC_CLIENT_SECRET']
316
- ? {
317
- clientSecret:
318
- process.env[
319
- 'SSO_OIDC_CLIENT_SECRET'
320
- ]
321
- }
322
- : {}),
323
- ...(process.env['SSO_OIDC_LABEL']
324
- ? { label: process.env['SSO_OIDC_LABEL'] }
325
- : {}),
326
- // The only gate on a first sign-in claiming an
327
- // existing account. A provider that omits the
328
- // claim — Entra ID, notably — links nobody until
329
- // an operator asserts that this directory owns
330
- // the addresses it reports.
331
- emailVerifiedWhenAbsent:
332
- process.env[
333
- 'SSO_OIDC_EMAIL_VERIFIED_WHEN_ABSENT'
334
- ] === 'true'
335
- }
336
- }
337
- : {})
338
- },
339
- // ortha:end
340
- // With an email set, an admin is provisioned on boot — idempotent
341
- // and non-destructive. This is how you get your first login.
342
- rootAdmin: {
343
- email: process.env['ORTHA_ROOT_ADMIN_EMAIL'] ?? '',
344
- password: process.env['ORTHA_ROOT_ADMIN_PASSWORD'] ?? '',
345
- name: process.env['ORTHA_ROOT_ADMIN_NAME'] ?? ''
346
- }
347
- },
348
- i18n: {
349
- // Content locales. Stable product configuration, hence literals.
350
- // The slugs are stored on entry rows, so removing one hides its
351
- // rows rather than deleting them — which is what `orphanedLocales`
352
- // is about below.
353
- locales: [{ slug: 'en', name: 'English', isDefault: true }],
354
- // Rows in a locale no longer listed above are intact and
355
- // unreachable, the worst shape for a silent failure. Fail the boot
356
- // and put the choice in front of whoever edited the array.
357
- orphanedLocales: 'fail'
358
- },
359
- media: {
360
- // ortha:if media-local
361
- storage: {
362
- // Point MEDIA_LOCAL_ROOT at a persistent volume in production:
363
- // a container's own disk is wiped on every deploy.
364
- rootDir: process.env['MEDIA_LOCAL_ROOT'] ?? './.storage/media'
365
- },
366
- // ortha:end
367
- // ortha:if media-vercel-blob
368
- storage: {
369
- // On Vercel the SDK reads BLOB_READ_WRITE_TOKEN itself, so this
370
- // is only for running the app elsewhere.
371
- ...(process.env['BLOB_READ_WRITE_TOKEN']
372
- ? { token: process.env['BLOB_READ_WRITE_TOKEN'] }
373
- : {})
374
- },
375
- // ortha:end
376
- // ortha:if media-gcs
377
- storage: {
378
- bucket: requireEnv('MEDIA_GCS_BUCKET'),
379
- // Everything else is optional: with no key file and no inline
380
- // credentials the client uses Application Default Credentials,
381
- // which is what a GKE or Cloud Run deployment wants.
382
- ...(process.env['MEDIA_GCS_PROJECT_ID']
383
- ? { projectId: process.env['MEDIA_GCS_PROJECT_ID'] }
384
- : {}),
385
- ...(process.env['MEDIA_GCS_KEY_FILE']
386
- ? { keyFilename: process.env['MEDIA_GCS_KEY_FILE'] }
387
- : {}),
388
- signWithIam: process.env['MEDIA_GCS_SIGN_WITH_IAM'] === 'true'
389
- },
390
- // ortha:end
391
- // ortha:if media-azure
392
- storage: {
393
- container: requireEnv('MEDIA_AZURE_CONTAINER'),
394
- connectionString: requireEnv('MEDIA_AZURE_CONNECTION_STRING')
395
- },
396
- // ortha:end
397
- // ortha:if media-s3
398
- storage: {
399
- bucket: requireEnv('MEDIA_S3_BUCKET'),
400
- // `auto` is what R2 expects; AWS needs its real region.
401
- region: process.env['MEDIA_S3_REGION'] ?? 'auto',
402
- // Omit for AWS S3 itself; set it for R2, MinIO, Spaces, B2…
403
- ...(process.env['MEDIA_S3_ENDPOINT']
404
- ? { endpoint: process.env['MEDIA_S3_ENDPOINT'] }
405
- : {}),
406
- forcePathStyle:
407
- process.env['MEDIA_S3_FORCE_PATH_STYLE'] === 'true',
408
- // Absent means "use the SDK's own provider chain" — an instance
409
- // role, IRSA, a shared config file. Passing blanks instead
410
- // would shadow all of that with credentials that cannot sign.
411
- ...(process.env['MEDIA_S3_ACCESS_KEY_ID'] &&
412
- process.env['MEDIA_S3_SECRET_ACCESS_KEY']
413
- ? {
414
- credentials: {
415
- accessKeyId: process.env['MEDIA_S3_ACCESS_KEY_ID'],
416
- secretAccessKey:
417
- process.env['MEDIA_S3_SECRET_ACCESS_KEY']
418
- }
419
- }
420
- : {})
421
- },
422
- // ortha:end
423
- // Redirect an already-authorized download straight to the
424
- // storage backend instead of streaming it through the app. Off
425
- // unless asked for, and only possible on a backend that can sign a
426
- // URL — the plugin refuses the combination at boot rather than
427
- // proxying while the operator believes otherwise.
428
- directServe:
429
- process.env['MEDIA_DIRECT_SERVE'] === 'signed-url'
430
- ? 'signed-url'
431
- : 'off',
432
- directServeTtlSeconds: readPositiveInt(
433
- 'MEDIA_DIRECT_SERVE_TTL_SECONDS',
434
- 300
435
- ),
436
- maxUploadBytes: readPositiveInt(
437
- 'MEDIA_MAX_UPLOAD_BYTES',
438
- 50 * 1024 * 1024
439
- )
440
- }
441
- ,
442
- copilot: {
443
- // Off by default: enabling a hosted provider sends workspace
444
- // content to a third party, which is an operator's decision to make
445
- // explicitly.
446
- enabled: process.env['COPILOT_ENABLED'] === 'true',
447
- maxOutputTokens: readPositiveInt('COPILOT_MAX_OUTPUT_TOKENS', 8192),
448
- providers: {
449
- // ortha:if copilot-anthropic
450
- // Setting the key is what REGISTERS this backend — leave it
451
- // empty and there is no `claude` in the picker at all, rather
452
- // than one that fails on the first message.
453
- ...(anthropicApiKey
454
- ? {
455
- claude: {
456
- apiKey: anthropicApiKey,
457
- models: readList(
458
- 'COPILOT_ANTHROPIC_MODELS',
459
- 'claude-sonnet-5'
460
- )
461
- }
462
- }
463
- : {}),
464
- // ortha:end
465
- // ortha:if copilot-openai
466
- ...(openAiBaseUrl
467
- ? {
468
- openai: {
469
- baseUrl: openAiBaseUrl,
470
- apiKey: process.env['COPILOT_OPENAI_API_KEY'] ?? '',
471
- models: readList(
472
- 'COPILOT_OPENAI_MODELS',
473
- 'llama3.1'
474
- )
475
- }
476
- }
477
- : {})
478
- // ortha:end
479
- }
480
- }
101
+ identity: identityConfig(),
102
+ i18n: i18nConfig(),
103
+ media: mediaConfig(),
104
+ copilot: copilotConfig(),
481
105
  // ortha:if mcp
482
- ,
483
- mcp: {
484
- // Off by default: once on, any holder of a full-scope API token can
485
- // drive content CRUD from an external agent.
486
- enabled: process.env['MCP_ENABLED'] === 'true',
487
- // The identity MCP clients display in their connector lists.
488
- name: '__APP_NAME__',
489
- version: '1.0.0',
490
- // A request/response transport owes its caller an answer, and the
491
- // tool registry has no deadline of its own — so without this the
492
- // only bound on a `tools/call` is the query underneath it, and a
493
- // blocked pool turns one call into a socket held until the client
494
- // gives up.
495
- callTimeoutMs: readPositiveInt('MCP_CALL_TIMEOUT_MS', 30_000),
496
- // Deliberately generous: the ceiling exists to stop a pathological
497
- // result being serialised several times over, not to shape normal
498
- // use. A result this large does not fit a model's context anyway.
499
- maxResultBytes: readPositiveInt('MCP_MAX_RESULT_BYTES', 4_194_304)
500
- }
106
+ mcp: mcpConfig()
501
107
  // ortha:end
502
108
  }
503
109
  };
@@ -33,6 +33,7 @@ const EXPECTED_PLUGINS = [
33
33
  'content-graphql',
34
34
  // ortha:end
35
35
  'i18n',
36
+ 'alarms',
36
37
  'media',
37
38
  'copilot'
38
39
  // ortha:if mcp
@@ -1,6 +1,6 @@
1
1
  import type { ServerPlugin } from '@orthacms/bootstrap-server';
2
2
  import { ActivityPlugin } from '@orthacms/activity-server';
3
- import { ContentPlugin } from '@orthacms/content-server';
3
+ import { ContentPlugin, ContentViewsPlugin } from '@orthacms/content-server';
4
4
  import { DatabasePlugin } from '@orthacms/database';
5
5
  import { I18nServerPlugin } from '@orthacms/i18n-server';
6
6
  import { IdentityPlugin } from '@orthacms/identity-server';
@@ -25,6 +25,7 @@ import { createGcsStorageProvider } from '@orthacms/media-provider-gcs';
25
25
  import { createVercelBlobStorageProvider } from '@orthacms/media-provider-vercel-blob';
26
26
  // ortha:end
27
27
  import { UsersPlugin } from '@orthacms/users-server';
28
+ import { AlarmsPlugin } from '@orthacms/alarms-server';
28
29
  // ortha:if graphql
29
30
  import { ContentGraphqlPlugin } from '@orthacms/content-graphql';
30
31
  // ortha:end
@@ -35,7 +36,6 @@ import {
35
36
  CopilotPlugin,
36
37
  type ProviderRegistration
37
38
  } from '@orthacms/copilot-server';
38
- import { createFakeProvider } from '@orthacms/copilot-provider-fake';
39
39
  // ortha:if copilot-anthropic
40
40
  import { createAnthropicProvider } from '@orthacms/copilot-provider-anthropic';
41
41
  // ortha:end
@@ -54,9 +54,10 @@ import type { OrthaConfig } from '../ortha.config';
54
54
  * one at the top of the list would be the house default and would fail on the
55
55
  * first message.
56
56
  *
57
- * `fake` is last and unconditional. It is a shipped adapter, not test
58
- * scaffolding it needs no key and no network, so it is what makes the chat
59
- * work offline, and being last it is the default only when it is the only one.
57
+ * **An app that configured no backend registers none**, and has no copilot.
58
+ * There is no scripted offline adapter to fall back on, so `COPILOT_ENABLED`
59
+ * must stay `false` until a backend is configured enabling it with an empty
60
+ * list fails at boot rather than shipping a chat that cannot answer.
60
61
  */
61
62
  // ortha:if sso-oidc
62
63
  /**
@@ -105,7 +106,6 @@ export function copilotProviders(config: OrthaConfig): ProviderRegistration[] {
105
106
  });
106
107
  }
107
108
  // ortha:end
108
- providers.push({ name: 'fake', provider: createFakeProvider() });
109
109
 
110
110
  return providers;
111
111
  }
@@ -169,6 +169,12 @@ export function buildPlugins(config: OrthaConfig): ServerPlugin[] {
169
169
  // Until then the plugin serves its generic routes with an empty
170
170
  // registry, and owns no tables of its own.
171
171
  content,
172
+ // Saved list views — the named filter/sort/column slices an editor
173
+ // returns to. A second plugin entry from the content package because
174
+ // `ServerPlugin.migrations` holds one descriptor per entry; this one
175
+ // ships the feature's own tables. Must follow identity and workspaces:
176
+ // its foreign keys point at their tables and migrations run in order.
177
+ ContentViewsPlugin({ content }),
172
178
  // ortha:if graphql
173
179
  // The same public content API over GraphQL, on /api/v1/graphql. It owns
174
180
  // no schema and adds no credential — it reuses content's bearer guards
@@ -187,6 +193,10 @@ export function buildPlugins(config: OrthaConfig): ServerPlugin[] {
187
193
  // provider, writing every upload to local disk. A deployment runs
188
194
  // exactly one; swapping backend is swapping this expression (and the
189
195
  // type of `media.storage` with it).
196
+ // Content alarms — rules that flag content problems without ever
197
+ // blocking a save or a publish. After content, whose registry and
198
+ // filter surface it evaluates rules through.
199
+ AlarmsPlugin(),
190
200
  MediaServerPlugin({
191
201
  // ortha:if media-local
192
202
  provider: createLocalStorageProvider(config.plugins.media.storage),
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2022",
4
+ "lib": [
5
+ "es2023"
6
+ ],
7
+ "module": "commonjs",
8
+ "moduleResolution": "node",
9
+ "types": [
10
+ "node",
11
+ "jest"
12
+ ],
13
+ "strict": true,
14
+ "skipLibCheck": true,
15
+ "esModuleInterop": true,
16
+ "forceConsistentCasingInFileNames": true,
17
+ "resolveJsonModule": true,
18
+ "experimentalDecorators": true,
19
+ "emitDecoratorMetadata": true,
20
+ "noEmit": true
21
+ },
22
+ "include": [
23
+ "src/**/*.spec.ts"
24
+ ]
25
+ }
@@ -75,8 +75,13 @@ ORTHA_ROOT_ADMIN_EMAIL=__ADMIN_EMAIL__
75
75
  ORTHA_ROOT_ADMIN_PASSWORD=__ADMIN_PASSWORD__
76
76
 
77
77
  # --- copilot ---
78
- # Global kill switch. OFF by default: enabling a hosted provider sends workspace
79
- # content to a third party, so an operator opts in explicitly.
78
+ # Global kill switch. OFF by default: whether this app offers Ortha AI at all is
79
+ # your call. `false` unregisters every /api/copilot route, so the endpoints 404
80
+ # and the admin's chat panel, Agents view and Skills page stand down.
81
+ #
82
+ # It is not what keeps your content in-house: a backend is registered only when
83
+ # it is configured below, so an app with no key reaches no third party whatever
84
+ # this says.
80
85
  COPILOT_ENABLED=false
81
86
  # Ceiling on a single model response, in tokens.
82
87
  COPILOT_MAX_OUTPUT_TOKENS=8192