create-ortha-app 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +277 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/lib/conditionals.d.ts +37 -0
- package/dist/lib/conditionals.d.ts.map +1 -0
- package/dist/lib/conditionals.js +115 -0
- package/dist/lib/features.d.ts +185 -0
- package/dist/lib/features.d.ts.map +1 -0
- package/dist/lib/features.js +328 -0
- package/dist/lib/template.d.ts +46 -0
- package/dist/lib/template.d.ts.map +1 -0
- package/dist/lib/template.js +115 -0
- package/dist/lib/ui.d.ts +76 -0
- package/dist/lib/ui.d.ts.map +1 -0
- package/dist/lib/ui.js +310 -0
- package/dist/lib/validate.d.ts +13 -0
- package/dist/lib/validate.d.ts.map +1 -0
- package/dist/lib/validate.js +51 -0
- package/package.json +37 -0
- package/templates/default/README.md.tmpl +199 -0
- package/templates/default/_gitignore +8 -0
- package/templates/default/apps/admin/index.html +38 -0
- package/templates/default/apps/admin/src/main.tsx +5 -0
- package/templates/default/apps/admin/src/plugins.spec.ts +58 -0
- package/templates/default/apps/admin/src/plugins.ts +57 -0
- package/templates/default/apps/admin/src/styles.css +254 -0
- package/templates/default/apps/admin/tsconfig.json +29 -0
- package/templates/default/apps/admin/vite.config.mts +67 -0
- package/templates/default/apps/admin-e2e/playwright.config.ts +51 -0
- package/templates/default/apps/admin-e2e/src/auth.spec.ts +55 -0
- package/templates/default/apps/admin-e2e/src/support/seed.ts +62 -0
- package/templates/default/apps/admin-e2e/tsconfig.json +23 -0
- package/templates/default/apps/server/jest.config.js +38 -0
- package/templates/default/apps/server/jest.setup.js +20 -0
- package/templates/default/apps/server/ortha.config.ts +505 -0
- package/templates/default/apps/server/src/main.ts +26 -0
- package/templates/default/apps/server/src/plugins.spec.ts +71 -0
- package/templates/default/apps/server/src/plugins.ts +229 -0
- package/templates/default/apps/server/tsconfig.json +31 -0
- package/templates/default/apps/server-e2e/jest.config.js +47 -0
- package/templates/default/apps/server-e2e/src/api.spec.ts +107 -0
- package/templates/default/apps/server-e2e/src/global-setup.ts +41 -0
- package/templates/default/apps/server-e2e/src/jest.setup.ts +28 -0
- package/templates/default/apps/server-e2e/src/support/db.ts +143 -0
- package/templates/default/apps/server-e2e/src/support/test-app.ts +64 -0
- package/templates/default/apps/server-e2e/tsconfig.json +26 -0
- package/templates/default/docker-compose.yml +21 -0
- package/templates/default/env.tmpl +140 -0
- package/templates/default/package.json.tmpl +51 -0
- package/templates/default/tsconfig.json +18 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { NestExpressApplication } from '@nestjs/platform-express';
|
|
2
|
+
import { createServer } from '@orthacms/bootstrap-server';
|
|
3
|
+
import { closeDatabase } from '@orthacms/database';
|
|
4
|
+
import type { Server } from 'node:http';
|
|
5
|
+
import config from '../../../server/ortha.config';
|
|
6
|
+
import { buildPlugins } from '../../../server/src/plugins';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* An origin the login route accepts.
|
|
10
|
+
*
|
|
11
|
+
* Login is guarded against cross-site POSTs by an allow-list, so a request
|
|
12
|
+
* with no `Origin` — or the wrong one — is refused with `403` before the
|
|
13
|
+
* credentials are ever looked at. Read from the config rather than hardcoded:
|
|
14
|
+
* the list follows `ADMIN_PORT`, so a hardcoded `:4200` fails on any app whose
|
|
15
|
+
* admin runs somewhere else, and fails as a confusing `403` rather than
|
|
16
|
+
* "wrong origin".
|
|
17
|
+
*/
|
|
18
|
+
export const ALLOWED_ORIGIN =
|
|
19
|
+
config.plugins.identity.allowedOrigins?.[0] ?? 'http://localhost:4200';
|
|
20
|
+
|
|
21
|
+
/** A booted app plus the raw server to hand to supertest. */
|
|
22
|
+
export interface TestApp {
|
|
23
|
+
app: NestExpressApplication;
|
|
24
|
+
server: Server;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Boots **this app** — the real `buildPlugins`, the real config — on an
|
|
29
|
+
* ephemeral port.
|
|
30
|
+
*
|
|
31
|
+
* Deliberately `createServer` rather than a hand-assembled Nest app: a harness
|
|
32
|
+
* that mirrors the bootstrap can never fail on a bootstrap defect, and this
|
|
33
|
+
* suite exists to catch exactly the things that only appear once everything is
|
|
34
|
+
* wired together. Port `0` keeps parallel runs from colliding; supertest is
|
|
35
|
+
* handed the server object, so nothing depends on the number.
|
|
36
|
+
*
|
|
37
|
+
* `createServer` also runs the `OnApplicationBootstrap` seeders, so the system
|
|
38
|
+
* roles and the root admin from `.env.e2e` exist by the time a test runs.
|
|
39
|
+
*/
|
|
40
|
+
export async function createTestApp(): Promise<TestApp> {
|
|
41
|
+
const app = await createServer({
|
|
42
|
+
plugins: buildPlugins(config),
|
|
43
|
+
port: 0,
|
|
44
|
+
globalPrefix: config.globalPrefix,
|
|
45
|
+
bodyLimit: config.bodyLimit,
|
|
46
|
+
// The reference is developer tooling and adds a route surface these
|
|
47
|
+
// tests do not describe. Off, whatever the environment says.
|
|
48
|
+
docs: { ...config.docs, enabled: false }
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return { app, server: app.getHttpServer() as Server };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Closes the app and the database pool.
|
|
56
|
+
*
|
|
57
|
+
* `app.close()` does not end the pool — nothing binds a shutdown hook to it —
|
|
58
|
+
* so without `closeDatabase` the Jest worker stays alive on an open handle and
|
|
59
|
+
* the run hangs after the last assertion passes.
|
|
60
|
+
*/
|
|
61
|
+
export async function closeTestApp(harness: TestApp): Promise<void> {
|
|
62
|
+
await harness.app.close();
|
|
63
|
+
await closeDatabase();
|
|
64
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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/**/*.ts",
|
|
24
|
+
"jest.config.js"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Postgres for local development. `docker compose up -d`, then `npm run migrate`.
|
|
2
|
+
services:
|
|
3
|
+
postgres:
|
|
4
|
+
image: postgres:17-alpine
|
|
5
|
+
restart: unless-stopped
|
|
6
|
+
environment:
|
|
7
|
+
POSTGRES_USER: ortha
|
|
8
|
+
POSTGRES_PASSWORD: ortha
|
|
9
|
+
POSTGRES_DB: __DATABASE_NAME__
|
|
10
|
+
ports:
|
|
11
|
+
- '5432:5432'
|
|
12
|
+
volumes:
|
|
13
|
+
- postgres-data:/var/lib/postgresql/data
|
|
14
|
+
healthcheck:
|
|
15
|
+
test: ['CMD-SHELL', 'pg_isready -U ortha']
|
|
16
|
+
interval: 5s
|
|
17
|
+
timeout: 5s
|
|
18
|
+
retries: 10
|
|
19
|
+
|
|
20
|
+
volumes:
|
|
21
|
+
postgres-data:
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Port the API listens on. `ortha start` serves the admin from here too.
|
|
2
|
+
PORT=3000
|
|
3
|
+
# Port the Vite dev server listens on (development only).
|
|
4
|
+
ADMIN_PORT=4200
|
|
5
|
+
|
|
6
|
+
# PostgreSQL connection string.
|
|
7
|
+
DATABASE_URL=__DATABASE_URL__
|
|
8
|
+
|
|
9
|
+
# How many reverse proxies sit in front of the app (Express `trust proxy`).
|
|
10
|
+
# REQUIRED behind a load balancer, ingress or CDN: left unset, Express ignores
|
|
11
|
+
# X-Forwarded-For, every request reports the proxy's address, and the login
|
|
12
|
+
# rate limit collapses into ONE bucket for the whole deployment. Prefer the hop
|
|
13
|
+
# count (1 for a single proxy) — a client cannot forge past it.
|
|
14
|
+
TRUST_PROXY=
|
|
15
|
+
|
|
16
|
+
# Serve the API reference on /reference. On outside production by default.
|
|
17
|
+
API_DOCS=
|
|
18
|
+
|
|
19
|
+
# Largest JSON request body the API accepts. Defaults to 1mb.
|
|
20
|
+
MAX_REQUEST_BODY=
|
|
21
|
+
|
|
22
|
+
# --- media ---
|
|
23
|
+
# ortha:if media-local
|
|
24
|
+
# Where uploads are written. Point this at a persistent volume in production:
|
|
25
|
+
# a container's own disk is wiped on every deploy.
|
|
26
|
+
MEDIA_LOCAL_ROOT=./.storage/media
|
|
27
|
+
# ortha:end
|
|
28
|
+
# ortha:if media-vercel-blob
|
|
29
|
+
# Vercel injects this on its own platform; set it only when running elsewhere.
|
|
30
|
+
# NOTE: every blob in this store gets a permanent, public URL — anyone who
|
|
31
|
+
# obtains one can fetch that asset without signing in. Do not pick this backend
|
|
32
|
+
# for confidential media.
|
|
33
|
+
BLOB_READ_WRITE_TOKEN=
|
|
34
|
+
# ortha:end
|
|
35
|
+
# ortha:if media-gcs
|
|
36
|
+
# The bucket every upload lands in. Required.
|
|
37
|
+
MEDIA_GCS_BUCKET=
|
|
38
|
+
# Leave the rest blank on GKE or Cloud Run: the client falls back to
|
|
39
|
+
# Application Default Credentials, which is the point of using this adapter
|
|
40
|
+
# instead of the S3-compatible one.
|
|
41
|
+
MEDIA_GCS_PROJECT_ID=
|
|
42
|
+
MEDIA_GCS_KEY_FILE=
|
|
43
|
+
# Sign URLs through IAM instead of a private key (Workload Identity). Needs
|
|
44
|
+
# the iam.serviceAccounts.signBlob permission.
|
|
45
|
+
MEDIA_GCS_SIGN_WITH_IAM=false
|
|
46
|
+
# ortha:end
|
|
47
|
+
# ortha:if media-azure
|
|
48
|
+
# The container every upload lands in, and how to reach the account. The portal
|
|
49
|
+
# hands you the connection string; Azurite prints one too. For managed identity,
|
|
50
|
+
# build the container client yourself in plugins.ts — see the package docs.
|
|
51
|
+
MEDIA_AZURE_CONTAINER=media
|
|
52
|
+
MEDIA_AZURE_CONNECTION_STRING=
|
|
53
|
+
# ortha:end
|
|
54
|
+
# ortha:if media-s3
|
|
55
|
+
# The bucket every upload lands in. Required.
|
|
56
|
+
MEDIA_S3_BUCKET=
|
|
57
|
+
# Leave the endpoint blank for AWS S3 itself. For anything else, set it:
|
|
58
|
+
# Cloudflare R2 https://<account>.r2.cloudflarestorage.com
|
|
59
|
+
# MinIO http://localhost:9000 (also set FORCE_PATH_STYLE=true)
|
|
60
|
+
MEDIA_S3_ENDPOINT=
|
|
61
|
+
MEDIA_S3_REGION=auto
|
|
62
|
+
MEDIA_S3_FORCE_PATH_STYLE=false
|
|
63
|
+
# Leave both blank on a host with an instance role or IRSA — the SDK finds
|
|
64
|
+
# those itself, and blanks here would shadow them with keys that cannot sign.
|
|
65
|
+
MEDIA_S3_ACCESS_KEY_ID=
|
|
66
|
+
MEDIA_S3_SECRET_ACCESS_KEY=
|
|
67
|
+
# ortha:end
|
|
68
|
+
MEDIA_MAX_UPLOAD_BYTES=52428800
|
|
69
|
+
|
|
70
|
+
# --- first admin ---
|
|
71
|
+
# With an email set, an active admin is provisioned on boot — idempotent and
|
|
72
|
+
# non-destructive, so an existing account is left alone. This is how you get
|
|
73
|
+
# your first login; clear it once you have one.
|
|
74
|
+
ORTHA_ROOT_ADMIN_EMAIL=__ADMIN_EMAIL__
|
|
75
|
+
ORTHA_ROOT_ADMIN_PASSWORD=__ADMIN_PASSWORD__
|
|
76
|
+
|
|
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.
|
|
80
|
+
COPILOT_ENABLED=false
|
|
81
|
+
# Ceiling on a single model response, in tokens.
|
|
82
|
+
COPILOT_MAX_OUTPUT_TOKENS=8192
|
|
83
|
+
# ortha:if copilot-anthropic
|
|
84
|
+
# Native Claude. Setting the key is what REGISTERS this backend — leave it empty
|
|
85
|
+
# and there is no `claude` in the picker at all, rather than one that fails on
|
|
86
|
+
# the first message. The key never leaves the server.
|
|
87
|
+
ANTHROPIC_API_KEY=
|
|
88
|
+
# Comma-separated. The FIRST is the default; the rest are what a user can switch
|
|
89
|
+
# to mid-conversation without a redeploy.
|
|
90
|
+
COPILOT_ANTHROPIC_MODELS=claude-sonnet-5
|
|
91
|
+
# ortha:end
|
|
92
|
+
# ortha:if copilot-openai
|
|
93
|
+
# An OpenAI-wire endpoint. Setting this REGISTERS the backend, and there is no
|
|
94
|
+
# default: an endpoint nobody named is a backend that can only time out. A local
|
|
95
|
+
# Ollama is http://localhost:11434/v1.
|
|
96
|
+
COPILOT_OPENAI_BASE_URL=
|
|
97
|
+
COPILOT_OPENAI_MODELS=llama3.1
|
|
98
|
+
# Leave empty for a local runtime that wants no auth.
|
|
99
|
+
COPILOT_OPENAI_API_KEY=
|
|
100
|
+
# ortha:end
|
|
101
|
+
# ortha:if mcp
|
|
102
|
+
# --- mcp ---
|
|
103
|
+
# Global kill switch. OFF by default: once on, any holder of a `full`-scope API
|
|
104
|
+
# token can drive content CRUD from an external agent. Agents connect to
|
|
105
|
+
# POST /api/v1/mcp with `Authorization: Bearer <token>`.
|
|
106
|
+
MCP_ENABLED=false
|
|
107
|
+
# Ceiling on one tools/call, in milliseconds. Defaults to 30000.
|
|
108
|
+
MCP_CALL_TIMEOUT_MS=
|
|
109
|
+
# Ceiling on one tool result, in bytes. Defaults to 4194304 (4 MB).
|
|
110
|
+
MCP_MAX_RESULT_BYTES=
|
|
111
|
+
# ortha:end
|
|
112
|
+
|
|
113
|
+
# --- single sign-on ---
|
|
114
|
+
# ortha:if sso-oidc
|
|
115
|
+
# Generic OpenID Connect — Okta, Auth0, Keycloak, Google, Entra ID and the rest.
|
|
116
|
+
# Both of these are what make the provider configured at all: an issuer with no
|
|
117
|
+
# client id is a sign-in button that can only fail.
|
|
118
|
+
#
|
|
119
|
+
# The issuer must match the `iss` claim byte for byte, trailing slash included
|
|
120
|
+
# (Auth0 issues with one). Register
|
|
121
|
+
# <origin>/api/auth/sso/<SSO_OIDC_NAME>/callback with the provider.
|
|
122
|
+
SSO_OIDC_ISSUER=
|
|
123
|
+
SSO_OIDC_CLIENT_ID=
|
|
124
|
+
SSO_OIDC_CLIENT_SECRET=
|
|
125
|
+
# What the route and every sso_identities row call this provider. Renaming it
|
|
126
|
+
# orphans the links that name it. Default: oidc
|
|
127
|
+
SSO_OIDC_NAME=
|
|
128
|
+
# The sign-in button's text. Defaults to the issuer's host.
|
|
129
|
+
SSO_OIDC_LABEL=
|
|
130
|
+
# Set to true ONLY if your provider never sends `email_verified` and you are
|
|
131
|
+
# asserting that this directory owns the addresses it reports (Microsoft Entra
|
|
132
|
+
# ID is the common case). That claim is the only gate on a first sign-in
|
|
133
|
+
# claiming an existing account.
|
|
134
|
+
SSO_OIDC_EMAIL_VERIFIED_WHEN_ABSENT=
|
|
135
|
+
# ortha:end
|
|
136
|
+
# The origin browsers reach the API on, when the admin is served from a
|
|
137
|
+
# different one. Leave empty for the usual same-origin deployment.
|
|
138
|
+
SSO_PUBLIC_BASE_URL=
|
|
139
|
+
# How long one sign-in attempt stays live, in seconds. Default 600.
|
|
140
|
+
SSO_REQUEST_TTL_SECONDS=
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "ortha dev",
|
|
8
|
+
"build": "ortha build",
|
|
9
|
+
"start": "ortha start",
|
|
10
|
+
"migrate": "ortha migrate",
|
|
11
|
+
"generate": "ortha generate",
|
|
12
|
+
"studio": "ortha studio",
|
|
13
|
+
"test": "npm run test:server && npm run test:admin",
|
|
14
|
+
"test:server": "jest --config apps/server/jest.config.js",
|
|
15
|
+
"test:admin": "vitest run --config apps/admin/vite.config.mts",
|
|
16
|
+
"e2e": "npm run e2e:server && npm run e2e:admin",
|
|
17
|
+
"typecheck": "tsc --build tsconfig.json",
|
|
18
|
+
"e2e:server": "jest --config apps/server-e2e/jest.config.js",
|
|
19
|
+
"e2e:admin": "playwright test --config apps/admin-e2e/playwright.config.ts"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"react": "^19.0.0",
|
|
23
|
+
"react-dom": "^19.0.0",
|
|
24
|
+
"react-router-dom": "6.30.3",
|
|
25
|
+
"reflect-metadata": "^0.1.13",
|
|
26
|
+
"rxjs": "^7.8.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@playwright/test": "^1.36.0",
|
|
30
|
+
"@swc/core": "~1.15.5",
|
|
31
|
+
"@swc/jest": "~0.2.38",
|
|
32
|
+
"@tailwindcss/vite": "^4.3.0",
|
|
33
|
+
"@testing-library/dom": "10.4.0",
|
|
34
|
+
"@testing-library/react": "16.3.0",
|
|
35
|
+
"@types/jest": "~30.0.0",
|
|
36
|
+
"@types/node": "^20.19.9",
|
|
37
|
+
"@types/pg": "^8.11.0",
|
|
38
|
+
"@types/react": "^19.0.0",
|
|
39
|
+
"@types/react-dom": "^19.0.0",
|
|
40
|
+
"@types/supertest": "^6.0.0",
|
|
41
|
+
"@vitejs/plugin-react": "^6.0.0",
|
|
42
|
+
"jest": "~30.3.0",
|
|
43
|
+
"jsdom": "~22.1.0",
|
|
44
|
+
"pg": "^8.13.0",
|
|
45
|
+
"supertest": "^7.0.0",
|
|
46
|
+
"tailwindcss": "^4.3.0",
|
|
47
|
+
"typescript": "~5.9.2",
|
|
48
|
+
"vite": "^8.0.0",
|
|
49
|
+
"vitest": "~4.1.0"
|
|
50
|
+
}
|
|
51
|
+
}
|