@percepta/create 4.1.1 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -109,8 +109,9 @@ This opens or updates an infra PR that registers the app service, adds the app
109
109
  to the customer OS blueprint's `app_databases` default, and adds the app's
110
110
  server installation to the customer OS blueprint. The generated installation
111
111
  wires database/auth secrets, consumes Mosaic runtime outputs for Inngest,
112
- Langfuse, OpenTelemetry, and SpiceDB, plus app-specific credential inputs for
113
- Inngest and Langfuse. Merge the OS blueprint PR before registering apps.
112
+ Langfuse, OpenTelemetry, and SpiceDB, and reuses shared customer OS inputs for
113
+ Inngest/Langfuse credentials. Merge the OS blueprint PR before registering
114
+ apps.
114
115
 
115
116
  ## Development
116
117
 
package/dist/index.js CHANGED
@@ -1208,7 +1208,7 @@ infra.command("register-os-blueprint").description("Register this customer monor
1208
1208
  await registerOsBlueprintCommand();
1209
1209
  });
1210
1210
  infra.command("register-app").description("Register a webapp database in this customer OS blueprint").argument("<app>", "Webapp package name").action(async (appName) => {
1211
- const { registerAppCommand } = await import("./register-app-CprpQn_h.js");
1211
+ const { registerAppCommand } = await import("./register-app-Dqm7Xgh_.js");
1212
1212
  await registerAppCommand(appName);
1213
1213
  });
