create-ortha-app 0.5.0 → 0.5.2

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/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # create-ortha-app
2
2
 
3
- Part of [Ortha CMS](https://github.com/ortha-source/ortha-cms).
3
+ Part of [Ortha CMS](https://orthacms.com) — the source is on
4
+ [GitHub](https://github.com/ortha-source/ortha-cms/tree/main/packages/create-ortha-app).
4
5
 
5
6
  ```sh
6
7
  npm install create-ortha-app
@@ -41,7 +41,7 @@ export interface Feature {
41
41
  *
42
42
  * The **copilot** is here — plugin and admin panel — even though it is a large
43
43
  * feature nobody may want, because its server half arrives anyway: five core
44
- * plugins (`content`, `activity`, `i18n`, `media`, `users`) depend on
44
+ * plugins (`content`, `i18n`, `media`, `segments`, `users`) depend on
45
45
  * `copilot-server` to contribute their tools, so the code is on disk whatever
46
46
  * the manifest says, and leaving it undeclared bought nothing but a missing chat
47
47
  * panel. No **model backend** comes with it: those are the opt-in
@@ -53,7 +53,8 @@ export interface Feature {
53
53
  * answer.
54
54
  *
55
55
  * The **extension points** are here for the same reason — `content-domain`,
56
- * `copilot-domain`, `tools-server`, `query-builder-admin`. Every one of them
56
+ * `copilot-domain`, `segments-domain`, `transfer-domain`, `webhooks-domain`,
57
+ * `tools-server`, `query-builder-admin`. Every one of them
57
58
  * already arrives transitively, so an import would resolve on npm's flat
58
59
  * `node_modules` today; declaring them is what makes that resolution something
59
60
  * the app owns rather than something it borrows. An undeclared import breaks
@@ -109,8 +110,9 @@ export declare const TRANSITIVE_PACKAGES: readonly string[];
109
110
  * Where uploads are written.
110
111
  *
111
112
  * A single-choice group: media always runs, the question is only which adapter
112
- * backs it. S3 is listed and disabled the package exists but has never been
113
- * released, and offering it would generate an app that cannot install.
113
+ * backs it. All five are published and selectable; `available` stays on the
114
+ * type for the next adapter that lands in the codebase before it lands on npm,
115
+ * which is shown greyed out rather than hidden.
114
116
  */
115
117
  export declare const MEDIA_PROVIDERS: readonly Feature[];
116
118
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"features.d.ts","sourceRoot":"","sources":["../../src/lib/features.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,qDAAqD;AACrD,MAAM,WAAW,OAAO;IACpB,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,gCAAgC;IAChC,gBAAgB,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,MAAM,EA6C1C,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAAsB,CAAC;AAEtE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAGhD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,OAAO,EAyC7C,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,EAAE,SAAS,OAAO,EAiB/C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,OAAO,EAyB3C,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,SAAS,EAAE,SAAS,OAAO,EA0BvC,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,YAAY,EAAE,SAAS,OAAO,EAK1C,CAAC;AAEF,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAC7B,gFAAgF;IAChF,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,gBAAgB,GAAG,MAAM,EAAE,CASrE;AAED,uDAAuD;AACvD,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,CAE7C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,gBAAgB,GAAG,GAAG,CAAC,MAAM,CAAC,CAQrE"}
1
+ {"version":3,"file":"features.d.ts","sourceRoot":"","sources":["../../src/lib/features.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,qDAAqD;AACrD,MAAM,WAAW,OAAO;IACpB,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,gCAAgC;IAChC,gBAAgB,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,MAAM,EA6C1C,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAAsB,CAAC;AAEtE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAGhD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,OAAO,EAyC7C,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,EAAE,SAAS,OAAO,EAiB/C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,OAAO,EAyB3C,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,SAAS,EAAE,SAAS,OAAO,EA0BvC,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,YAAY,EAAE,SAAS,OAAO,EAK1C,CAAC;AAEF,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAC7B,gFAAgF;IAChF,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,gBAAgB,GAAG,MAAM,EAAE,CASrE;AAED,uDAAuD;AACvD,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,CAE7C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,gBAAgB,GAAG,GAAG,CAAC,MAAM,CAAC,CAQrE"}
@@ -23,7 +23,7 @@ exports.resolveFlags = resolveFlags;
23
23
  *
24
24
  * The **copilot** is here — plugin and admin panel — even though it is a large
25
25
  * feature nobody may want, because its server half arrives anyway: five core
26
- * plugins (`content`, `activity`, `i18n`, `media`, `users`) depend on
26
+ * plugins (`content`, `i18n`, `media`, `segments`, `users`) depend on
27
27
  * `copilot-server` to contribute their tools, so the code is on disk whatever
28
28
  * the manifest says, and leaving it undeclared bought nothing but a missing chat
29
29
  * panel. No **model backend** comes with it: those are the opt-in
@@ -35,7 +35,8 @@ exports.resolveFlags = resolveFlags;
35
35
  * answer.
36
36
  *
37
37
  * The **extension points** are here for the same reason — `content-domain`,
38
- * `copilot-domain`, `tools-server`, `query-builder-admin`. Every one of them
38
+ * `copilot-domain`, `segments-domain`, `transfer-domain`, `webhooks-domain`,
39
+ * `tools-server`, `query-builder-admin`. Every one of them
39
40
  * already arrives transitively, so an import would resolve on npm's flat
40
41
  * `node_modules` today; declaring them is what makes that resolution something
41
42
  * the app owns rather than something it borrows. An undeclared import breaks
@@ -139,8 +140,9 @@ exports.TRANSITIVE_PACKAGES = [
139
140
  * Where uploads are written.
140
141
  *
141
142
  * A single-choice group: media always runs, the question is only which adapter
142
- * backs it. S3 is listed and disabled the package exists but has never been
143
- * released, and offering it would generate an app that cannot install.
143
+ * backs it. All five are published and selectable; `available` stays on the
144
+ * type for the next adapter that lands in the codebase before it lands on npm,
145
+ * which is shown greyed out rather than hidden.
144
146
  */
145
147
  exports.MEDIA_PROVIDERS = [
146
148
  {
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/lib/run.ts"],"names":[],"mappings":"AAeA,OAAO,EAMH,KAAK,OAAO,EACZ,KAAK,gBAAgB,EACxB,MAAM,YAAY,CAAC;AASpB,eAAO,MAAM,KAAK,8oBAcjB,CAAC;AA6HF,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,EAAE,CAI/D;AA6BD,sCAAsC;AACtC,MAAM,WAAW,OAAO;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,gBAAgB,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,WAAW,EAAE,MAAM,GACpB,OAAO,CAAC,OAAO,CAAC,CAuFlB;AA+BD,wBAAsB,IAAI,CACtB,IAAI,GAAE,SAAS,MAAM,EAA0B,GAChD,OAAO,CAAC,IAAI,CAAC,CAsFf"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/lib/run.ts"],"names":[],"mappings":"AAeA,OAAO,EAMH,KAAK,OAAO,EACZ,KAAK,gBAAgB,EACxB,MAAM,YAAY,CAAC;AASpB,eAAO,MAAM,KAAK,8oBAcjB,CAAC;AA6HF,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,EAAE,CAI/D;AA6BD,sCAAsC;AACtC,MAAM,WAAW,OAAO;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,gBAAgB,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,WAAW,EAAE,MAAM,GACpB,OAAO,CAAC,OAAO,CAAC,CAwFlB;AA+BD,wBAAsB,IAAI,CACtB,IAAI,GAAE,SAAS,MAAM,EAA0B,GAChD,OAAO,CAAC,IAAI,CAAC,CAsFf"}
package/dist/lib/run.js CHANGED
@@ -202,9 +202,10 @@ async function resolveAnswers(argv, defaultName) {
202
202
  ui.section('Features');
203
203
  ui.note('Everything else is installed for you. These are the choices.');
204
204
  console.log('');
205
- // Only ask when there is more than one answer available. S3 is not
206
- // published, so today this is a question with a single possible reply, and
207
- // asking it would be noise pretending to be a choice.
205
+ // Only ask when there is more than one answer available a question with a
206
+ // single possible reply is noise pretending to be a choice. Every adapter
207
+ // is published today, so the question is asked; it stops being asked by
208
+ // itself if all but one are ever marked unavailable.
208
209
  const selectable = features_1.MEDIA_PROVIDERS.filter((provider) => provider.available);
209
210
  const chosenMedia = selectable.length > 1
210
211
  ? [
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "create-ortha-app",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Create an Ortha CMS app: npx create-ortha-app my-cms",
5
5
  "license": "MIT",
6
- "homepage": "https://github.com/ortha-source/ortha-cms/tree/main/packages/create-ortha-app",
6
+ "homepage": "https://orthacms.com",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/ortha-source/ortha-cms.git",
@@ -0,0 +1,25 @@
1
+ // ortha:if graphql
2
+ import type { ContentGraphqlPluginConfig } from '@orthacms/content-graphql';
3
+ import { readPositiveInt } from '@orthacms/utils-server';
4
+
5
+ /** The public GraphQL endpoint's cost budget. */
6
+ export function contentGraphqlConfig(): ContentGraphqlPluginConfig {
7
+ return {
8
+ // The cost budget one GraphQL operation may spend. REST bounded a
9
+ // request structurally — one route, one page — and a GraphQL document
10
+ // does not, so these are the replacement bound. Stable tuning, hence
11
+ // literals, with env overrides for an operator who needs to loosen or
12
+ // tighten them without a redeploy.
13
+ limits: {
14
+ maxDepth: readPositiveInt('GRAPHQL_MAX_DEPTH', 8),
15
+ maxComplexity: readPositiveInt('GRAPHQL_MAX_COMPLEXITY', 1000),
16
+ maxFields: readPositiveInt('GRAPHQL_MAX_FIELDS', 500),
17
+ maxQueryLength: readPositiveInt('GRAPHQL_MAX_QUERY_LENGTH', 16_384)
18
+ },
19
+ // How long a built schema is reused before it is derived again from the
20
+ // workspace's content grants. Freshness only — every read is authorized
21
+ // against the live grants regardless.
22
+ schemaCacheTtlMs: readPositiveInt('GRAPHQL_SCHEMA_CACHE_TTL_MS', 60_000)
23
+ };
24
+ }
25
+ // ortha:end
@@ -0,0 +1,23 @@
1
+ import type { SegmentsPluginConfig } from '@orthacms/segments-server';
2
+
3
+ /** Reader entitlements — where a reader's tags come from. */
4
+ export function segmentsConfig(): SegmentsPluginConfig {
5
+ return {
6
+ // Where a reader's tags come from — the one line this feature needs per
7
+ // install. A resolver receives the request, so a JWT claim, a header the
8
+ // CDN sets, or a lookup against a billing system are all equally
9
+ // reachable:
10
+ //
11
+ // resolver: {
12
+ // resolve: async (request) => readTagsFrom(request)
13
+ // }
14
+ //
15
+ // Left out — as it is here — every reader is anonymous, so unrestricted
16
+ // content serves and restricted content does not. That is a working
17
+ // configuration, and it fails in the safe direction: an audience nobody
18
+ // can be resolved into cannot accidentally be admitted.
19
+ //
20
+ // It is a function rather than an environment value, which is why this
21
+ // module reads no env: what it needs is code only you can write.
22
+ };
23
+ }
@@ -0,0 +1,28 @@
1
+ import type { TransferPluginConfig } from '@orthacms/transfer-server';
2
+
3
+ /** Export/import — the per-type identity fields and the transfer ceilings. */
4
+ export function transferConfig(): TransferPluginConfig {
5
+ return {
6
+ // Which field identifies a record of each type, per content type.
7
+ //
8
+ // This is the setting that decides whether importing the same file
9
+ // twice updates the records or duplicates them. Left out, a type falls
10
+ // back to a derived guess — a field *named* like an identifier
11
+ // (`slug`, `sku`, `email`), then the first required text field — which
12
+ // is usually right and is reported in every export's manifest, but is
13
+ // still a guess. Name the fields for any type where being wrong would
14
+ // be expensive:
15
+ //
16
+ // identity: { product: ['sku'], author: ['email'] }
17
+ //
18
+ // This app registers no content types yet, so there is nothing to key
19
+ // here until you add some.
20
+ identity: {},
21
+ // Ceilings on one transfer. The defaults (see `DEFAULT_TRANSFER_LIMITS`)
22
+ // sit comfortably above real editorial work and far below "the whole
23
+ // library"; the import-side archive limits are a safety boundary rather
24
+ // than a capacity setting, so lowering them costs nothing and raising
25
+ // them should be deliberate.
26
+ limits: {}
27
+ };
28
+ }
@@ -0,0 +1,34 @@
1
+ import type { WebhooksPluginConfig } from '@orthacms/webhooks-server';
2
+ import { readFlag, readPositiveInt } from '@orthacms/utils-server';
3
+
4
+ /**
5
+ * Outgoing webhooks — how hard the sender pushes, and where it may reach.
6
+ *
7
+ * Endpoints themselves live in the database; an administrator adds them on the
8
+ * admin's Webhooks page. Only the five values a deployment genuinely varies are
9
+ * env-sourced, and the rest are left to the plugin's own defaults, where the
10
+ * reasoning behind them is written down.
11
+ *
12
+ * The two `allow*` flags are the ones worth reading twice. Both default to
13
+ * **off**, and both widen what a URL an operator types can reach:
14
+ * `WEBHOOKS_ALLOW_INSECURE_URLS` permits plain HTTP, and
15
+ * `WEBHOOKS_ALLOW_PRIVATE_NETWORKS` permits loopback and RFC 1918 — which a
16
+ * self-hosted install with an in-cluster receiver genuinely needs, and an
17
+ * internet-facing one must not have. A webhook is this server making a request
18
+ * to an address a user typed, which is the shape of every SSRF.
19
+ */
20
+ export function webhooksConfig(): WebhooksPluginConfig {
21
+ return {
22
+ // 0 turns the sender off in this process. Rows still queue, so a
23
+ // deployment can run web nodes that only enqueue and one that sends.
24
+ deliveryIntervalMs: readPositiveInt(
25
+ 'WEBHOOKS_DELIVERY_INTERVAL',
26
+ 2_000
27
+ ),
28
+ timeoutMs: readPositiveInt('WEBHOOKS_TIMEOUT', 10_000),
29
+ // Nothing else prunes the delivery log.
30
+ retentionDays: readPositiveInt('WEBHOOKS_RETENTION_DAYS', 30),
31
+ allowInsecureUrls: readFlag('WEBHOOKS_ALLOW_INSECURE_URLS', false),
32
+ allowPrivateNetworks: readFlag('WEBHOOKS_ALLOW_PRIVATE_NETWORKS', false)
33
+ };
34
+ }
@@ -34,6 +34,12 @@ import type {
34
34
  TrustProxySetting
35
35
  } from '@orthacms/bootstrap-server';
36
36
  import type { I18nPluginConfig } from '@orthacms/i18n-server';
37
+ import type { SegmentsPluginConfig } from '@orthacms/segments-server';
38
+ import type { TransferPluginConfig } from '@orthacms/transfer-server';
39
+ import type { WebhooksPluginConfig } from '@orthacms/webhooks-server';
40
+ // ortha:if graphql
41
+ import type { ContentGraphqlPluginConfig } from '@orthacms/content-graphql';
42
+ // ortha:end
37
43
  // ortha:if mcp
38
44
  import type { McpPluginConfig } from '@orthacms/mcp-server';
39
45
  // ortha:end
@@ -49,6 +55,12 @@ import { identityConfig, type AppIdentityConfig } from './config/identity';
49
55
  import { i18nConfig } from './config/i18n';
50
56
  import { mediaConfig, type AppMediaConfig } from './config/media';
51
57
  import { copilotConfig, type AppCopilotConfig } from './config/copilot';
58
+ import { segmentsConfig } from './config/segments';
59
+ import { transferConfig } from './config/transfer';
60
+ import { webhooksConfig } from './config/webhooks';
61
+ // ortha:if graphql
62
+ import { contentGraphqlConfig } from './config/graphql';
63
+ // ortha:end
52
64
  // ortha:if mcp
53
65
  import { mcpConfig } from './config/mcp';
54
66
  // ortha:end
@@ -74,6 +86,16 @@ export interface OrthaConfig {
74
86
  i18n: I18nPluginConfig;
75
87
  media: AppMediaConfig;
76
88
  copilot: AppCopilotConfig;
89
+ /** Export/import — per-type identity fields and transfer ceilings. */
90
+ transfer: TransferPluginConfig;
91
+ /** Reader entitlements — where a reader's tags come from. */
92
+ segments: SegmentsPluginConfig;
93
+ /** Outgoing webhooks — delivery pacing and the URL policy. */
94
+ webhooks: WebhooksPluginConfig;
95
+ // ortha:if graphql
96
+ /** Public GraphQL endpoint — the per-operation cost budget. */
97
+ contentGraphql: ContentGraphqlPluginConfig;
98
+ // ortha:end
77
99
  // ortha:if mcp
78
100
  mcp: McpPluginConfig;
79
101
  // ortha:end
@@ -107,6 +129,12 @@ const config: OrthaConfig = {
107
129
  i18n: i18nConfig(),
108
130
  media: mediaConfig(),
109
131
  copilot: copilotConfig(),
132
+ transfer: transferConfig(),
133
+ segments: segmentsConfig(),
134
+ webhooks: webhooksConfig(),
135
+ // ortha:if graphql
136
+ contentGraphql: contentGraphqlConfig(),
137
+ // ortha:end
110
138
  // ortha:if mcp
111
139
  mcp: mcpConfig()
112
140
  // ortha:end
@@ -212,6 +212,10 @@ export function buildPlugins(config: OrthaConfig): ServerPlugin[] {
212
212
  // first request from a workspace granted both.
213
213
  ContentGraphqlPlugin({
214
214
  content,
215
+ ...config.plugins.contentGraphql,
216
+ // GraphiQL rides the same switch as the Scalar reference: both are
217
+ // developer tooling, and neither should be reachable in production
218
+ // unless the operator asks (`API_DOCS=true`).
215
219
  playground: config.docs.enabled === true
216
220
  }),
217
221
  // ortha:end
@@ -227,8 +231,9 @@ export function buildPlugins(config: OrthaConfig): ServerPlugin[] {
227
231
  AlarmsPlugin(),
228
232
  // Outgoing webhooks. Inert until someone adds an endpoint in the admin,
229
233
  // and it only subscribes to the outbox, so nothing depends on it being
230
- // registered any earlier than this.
231
- WebhooksPlugin(),
234
+ // registered any earlier than this. The settings worth knowing about
235
+ // are the two `WEBHOOKS_ALLOW_*` flags — see `config/webhooks.ts`.
236
+ WebhooksPlugin(config.plugins.webhooks),
232
237
  MediaServerPlugin({
233
238
  // ortha:if media-local
234
239
  provider: createLocalStorageProvider(config.plugins.media.storage),
@@ -259,9 +264,10 @@ export function buildPlugins(config: OrthaConfig): ServerPlugin[] {
259
264
  // The setting worth filling in per install is `identity`: it says
260
265
  // which field identifies a record of each type, which is what lets an
261
266
  // import recognise "this is that record" instead of adding a
262
- // duplicate. Without it the natural key is a heuristic
263
- // `TransferPlugin({ identity: { post: ['slug'] } })`.
264
- TransferPlugin(),
267
+ // duplicate. Without it the natural key is a heuristic. It is a map of
268
+ // your own content types rather than an environment value, so it lives
269
+ // in `config/transfer.ts`.
270
+ TransferPlugin(config.plugins.transfer),
265
271
  // Reader entitlements — who may *read* published content, as against
266
272
  // who may touch it. After content, whose read-scope port it binds, so
267
273
  // one decision covers REST, GraphQL and MCP at once.
@@ -270,8 +276,10 @@ export function buildPlugins(config: OrthaConfig): ServerPlugin[] {
270
276
  // in the admin no predicate is emitted and every read costs what it
271
277
  // did before. The line to fill in per install is `resolver` — it says
272
278
  // where a reader's tags come from, and its absence means every reader
273
- // is anonymous, which serves unrestricted content and nothing else.
274
- SegmentsPlugin(),
279
+ // is anonymous, which serves unrestricted content and nothing else. It
280
+ // is a function you write, not an environment value, so it lives in
281
+ // `config/segments.ts`.
282
+ SegmentsPlugin(config.plugins.segments),
275
283
  // Registered after workspaces (runs are workspace-scoped) and identity
276
284
  // (runs execute as the calling user, gated on `copilot:use`). The
277
285
  // composition root is the single place that selects a backend: the
@@ -103,6 +103,21 @@ COPILOT_OPENAI_MODELS=llama3.1
103
103
  # Leave empty for a local runtime that wants no auth.
104
104
  COPILOT_OPENAI_API_KEY=
105
105
  # ortha:end
106
+ # ortha:if graphql
107
+ # --- graphql ---
108
+ # The cost budget one GraphQL operation may spend. REST bounds a request
109
+ # structurally — one route, one page — and a document does not, so these are the
110
+ # replacement bound. The defaults suit an ordinary content API; loosen them only
111
+ # for a client you control.
112
+ GRAPHQL_MAX_DEPTH=8
113
+ GRAPHQL_MAX_COMPLEXITY=1000
114
+ GRAPHQL_MAX_FIELDS=500
115
+ GRAPHQL_MAX_QUERY_LENGTH=16384
116
+ # How long a built schema is reused before it is derived again from the
117
+ # workspace's content grants. Freshness only — every read is authorized against
118
+ # the live grants regardless.
119
+ GRAPHQL_SCHEMA_CACHE_TTL_MS=60000
120
+ # ortha:end
106
121
  # ortha:if mcp
107
122
  # --- mcp ---
108
123
  # Global kill switch. OFF by default: once on, any holder of a `full`-scope API