create-ortha-app 0.4.3 → 0.5.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.
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * Typed configuration for this app.
3
3
  *
4
- * **`config/` is the single place that reads `process.env`.** Everything
4
+ * **`config/` is the single place that reads the environment**, and it reads it
5
+ * only through the readers in `@orthacms/utils-server` — never `process.env`
6
+ * directly. `readEnv` is where "an empty value means the setting is absent" is
7
+ * decided, and `.env` ships keys with nothing on the right-hand side; a raw
8
+ * `process.env['X'] ?? default` skips that decision and lets a blank line win
9
+ * over the default. Everything
5
10
  * downstream — the host, every plugin — receives typed values, so "where does
6
11
  * this setting come from" has exactly one answer. Deploy-specific values come
7
12
  * from the environment; stable product tuning lives in the builders as literals.
@@ -29,12 +29,18 @@ const EXPECTED_PLUGINS = [
29
29
  'activity',
30
30
  'users',
31
31
  'content',
32
+ // A second entry from the content package: `ServerPlugin.migrations` holds
33
+ // one descriptor per entry, and saved list views ship their own tables.
34
+ 'content-views',
32
35
  // ortha:if graphql
33
36
  'content-graphql',
34
37
  // ortha:end
35
38
  'i18n',
36
39
  'alarms',
40
+ 'webhooks',
37
41
  'media',
42
+ 'transfer',
43
+ 'segments',
38
44
  'copilot'
39
45
  // ortha:if mcp
40
46
  ,
@@ -4,10 +4,18 @@ 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';
7
- // ortha:if sso-oidc
7
+ // ortha:if sso
8
8
  import type { SsoRegistration } from '@orthacms/identity-domain';
9
+ // ortha:end
10
+ // ortha:if sso-oidc
9
11
  import { createOidcProvider } from '@orthacms/identity-provider-oidc';
10
12
  // ortha:end
13
+ // ortha:if sso-github
14
+ import { createGithubProvider } from '@orthacms/identity-provider-github';
15
+ // ortha:end
16
+ // ortha:if sso-saml
17
+ import { createSamlProvider } from '@orthacms/identity-provider-saml';
18
+ // ortha:end
11
19
  import { MediaServerPlugin } from '@orthacms/media-server';
12
20
  // ortha:if media-local
13
21
  import { createLocalStorageProvider } from '@orthacms/media-provider-local';
@@ -26,6 +34,9 @@ import { createVercelBlobStorageProvider } from '@orthacms/media-provider-vercel
26
34
  // ortha:end
27
35
  import { UsersPlugin } from '@orthacms/users-server';
28
36
  import { AlarmsPlugin } from '@orthacms/alarms-server';
37
+ import { SegmentsPlugin } from '@orthacms/segments-server';
38
+ import { TransferPlugin } from '@orthacms/transfer-server';
39
+ import { WebhooksPlugin } from '@orthacms/webhooks-server';
29
40
  // ortha:if graphql
30
41
  import { ContentGraphqlPlugin } from '@orthacms/content-graphql';
31
42
  // ortha:end
@@ -59,13 +70,14 @@ import type { OrthaConfig } from '../ortha.config';
59
70
  * must stay `false` until a backend is configured — enabling it with an empty
60
71
  * list fails at boot rather than shipping a chat that cannot answer.
61
72
  */
62
- // ortha:if sso-oidc
73
+ // ortha:if sso
63
74
  /**
64
75
  * The identity providers this app can actually reach.
65
76
  *
66
77
  * **Only what is configured is registered.** `ortha.config.ts` omits a provider
67
- * whose issuer or client id is missing, and an unconfigured one is skipped here
68
- * too — it would appear on the sign-in page as a button that can only fail.
78
+ * whose connection settings are missing, and an unconfigured one is skipped
79
+ * here too — it would appear on the sign-in page as a button that can only
80
+ * fail.
69
81
  *
70
82
  * Running two directories at once is another entry. The name is what
71
83
  * `/api/auth/sso/<name>/start` and every `sso_identities` row refer to the
@@ -75,12 +87,28 @@ import type { OrthaConfig } from '../ortha.config';
75
87
  * string, which matters because most providers match it byte for byte.
76
88
  */
77
89
  export function ssoProviders(config: OrthaConfig): SsoRegistration[] {
78
- const oidc = config.plugins.identity.ssoProviders?.oidc;
79
- if (!oidc) {
80
- return [];
90
+ const configured = config.plugins.identity.ssoProviders;
91
+ const providers: SsoRegistration[] = [];
92
+ // ortha:if sso-oidc
93
+ if (configured?.oidc) {
94
+ const { name, ...settings } = configured.oidc;
95
+ providers.push({ name, provider: createOidcProvider(settings) });
96
+ }
97
+ // ortha:end
98
+ // ortha:if sso-github
99
+ if (configured?.github) {
100
+ const { name, ...settings } = configured.github;
101
+ providers.push({ name, provider: createGithubProvider(settings) });
81
102
  }
82
- const { name, ...settings } = oidc;
83
- return [{ name, provider: createOidcProvider(settings) }];
103
+ // ortha:end
104
+ // ortha:if sso-saml
105
+ if (configured?.saml) {
106
+ const { name, ...settings } = configured.saml;
107
+ providers.push({ name, provider: createSamlProvider(settings) });
108
+ }
109
+ // ortha:end
110
+
111
+ return providers;
84
112
  }
85
113
  // ortha:end
86
114
 
@@ -138,7 +166,7 @@ export function buildPlugins(config: OrthaConfig): ServerPlugin[] {
138
166
  return [
139
167
  // First: the only plugin that opens a resource in `onPluginInit`.
140
168
  DatabasePlugin({ connectionString: config.database.url }),
141
- // ortha:if sso-oidc
169
+ // ortha:if sso
142
170
  // Identity, plus the identity providers this app offers. The second
143
171
  // argument is where constructed adapters go: `ortha.config.ts` holds
144
172
  // the typed view of the environment, and an adapter instance is not an
@@ -147,14 +175,14 @@ export function buildPlugins(config: OrthaConfig): ServerPlugin[] {
147
175
  sso: { providers: ssoProviders(config) }
148
176
  }),
149
177
  // ortha:end
150
- // ortha:ifnot sso-oidc
178
+ // ortha:ifnot sso
151
179
  IdentityPlugin(config.plugins.identity),
152
180
  // ortha:end
153
181
  WorkspacesPlugin(),
154
182
  ActivityPlugin(),
155
183
  UsersPlugin(),
156
- // No content types yet. Define some in `src/server/content/`, pass them
157
- // here as `types`, then add a `drizzle.config.ts` pointing at them and
184
+ // No content types yet. Define some in `apps/server/src/content/`, pass
185
+ // them here as `types`, then add a `drizzle.config.ts` pointing at them and
158
186
  // a `migrations` descriptor so `ortha generate` / `ortha migrate` can
159
187
  // manage their tables:
160
188
  //
@@ -197,6 +225,10 @@ export function buildPlugins(config: OrthaConfig): ServerPlugin[] {
197
225
  // blocking a save or a publish. After content, whose registry and
198
226
  // filter surface it evaluates rules through.
199
227
  AlarmsPlugin(),
228
+ // Outgoing webhooks. Inert until someone adds an endpoint in the admin,
229
+ // and it only subscribes to the outbox, so nothing depends on it being
230
+ // registered any earlier than this.
231
+ WebhooksPlugin(),
200
232
  MediaServerPlugin({
201
233
  // ortha:if media-local
202
234
  provider: createLocalStorageProvider(config.plugins.media.storage),
@@ -217,6 +249,29 @@ export function buildPlugins(config: OrthaConfig): ServerPlugin[] {
217
249
  // ortha:end
218
250
  config: config.plugins.media
219
251
  }),
252
+ // Content export and import, one hop deep: relations, files and
253
+ // locales travel with a record, relations-of-relations stay as
254
+ // references. After content (every write goes through its writer, so
255
+ // an import cannot outrun validation or your own permissions) and
256
+ // after media (files travel with the records that use them). Owns no
257
+ // tables.
258
+ //
259
+ // The setting worth filling in per install is `identity`: it says
260
+ // which field identifies a record of each type, which is what lets an
261
+ // 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(),
265
+ // Reader entitlements — who may *read* published content, as against
266
+ // who may touch it. After content, whose read-scope port it binds, so
267
+ // one decision covers REST, GraphQL and MCP at once.
268
+ //
269
+ // Registering it changes nothing on its own: with no audience created
270
+ // in the admin no predicate is emitted and every read costs what it
271
+ // did before. The line to fill in per install is `resolver` — it says
272
+ // 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(),
220
275
  // Registered after workspaces (runs are workspace-scoped) and identity
221
276
  // (runs execute as the calling user, gated on `copilot:use`). The
222
277
  // composition root is the single place that selects a backend: the
@@ -138,8 +138,85 @@ SSO_OIDC_LABEL=
138
138
  # claiming an existing account.
139
139
  SSO_OIDC_EMAIL_VERIFIED_WHEN_ABSENT=
140
140
  # ortha:end
141
+ # ortha:if sso-github
142
+ # GitHub, or GitHub Enterprise Server. Both of these are what make the provider
143
+ # configured at all — and unlike OIDC the secret is not optional: GitHub's code
144
+ # exchange has no PKCE, so the secret is the only thing proving the code is
145
+ # being redeemed by this application.
146
+ # Register <origin>/api/auth/sso/<SSO_GITHUB_NAME>/callback as the OAuth app's
147
+ # authorization callback URL.
148
+ SSO_GITHUB_CLIENT_ID=
149
+ SSO_GITHUB_CLIENT_SECRET=
150
+ # What the route and every sso_identities row call this provider. Renaming it
151
+ # orphans the links that name it. Default: github
152
+ SSO_GITHUB_NAME=
153
+ # The sign-in button's text. Default: GitHub
154
+ SSO_GITHUB_LABEL=
155
+ # Comma-separated. Defaults to `read:user,user:email` — a profile and the
156
+ # verified addresses on it. GitHub's scopes are coarse, so anything wider hands
157
+ # this CMS access it has no use for.
158
+ SSO_GITHUB_SCOPES=
159
+ # For GitHub Enterprise Server, e.g. https://github.acme.com. Leave empty for
160
+ # github.com.
161
+ SSO_GITHUB_ENTERPRISE_BASE_URL=
162
+ # Restricts the account chooser to one organisation's members. Cosmetic: who
163
+ # actually gets in is this CMS's decision, not GitHub's.
164
+ SSO_GITHUB_ORGANIZATION=
165
+ # ortha:end
166
+ # ortha:if sso-saml
167
+ # SAML 2.0. All three are required and none has a fallback: SAML has no
168
+ # discovery document and no key endpoint, so the certificate you copy out of
169
+ # your identity provider is the whole of the trust relationship.
170
+ # The IdP's single-sign-on URL, where the AuthnRequest is sent.
171
+ SSO_SAML_ENTRY_POINT=
172
+ # The IdP's signing certificate, PEM or bare base64. An environment variable
173
+ # cannot hold real newlines, so `\n` escapes are accepted and put back.
174
+ SSO_SAML_IDP_CERT=
175
+ # This CMS's entity id — the Issuer on the AuthnRequest, and what the IdP has
176
+ # registered as the service provider.
177
+ SSO_SAML_ISSUER=
178
+ # What the route and every sso_identities row call this provider. Renaming it
179
+ # orphans the links that name it. Default: saml
180
+ SSO_SAML_NAME=
181
+ # The sign-in button's text. Default: SAML
182
+ SSO_SAML_LABEL=
183
+ # The attribute holding a STABLE identifier for the person. Worth setting
184
+ # whenever your IdP's NameID format is emailAddress: an address is not a stable
185
+ # identifier, and a profile whose subject is one is refused.
186
+ SSO_SAML_SUBJECT_ATTRIBUTE=
187
+ # Which attributes to read, when the usual spellings are not what your IdP
188
+ # sends. Left empty, each falls back to a list of the common ones.
189
+ SSO_SAML_EMAIL_ATTRIBUTE=
190
+ SSO_SAML_NAME_ATTRIBUTE=
191
+ SSO_SAML_GROUPS_ATTRIBUTE=
192
+ # Set to true ONLY if you are asserting that this directory owns the addresses
193
+ # it reports. SAML carries no verification claim at all — there is nothing to
194
+ # read — and this is the only gate on a first sign-in claiming an existing
195
+ # account.
196
+ SSO_SAML_EMAIL_VERIFIED=
197
+ # ortha:end
141
198
  # The origin browsers reach the API on, when the admin is served from a
142
199
  # different one. Leave empty for the usual same-origin deployment.
143
200
  SSO_PUBLIC_BASE_URL=
144
201
  # How long one sign-in attempt stays live, in seconds. Default 600.
145
202
  SSO_REQUEST_TTL_SECONDS=
203
+
204
+ # --- webhooks (outgoing notifications on content changes) ---
205
+ # Endpoints live in the database — add them in the admin's Webhooks page
206
+ # (administrator-only). These settings are how hard the sender pushes and where
207
+ # it is allowed to reach.
208
+ #
209
+ # How often the sender looks for queued deliveries, in milliseconds. 0 queues
210
+ # but never sends from this process.
211
+ WEBHOOKS_DELIVERY_INTERVAL=2000
212
+ # Per-request timeout, in milliseconds.
213
+ WEBHOOKS_TIMEOUT=10000
214
+ # How long a finished delivery stays in the log. Nothing else prunes it.
215
+ WEBHOOKS_RETENTION_DAYS=30
216
+ # Permit http:// destinations — for local development only; the payload is
217
+ # readable to anyone on the path even though it is signed.
218
+ WEBHOOKS_ALLOW_INSECURE_URLS=false
219
+ # Permit loopback and RFC 1918 destinations. OFF by default: a webhook is the
220
+ # server making a request to an address a user typed, which is the shape of
221
+ # every SSRF. Turn it on only if your receiver really is inside this network.
222
+ WEBHOOKS_ALLOW_PRIVATE_NETWORKS=false