@percepta/create 4.1.15 → 4.2.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/dist/index.js +24 -152
- package/dist/index.js.map +1 -1
- package/dist/{register-app-BeSQEsel.js → register-app-BtvxQeo0.js} +91 -1
- package/dist/register-app-BtvxQeo0.js.map +1 -0
- package/package.json +5 -2
- package/template-versions.json +2 -2
- package/templates/monorepo/README.md +28 -1
- package/templates/monorepo/auth/package.json +1 -1
- package/templates/monorepo/auth/src/auth.ts +7 -103
- package/templates/monorepo/authentik/blueprints/local-dev.yaml +123 -0
- package/templates/monorepo/authentik/initdb/00-authentik.sql +5 -0
- package/templates/monorepo/docker-compose.yml +70 -0
- package/templates/monorepo/oxlint.config.ts.template +5 -1
- package/templates/monorepo/package.json.template +2 -1
- package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +22 -89
- package/templates/webapp/AGENTS.md +4 -4
- package/templates/webapp/README.md +22 -33
- package/templates/webapp/agent-skills/langfuse.md +8 -11
- package/templates/webapp/agent-skills/oneshot.md +1 -1
- package/templates/webapp/e2e/rbac.spec.ts +28 -4
- package/templates/webapp/env.example.template +8 -12
- package/templates/webapp/package.json.template +4 -5
- package/templates/webapp/scripts/seed.ts +28 -52
- package/templates/webapp/scripts/with-local-env.ts +12 -64
- package/templates/webapp/src/app/(auth)/auth/signin/SignInForm.tsx +59 -0
- package/templates/webapp/src/app/(auth)/auth/signin/page.tsx +3 -3
- package/templates/webapp/src/drizzle/db.ts +5 -9
- package/templates/webapp/src/instrumentation.ts +5 -72
- package/templates/webapp/src/lib/auth/index.ts +1 -2
- package/templates/webapp/src/services/DatabaseService.ts +3 -51
- package/templates/webapp/src/services/observability/initFaro.ts +5 -17
- package/dist/register-app-BeSQEsel.js.map +0 -1
- package/templates/monorepo/scripts/setup-local-databases.mjs +0 -183
- package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +0 -179
- package/templates/webapp/src/app/(auth)/auth/signup/CredentialsSignUpForm.tsx +0 -135
- package/templates/webapp/src/app/(auth)/auth/signup/page.tsx +0 -53
- package/templates/webapp/src/drizzle/schema/utils/jsonbFromZod.ts +0 -25
- package/templates/webapp/src/lib/auth/app-auth-mode.ts +0 -20
|
@@ -5,6 +5,8 @@ import chalk from "chalk";
|
|
|
5
5
|
import fs from "fs-extra";
|
|
6
6
|
import { isMap, isSeq, parseDocument } from "yaml";
|
|
7
7
|
//#region src/commands/infra/register-app.ts
|
|
8
|
+
const AUTHENTIK_WEBAPP_OIDC_SERVICE = "authentik-webapp-oidc";
|
|
9
|
+
const WEBAPP_OIDC_CLIENTS_INPUT = "webapp_oidc_clients";
|
|
8
10
|
const OS_POSTGRESQL_TERRAFORM_SUFFIX = "postgresql-terraform";
|
|
9
11
|
const LEGACY_OS_POSTGRESQL_TERRAFORM_ALIAS = "os-postgresql-terraform";
|
|
10
12
|
const OS_POSTGRESQL_TERRAFORM_SERVICES = new Set(["os-postgresql-terraform-aws", "os-postgresql-terraform-azure"]);
|
|
@@ -144,6 +146,7 @@ function updateBlueprint(blueprintContent, appName, options) {
|
|
|
144
146
|
changed = addAppInput(document, inputs, renderLangfuseSecretKeyInput()) || changed;
|
|
145
147
|
changed = addAppInput(document, inputs, renderInngestEventKeyInput()) || changed;
|
|
146
148
|
changed = addAppInput(document, inputs, renderInngestSigningKeyInput()) || changed;
|
|
149
|
+
changed = addAppInput(document, inputs, renderWebappOidcClientsInput()) || changed;
|
|
147
150
|
}
|
|
148
151
|
if (options.appDatabase) changed = addAppDatabase(document, inputs, appName) || changed;
|
|
149
152
|
if (options.appInstallation) {
|
|
@@ -153,6 +156,9 @@ function updateBlueprint(blueprintContent, appName, options) {
|
|
|
153
156
|
changed = ensureOsPostgresqlInstallationAlias(installations, postgresqlInstallationName) || changed;
|
|
154
157
|
changed = ensureAppInstallationPostgresqlOutputRefs(installations, postgresqlInstallationName) || changed;
|
|
155
158
|
changed = addAppInstallation(document, installations, appName, postgresqlInstallationName) || changed;
|
|
159
|
+
changed = ensureAuthentikWebappOidcInstallation(document, installations) || changed;
|
|
160
|
+
changed = ensureAppInstallationAuthentikEnv(document, installations, appName) || changed;
|
|
161
|
+
changed = addWebappOidcClient(document, inputs, appName) || changed;
|
|
156
162
|
}
|
|
157
163
|
return changed ? document.toString() : blueprintContent;
|
|
158
164
|
}
|
|
@@ -242,6 +248,63 @@ function addAppInstallation(document, installations, appName, postgresqlInstalla
|
|
|
242
248
|
}));
|
|
243
249
|
return true;
|
|
244
250
|
}
|
|
251
|
+
function addWebappOidcClient(document, inputs, appName) {
|
|
252
|
+
const clientsInput = inputs.items.find((item) => isMap(item) && item.get("name") === WEBAPP_OIDC_CLIENTS_INPUT);
|
|
253
|
+
if (!isMap(clientsInput)) throw new Error(`OS blueprint must include a ${WEBAPP_OIDC_CLIENTS_INPUT} input.`);
|
|
254
|
+
const defaultValue = clientsInput.get("default", true);
|
|
255
|
+
if (!isSeq(defaultValue)) throw new Error(`OS blueprint ${WEBAPP_OIDC_CLIENTS_INPUT} default must be a sequence.`);
|
|
256
|
+
if (defaultValue.items.some((item) => isMap(item) && item.get("slug") === appName)) return false;
|
|
257
|
+
defaultValue.add(document.createNode({
|
|
258
|
+
slug: appName,
|
|
259
|
+
name: toTitleCase(appName)
|
|
260
|
+
}));
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
function ensureAuthentikWebappOidcInstallation(document, installations) {
|
|
264
|
+
if (installations.items.some((item) => isMap(item) && item.get("service") === AUTHENTIK_WEBAPP_OIDC_SERVICE)) return false;
|
|
265
|
+
installations.add(document.createNode({
|
|
266
|
+
service: AUTHENTIK_WEBAPP_OIDC_SERVICE,
|
|
267
|
+
config: renderAuthentikWebappOidcConfig()
|
|
268
|
+
}));
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
function ensureAppInstallationAuthentikEnv(document, installations, appName) {
|
|
272
|
+
const installation = installations.items.find((item) => isMap(item) && item.get("service") === appName);
|
|
273
|
+
if (!isMap(installation)) return false;
|
|
274
|
+
const env = installation.get("env", true);
|
|
275
|
+
if (!isSeq(env)) return false;
|
|
276
|
+
const existingKeys = /* @__PURE__ */ new Set();
|
|
277
|
+
for (const item of env.items) if (isMap(item)) existingKeys.add(item.get("key"));
|
|
278
|
+
let changed = false;
|
|
279
|
+
for (const entry of renderAppAuthentikEnv(appName)) {
|
|
280
|
+
if (existingKeys.has(entry.key)) continue;
|
|
281
|
+
env.add(document.createNode(entry));
|
|
282
|
+
changed = true;
|
|
283
|
+
}
|
|
284
|
+
return changed;
|
|
285
|
+
}
|
|
286
|
+
function renderAuthentikWebappOidcConfig() {
|
|
287
|
+
const ingressDomain = ingressDomainInputName();
|
|
288
|
+
return [
|
|
289
|
+
`authentik_url: 'https://authentik.{{ input "${ingressDomain}" }}'`,
|
|
290
|
+
`ingress_domain: '{{ input "${ingressDomain}" }}'`,
|
|
291
|
+
"namespace: '{{ .ryvn.env.name }}'",
|
|
292
|
+
"env_secret_name: '{{ (blueprintInstallation \"mosaic\").outputs.authentik_env_secret_name }}'",
|
|
293
|
+
`${WEBAPP_OIDC_CLIENTS_INPUT}:`,
|
|
294
|
+
`{{ input "${WEBAPP_OIDC_CLIENTS_INPUT}" | toYaml | nindent 2 }}`,
|
|
295
|
+
""
|
|
296
|
+
].join("\n");
|
|
297
|
+
}
|
|
298
|
+
function renderWebappOidcClientsInput() {
|
|
299
|
+
return {
|
|
300
|
+
name: WEBAPP_OIDC_CLIENTS_INPUT,
|
|
301
|
+
type: "array",
|
|
302
|
+
group: "applications",
|
|
303
|
+
displayName: "Webapp OIDC Clients",
|
|
304
|
+
description: "OS webapps registered as Authentik OIDC clients (slug + display name). The authentik-webapp-oidc service provisions a provider/application per entry; the generated client_id/client_secret/issuer are wired into each app as AUTHENTIK_*.",
|
|
305
|
+
default: []
|
|
306
|
+
};
|
|
307
|
+
}
|
|
245
308
|
function renderIngressDomainInput() {
|
|
246
309
|
return {
|
|
247
310
|
name: ingressDomainInputName(),
|
|
@@ -421,6 +484,33 @@ function renderAppInstallationEnv(appName, postgresqlInstallationName) {
|
|
|
421
484
|
blueprintInstallation: "mosaic",
|
|
422
485
|
name: "spicedb_insecure"
|
|
423
486
|
}
|
|
487
|
+
},
|
|
488
|
+
...renderAppAuthentikEnv(appName)
|
|
489
|
+
];
|
|
490
|
+
}
|
|
491
|
+
function renderAppAuthentikEnv(appName) {
|
|
492
|
+
return [
|
|
493
|
+
{
|
|
494
|
+
key: "AUTHENTIK_ISSUER",
|
|
495
|
+
valueFromOutput: {
|
|
496
|
+
serviceInstallation: AUTHENTIK_WEBAPP_OIDC_SERVICE,
|
|
497
|
+
name: `issuers.${appName}`
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
key: "AUTHENTIK_CLIENT_ID",
|
|
502
|
+
valueFromOutput: {
|
|
503
|
+
serviceInstallation: AUTHENTIK_WEBAPP_OIDC_SERVICE,
|
|
504
|
+
name: `client_ids.${appName}`
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
key: "AUTHENTIK_CLIENT_SECRET",
|
|
509
|
+
isSecret: true,
|
|
510
|
+
valueFromOutput: {
|
|
511
|
+
serviceInstallation: AUTHENTIK_WEBAPP_OIDC_SERVICE,
|
|
512
|
+
name: `client_secrets.${appName}`
|
|
513
|
+
}
|
|
424
514
|
}
|
|
425
515
|
];
|
|
426
516
|
}
|
|
@@ -502,4 +592,4 @@ function normalizeAppName(appNameInput) {
|
|
|
502
592
|
//#endregion
|
|
503
593
|
export { registerAppCommand };
|
|
504
594
|
|
|
505
|
-
//# sourceMappingURL=register-app-
|
|
595
|
+
//# sourceMappingURL=register-app-BtvxQeo0.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-app-BtvxQeo0.js","names":[],"sources":["../src/commands/infra/register-app.ts"],"sourcesContent":["import path from \"node:path\";\nimport chalk from \"chalk\";\nimport fs from \"fs-extra\";\nimport { isMap, isSeq, parseDocument } from \"yaml\";\nimport {\n toKebabCase,\n toSnakeCase,\n toTitleCase,\n} from \"../../utils/case-converters.js\";\nimport { detectMonorepo } from \"../../utils/detect-monorepo.js\";\nimport { validateProjectName } from \"../../utils/validate.js\";\nimport { readWorkspaceManifest } from \"../../utils/workspace-manifest.js\";\nimport {\n createInfraGitHubApi,\n createOrUpdateInfraPullRequestFiles,\n INFRA_BASE_BRANCH,\n INFRA_REPOSITORY,\n type InfraGitHubApi,\n type InfraPullRequestFile,\n resolveGitHubToken,\n} from \"./github.js\";\n\nconst AUTHENTIK_WEBAPP_OIDC_SERVICE = \"authentik-webapp-oidc\";\nconst WEBAPP_OIDC_CLIENTS_INPUT = \"webapp_oidc_clients\";\nconst OS_POSTGRESQL_TERRAFORM_SUFFIX = \"postgresql-terraform\";\nconst LEGACY_OS_POSTGRESQL_TERRAFORM_ALIAS = \"os-postgresql-terraform\";\nconst OS_POSTGRESQL_TERRAFORM_SERVICES = new Set([\n \"os-postgresql-terraform-aws\",\n \"os-postgresql-terraform-azure\",\n]);\nconst OS_BLUEPRINT_INPUT_GROUPS = [\n {\n name: \"general\",\n displayName: \"General\",\n description: \"Shared OS infrastructure settings.\",\n },\n {\n name: \"applications\",\n displayName: \"Applications\",\n description: \"Generated OS webapp settings.\",\n },\n {\n name: \"aws_postgresql\",\n displayName: \"AWS PostgreSQL\",\n description: \"AWS Aurora PostgreSQL settings.\",\n condition: '{{ eq EnvironmentProviderType \"aws\" }}',\n },\n {\n name: \"azure_postgresql\",\n displayName: \"Azure PostgreSQL\",\n description: \"Azure PostgreSQL Flexible Server settings.\",\n condition: '{{ eq EnvironmentProviderType \"azure\" }}',\n },\n];\n\nexport interface RegisterAppResult {\n appName: string;\n blueprintName: string;\n blueprintPath: string;\n branchName: string;\n customerSlug: string;\n pullRequestUrl: string | null;\n repository: typeof INFRA_REPOSITORY;\n status: \"already_registered\" | \"created_pr\" | \"updated_pr\";\n servicePath: string;\n targetPath: string;\n}\n\nexport async function registerApp(\n appNameInput: string,\n args: {\n cwd?: string;\n github?: InfraGitHubApi;\n } = {},\n): Promise<RegisterAppResult> {\n const appName = normalizeAppName(appNameInput);\n const cwd = args.cwd ?? process.cwd();\n const monorepoContext = await detectMonorepo(cwd);\n if (!monorepoContext.found || !monorepoContext.rootDir) {\n throw new Error(\n \"Run this command from a Mosaic customer monorepo with a .mosaic-workspace.json file.\",\n );\n }\n\n const workspaceManifest = await readWorkspaceManifest(\n monorepoContext.rootDir,\n );\n const customerSlug = workspaceManifest?.customerSlug;\n if (!customerSlug) {\n throw new Error(\n \".mosaic-workspace.json is missing customerSlug. Recreate the monorepo with a current @percepta/create.\",\n );\n }\n\n const github = args.github ?? createInfraGitHubApi(resolveGitHubToken());\n const blueprintName = `${customerSlug}-os`;\n const branchName = `blueberry/register-${customerSlug}-${appName}`;\n const blueprintPath = [\n \"ryvn\",\n \"definitions\",\n customerSlug,\n \"blueprints\",\n `${blueprintName}.blueprint.yaml`,\n ].join(\"/\");\n const servicePath = [\n \"ryvn\",\n \"definitions\",\n customerSlug,\n \"services\",\n `${appName}.service.yaml`,\n ].join(\"/\");\n\n const mainBlueprintFile = await github.getFile(\n blueprintPath,\n INFRA_BASE_BRANCH,\n );\n if (!mainBlueprintFile) {\n throw new Error(\n `${blueprintPath} does not exist in ${INFRA_REPOSITORY}. Run \\`pnpm mosaic infra register-os-blueprint\\` and merge that infra PR first.`,\n );\n }\n\n const mainServiceFile = await github.getFile(servicePath, INFRA_BASE_BRANCH);\n const serviceContent =\n mainServiceFile == null\n ? await readLocalServiceDefinition(monorepoContext.rootDir, appName)\n : null;\n const blueprintContent = registerAppInBlueprint(\n mainBlueprintFile.content,\n appName,\n );\n\n const files: InfraPullRequestFile[] = [];\n if (blueprintContent !== mainBlueprintFile.content) {\n files.push({\n baseFileSha: mainBlueprintFile.sha,\n content: blueprintContent,\n message: `Register ${appName} in ${blueprintName}`,\n path: blueprintPath,\n });\n }\n if (serviceContent != null) {\n files.push({\n content: serviceContent,\n message: `Register ${appName} service`,\n path: servicePath,\n });\n }\n\n if (files.length === 0) {\n return {\n appName,\n blueprintName,\n blueprintPath,\n branchName,\n customerSlug,\n pullRequestUrl: null,\n repository: INFRA_REPOSITORY,\n status: \"already_registered\",\n servicePath,\n targetPath: blueprintPath,\n };\n }\n\n const pullRequest = await createOrUpdateInfraPullRequestFiles({\n branchName,\n github,\n files,\n title: `Register ${appName} app`,\n body: [\n `Registers the ${appName} service and deployment in ${blueprintName}.`,\n \"\",\n \"Generated by `mosaic infra register-app`.\",\n ].join(\"\\n\"),\n });\n\n return {\n appName,\n blueprintName,\n blueprintPath,\n branchName,\n customerSlug,\n pullRequestUrl: pullRequest.pullRequestUrl,\n repository: INFRA_REPOSITORY,\n status: pullRequest.status,\n servicePath,\n targetPath: blueprintPath,\n };\n}\n\nexport async function registerAppCommand(appName: string): Promise<void> {\n try {\n const result = await registerApp(appName);\n\n if (result.status === \"already_registered\") {\n console.log(\n chalk.green(\"✔\"),\n `${result.appName} is already registered in ${result.repository} at`,\n chalk.cyan(result.targetPath),\n );\n return;\n }\n\n const verb =\n result.status === \"created_pr\" ? \"Created\" : \"Updated existing\";\n console.log(\n chalk.green(\"✔\"),\n `${verb} infra PR for ${result.appName}:`,\n chalk.cyan(result.pullRequestUrl),\n );\n } catch (error) {\n console.error(chalk.red(\"Error:\"), (error as Error).message);\n process.exit(1);\n }\n}\n\nexport function addAppDatabaseToBlueprint(\n blueprintContent: string,\n appName: string,\n): string {\n return updateBlueprint(blueprintContent, appName, {\n appDatabase: true,\n appInstallation: false,\n appInputs: false,\n });\n}\n\nexport function registerAppInBlueprint(\n blueprintContent: string,\n appName: string,\n): string {\n return updateBlueprint(blueprintContent, appName, {\n appDatabase: true,\n appInstallation: true,\n appInputs: true,\n });\n}\n\nfunction updateBlueprint(\n blueprintContent: string,\n appName: string,\n options: {\n appDatabase: boolean;\n appInstallation: boolean;\n appInputs: boolean;\n },\n): string {\n const document = parseDocument(blueprintContent);\n if (document.errors.length > 0) {\n throw new Error(\n `Invalid OS blueprint YAML: ${document.errors.map((error) => error.message).join(\"; \")}`,\n );\n }\n\n const spec = document.get(\"spec\", true);\n if (!isMap(spec)) {\n throw new Error(\"OS blueprint must include a spec map.\");\n }\n\n let changed = false;\n const inputs = spec.get(\"inputs\", true);\n if (!isSeq(inputs)) {\n throw new Error(\"OS blueprint spec.inputs must be a sequence.\");\n }\n\n changed = ensureInputGroups(document, spec) || changed;\n\n if (options.appInputs) {\n changed =\n addAppInput(document, inputs, renderIngressDomainInput()) || changed;\n changed =\n addAppInput(document, inputs, renderBetterAuthSecretInput(appName)) ||\n changed;\n changed =\n addAppInput(document, inputs, renderLangfusePublicKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderLangfuseSecretKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderInngestEventKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderInngestSigningKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderWebappOidcClientsInput()) || changed;\n }\n\n if (options.appDatabase) {\n changed = addAppDatabase(document, inputs, appName) || changed;\n }\n\n if (options.appInstallation) {\n const installations = spec.get(\"installations\", true);\n if (!isSeq(installations)) {\n throw new Error(\"OS blueprint spec.installations must be a sequence.\");\n }\n const postgresqlInstallationName = getOsPostgresqlInstallationName(\n getBlueprintName(document),\n );\n changed =\n ensureOsPostgresqlInstallationAlias(\n installations,\n postgresqlInstallationName,\n ) || changed;\n changed =\n ensureAppInstallationPostgresqlOutputRefs(\n installations,\n postgresqlInstallationName,\n ) || changed;\n changed =\n addAppInstallation(\n document,\n installations,\n appName,\n postgresqlInstallationName,\n ) || changed;\n changed =\n ensureAuthentikWebappOidcInstallation(document, installations) || changed;\n changed =\n ensureAppInstallationAuthentikEnv(document, installations, appName) ||\n changed;\n changed = addWebappOidcClient(document, inputs, appName) || changed;\n }\n\n return changed ? document.toString() : blueprintContent;\n}\n\nfunction ensureInputGroups(\n document: ReturnType<typeof parseDocument>,\n spec: {\n get(key: string, keepScalar?: true): unknown;\n set(key: string, value: unknown): void;\n },\n): boolean {\n let changed = false;\n const inputGroups = spec.get(\"inputGroups\", true);\n\n if (inputGroups == null) {\n spec.set(\"inputGroups\", document.createNode(OS_BLUEPRINT_INPUT_GROUPS));\n return true;\n }\n\n if (!isSeq(inputGroups)) {\n throw new Error(\"OS blueprint spec.inputGroups must be a sequence.\");\n }\n\n for (const group of OS_BLUEPRINT_INPUT_GROUPS) {\n const exists = inputGroups.items.some(\n (item) => isMap(item) && item.get(\"name\") === group.name,\n );\n if (exists) continue;\n\n inputGroups.add(document.createNode(group));\n changed = true;\n }\n\n return changed;\n}\n\nfunction ensureOsPostgresqlInstallationAlias(\n installations: { items: unknown[] },\n postgresqlInstallationName: string,\n): boolean {\n let changed = false;\n\n for (const installation of installations.items) {\n if (!isMap(installation)) continue;\n\n const service = installation.get(\"service\");\n if (\n typeof service !== \"string\" ||\n !OS_POSTGRESQL_TERRAFORM_SERVICES.has(service)\n ) {\n continue;\n }\n\n if (installation.get(\"name\") === postgresqlInstallationName) continue;\n\n installation.set(\"name\", postgresqlInstallationName);\n changed = true;\n }\n\n return changed;\n}\n\nfunction ensureAppInstallationPostgresqlOutputRefs(\n installations: { items: unknown[] },\n postgresqlInstallationName: string,\n): boolean {\n let changed = false;\n\n for (const installation of installations.items) {\n if (!isMap(installation)) continue;\n if (isOsPostgresqlInstallation(installation)) continue;\n\n const env = installation.get(\"env\", true);\n if (!isSeq(env)) continue;\n\n for (const envVar of env.items) {\n if (!isMap(envVar)) continue;\n\n const valueFromOutput = envVar.get(\"valueFromOutput\", true);\n if (!isMap(valueFromOutput)) continue;\n if (\n valueFromOutput.get(\"serviceInstallation\") !==\n LEGACY_OS_POSTGRESQL_TERRAFORM_ALIAS\n ) {\n continue;\n }\n\n const outputName = valueFromOutput.get(\"name\");\n if (\n typeof outputName !== \"string\" ||\n (outputName !== \"auth_database_url\" &&\n !outputName.startsWith(\"app_database_urls.\"))\n ) {\n continue;\n }\n\n valueFromOutput.set(\"serviceInstallation\", postgresqlInstallationName);\n changed = true;\n }\n }\n\n return changed;\n}\n\nfunction isOsPostgresqlInstallation(installation: {\n get(key: string, keepScalar?: true): unknown;\n}): boolean {\n const service = installation.get(\"service\");\n return (\n typeof service === \"string\" && OS_POSTGRESQL_TERRAFORM_SERVICES.has(service)\n );\n}\n\nfunction getBlueprintName(document: ReturnType<typeof parseDocument>): string {\n const metadata = document.get(\"metadata\", true);\n if (!isMap(metadata)) {\n throw new Error(\"OS blueprint must include a metadata map.\");\n }\n\n const name = metadata.get(\"name\");\n if (typeof name !== \"string\" || name.length === 0) {\n throw new Error(\"OS blueprint metadata.name must be a non-empty string.\");\n }\n\n return name;\n}\n\nfunction getOsPostgresqlInstallationName(blueprintName: string): string {\n return `${blueprintName}-${OS_POSTGRESQL_TERRAFORM_SUFFIX}`;\n}\n\nfunction addAppInput(\n document: ReturnType<typeof parseDocument>,\n inputs: { add(value: unknown): void; items: unknown[] },\n input: Record<string, unknown> & { name: string },\n): boolean {\n if (\n inputs.items.some((item) => isMap(item) && item.get(\"name\") === input.name)\n ) {\n return false;\n }\n\n inputs.add(document.createNode(input));\n return true;\n}\n\nfunction addAppDatabase(\n document: ReturnType<typeof parseDocument>,\n inputs: { items: unknown[] },\n appName: string,\n): boolean {\n const appDatabasesInput = inputs.items.find(\n (item) => isMap(item) && item.get(\"name\") === \"app_databases\",\n );\n if (!isMap(appDatabasesInput)) {\n throw new Error(\"OS blueprint must include an app_databases input.\");\n }\n\n const defaultValue = appDatabasesInput.get(\"default\", true);\n if (!isMap(defaultValue)) {\n throw new Error(\"OS blueprint app_databases default must be a map.\");\n }\n\n if (defaultValue.has(appName)) return false;\n\n defaultValue.flow = false;\n const appDatabaseValue = document.createNode({\n schema_name: toSnakeCase(appName),\n });\n defaultValue.set(appName, appDatabaseValue);\n return true;\n}\n\nfunction addAppInstallation(\n document: ReturnType<typeof parseDocument>,\n installations: { add(value: unknown): void; items: unknown[] },\n appName: string,\n postgresqlInstallationName: string,\n): boolean {\n if (\n installations.items.some(\n (item) => isMap(item) && item.get(\"service\") === appName,\n )\n ) {\n return false;\n }\n\n installations.add(\n document.createNode({\n service: appName,\n env: renderAppInstallationEnv(appName, postgresqlInstallationName),\n config: renderAppInstallationConfig(appName),\n }),\n );\n return true;\n}\n\n// Adds the app (slug + display name) to the shared webapp_oidc_clients input\n// the authentik-webapp-oidc service consumes. Mirrors addAppDatabase, but the\n// default is a sequence of objects rather than a map.\nfunction addWebappOidcClient(\n document: ReturnType<typeof parseDocument>,\n inputs: { items: unknown[] },\n appName: string,\n): boolean {\n const clientsInput = inputs.items.find(\n (item) => isMap(item) && item.get(\"name\") === WEBAPP_OIDC_CLIENTS_INPUT,\n );\n if (!isMap(clientsInput)) {\n throw new Error(\n `OS blueprint must include a ${WEBAPP_OIDC_CLIENTS_INPUT} input.`,\n );\n }\n\n const defaultValue = clientsInput.get(\"default\", true);\n if (!isSeq(defaultValue)) {\n throw new Error(\n `OS blueprint ${WEBAPP_OIDC_CLIENTS_INPUT} default must be a sequence.`,\n );\n }\n\n if (\n defaultValue.items.some(\n (item) => isMap(item) && item.get(\"slug\") === appName,\n )\n ) {\n return false;\n }\n\n defaultValue.add(\n document.createNode({ slug: appName, name: toTitleCase(appName) }),\n );\n return true;\n}\n\n// Ensures the customer OS blueprint installs the authentik-webapp-oidc service\n// (one per blueprint). It provisions the per-app OIDC clients against this\n// environment's Authentik and exposes their credentials as outputs the app\n// installations read.\nfunction ensureAuthentikWebappOidcInstallation(\n document: ReturnType<typeof parseDocument>,\n installations: { add(value: unknown): void; items: unknown[] },\n): boolean {\n if (\n installations.items.some(\n (item) =>\n isMap(item) && item.get(\"service\") === AUTHENTIK_WEBAPP_OIDC_SERVICE,\n )\n ) {\n return false;\n }\n\n installations.add(\n document.createNode({\n service: AUTHENTIK_WEBAPP_OIDC_SERVICE,\n config: renderAuthentikWebappOidcConfig(),\n }),\n );\n return true;\n}\n\n// Ensures the app's server installation carries the AUTHENTIK_* env. New\n// installs already get it via renderAppInstallationEnv; this backfills apps that\n// were registered before SSO wiring existed, so re-running register-app doesn't\n// leave them provisioned-but-unwired.\nfunction ensureAppInstallationAuthentikEnv(\n document: ReturnType<typeof parseDocument>,\n installations: { items: unknown[] },\n appName: string,\n): boolean {\n const installation = installations.items.find(\n (item) => isMap(item) && item.get(\"service\") === appName,\n );\n if (!isMap(installation)) return false;\n\n const env = installation.get(\"env\", true);\n if (!isSeq(env)) return false;\n\n const existingKeys = new Set<unknown>();\n for (const item of env.items) {\n if (isMap(item)) existingKeys.add(item.get(\"key\"));\n }\n\n let changed = false;\n for (const entry of renderAppAuthentikEnv(appName)) {\n if (existingKeys.has(entry.key)) continue;\n env.add(document.createNode(entry));\n changed = true;\n }\n return changed;\n}\n\nfunction renderAuthentikWebappOidcConfig(): string {\n const ingressDomain = ingressDomainInputName();\n return [\n `authentik_url: 'https://authentik.{{ input \"${ingressDomain}\" }}'`,\n `ingress_domain: '{{ input \"${ingressDomain}\" }}'`,\n \"namespace: '{{ .ryvn.env.name }}'\",\n \"env_secret_name: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.authentik_env_secret_name }}'\",\n `${WEBAPP_OIDC_CLIENTS_INPUT}:`,\n `{{ input \"${WEBAPP_OIDC_CLIENTS_INPUT}\" | toYaml | nindent 2 }}`,\n \"\",\n ].join(\"\\n\");\n}\n\nfunction renderWebappOidcClientsInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: WEBAPP_OIDC_CLIENTS_INPUT,\n type: \"array\",\n group: \"applications\",\n displayName: \"Webapp OIDC Clients\",\n description:\n \"OS webapps registered as Authentik OIDC clients (slug + display name). The authentik-webapp-oidc service provisions a provider/application per entry; the generated client_id/client_secret/issuer are wired into each app as AUTHENTIK_*.\",\n default: [],\n };\n}\n\nfunction renderIngressDomainInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: ingressDomainInputName(),\n type: \"string\",\n group: \"applications\",\n displayName: \"Ingress Domain\",\n description: \"Shared ingress domain for generated OS webapps.\",\n default: '{{ default \"example.local\" .ryvn.env.state.public_domain.name }}',\n };\n}\n\nfunction renderBetterAuthSecretInput(\n appName: string,\n): Record<string, unknown> & { name: string } {\n return {\n name: betterAuthSecretInputName(appName),\n type: \"string\",\n isSecret: true,\n group: \"applications\",\n displayName: `${toTitleCase(appName)} Better Auth Secret`,\n description: `Generated Better Auth signing secret for ${appName}.`,\n hidden: true,\n generated: {\n type: \"random-bytes\",\n length: 32,\n },\n };\n}\n\nfunction renderLangfusePublicKeyInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: langfusePublicKeyInputName(),\n type: \"string\",\n group: \"applications\",\n displayName: \"Langfuse Public Key\",\n description:\n \"Shared Langfuse public key for generated OS webapps. Leave empty to disable Langfuse export.\",\n default: \"\",\n };\n}\n\nfunction renderLangfuseSecretKeyInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: langfuseSecretKeyInputName(),\n type: \"string\",\n isSecret: true,\n group: \"applications\",\n displayName: \"Langfuse Secret Key\",\n description:\n \"Shared Langfuse secret key for generated OS webapps. Leave unset to disable Langfuse export.\",\n condition: `{{ ne (input \"${langfusePublicKeyInputName()}\") \"\" }}`,\n };\n}\n\nfunction renderInngestEventKeyInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: inngestEventKeyInputName(),\n type: \"string\",\n isSecret: true,\n group: \"applications\",\n displayName: \"Inngest Event Key\",\n description: \"Shared Inngest event key for generated OS webapps.\",\n };\n}\n\nfunction renderInngestSigningKeyInput(): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: inngestSigningKeyInputName(),\n type: \"string\",\n isSecret: true,\n group: \"applications\",\n displayName: \"Inngest Signing Key\",\n description: \"Shared Inngest signing key for generated OS webapps.\",\n };\n}\n\nfunction renderAppInstallationEnv(\n appName: string,\n postgresqlInstallationName: string,\n): Array<Record<string, unknown>> {\n const appInternalEndpoint = `http://${appName}-web-server.{{ .ryvn.env.name }}.svc.cluster.local:3000/api/inngest`;\n\n return [\n {\n key: \"DATABASE_URL\",\n isSecret: true,\n valueFromOutput: {\n serviceInstallation: postgresqlInstallationName,\n name: `app_database_urls.${appName}`,\n },\n },\n {\n key: \"AUTH_DATABASE_URL\",\n isSecret: true,\n valueFromOutput: {\n serviceInstallation: postgresqlInstallationName,\n name: \"auth_database_url\",\n },\n },\n {\n key: \"DATABASE_SCHEMA\",\n value: toSnakeCase(appName),\n },\n {\n key: \"INGRESS_DOMAIN\",\n valueFromInput: {\n name: ingressDomainInputName(),\n },\n },\n {\n key: \"APP_BASE_URL\",\n value: `https://${appName}.$(INGRESS_DOMAIN)`,\n },\n {\n key: \"BETTER_AUTH_URL\",\n value: `https://${appName}.$(INGRESS_DOMAIN)`,\n },\n {\n key: \"DEPLOYMENT_ENVIRONMENT\",\n value: \"{{ .ryvn.env.name }}\",\n },\n {\n key: \"BETTER_AUTH_SECRET\",\n isSecret: true,\n valueFromInput: {\n name: betterAuthSecretInputName(appName),\n },\n },\n {\n key: \"INNGEST_BASE_URL\",\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"inngest_base_url\",\n },\n },\n {\n key: \"INNGEST_EVENT_KEY\",\n isSecret: true,\n valueFromInput: {\n name: inngestEventKeyInputName(),\n },\n },\n {\n key: \"INNGEST_SIGNING_KEY\",\n isSecret: true,\n valueFromInput: {\n name: inngestSigningKeyInputName(),\n },\n },\n {\n key: \"INNGEST_APP_URL\",\n value: appInternalEndpoint,\n },\n {\n key: \"INNGEST_SERVE_HOST\",\n value: appInternalEndpoint,\n },\n {\n key: \"LANGFUSE_BASE_URL\",\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"langfuse_internal_url\",\n },\n },\n {\n key: \"LANGFUSE_PUBLIC_KEY\",\n valueFromInput: {\n name: langfusePublicKeyInputName(),\n },\n },\n {\n key: \"LANGFUSE_SECRET_KEY\",\n isSecret: true,\n valueFromInput: {\n name: langfuseSecretKeyInputName(),\n },\n },\n {\n key: \"OTEL_EXPORTER_OTLP_ENDPOINT\",\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"otel_exporter_otlp_endpoint\",\n },\n },\n {\n key: \"SPICEDB_ENDPOINT\",\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"spicedb_endpoint\",\n },\n },\n {\n key: \"SPICEDB_PRESHARED_KEY\",\n isSecret: true,\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"spicedb_preshared_key\",\n },\n },\n {\n key: \"SPICEDB_INSECURE\",\n valueFromOutput: {\n blueprintInstallation: \"mosaic\",\n name: \"spicedb_insecure\",\n },\n },\n ...renderAppAuthentikEnv(appName),\n ];\n}\n\n// AUTHENTIK_* env read from the authentik-webapp-oidc service's per-slug map\n// outputs. Shared by renderAppInstallationEnv (new installs) and\n// ensureAppInstallationAuthentikEnv (existing installs) so re-running\n// register-app wires already-registered apps too.\nfunction renderAppAuthentikEnv(\n appName: string,\n): Array<Record<string, unknown>> {\n return [\n {\n key: \"AUTHENTIK_ISSUER\",\n valueFromOutput: {\n serviceInstallation: AUTHENTIK_WEBAPP_OIDC_SERVICE,\n name: `issuers.${appName}`,\n },\n },\n {\n key: \"AUTHENTIK_CLIENT_ID\",\n valueFromOutput: {\n serviceInstallation: AUTHENTIK_WEBAPP_OIDC_SERVICE,\n name: `client_ids.${appName}`,\n },\n },\n {\n key: \"AUTHENTIK_CLIENT_SECRET\",\n isSecret: true,\n valueFromOutput: {\n serviceInstallation: AUTHENTIK_WEBAPP_OIDC_SERVICE,\n name: `client_secrets.${appName}`,\n },\n },\n ];\n}\n\nfunction renderAppInstallationConfig(appName: string): string {\n const appHost = `${appName}.{{ input \"${ingressDomainInputName()}\" }}`;\n\n return [\n \"replicaCount: 1\",\n \"\",\n \"service:\",\n \" port: 3000\",\n \"\",\n \"livenessEnabled: true\",\n \"readinessEnabled: true\",\n \"startupEnabled: true\",\n \"\",\n \"resources:\",\n \" requests:\",\n ' cpu: \"100m\"',\n \" memory: 256Mi\",\n \" limits:\",\n ' cpu: \"500m\"',\n \" memory: 512Mi\",\n \"\",\n \"ingress:\",\n \" enabled: true\",\n \" className: external-nginx\",\n \" annotations:\",\n \" cert-manager.io/cluster-issuer: external-issuer\",\n ' nginx.ingress.kubernetes.io/ssl-redirect: \"true\"',\n \" hosts:\",\n ` - host: '${appHost}'`,\n \" paths:\",\n \" - path: /\",\n \" pathType: Prefix\",\n \" tls:\",\n ` - secretName: ${appName}-tls`,\n \" hosts:\",\n ` - '${appHost}'`,\n \"\",\n ].join(\"\\n\");\n}\n\nasync function readLocalServiceDefinition(\n monorepoRoot: string,\n appName: string,\n): Promise<string> {\n const serviceDefinitionPath = path.join(\n monorepoRoot,\n \"packages\",\n appName,\n \"deploy\",\n \"ryvn\",\n `${appName}.service.yaml`,\n );\n if (!(await fs.pathExists(serviceDefinitionPath))) {\n throw new Error(\n `${serviceDefinitionPath} does not exist. Add the app's Ryvn service definition before registering it in infra.`,\n );\n }\n\n const content = await fs.readFile(serviceDefinitionPath, \"utf-8\");\n validateLocalServiceDefinition(content, appName, serviceDefinitionPath);\n return content.endsWith(\"\\n\") ? content : `${content}\\n`;\n}\n\nfunction validateLocalServiceDefinition(\n content: string,\n appName: string,\n serviceDefinitionPath: string,\n): void {\n const document = parseDocument(content);\n if (document.errors.length > 0) {\n throw new Error(\n `Invalid Ryvn service YAML at ${serviceDefinitionPath}: ${document.errors.map((error) => error.message).join(\"; \")}`,\n );\n }\n\n const service = document.toJS() as {\n kind?: unknown;\n metadata?: { name?: unknown };\n };\n if (service.kind !== \"Service\" || service.metadata?.name !== appName) {\n throw new Error(\n `${serviceDefinitionPath} must define kind: Service with metadata.name: ${appName}.`,\n );\n }\n}\n\nfunction ingressDomainInputName(): string {\n return \"ingress_domain\";\n}\n\nfunction betterAuthSecretInputName(appName: string): string {\n return `${toSnakeCase(appName)}_better_auth_secret`;\n}\n\nfunction langfusePublicKeyInputName(): string {\n return \"langfuse_public_key\";\n}\n\nfunction langfuseSecretKeyInputName(): string {\n return \"langfuse_secret_key\";\n}\n\nfunction inngestEventKeyInputName(): string {\n return \"inngest_event_key\";\n}\n\nfunction inngestSigningKeyInputName(): string {\n return \"inngest_signing_key\";\n}\n\nfunction normalizeAppName(appNameInput: string): string {\n const appName = toKebabCase(appNameInput);\n const validation = validateProjectName(appName);\n if (!validation.valid) {\n throw new Error(`Invalid app name: ${validation.error}`);\n }\n return appName;\n}\n"],"mappings":";;;;;;;AAsBA,MAAM,gCAAgC;AACtC,MAAM,4BAA4B;AAClC,MAAM,iCAAiC;AACvC,MAAM,uCAAuC;AAC7C,MAAM,mCAAmC,IAAI,IAAI,CAC/C,+BACA,+BACF,CAAC;AACD,MAAM,4BAA4B;CAChC;EACE,MAAM;EACN,aAAa;EACb,aAAa;CACf;CACA;EACE,MAAM;EACN,aAAa;EACb,aAAa;CACf;CACA;EACE,MAAM;EACN,aAAa;EACb,aAAa;EACb,WAAW;CACb;CACA;EACE,MAAM;EACN,aAAa;EACb,aAAa;EACb,WAAW;CACb;AACF;AAeA,eAAsB,YACpB,cACA,OAGI,CAAC,GACuB;CAC5B,MAAM,UAAU,iBAAiB,YAAY;CAE7C,MAAM,kBAAkB,MAAM,eADlB,KAAK,OAAO,QAAQ,IAAI,CACY;CAChD,IAAI,CAAC,gBAAgB,SAAS,CAAC,gBAAgB,SAC7C,MAAM,IAAI,MACR,sFACF;CAMF,MAAM,gBAAe,MAHW,sBAC9B,gBAAgB,OAClB,IACwC;CACxC,IAAI,CAAC,cACH,MAAM,IAAI,MACR,wGACF;CAGF,MAAM,SAAS,KAAK,UAAU,qBAAqB,mBAAmB,CAAC;CACvE,MAAM,gBAAgB,GAAG,aAAa;CACtC,MAAM,aAAa,sBAAsB,aAAa,GAAG;CACzD,MAAM,gBAAgB;EACpB;EACA;EACA;EACA;EACA,GAAG,cAAc;CACnB,EAAE,KAAK,GAAG;CACV,MAAM,cAAc;EAClB;EACA;EACA;EACA;EACA,GAAG,QAAQ;CACb,EAAE,KAAK,GAAG;CAEV,MAAM,oBAAoB,MAAM,OAAO,QACrC,eACA,iBACF;CACA,IAAI,CAAC,mBACH,MAAM,IAAI,MACR,GAAG,cAAc,qBAAqB,iBAAiB,iFACzD;CAIF,MAAM,iBACJ,MAF4B,OAAO,QAAQ,aAAA,MAA8B,KAEtD,OACf,MAAM,2BAA2B,gBAAgB,SAAS,OAAO,IACjE;CACN,MAAM,mBAAmB,uBACvB,kBAAkB,SAClB,OACF;CAEA,MAAM,QAAgC,CAAC;CACvC,IAAI,qBAAqB,kBAAkB,SACzC,MAAM,KAAK;EACT,aAAa,kBAAkB;EAC/B,SAAS;EACT,SAAS,YAAY,QAAQ,MAAM;EACnC,MAAM;CACR,CAAC;CAEH,IAAI,kBAAkB,MACpB,MAAM,KAAK;EACT,SAAS;EACT,SAAS,YAAY,QAAQ;EAC7B,MAAM;CACR,CAAC;CAGH,IAAI,MAAM,WAAW,GACnB,OAAO;EACL;EACA;EACA;EACA;EACA;EACA,gBAAgB;EAChB,YAAY;EACZ,QAAQ;EACR;EACA,YAAY;CACd;CAGF,MAAM,cAAc,MAAM,oCAAoC;EAC5D;EACA;EACA;EACA,OAAO,YAAY,QAAQ;EAC3B,MAAM;GACJ,iBAAiB,QAAQ,6BAA6B,cAAc;GACpE;GACA;EACF,EAAE,KAAK,IAAI;CACb,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA;EACA;EACA,gBAAgB,YAAY;EAC5B,YAAY;EACZ,QAAQ,YAAY;EACpB;EACA,YAAY;CACd;AACF;AAEA,eAAsB,mBAAmB,SAAgC;CACvE,IAAI;EACF,MAAM,SAAS,MAAM,YAAY,OAAO;EAExC,IAAI,OAAO,WAAW,sBAAsB;GAC1C,QAAQ,IACN,MAAM,MAAM,GAAG,GACf,GAAG,OAAO,QAAQ,4BAA4B,OAAO,WAAW,MAChE,MAAM,KAAK,OAAO,UAAU,CAC9B;GACA;EACF;EAEA,MAAM,OACJ,OAAO,WAAW,eAAe,YAAY;EAC/C,QAAQ,IACN,MAAM,MAAM,GAAG,GACf,GAAG,KAAK,gBAAgB,OAAO,QAAQ,IACvC,MAAM,KAAK,OAAO,cAAc,CAClC;CACF,SAAS,OAAO;EACd,QAAQ,MAAM,MAAM,IAAI,QAAQ,GAAI,MAAgB,OAAO;EAC3D,QAAQ,KAAK,CAAC;CAChB;AACF;AAaA,SAAgB,uBACd,kBACA,SACQ;CACR,OAAO,gBAAgB,kBAAkB,SAAS;EAChD,aAAa;EACb,iBAAiB;EACjB,WAAW;CACb,CAAC;AACH;AAEA,SAAS,gBACP,kBACA,SACA,SAKQ;CACR,MAAM,WAAW,cAAc,gBAAgB;CAC/C,IAAI,SAAS,OAAO,SAAS,GAC3B,MAAM,IAAI,MACR,8BAA8B,SAAS,OAAO,KAAK,UAAU,MAAM,OAAO,EAAE,KAAK,IAAI,GACvF;CAGF,MAAM,OAAO,SAAS,IAAI,QAAQ,IAAI;CACtC,IAAI,CAAC,MAAM,IAAI,GACb,MAAM,IAAI,MAAM,uCAAuC;CAGzD,IAAI,UAAU;CACd,MAAM,SAAS,KAAK,IAAI,UAAU,IAAI;CACtC,IAAI,CAAC,MAAM,MAAM,GACf,MAAM,IAAI,MAAM,8CAA8C;CAGhE,UAAU,kBAAkB,UAAU,IAAI,KAAK;CAE/C,IAAI,QAAQ,WAAW;EACrB,UACE,YAAY,UAAU,QAAQ,yBAAyB,CAAC,KAAK;EAC/D,UACE,YAAY,UAAU,QAAQ,4BAA4B,OAAO,CAAC,KAClE;EACF,UACE,YAAY,UAAU,QAAQ,6BAA6B,CAAC,KAAK;EACnE,UACE,YAAY,UAAU,QAAQ,6BAA6B,CAAC,KAAK;EACnE,UACE,YAAY,UAAU,QAAQ,2BAA2B,CAAC,KAAK;EACjE,UACE,YAAY,UAAU,QAAQ,6BAA6B,CAAC,KAAK;EACnE,UACE,YAAY,UAAU,QAAQ,6BAA6B,CAAC,KAAK;CACrE;CAEA,IAAI,QAAQ,aACV,UAAU,eAAe,UAAU,QAAQ,OAAO,KAAK;CAGzD,IAAI,QAAQ,iBAAiB;EAC3B,MAAM,gBAAgB,KAAK,IAAI,iBAAiB,IAAI;EACpD,IAAI,CAAC,MAAM,aAAa,GACtB,MAAM,IAAI,MAAM,qDAAqD;EAEvE,MAAM,6BAA6B,gCACjC,iBAAiB,QAAQ,CAC3B;EACA,UACE,oCACE,eACA,0BACF,KAAK;EACP,UACE,0CACE,eACA,0BACF,KAAK;EACP,UACE,mBACE,UACA,eACA,SACA,0BACF,KAAK;EACP,UACE,sCAAsC,UAAU,aAAa,KAAK;EACpE,UACE,kCAAkC,UAAU,eAAe,OAAO,KAClE;EACF,UAAU,oBAAoB,UAAU,QAAQ,OAAO,KAAK;CAC9D;CAEA,OAAO,UAAU,SAAS,SAAS,IAAI;AACzC;AAEA,SAAS,kBACP,UACA,MAIS;CACT,IAAI,UAAU;CACd,MAAM,cAAc,KAAK,IAAI,eAAe,IAAI;CAEhD,IAAI,eAAe,MAAM;EACvB,KAAK,IAAI,eAAe,SAAS,WAAW,yBAAyB,CAAC;EACtE,OAAO;CACT;CAEA,IAAI,CAAC,MAAM,WAAW,GACpB,MAAM,IAAI,MAAM,mDAAmD;CAGrE,KAAK,MAAM,SAAS,2BAA2B;EAI7C,IAHe,YAAY,MAAM,MAC9B,SAAS,MAAM,IAAI,KAAK,KAAK,IAAI,MAAM,MAAM,MAAM,IAE7C,GAAG;EAEZ,YAAY,IAAI,SAAS,WAAW,KAAK,CAAC;EAC1C,UAAU;CACZ;CAEA,OAAO;AACT;AAEA,SAAS,oCACP,eACA,4BACS;CACT,IAAI,UAAU;CAEd,KAAK,MAAM,gBAAgB,cAAc,OAAO;EAC9C,IAAI,CAAC,MAAM,YAAY,GAAG;EAE1B,MAAM,UAAU,aAAa,IAAI,SAAS;EAC1C,IACE,OAAO,YAAY,YACnB,CAAC,iCAAiC,IAAI,OAAO,GAE7C;EAGF,IAAI,aAAa,IAAI,MAAM,MAAM,4BAA4B;EAE7D,aAAa,IAAI,QAAQ,0BAA0B;EACnD,UAAU;CACZ;CAEA,OAAO;AACT;AAEA,SAAS,0CACP,eACA,4BACS;CACT,IAAI,UAAU;CAEd,KAAK,MAAM,gBAAgB,cAAc,OAAO;EAC9C,IAAI,CAAC,MAAM,YAAY,GAAG;EAC1B,IAAI,2BAA2B,YAAY,GAAG;EAE9C,MAAM,MAAM,aAAa,IAAI,OAAO,IAAI;EACxC,IAAI,CAAC,MAAM,GAAG,GAAG;EAEjB,KAAK,MAAM,UAAU,IAAI,OAAO;GAC9B,IAAI,CAAC,MAAM,MAAM,GAAG;GAEpB,MAAM,kBAAkB,OAAO,IAAI,mBAAmB,IAAI;GAC1D,IAAI,CAAC,MAAM,eAAe,GAAG;GAC7B,IACE,gBAAgB,IAAI,qBAAqB,MACzC,sCAEA;GAGF,MAAM,aAAa,gBAAgB,IAAI,MAAM;GAC7C,IACE,OAAO,eAAe,YACrB,eAAe,uBACd,CAAC,WAAW,WAAW,oBAAoB,GAE7C;GAGF,gBAAgB,IAAI,uBAAuB,0BAA0B;GACrE,UAAU;EACZ;CACF;CAEA,OAAO;AACT;AAEA,SAAS,2BAA2B,cAExB;CACV,MAAM,UAAU,aAAa,IAAI,SAAS;CAC1C,OACE,OAAO,YAAY,YAAY,iCAAiC,IAAI,OAAO;AAE/E;AAEA,SAAS,iBAAiB,UAAoD;CAC5E,MAAM,WAAW,SAAS,IAAI,YAAY,IAAI;CAC9C,IAAI,CAAC,MAAM,QAAQ,GACjB,MAAM,IAAI,MAAM,2CAA2C;CAG7D,MAAM,OAAO,SAAS,IAAI,MAAM;CAChC,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAC9C,MAAM,IAAI,MAAM,wDAAwD;CAG1E,OAAO;AACT;AAEA,SAAS,gCAAgC,eAA+B;CACtE,OAAO,GAAG,cAAc,GAAG;AAC7B;AAEA,SAAS,YACP,UACA,QACA,OACS;CACT,IACE,OAAO,MAAM,MAAM,SAAS,MAAM,IAAI,KAAK,KAAK,IAAI,MAAM,MAAM,MAAM,IAAI,GAE1E,OAAO;CAGT,OAAO,IAAI,SAAS,WAAW,KAAK,CAAC;CACrC,OAAO;AACT;AAEA,SAAS,eACP,UACA,QACA,SACS;CACT,MAAM,oBAAoB,OAAO,MAAM,MACpC,SAAS,MAAM,IAAI,KAAK,KAAK,IAAI,MAAM,MAAM,eAChD;CACA,IAAI,CAAC,MAAM,iBAAiB,GAC1B,MAAM,IAAI,MAAM,mDAAmD;CAGrE,MAAM,eAAe,kBAAkB,IAAI,WAAW,IAAI;CAC1D,IAAI,CAAC,MAAM,YAAY,GACrB,MAAM,IAAI,MAAM,mDAAmD;CAGrE,IAAI,aAAa,IAAI,OAAO,GAAG,OAAO;CAEtC,aAAa,OAAO;CACpB,MAAM,mBAAmB,SAAS,WAAW,EAC3C,aAAa,YAAY,OAAO,EAClC,CAAC;CACD,aAAa,IAAI,SAAS,gBAAgB;CAC1C,OAAO;AACT;AAEA,SAAS,mBACP,UACA,eACA,SACA,4BACS;CACT,IACE,cAAc,MAAM,MACjB,SAAS,MAAM,IAAI,KAAK,KAAK,IAAI,SAAS,MAAM,OACnD,GAEA,OAAO;CAGT,cAAc,IACZ,SAAS,WAAW;EAClB,SAAS;EACT,KAAK,yBAAyB,SAAS,0BAA0B;EACjE,QAAQ,4BAA4B,OAAO;CAC7C,CAAC,CACH;CACA,OAAO;AACT;AAKA,SAAS,oBACP,UACA,QACA,SACS;CACT,MAAM,eAAe,OAAO,MAAM,MAC/B,SAAS,MAAM,IAAI,KAAK,KAAK,IAAI,MAAM,MAAM,yBAChD;CACA,IAAI,CAAC,MAAM,YAAY,GACrB,MAAM,IAAI,MACR,+BAA+B,0BAA0B,QAC3D;CAGF,MAAM,eAAe,aAAa,IAAI,WAAW,IAAI;CACrD,IAAI,CAAC,MAAM,YAAY,GACrB,MAAM,IAAI,MACR,gBAAgB,0BAA0B,6BAC5C;CAGF,IACE,aAAa,MAAM,MAChB,SAAS,MAAM,IAAI,KAAK,KAAK,IAAI,MAAM,MAAM,OAChD,GAEA,OAAO;CAGT,aAAa,IACX,SAAS,WAAW;EAAE,MAAM;EAAS,MAAM,YAAY,OAAO;CAAE,CAAC,CACnE;CACA,OAAO;AACT;AAMA,SAAS,sCACP,UACA,eACS;CACT,IACE,cAAc,MAAM,MACjB,SACC,MAAM,IAAI,KAAK,KAAK,IAAI,SAAS,MAAM,6BAC3C,GAEA,OAAO;CAGT,cAAc,IACZ,SAAS,WAAW;EAClB,SAAS;EACT,QAAQ,gCAAgC;CAC1C,CAAC,CACH;CACA,OAAO;AACT;AAMA,SAAS,kCACP,UACA,eACA,SACS;CACT,MAAM,eAAe,cAAc,MAAM,MACtC,SAAS,MAAM,IAAI,KAAK,KAAK,IAAI,SAAS,MAAM,OACnD;CACA,IAAI,CAAC,MAAM,YAAY,GAAG,OAAO;CAEjC,MAAM,MAAM,aAAa,IAAI,OAAO,IAAI;CACxC,IAAI,CAAC,MAAM,GAAG,GAAG,OAAO;CAExB,MAAM,+BAAe,IAAI,IAAa;CACtC,KAAK,MAAM,QAAQ,IAAI,OACrB,IAAI,MAAM,IAAI,GAAG,aAAa,IAAI,KAAK,IAAI,KAAK,CAAC;CAGnD,IAAI,UAAU;CACd,KAAK,MAAM,SAAS,sBAAsB,OAAO,GAAG;EAClD,IAAI,aAAa,IAAI,MAAM,GAAG,GAAG;EACjC,IAAI,IAAI,SAAS,WAAW,KAAK,CAAC;EAClC,UAAU;CACZ;CACA,OAAO;AACT;AAEA,SAAS,kCAA0C;CACjD,MAAM,gBAAgB,uBAAuB;CAC7C,OAAO;EACL,+CAA+C,cAAc;EAC7D,8BAA8B,cAAc;EAC5C;EACA;EACA,GAAG,0BAA0B;EAC7B,aAAa,0BAA0B;EACvC;CACF,EAAE,KAAK,IAAI;AACb;AAEA,SAAS,+BAEP;CACA,OAAO;EACL,MAAM;EACN,MAAM;EACN,OAAO;EACP,aAAa;EACb,aACE;EACF,SAAS,CAAC;CACZ;AACF;AAEA,SAAS,2BAEP;CACA,OAAO;EACL,MAAM,uBAAuB;EAC7B,MAAM;EACN,OAAO;EACP,aAAa;EACb,aAAa;EACb,SAAS;CACX;AACF;AAEA,SAAS,4BACP,SAC4C;CAC5C,OAAO;EACL,MAAM,0BAA0B,OAAO;EACvC,MAAM;EACN,UAAU;EACV,OAAO;EACP,aAAa,GAAG,YAAY,OAAO,EAAE;EACrC,aAAa,4CAA4C,QAAQ;EACjE,QAAQ;EACR,WAAW;GACT,MAAM;GACN,QAAQ;EACV;CACF;AACF;AAEA,SAAS,+BAEP;CACA,OAAO;EACL,MAAM,2BAA2B;EACjC,MAAM;EACN,OAAO;EACP,aAAa;EACb,aACE;EACF,SAAS;CACX;AACF;AAEA,SAAS,+BAEP;CACA,OAAO;EACL,MAAM,2BAA2B;EACjC,MAAM;EACN,UAAU;EACV,OAAO;EACP,aAAa;EACb,aACE;EACF,WAAW,iBAAiB,2BAA2B,EAAE;CAC3D;AACF;AAEA,SAAS,6BAEP;CACA,OAAO;EACL,MAAM,yBAAyB;EAC/B,MAAM;EACN,UAAU;EACV,OAAO;EACP,aAAa;EACb,aAAa;CACf;AACF;AAEA,SAAS,+BAEP;CACA,OAAO;EACL,MAAM,2BAA2B;EACjC,MAAM;EACN,UAAU;EACV,OAAO;EACP,aAAa;EACb,aAAa;CACf;AACF;AAEA,SAAS,yBACP,SACA,4BACgC;CAChC,MAAM,sBAAsB,UAAU,QAAQ;CAE9C,OAAO;EACL;GACE,KAAK;GACL,UAAU;GACV,iBAAiB;IACf,qBAAqB;IACrB,MAAM,qBAAqB;GAC7B;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,iBAAiB;IACf,qBAAqB;IACrB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,OAAO,YAAY,OAAO;EAC5B;EACA;GACE,KAAK;GACL,gBAAgB,EACd,MAAM,uBAAuB,EAC/B;EACF;EACA;GACE,KAAK;GACL,OAAO,WAAW,QAAQ;EAC5B;EACA;GACE,KAAK;GACL,OAAO,WAAW,QAAQ;EAC5B;EACA;GACE,KAAK;GACL,OAAO;EACT;EACA;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,0BAA0B,OAAO,EACzC;EACF;EACA;GACE,KAAK;GACL,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,yBAAyB,EACjC;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,2BAA2B,EACnC;EACF;EACA;GACE,KAAK;GACL,OAAO;EACT;EACA;GACE,KAAK;GACL,OAAO;EACT;EACA;GACE,KAAK;GACL,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,gBAAgB,EACd,MAAM,2BAA2B,EACnC;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,gBAAgB,EACd,MAAM,2BAA2B,EACnC;EACF;EACA;GACE,KAAK;GACL,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA;GACE,KAAK;GACL,iBAAiB;IACf,uBAAuB;IACvB,MAAM;GACR;EACF;EACA,GAAG,sBAAsB,OAAO;CAClC;AACF;AAMA,SAAS,sBACP,SACgC;CAChC,OAAO;EACL;GACE,KAAK;GACL,iBAAiB;IACf,qBAAqB;IACrB,MAAM,WAAW;GACnB;EACF;EACA;GACE,KAAK;GACL,iBAAiB;IACf,qBAAqB;IACrB,MAAM,cAAc;GACtB;EACF;EACA;GACE,KAAK;GACL,UAAU;GACV,iBAAiB;IACf,qBAAqB;IACrB,MAAM,kBAAkB;GAC1B;EACF;CACF;AACF;AAEA,SAAS,4BAA4B,SAAyB;CAC5D,MAAM,UAAU,GAAG,QAAQ,aAAa,uBAAuB,EAAE;CAEjE,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,gBAAgB,QAAQ;EACxB;EACA;EACA;EACA;EACA,qBAAqB,QAAQ;EAC7B;EACA,cAAc,QAAQ;EACtB;CACF,EAAE,KAAK,IAAI;AACb;AAEA,eAAe,2BACb,cACA,SACiB;CACjB,MAAM,wBAAwB,KAAK,KACjC,cACA,YACA,SACA,UACA,QACA,GAAG,QAAQ,cACb;CACA,IAAI,CAAE,MAAM,GAAG,WAAW,qBAAqB,GAC7C,MAAM,IAAI,MACR,GAAG,sBAAsB,uFAC3B;CAGF,MAAM,UAAU,MAAM,GAAG,SAAS,uBAAuB,OAAO;CAChE,+BAA+B,SAAS,SAAS,qBAAqB;CACtE,OAAO,QAAQ,SAAS,IAAI,IAAI,UAAU,GAAG,QAAQ;AACvD;AAEA,SAAS,+BACP,SACA,SACA,uBACM;CACN,MAAM,WAAW,cAAc,OAAO;CACtC,IAAI,SAAS,OAAO,SAAS,GAC3B,MAAM,IAAI,MACR,gCAAgC,sBAAsB,IAAI,SAAS,OAAO,KAAK,UAAU,MAAM,OAAO,EAAE,KAAK,IAAI,GACnH;CAGF,MAAM,UAAU,SAAS,KAAK;CAI9B,IAAI,QAAQ,SAAS,aAAa,QAAQ,UAAU,SAAS,SAC3D,MAAM,IAAI,MACR,GAAG,sBAAsB,iDAAiD,QAAQ,EACpF;AAEJ;AAEA,SAAS,yBAAiC;CACxC,OAAO;AACT;AAEA,SAAS,0BAA0B,SAAyB;CAC1D,OAAO,GAAG,YAAY,OAAO,EAAE;AACjC;AAEA,SAAS,6BAAqC;CAC5C,OAAO;AACT;AAEA,SAAS,6BAAqC;CAC5C,OAAO;AACT;AAEA,SAAS,2BAAmC;CAC1C,OAAO;AACT;AAEA,SAAS,6BAAqC;CAC5C,OAAO;AACT;AAEA,SAAS,iBAAiB,cAA8B;CACtD,MAAM,UAAU,YAAY,YAAY;CACxC,MAAM,aAAa,oBAAoB,OAAO;CAC9C,IAAI,CAAC,WAAW,OACd,MAAM,IAAI,MAAM,qBAAqB,WAAW,OAAO;CAEzD,OAAO;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percepta/create",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "Scaffold a new Mosaic package",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -50,7 +50,10 @@
|
|
|
50
50
|
"typecheck": "tsc --noEmit",
|
|
51
51
|
"test": "vitest run",
|
|
52
52
|
"test:watch": "vitest",
|
|
53
|
-
"
|
|
53
|
+
"lint:templates:logging": "oxlint -c oxlint.template-logging.config.json templates --no-error-on-unmatched-pattern",
|
|
54
|
+
"test:template": "pnpm lint:templates:logging && pnpm test:template:contract && pnpm build && pnpm test:template:build",
|
|
55
|
+
"test:template:contract": "vitest run src/commands/create-output.test.ts",
|
|
56
|
+
"test:template:build": "bash scripts/test-template.sh",
|
|
54
57
|
"create:local": "pnpm build && node dist/index.js",
|
|
55
58
|
"sync-template": "tsx scripts/sync-template.ts",
|
|
56
59
|
"template:tag": "tsx scripts/template-tag.ts"
|
package/template-versions.json
CHANGED
|
@@ -35,7 +35,8 @@ pnpm lint
|
|
|
35
35
|
```
|
|
36
36
|
access/ # Customer-level SpiceDB fixtures and generated merge artifacts
|
|
37
37
|
auth/ # Shared Better Auth users/groups package for this customer
|
|
38
|
-
|
|
38
|
+
authentik/ # Local Authentik provisioning (initdb SQL + dev blueprints)
|
|
39
|
+
docker-compose.yml # Root-owned local PostgreSQL, SpiceDB, and Authentik services
|
|
39
40
|
scripts/ # Root-owned local infrastructure helpers
|
|
40
41
|
packages/
|
|
41
42
|
└── your-package/ # Application and library packages
|
|
@@ -52,6 +53,32 @@ When you add another webapp with `pnpm mosaic add webapp my-app`, the generated
|
|
|
52
53
|
app gets its own local `DATABASE_URL`. Re-running `pnpm run setup` creates that
|
|
53
54
|
database before running migrations.
|
|
54
55
|
|
|
56
|
+
## Authentication (local Authentik)
|
|
57
|
+
|
|
58
|
+
Apps authenticate via Authentik SSO in every environment — there is no
|
|
59
|
+
username/password fallback. `pnpm run setup` brings up a local **Authentik**
|
|
60
|
+
container (admin UI at <http://localhost:9000>, `akadmin` / `akadmin`) on the
|
|
61
|
+
shared local Postgres + a Redis cache. Its worker applies the blueprints in
|
|
62
|
+
`authentik/blueprints/` on startup, provisioning one shared dev OIDC client
|
|
63
|
+
(application slug `mosaic-local`) and the seed users — `app-admin@example.com`,
|
|
64
|
+
`app-user@example.com`, `customer-admin@example.com`, `non-user@example.com`,
|
|
65
|
+
all with password `password`.
|
|
66
|
+
|
|
67
|
+
Every webapp's generated `.env.local` already points at this client, so locally
|
|
68
|
+
you just visit the app, click **Continue with Authentik**, and sign in as one of
|
|
69
|
+
the seed users; Better Auth links the SSO identity to the seeded local principal
|
|
70
|
+
(and its SpiceDB grants) by email.
|
|
71
|
+
|
|
72
|
+
> Local SSO is wired to `http://localhost:3000` (each app's `APP_BASE_URL`). The
|
|
73
|
+
> OIDC client also allows callbacks on `3000-3003`, so if port 3000 is taken or
|
|
74
|
+
> you run several apps at once, set that app's `BETTER_AUTH_URL` to its actual
|
|
75
|
+
> port (e.g. `http://localhost:3001`) so the OAuth redirect matches.
|
|
76
|
+
|
|
77
|
+
> The `authentik` database is created by `authentik/initdb/` on a **fresh**
|
|
78
|
+
> Postgres volume. If you have a pre-existing volume from before Authentik was
|
|
79
|
+
> added, run `pnpm run docker:down` then `docker compose down -v` once to reset
|
|
80
|
+
> it before `pnpm run setup`.
|
|
81
|
+
|
|
55
82
|
## Access Control
|
|
56
83
|
|
|
57
84
|
Application builders define app-local Zed schemas in each package's
|
|
@@ -1,120 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
createLazyAuth,
|
|
3
|
+
createPerceptaAuthFromEnv,
|
|
4
|
+
} from "@percepta/auth/better-auth";
|
|
5
5
|
import { db } from "./drizzle/db";
|
|
6
6
|
import { accounts } from "./drizzle/schema/auth/accounts";
|
|
7
7
|
import { sessions } from "./drizzle/schema/auth/sessions";
|
|
8
8
|
import { verifications } from "./drizzle/schema/auth/verifications";
|
|
9
9
|
import { users } from "./drizzle/schema/users";
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const DEFAULT_AUTH_MODE: AuthMode = "__AUTH_MODE__" as AuthMode;
|
|
14
|
-
const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build";
|
|
15
|
-
|
|
16
|
-
function isAuthMode(value: string | undefined): value is AuthMode {
|
|
17
|
-
return (
|
|
18
|
-
value === "username-password" || value === "google" || value === "okta"
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function getAuthMode(): AuthMode {
|
|
23
|
-
return isAuthMode(process.env.AUTH_MODE)
|
|
24
|
-
? process.env.AUTH_MODE
|
|
25
|
-
: DEFAULT_AUTH_MODE;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function requiredEnv(name: string): string {
|
|
29
|
-
const value = process.env[name];
|
|
30
|
-
if (value == null || value.length === 0) {
|
|
31
|
-
throw new Error(`${name} is required.`);
|
|
32
|
-
}
|
|
33
|
-
return value;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function optionalEnv(name: string): string | undefined {
|
|
37
|
-
const value = process.env[name];
|
|
38
|
-
return value == null || value.length === 0 ? undefined : value;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function getSecret(): string {
|
|
42
|
-
if (isBuildPhase) {
|
|
43
|
-
return "build-placeholder-not-used-at-runtime";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return requiredEnv("BETTER_AUTH_SECRET");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function getBaseUrl(): string {
|
|
50
|
-
return (
|
|
51
|
-
process.env.BETTER_AUTH_URL ??
|
|
52
|
-
process.env.APP_BASE_URL ??
|
|
53
|
-
"http://localhost:3000"
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function getSocialProviders(
|
|
58
|
-
authMode: AuthMode,
|
|
59
|
-
): BetterAuthOptions["socialProviders"] {
|
|
60
|
-
if (authMode !== "google") return undefined;
|
|
61
|
-
|
|
62
|
-
const clientId = optionalEnv("GOOGLE_CLIENT_ID");
|
|
63
|
-
const clientSecret = optionalEnv("GOOGLE_CLIENT_SECRET");
|
|
64
|
-
if (clientId == null || clientSecret == null) return undefined;
|
|
65
|
-
|
|
66
|
-
const hostedDomain = optionalEnv("GOOGLE_HOSTED_DOMAIN");
|
|
67
|
-
return {
|
|
68
|
-
google: {
|
|
69
|
-
clientId,
|
|
70
|
-
clientSecret,
|
|
71
|
-
...(hostedDomain == null ? {} : { hd: hostedDomain }),
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function getPlugins(authMode: AuthMode): BetterAuthOptions["plugins"] {
|
|
77
|
-
if (authMode !== "okta") return undefined;
|
|
78
|
-
|
|
79
|
-
const clientId = optionalEnv("OKTA_CLIENT_ID");
|
|
80
|
-
const clientSecret = optionalEnv("OKTA_CLIENT_SECRET");
|
|
81
|
-
const issuer = optionalEnv("OKTA_ISSUER");
|
|
82
|
-
if (clientId == null || clientSecret == null || issuer == null) {
|
|
83
|
-
return undefined;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return [
|
|
87
|
-
admin(),
|
|
88
|
-
genericOAuth({
|
|
89
|
-
config: [
|
|
90
|
-
okta({
|
|
91
|
-
clientId,
|
|
92
|
-
clientSecret,
|
|
93
|
-
issuer,
|
|
94
|
-
}),
|
|
95
|
-
],
|
|
96
|
-
}),
|
|
97
|
-
];
|
|
98
|
-
}
|
|
99
|
-
|
|
11
|
+
// Authentik SSO is required in every environment (local + deployed). There is
|
|
12
|
+
// no auth mode to select and no username/password fallback — see @percepta/auth.
|
|
100
13
|
function createAuth() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return createPerceptaAuth({
|
|
104
|
-
baseURL: getBaseUrl(),
|
|
14
|
+
return createPerceptaAuthFromEnv({
|
|
105
15
|
database: db,
|
|
106
|
-
emailAndPassword: {
|
|
107
|
-
enabled: authMode === "username-password",
|
|
108
|
-
},
|
|
109
|
-
plugins: getPlugins(authMode),
|
|
110
16
|
schema: {
|
|
111
17
|
user: users,
|
|
112
18
|
session: sessions,
|
|
113
19
|
account: accounts,
|
|
114
20
|
verification: verifications,
|
|
115
21
|
},
|
|
116
|
-
secret: getSecret(),
|
|
117
|
-
socialProviders: getSocialProviders(authMode),
|
|
118
22
|
});
|
|
119
23
|
}
|
|
120
24
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Local-dev Authentik provisioning, applied automatically by the worker on
|
|
2
|
+
# startup. Provisions ONE shared OIDC client (application slug `mosaic-local`)
|
|
3
|
+
# that every app in this monorepo uses locally — apps run one-at-a-time (or on
|
|
4
|
+
# 3000-3003) against the same local Authentik. Deployments use per-app clients
|
|
5
|
+
# instead. All credentials here are throwaway local-dev values.
|
|
6
|
+
version: 1
|
|
7
|
+
metadata:
|
|
8
|
+
name: mosaic-local-dev
|
|
9
|
+
entries:
|
|
10
|
+
- model: authentik_providers_oauth2.oauth2provider
|
|
11
|
+
id: mosaic-local-provider
|
|
12
|
+
identifiers:
|
|
13
|
+
name: mosaic-local
|
|
14
|
+
state: present
|
|
15
|
+
attrs:
|
|
16
|
+
name: mosaic-local
|
|
17
|
+
client_type: confidential
|
|
18
|
+
client_id: mosaic-local-client
|
|
19
|
+
client_secret: mosaic-local-secret
|
|
20
|
+
issuer_mode: per_provider
|
|
21
|
+
sub_mode: hashed_user_id
|
|
22
|
+
include_claims_in_id_token: true
|
|
23
|
+
grant_types:
|
|
24
|
+
- authorization_code
|
|
25
|
+
- refresh_token
|
|
26
|
+
redirect_uris:
|
|
27
|
+
# Better Auth's generic-oauth callback. 3000-3003 cover `pnpm dev`
|
|
28
|
+
# running several apps in parallel (Next auto-increments the port).
|
|
29
|
+
- matching_mode: strict
|
|
30
|
+
url: http://localhost:3000/api/auth/oauth2/callback/authentik
|
|
31
|
+
redirect_uri_type: authorization
|
|
32
|
+
- matching_mode: strict
|
|
33
|
+
url: http://localhost:3001/api/auth/oauth2/callback/authentik
|
|
34
|
+
redirect_uri_type: authorization
|
|
35
|
+
- matching_mode: strict
|
|
36
|
+
url: http://localhost:3002/api/auth/oauth2/callback/authentik
|
|
37
|
+
redirect_uri_type: authorization
|
|
38
|
+
- matching_mode: strict
|
|
39
|
+
url: http://localhost:3003/api/auth/oauth2/callback/authentik
|
|
40
|
+
redirect_uri_type: authorization
|
|
41
|
+
authorization_flow:
|
|
42
|
+
!Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
|
|
43
|
+
invalidation_flow:
|
|
44
|
+
!Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
|
|
45
|
+
signing_key:
|
|
46
|
+
!Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
|
|
47
|
+
property_mappings:
|
|
48
|
+
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]]
|
|
49
|
+
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]]
|
|
50
|
+
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile]]
|
|
51
|
+
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-offline_access]]
|
|
52
|
+
|
|
53
|
+
- model: authentik_core.application
|
|
54
|
+
identifiers:
|
|
55
|
+
slug: mosaic-local
|
|
56
|
+
state: present
|
|
57
|
+
attrs:
|
|
58
|
+
name: Mosaic (local dev)
|
|
59
|
+
slug: mosaic-local
|
|
60
|
+
provider: !KeyOf mosaic-local-provider
|
|
61
|
+
|
|
62
|
+
# Seed users — mirror packages/*/scripts/seed.ts SEEDED_USERS (same emails).
|
|
63
|
+
# Local seeding creates the matching local user row + SpiceDB grants; the
|
|
64
|
+
# first Authentik sign-in links to it by email. Password for all: "password".
|
|
65
|
+
- model: authentik_core.user
|
|
66
|
+
id: seed-customer-admin
|
|
67
|
+
state: present
|
|
68
|
+
identifiers:
|
|
69
|
+
username: customer-admin@example.com
|
|
70
|
+
attrs:
|
|
71
|
+
name: Customer Admin
|
|
72
|
+
email: customer-admin@example.com
|
|
73
|
+
is_active: true
|
|
74
|
+
type: internal
|
|
75
|
+
password: password
|
|
76
|
+
|
|
77
|
+
- model: authentik_core.user
|
|
78
|
+
id: seed-app-admin
|
|
79
|
+
state: present
|
|
80
|
+
identifiers:
|
|
81
|
+
username: app-admin@example.com
|
|
82
|
+
attrs:
|
|
83
|
+
name: App Admin
|
|
84
|
+
email: app-admin@example.com
|
|
85
|
+
is_active: true
|
|
86
|
+
type: internal
|
|
87
|
+
password: password
|
|
88
|
+
|
|
89
|
+
- model: authentik_core.user
|
|
90
|
+
id: seed-app-user
|
|
91
|
+
state: present
|
|
92
|
+
identifiers:
|
|
93
|
+
username: app-user@example.com
|
|
94
|
+
attrs:
|
|
95
|
+
name: App User
|
|
96
|
+
email: app-user@example.com
|
|
97
|
+
is_active: true
|
|
98
|
+
type: internal
|
|
99
|
+
password: password
|
|
100
|
+
|
|
101
|
+
- model: authentik_core.user
|
|
102
|
+
id: seed-non-user
|
|
103
|
+
state: present
|
|
104
|
+
identifiers:
|
|
105
|
+
username: non-user@example.com
|
|
106
|
+
attrs:
|
|
107
|
+
name: App Non User
|
|
108
|
+
email: non-user@example.com
|
|
109
|
+
is_active: true
|
|
110
|
+
type: internal
|
|
111
|
+
password: password
|
|
112
|
+
|
|
113
|
+
- model: authentik_core.group
|
|
114
|
+
id: mosaic-local-group
|
|
115
|
+
state: present
|
|
116
|
+
identifiers:
|
|
117
|
+
name: mosaic-local-users
|
|
118
|
+
attrs:
|
|
119
|
+
users:
|
|
120
|
+
- !KeyOf seed-customer-admin
|
|
121
|
+
- !KeyOf seed-app-admin
|
|
122
|
+
- !KeyOf seed-app-user
|
|
123
|
+
- !KeyOf seed-non-user
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- Creates the dedicated database + role that the local Authentik container uses
|
|
2
|
+
-- on the shared local postgres instance. Runs once, only on a fresh data dir
|
|
3
|
+
-- (Postgres docker-entrypoint-initdb.d). Local-dev throwaway credentials.
|
|
4
|
+
CREATE USER authentik WITH PASSWORD 'authentik';
|
|
5
|
+
CREATE DATABASE authentik OWNER authentik;
|
|
@@ -25,11 +25,81 @@ services:
|
|
|
25
25
|
POSTGRES_DB: postgres
|
|
26
26
|
volumes:
|
|
27
27
|
- postgres_data:/var/lib/postgresql/data
|
|
28
|
+
# Creates the dedicated `authentik` database + role on a fresh volume.
|
|
29
|
+
# (Runs only when the data dir is empty — `docker compose down -v` to reset.)
|
|
30
|
+
- ./authentik/initdb:/docker-entrypoint-initdb.d:ro
|
|
28
31
|
healthcheck:
|
|
29
32
|
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
|
|
30
33
|
interval: 5s
|
|
31
34
|
timeout: 5s
|
|
32
35
|
retries: 5
|
|
33
36
|
|
|
37
|
+
# --- Local Authentik (SSO) -------------------------------------------------
|
|
38
|
+
# Apps authenticate via Authentik in every environment; locally this container
|
|
39
|
+
# plays the role of deployed Authentik. The worker applies the blueprints under
|
|
40
|
+
# ./authentik/blueprints on startup, provisioning a shared dev OIDC client
|
|
41
|
+
# (application slug `mosaic-local`) plus the seed users. Admin UI:
|
|
42
|
+
# http://localhost:9000 (akadmin / akadmin). All secrets here are throwaway
|
|
43
|
+
# local-dev values.
|
|
44
|
+
authentik-redis:
|
|
45
|
+
image: redis:alpine
|
|
46
|
+
command: --save 60 1 --loglevel warning
|
|
47
|
+
healthcheck:
|
|
48
|
+
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
|
49
|
+
interval: 5s
|
|
50
|
+
timeout: 3s
|
|
51
|
+
retries: 10
|
|
52
|
+
|
|
53
|
+
authentik-server:
|
|
54
|
+
image: ghcr.io/goauthentik/server:2026.5.3
|
|
55
|
+
command: server
|
|
56
|
+
environment: &authentik-env
|
|
57
|
+
AUTHENTIK_SECRET_KEY: mosaic-local-dev-secret-key
|
|
58
|
+
AUTHENTIK_POSTGRESQL__HOST: postgres
|
|
59
|
+
AUTHENTIK_POSTGRESQL__NAME: authentik
|
|
60
|
+
AUTHENTIK_POSTGRESQL__USER: authentik
|
|
61
|
+
AUTHENTIK_POSTGRESQL__PASSWORD: authentik
|
|
62
|
+
AUTHENTIK_REDIS__HOST: authentik-redis
|
|
63
|
+
AUTHENTIK_BOOTSTRAP_PASSWORD: akadmin
|
|
64
|
+
AUTHENTIK_BOOTSTRAP_EMAIL: akadmin@example.com
|
|
65
|
+
AUTHENTIK_DISABLE_UPDATE_CHECK: "true"
|
|
66
|
+
AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
|
|
67
|
+
ports:
|
|
68
|
+
- "9000:9000"
|
|
69
|
+
volumes:
|
|
70
|
+
- ./authentik/blueprints:/blueprints/custom:ro
|
|
71
|
+
healthcheck:
|
|
72
|
+
test: ["CMD", "ak", "healthcheck"]
|
|
73
|
+
start_period: 120s
|
|
74
|
+
interval: 10s
|
|
75
|
+
timeout: 30s
|
|
76
|
+
retries: 15
|
|
77
|
+
depends_on:
|
|
78
|
+
postgres:
|
|
79
|
+
condition: service_healthy
|
|
80
|
+
authentik-redis:
|
|
81
|
+
condition: service_healthy
|
|
82
|
+
|
|
83
|
+
authentik-worker:
|
|
84
|
+
image: ghcr.io/goauthentik/server:2026.5.3
|
|
85
|
+
command: worker
|
|
86
|
+
# The worker manages embedded outpost containers via the docker socket.
|
|
87
|
+
user: root
|
|
88
|
+
environment: *authentik-env
|
|
89
|
+
volumes:
|
|
90
|
+
- /var/run/docker.sock:/var/run/docker.sock
|
|
91
|
+
- ./authentik/blueprints:/blueprints/custom:ro
|
|
92
|
+
healthcheck:
|
|
93
|
+
test: ["CMD", "ak", "healthcheck"]
|
|
94
|
+
start_period: 120s
|
|
95
|
+
interval: 10s
|
|
96
|
+
timeout: 30s
|
|
97
|
+
retries: 15
|
|
98
|
+
depends_on:
|
|
99
|
+
postgres:
|
|
100
|
+
condition: service_healthy
|
|
101
|
+
authentik-redis:
|
|
102
|
+
condition: service_healthy
|
|
103
|
+
|
|
34
104
|
volumes:
|
|
35
105
|
postgres_data: {}
|