1214
1214
  program.command("status").description("Show template sync status for current app").option("--mosaic-template-path <path>", "Path to local mosaic repo checkout").action(async (options) => {
@@ -112,12 +112,12 @@ function updateBlueprint(blueprintContent, appName, options) {
112
112
  const inputs = spec.get("inputs", true);
113
113
  if (!isSeq(inputs)) throw new Error("OS blueprint spec.inputs must be a sequence.");
114
114
  if (options.appInputs) {
115
- changed = addAppInput(document, inputs, renderHostInput(appName)) || changed;
115
+ changed = addAppInput(document, inputs, renderIngressDomainInput()) || changed;
116
116
  changed = addAppInput(document, inputs, renderBetterAuthSecretInput(appName)) || changed;
117
- changed = addAppInput(document, inputs, renderInngestEventKeyInput(appName)) || changed;
118
- changed = addAppInput(document, inputs, renderInngestSigningKeyInput(appName)) || changed;
119
- changed = addAppInput(document, inputs, renderLangfusePublicKeyInput(appName)) || changed;
120
- changed = addAppInput(document, inputs, renderLangfuseSecretKeyInput(appName)) || changed;
117
+ changed = addAppInput(document, inputs, renderInngestEventKeyInput()) || changed;
118
+ changed = addAppInput(document, inputs, renderInngestSigningKeyInput()) || changed;
119
+ changed = addAppInput(document, inputs, renderLangfusePublicKeyInput()) || changed;
120
+ changed = addAppInput(document, inputs, renderLangfuseSecretKeyInput()) || changed;
121
121
  }
122
122
  if (options.appDatabase) changed = addAppDatabase(document, inputs, appName) || changed;
123
123
  if (options.appInstallation) {
@@ -153,14 +153,14 @@ function addAppInstallation(document, installations, appName) {
153
153
  }));
154
154
  return true;
155
155
  }
156
- function renderHostInput(appName) {
156
+ function renderIngressDomainInput() {
157
157
  return {
158
- name: hostInputName(appName),
158
+ name: ingressDomainInputName(),
159
159
  type: "string",
160
160
  group: "Applications",
161
- displayName: `${toTitleCase(appName)} Host`,
162
- description: `Ingress host for ${appName}.`,
163
- default: `${appName}.{{ default "example.local" .ryvn.env.state.public_domain.name }}`
161
+ displayName: "Ingress Domain",
162
+ description: "Shared ingress domain for generated OS webapps.",
163
+ default: "{{ default \"example.local\" .ryvn.env.state.public_domain.name }}"
164
164
  };
165
165
  }
166
166
  function renderBetterAuthSecretInput(appName) {
@@ -178,47 +178,44 @@ function renderBetterAuthSecretInput(appName) {
178
178
  }
179
179
  };
180
180
  }
181
- function renderInngestEventKeyInput(appName) {
181
+ function renderInngestEventKeyInput() {
182
182
  return {
183
- name: inngestEventKeyInputName(appName),
183
+ name: inngestEventKeyInputName(),
184
184
  type: "string",
185
185
  isSecret: true,
186
186
  group: "Applications",
187
- displayName: `${toTitleCase(appName)} Inngest Event Key`,
188
- description: `Inngest event key for ${appName}. Leave empty when the target Inngest installation does not require one.`,
189
- default: ""
187
+ displayName: "Inngest Event Key",
188
+ description: "Shared Inngest event key for generated OS webapps. Leave unset when the target Inngest installation does not require one."
190
189
  };
191
190
  }
192
- function renderInngestSigningKeyInput(appName) {
191
+ function renderInngestSigningKeyInput() {
193
192
  return {
194
- name: inngestSigningKeyInputName(appName),
193
+ name: inngestSigningKeyInputName(),
195
194
  type: "string",
196
195
  isSecret: true,
197
196
  group: "Applications",
198
- displayName: `${toTitleCase(appName)} Inngest Signing Key`,
199
- description: `Inngest signing key for ${appName}. Leave empty when the target Inngest installation does not require one.`,
200
- default: ""
197
+ displayName: "Inngest Signing Key",
198
+ description: "Shared Inngest signing key for generated OS webapps. Leave unset when the target Inngest installation does not require one."
201
199
  };
202
200
  }
203
- function renderLangfusePublicKeyInput(appName) {
201
+ function renderLangfusePublicKeyInput() {
204
202
  return {
205
- name: langfusePublicKeyInputName(appName),
203
+ name: langfusePublicKeyInputName(),
206
204
  type: "string",
207
205
  group: "Applications",
208
- displayName: `${toTitleCase(appName)} Langfuse Public Key`,
209
- description: `Langfuse public key for ${appName}. Leave empty to disable Langfuse export.`,
206
+ displayName: "Langfuse Public Key",
207
+ description: "Shared Langfuse public key for generated OS webapps. Leave empty to disable Langfuse export.",
210
208
  default: ""
211
209
  };
212
210
  }
213
- function renderLangfuseSecretKeyInput(appName) {
211
+ function renderLangfuseSecretKeyInput() {
214
212
  return {
215
- name: langfuseSecretKeyInputName(appName),
213
+ name: langfuseSecretKeyInputName(),
216
214
  type: "string",
217
215
  isSecret: true,
218
216
  group: "Applications",
219
- displayName: `${toTitleCase(appName)} Langfuse Secret Key`,
220
- description: `Langfuse secret key for ${appName}. Leave empty to disable Langfuse export.`,
221
- default: ""
217
+ displayName: "Langfuse Secret Key",
218
+ description: "Shared Langfuse secret key for generated OS webapps. Leave unset to disable Langfuse export."
222
219
  };
223
220
  }
224
221
  function renderAppInstallationEnv(appName) {
@@ -231,23 +228,22 @@ function renderAppInstallationEnv(appName) {
231
228
  {
232
229
  key: "INNGEST_EVENT_KEY",
233
230
  isSecret: true,
234
- valueFromInput: { name: inngestEventKeyInputName(appName) }
231
+ valueFromInput: { name: inngestEventKeyInputName() }
235
232
  },
236
233
  {
237
234
  key: "INNGEST_SIGNING_KEY",
238
235
  isSecret: true,
239
- valueFromInput: { name: inngestSigningKeyInputName(appName) }
236
+ valueFromInput: { name: inngestSigningKeyInputName() }
240
237
  },
241
238
  {
242
239
  key: "LANGFUSE_SECRET_KEY",
243
240
  isSecret: true,
244
- valueFromInput: { name: langfuseSecretKeyInputName(appName) }
241
+ valueFromInput: { name: langfuseSecretKeyInputName() }
245
242
  }
246
243
  ];
247
244
  }
248
245
  function renderAppInstallationConfig(appName) {
249
- const hostInput = hostInputName(appName);
250
- const langfusePublicKeyInput = langfusePublicKeyInputName(appName);
246
+ const appHost = `${appName}.{{ input "${ingressDomainInputName()}" }}`;
251
247
  return [
252
248
  "replicaCount: 1",
253
249
  "",
@@ -273,14 +269,14 @@ function renderAppInstallationConfig(appName) {
273
269
  " cert-manager.io/cluster-issuer: external-issuer",
274
270
  " nginx.ingress.kubernetes.io/ssl-redirect: \"true\"",
275
271
  " hosts:",
276
- ` - host: '{{ input "${hostInput}" }}'`,
272
+ ` - host: '${appHost}'`,
277
273
  " paths:",
278
274
  " - path: /",
279
275
  " pathType: Prefix",
280
276
  " tls:",
281
277
  ` - secretName: ${appName}-tls`,
282
278
  " hosts:",
283
- ` - '{{ input "${hostInput}" }}'`,
279
+ ` - '${appHost}'`,
284
280
  "",
285
281
  "env:",
286
282
  " - name: DATABASE_URL",
@@ -302,9 +298,9 @@ function renderAppInstallationConfig(appName) {
302
298
  " name: '{{ (serviceInstallation \"os-postgresql-terraform-azure\").outputs.auth_secret_name }}'",
303
299
  " {{ end }}",
304
300
  " - name: APP_BASE_URL",
305
- ` value: 'https://{{ input "${hostInput}" }}'`,
301
+ ` value: 'https://${appHost}'`,
306
302
  " - name: BETTER_AUTH_URL",
307
- ` value: 'https://{{ input "${hostInput}" }}'`,
303
+ ` value: 'https://${appHost}'`,
308
304
  " - name: NODE_ENV",
309
305
  " value: production",
310
306
  " - name: PORT",
@@ -318,7 +314,7 @@ function renderAppInstallationConfig(appName) {
318
314
  " - name: LANGFUSE_BASE_URL",
319
315
  " value: '{{ (blueprintInstallation \"mosaic\").outputs.langfuse_base_url }}'",
320
316
  " - name: LANGFUSE_PUBLIC_KEY",
321
- ` value: '{{ input "${langfusePublicKeyInput}" }}'`,
317
+ ` value: '{{ input "${langfusePublicKeyInputName()}" }}'`,
322
318
  " - name: OTEL_SERVICE_NAME",
323
319
  ` value: ${appName}`,
324
320
  " - name: OTEL_RESOURCE_ATTRIBUTES",
@@ -356,23 +352,23 @@ function validateLocalServiceDefinition(content, appName, serviceDefinitionPath)
356
352
  const service = document.toJS();
357
353
  if (service.kind !== "Service" || service.metadata?.name !== appName) throw new Error(`${serviceDefinitionPath} must define kind: Service with metadata.name: ${appName}.`);
358
354
  }
359
- function hostInputName(appName) {
360
- return `${toSnakeCase(appName)}_host`;
355
+ function ingressDomainInputName() {
356
+ return "ingress_domain";
361
357
  }
362
358
  function betterAuthSecretInputName(appName) {
363
359
  return `${toSnakeCase(appName)}_better_auth_secret`;
364
360
  }
365
- function inngestEventKeyInputName(appName) {
366
- return `${toSnakeCase(appName)}_inngest_event_key`;
361
+ function inngestEventKeyInputName() {
362
+ return "inngest_event_key";
367
363
  }
368
- function inngestSigningKeyInputName(appName) {
369
- return `${toSnakeCase(appName)}_inngest_signing_key`;
364
+ function inngestSigningKeyInputName() {
365
+ return "inngest_signing_key";
370
366
  }
371
- function langfusePublicKeyInputName(appName) {
372
- return `${toSnakeCase(appName)}_langfuse_public_key`;
367
+ function langfusePublicKeyInputName() {
368
+ return "langfuse_public_key";
373
369
  }
374
- function langfuseSecretKeyInputName(appName) {
375
- return `${toSnakeCase(appName)}_langfuse_secret_key`;
370
+ function langfuseSecretKeyInputName() {
371
+ return "langfuse_secret_key";
376
372
  }
377
373
  function normalizeAppName(appNameInput) {
378
374
  const appName = toKebabCase(appNameInput);
@@ -383,4 +379,4 @@ function normalizeAppName(appNameInput) {
383
379
  //#endregion
384
380
  export { registerAppCommand };
385
381
 
386
- //# sourceMappingURL=register-app-CprpQn_h.js.map
382
+ //# sourceMappingURL=register-app-Dqm7Xgh_.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-app-Dqm7Xgh_.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\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 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, renderInngestEventKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderInngestSigningKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderLangfusePublicKeyInput()) || changed;\n changed =\n addAppInput(document, inputs, renderLangfuseSecretKeyInput()) || 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 changed = addAppInstallation(document, installations, appName) || changed;\n }\n\n return changed ? document.toString() : blueprintContent;\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 if (isMap(appDatabaseValue)) appDatabaseValue.flow = true;\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): 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),\n config: renderAppInstallationConfig(appName),\n }),\n );\n return true;\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 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:\n \"Shared Inngest event key for generated OS webapps. Leave unset when the target Inngest installation does not require one.\",\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:\n \"Shared Inngest signing key for generated OS webapps. Leave unset when the target Inngest installation does not require one.\",\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 };\n}\n\nfunction renderAppInstallationEnv(\n appName: string,\n): Array<Record<string, unknown>> {\n return [\n {\n key: \"BETTER_AUTH_SECRET\",\n isSecret: true,\n valueFromInput: {\n name: betterAuthSecretInputName(appName),\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: \"LANGFUSE_SECRET_KEY\",\n isSecret: true,\n valueFromInput: {\n name: langfuseSecretKeyInputName(),\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 \"env:\",\n \" - name: DATABASE_URL\",\n \" valueFrom:\",\n \" secretKeyRef:\",\n \" key: database_url\",\n ' {{ if eq EnvironmentProviderType \"aws\" }}',\n ` name: '{{ index (serviceInstallation \"os-postgresql-terraform-aws\").outputs.app_database_secret_names \"${appName}\" }}'`,\n ' {{ else if eq EnvironmentProviderType \"azure\" }}',\n ` name: '{{ index (serviceInstallation \"os-postgresql-terraform-azure\").outputs.app_database_secret_names \"${appName}\" }}'`,\n \" {{ end }}\",\n \" - name: AUTH_DATABASE_URL\",\n \" valueFrom:\",\n \" secretKeyRef:\",\n \" key: database_url\",\n ' {{ if eq EnvironmentProviderType \"aws\" }}',\n \" name: '{{ (serviceInstallation \\\"os-postgresql-terraform-aws\\\").outputs.auth_secret_name }}'\",\n ' {{ else if eq EnvironmentProviderType \"azure\" }}',\n \" name: '{{ (serviceInstallation \\\"os-postgresql-terraform-azure\\\").outputs.auth_secret_name }}'\",\n \" {{ end }}\",\n \" - name: APP_BASE_URL\",\n ` value: 'https://${appHost}'`,\n \" - name: BETTER_AUTH_URL\",\n ` value: 'https://${appHost}'`,\n \" - name: NODE_ENV\",\n \" value: production\",\n \" - name: PORT\",\n ' value: \"3000\"',\n \" - name: AUTH_TRUST_HOST\",\n ' value: \"true\"',\n \" - name: INNGEST_BASE_URL\",\n \" value: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.inngest_base_url }}'\",\n \" - name: INNGEST_APP_URL\",\n ` value: 'http://${appName}-web-server.{{ EnvironmentNamespace }}.svc.cluster.local:3000/api/inngest'`,\n \" - name: LANGFUSE_BASE_URL\",\n \" value: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.langfuse_base_url }}'\",\n \" - name: LANGFUSE_PUBLIC_KEY\",\n ` value: '{{ input \"${langfusePublicKeyInputName()}\" }}'`,\n \" - name: OTEL_SERVICE_NAME\",\n ` value: ${appName}`,\n \" - name: OTEL_RESOURCE_ATTRIBUTES\",\n ` value: 'deployment.environment={{ EnvironmentName }},service.name=${appName}'`,\n \" - name: OTEL_TRACES_EXPORTER\",\n \" value: otlp\",\n \" - name: OTEL_METRICS_EXPORTER\",\n \" value: otlp\",\n \" - name: OTEL_EXPORTER_OTLP_PROTOCOL\",\n \" value: http/protobuf\",\n \" - name: OTEL_EXPORTER_OTLP_ENDPOINT\",\n \" value: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.otel_exporter_otlp_endpoint }}'\",\n \" - name: SPICEDB_ENDPOINT\",\n \" value: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.spicedb_endpoint }}'\",\n \" - name: SPICEDB_PRESHARED_KEY\",\n \" valueFrom:\",\n \" secretKeyRef:\",\n \" key: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.spicedb_preshared_key_secret_key }}'\",\n \" name: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.spicedb_config_secret_name }}'\",\n \" - name: SPICEDB_INSECURE\",\n \" value: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.spicedb_insecure }}'\",\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 inngestEventKeyInputName(): string {\n return \"inngest_event_key\";\n}\n\nfunction inngestSigningKeyInputName(): string {\n return \"inngest_signing_key\";\n}\n\nfunction langfusePublicKeyInputName(): string {\n return \"langfuse_public_key\";\n}\n\nfunction langfuseSecretKeyInputName(): string {\n return \"langfuse_secret_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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percepta/create",
3
- "version": "4.1.1",
3
+ "version": "4.1.2",
4
4
  "description": "Scaffold a new Mosaic package",
5
5
  "keywords": [
6
6
  "cli",
@@ -13,6 +13,36 @@ spec:
13
13
  displayName: "App Databases"
14
14
  description: "Application database declarations keyed by app name. This default is the __CUSTOMER_TITLE__ OS app registry; BlueprintInstallation inputs should only override it for environment-specific drift. Each value may set database_name, schema_name, username, and secret_name."
15
15
  default: {}
16
+ - name: ingress_domain
17
+ type: string
18
+ group: Applications
19
+ displayName: "Ingress Domain"
20
+ description: "Shared ingress domain for generated OS webapps."
21
+ default: '{{ default "example.local" .ryvn.env.state.public_domain.name }}'
22
+ - name: inngest_event_key
23
+ type: string
24
+ isSecret: true
25
+ group: Applications
26
+ displayName: "Inngest Event Key"
27
+ description: "Shared Inngest event key for generated OS webapps. Leave unset when the target Inngest installation does not require one."
28
+ - name: inngest_signing_key
29
+ type: string
30
+ isSecret: true
31
+ group: Applications
32
+ displayName: "Inngest Signing Key"
33
+ description: "Shared Inngest signing key for generated OS webapps. Leave unset when the target Inngest installation does not require one."
34
+ - name: langfuse_public_key
35
+ type: string
36
+ group: Applications
37
+ displayName: "Langfuse Public Key"
38
+ description: "Shared Langfuse public key for generated OS webapps. Leave empty to disable Langfuse export."
39
+ default: ""
40
+ - name: langfuse_secret_key
41
+ type: string
42
+ isSecret: true
43
+ group: Applications
44
+ displayName: "Langfuse Secret Key"
45
+ description: "Shared Langfuse secret key for generated OS webapps. Leave unset to disable Langfuse export."
16
46
  - name: auth_secret_name
17
47
  type: string
18
48
  group: General
@@ -123,5 +123,6 @@ This opens or updates an infra PR that registers the app service, adds the app
123
123
  to the customer OS blueprint's `app_databases` default, and adds the app's
124
124
  server installation to the customer OS blueprint. The generated installation
125
125
  wires database/auth secrets, consumes Mosaic runtime outputs for Inngest,
126
- Langfuse, OpenTelemetry, and SpiceDB, plus app-specific credential inputs for
127
- Inngest and Langfuse. Merge the OS blueprint PR before registering apps.
126
+ Langfuse, OpenTelemetry, and SpiceDB, and reuses shared customer OS inputs for
127
+ Inngest/Langfuse credentials. Merge the OS blueprint PR before registering
128
+ apps.
@@ -1 +0,0 @@
1
- {"version":3,"file":"register-app-CprpQn_h.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\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 if (options.appInputs) {\n changed =\n addAppInput(document, inputs, renderHostInput(appName)) || changed;\n changed =\n addAppInput(document, inputs, renderBetterAuthSecretInput(appName)) ||\n changed;\n changed =\n addAppInput(document, inputs, renderInngestEventKeyInput(appName)) ||\n changed;\n changed =\n addAppInput(document, inputs, renderInngestSigningKeyInput(appName)) ||\n changed;\n changed =\n addAppInput(document, inputs, renderLangfusePublicKeyInput(appName)) ||\n changed;\n changed =\n addAppInput(document, inputs, renderLangfuseSecretKeyInput(appName)) ||\n 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 changed = addAppInstallation(document, installations, appName) || changed;\n }\n\n return changed ? document.toString() : blueprintContent;\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 if (isMap(appDatabaseValue)) appDatabaseValue.flow = true;\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): 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),\n config: renderAppInstallationConfig(appName),\n }),\n );\n return true;\n}\n\nfunction renderHostInput(appName: string): Record<string, unknown> & {\n name: string;\n} {\n return {\n name: hostInputName(appName),\n type: \"string\",\n group: \"Applications\",\n displayName: `${toTitleCase(appName)} Host`,\n description: `Ingress host for ${appName}.`,\n default: `${appName}.{{ 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 renderInngestEventKeyInput(\n appName: string,\n): Record<string, unknown> & { name: string } {\n return {\n name: inngestEventKeyInputName(appName),\n type: \"string\",\n isSecret: true,\n group: \"Applications\",\n displayName: `${toTitleCase(appName)} Inngest Event Key`,\n description: `Inngest event key for ${appName}. Leave empty when the target Inngest installation does not require one.`,\n default: \"\",\n };\n}\n\nfunction renderInngestSigningKeyInput(\n appName: string,\n): Record<string, unknown> & { name: string } {\n return {\n name: inngestSigningKeyInputName(appName),\n type: \"string\",\n isSecret: true,\n group: \"Applications\",\n displayName: `${toTitleCase(appName)} Inngest Signing Key`,\n description: `Inngest signing key for ${appName}. Leave empty when the target Inngest installation does not require one.`,\n default: \"\",\n };\n}\n\nfunction renderLangfusePublicKeyInput(\n appName: string,\n): Record<string, unknown> & { name: string } {\n return {\n name: langfusePublicKeyInputName(appName),\n type: \"string\",\n group: \"Applications\",\n displayName: `${toTitleCase(appName)} Langfuse Public Key`,\n description: `Langfuse public key for ${appName}. Leave empty to disable Langfuse export.`,\n default: \"\",\n };\n}\n\nfunction renderLangfuseSecretKeyInput(\n appName: string,\n): Record<string, unknown> & { name: string } {\n return {\n name: langfuseSecretKeyInputName(appName),\n type: \"string\",\n isSecret: true,\n group: \"Applications\",\n displayName: `${toTitleCase(appName)} Langfuse Secret Key`,\n description: `Langfuse secret key for ${appName}. Leave empty to disable Langfuse export.`,\n default: \"\",\n };\n}\n\nfunction renderAppInstallationEnv(\n appName: string,\n): Array<Record<string, unknown>> {\n return [\n {\n key: \"BETTER_AUTH_SECRET\",\n isSecret: true,\n valueFromInput: {\n name: betterAuthSecretInputName(appName),\n },\n },\n {\n key: \"INNGEST_EVENT_KEY\",\n isSecret: true,\n valueFromInput: {\n name: inngestEventKeyInputName(appName),\n },\n },\n {\n key: \"INNGEST_SIGNING_KEY\",\n isSecret: true,\n valueFromInput: {\n name: inngestSigningKeyInputName(appName),\n },\n },\n {\n key: \"LANGFUSE_SECRET_KEY\",\n isSecret: true,\n valueFromInput: {\n name: langfuseSecretKeyInputName(appName),\n },\n },\n ];\n}\n\nfunction renderAppInstallationConfig(appName: string): string {\n const hostInput = hostInputName(appName);\n const langfusePublicKeyInput = langfusePublicKeyInputName(appName);\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: '{{ input \"${hostInput}\" }}'`,\n \" paths:\",\n \" - path: /\",\n \" pathType: Prefix\",\n \" tls:\",\n ` - secretName: ${appName}-tls`,\n \" hosts:\",\n ` - '{{ input \"${hostInput}\" }}'`,\n \"\",\n \"env:\",\n \" - name: DATABASE_URL\",\n \" valueFrom:\",\n \" secretKeyRef:\",\n \" key: database_url\",\n ' {{ if eq EnvironmentProviderType \"aws\" }}',\n ` name: '{{ index (serviceInstallation \"os-postgresql-terraform-aws\").outputs.app_database_secret_names \"${appName}\" }}'`,\n ' {{ else if eq EnvironmentProviderType \"azure\" }}',\n ` name: '{{ index (serviceInstallation \"os-postgresql-terraform-azure\").outputs.app_database_secret_names \"${appName}\" }}'`,\n \" {{ end }}\",\n \" - name: AUTH_DATABASE_URL\",\n \" valueFrom:\",\n \" secretKeyRef:\",\n \" key: database_url\",\n ' {{ if eq EnvironmentProviderType \"aws\" }}',\n \" name: '{{ (serviceInstallation \\\"os-postgresql-terraform-aws\\\").outputs.auth_secret_name }}'\",\n ' {{ else if eq EnvironmentProviderType \"azure\" }}',\n \" name: '{{ (serviceInstallation \\\"os-postgresql-terraform-azure\\\").outputs.auth_secret_name }}'\",\n \" {{ end }}\",\n \" - name: APP_BASE_URL\",\n ` value: 'https://{{ input \"${hostInput}\" }}'`,\n \" - name: BETTER_AUTH_URL\",\n ` value: 'https://{{ input \"${hostInput}\" }}'`,\n \" - name: NODE_ENV\",\n \" value: production\",\n \" - name: PORT\",\n ' value: \"3000\"',\n \" - name: AUTH_TRUST_HOST\",\n ' value: \"true\"',\n \" - name: INNGEST_BASE_URL\",\n \" value: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.inngest_base_url }}'\",\n \" - name: INNGEST_APP_URL\",\n ` value: 'http://${appName}-web-server.{{ EnvironmentNamespace }}.svc.cluster.local:3000/api/inngest'`,\n \" - name: LANGFUSE_BASE_URL\",\n \" value: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.langfuse_base_url }}'\",\n \" - name: LANGFUSE_PUBLIC_KEY\",\n ` value: '{{ input \"${langfusePublicKeyInput}\" }}'`,\n \" - name: OTEL_SERVICE_NAME\",\n ` value: ${appName}`,\n \" - name: OTEL_RESOURCE_ATTRIBUTES\",\n ` value: 'deployment.environment={{ EnvironmentName }},service.name=${appName}'`,\n \" - name: OTEL_TRACES_EXPORTER\",\n \" value: otlp\",\n \" - name: OTEL_METRICS_EXPORTER\",\n \" value: otlp\",\n \" - name: OTEL_EXPORTER_OTLP_PROTOCOL\",\n \" value: http/protobuf\",\n \" - name: OTEL_EXPORTER_OTLP_ENDPOINT\",\n \" value: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.otel_exporter_otlp_endpoint }}'\",\n \" - name: SPICEDB_ENDPOINT\",\n \" value: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.spicedb_endpoint }}'\",\n \" - name: SPICEDB_PRESHARED_KEY\",\n \" valueFrom:\",\n \" secretKeyRef:\",\n \" key: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.spicedb_preshared_key_secret_key }}'\",\n \" name: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.spicedb_config_secret_name }}'\",\n \" - name: SPICEDB_INSECURE\",\n \" value: '{{ (blueprintInstallation \\\"mosaic\\\").outputs.spicedb_insecure }}'\",\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 hostInputName(appName: string): string {\n return `${toSnakeCase(appName)}_host`;\n}\n\nfunction betterAuthSecretInputName(appName: string): string {\n return `${toSnakeCase(appName)}_better_auth_secret`;\n}\n\nfunction inngestEventKeyInputName(appName: string): string {\n return `${toSnakeCase(appName)}_inngest_event_key`;\n}\n\nfunction inngestSigningKeyInputName(appName: string): string {\n return `${toSnakeCase(appName)}_inngest_signing_key`;\n}\n\nfunction langfusePublicKeyInputName(appName: string): string {\n return `${toSnakeCase(appName)}_langfuse_public_key`;\n}\n\nfunction langfuseSecretKeyInputName(appName: string): string {\n return `${toSnakeCase(appName)}_langfuse_secret_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":"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"}