From 24092ed2b4d789535476e361b4530a2431c8b752 Mon Sep 17 00:00:00 2001 From: Sean Matthews Date: Fri, 23 Oct 2026 11:36:05 -0400 Subject: [PATCH 1/2] feat(schemas): versioned schema ids, hosted on friggframework.org Every schema in @friggframework/schemas now has the $id https://friggframework.org/schemas//v1.0.json (ADR-051, "Hosted schemas" amendment), replacing the two unversioned styles (friggframework.org/schemas/.json and schemas.friggframework.org/.schema.json), neither of which resolved. - The old ids stay registered as Ajv aliases for one release, in the package and in the middleware; getSchema() accepts a name, an $id or a legacy id. - scripts/sync-hosted-schemas.js copies each schema to website/schemas//v.json, the path its $id names; the copies are committed so old versions keep resolving and a schema change is a website change. website/_headers serves them as application/schema+json with open CORS. - tests/hosted-schemas.test.js checks the id pattern, that every website copy matches its source and sits at its $id's path, and that every id and alias resolves from the package with http, https and fetch stubbed to throw. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7 --- packages/schemas/README.md | 32 +- packages/schemas/index.js | 26 +- .../schemas/middleware/schema-validation.js | 3 +- packages/schemas/package.json | 5 +- packages/schemas/schema-ids.js | 69 ++ .../schemas/api-authorization.schema.json | 2 +- .../schemas/api-credentials.schema.json | 2 +- .../schemas/schemas/api-entities.schema.json | 2 +- .../schemas/api-module-definition.schema.json | 2 +- .../schemas/schemas/api-proxy.schema.json | 2 +- .../schemas/app-definition.schema.json | 2 +- .../schemas/schemas/core-models.schema.json | 2 +- .../schemas/environment-config.schema.json | 2 +- .../integration-definition.schema.json | 2 +- .../schemas/serverless-config.schema.json | 2 +- .../schemas/scripts/sync-hosted-schemas.js | 102 +++ packages/schemas/tests/hosted-schemas.test.js | 177 ++++ website/_headers | 12 + website/schemas/api-authorization/v1.0.json | 302 +++++++ website/schemas/api-credentials/v1.0.json | 176 ++++ website/schemas/api-entities/v1.0.json | 292 +++++++ .../schemas/api-module-definition/v1.0.json | 379 +++++++++ website/schemas/api-proxy/v1.0.json | 251 ++++++ website/schemas/app-definition/v1.0.json | 761 ++++++++++++++++++ website/schemas/core-models/v1.0.json | 492 +++++++++++ website/schemas/environment-config/v1.0.json | 387 +++++++++ .../schemas/integration-definition/v1.0.json | 389 +++++++++ website/schemas/serverless-config/v1.0.json | 398 +++++++++ 28 files changed, 4255 insertions(+), 18 deletions(-) create mode 100644 packages/schemas/schema-ids.js create mode 100644 packages/schemas/scripts/sync-hosted-schemas.js create mode 100644 packages/schemas/tests/hosted-schemas.test.js create mode 100644 website/_headers create mode 100644 website/schemas/api-authorization/v1.0.json create mode 100644 website/schemas/api-credentials/v1.0.json create mode 100644 website/schemas/api-entities/v1.0.json create mode 100644 website/schemas/api-module-definition/v1.0.json create mode 100644 website/schemas/api-proxy/v1.0.json create mode 100644 website/schemas/app-definition/v1.0.json create mode 100644 website/schemas/core-models/v1.0.json create mode 100644 website/schemas/environment-config/v1.0.json create mode 100644 website/schemas/integration-definition/v1.0.json create mode 100644 website/schemas/serverless-config/v1.0.json diff --git a/packages/schemas/README.md b/packages/schemas/README.md index 24f30ba63..8fce3a201 100644 --- a/packages/schemas/README.md +++ b/packages/schemas/README.md @@ -95,6 +95,31 @@ const appSchema = getSchema('app-definition'); console.log('App definition schema:', appSchema); ``` +## Schema Identifiers and Hosting + +Every schema's `$id` is its public URL: + +``` +https://friggframework.org/schemas//v..json +``` + +for example `https://friggframework.org/schemas/app-definition/v1.0.json`. +friggframework.org serves each file at that URL (with +`Content-Type: application/schema+json` and open CORS), so editors can fetch +it from a `$schema` reference. The URL is a name, not a dependency: this +package, `frigg validate` and the runtime resolve every `$id` from the +installed files and never fetch it. + +- A version is immutable once a stable release ships it. A backward-compatible + change gets a new minor version, a breaking change a new major version. +- `validate()` and `getSchema()` accept a schema name, its `$id`, or the + unversioned id it had before 2.0.1 (kept as an alias for one release). +- The website copies live in `website/schemas/` and are committed. After you + change a schema, run `npm run sync:hosted` here and commit the result; + `tests/hosted-schemas.test.js` fails while a copy is missing or stale. + +The rules are in ADR-051, "Hosted schemas" amendment. + ## Available Schemas ### App Definition Schema @@ -353,7 +378,12 @@ Validates core data models (User, Entity, Credential). Returns all available schemas as an object. #### `getSchema(schemaName)` -Returns a specific schema by name. +Returns a specific schema by name (`app-definition`), by `$id`, or by its legacy unversioned id. + +#### `parseSchemaId(id)` +Returns `{ name, version, hostedPath }` for an `$id` on the +`https://friggframework.org/schemas//v.json` pattern, or `null`. +`SCHEMA_ID_PATTERN` and `LEGACY_SCHEMA_IDS` are exported too. ### Utility Functions diff --git a/packages/schemas/index.js b/packages/schemas/index.js index e8d97652e..d6dc36f35 100644 --- a/packages/schemas/index.js +++ b/packages/schemas/index.js @@ -16,6 +16,7 @@ const { applyAppDefinitionDefaults, toAppDefinitionSchemaView, } = require('./app-definition'); +const schemaIds = require('./schema-ids'); // Initialize AJV with formats const ajv = new Ajv({ @@ -49,7 +50,16 @@ schemaFiles.forEach(file => { const schemaName = file.replace('.schema.json', ''); schemas[schemaName] = schemaContent; - ajv.addSchema(schemaContent, schemaName); + schemaIds.addSchemaWithAliases(ajv, schemaContent, schemaName); +}); + +// Schema name for each `$id` and legacy id, so getSchema() also accepts a URL. +const schemaNamesById = {}; +Object.entries(schemas).forEach(([schemaName, schema]) => { + schemaNamesById[schema.$id] = schemaName; + (schemaIds.LEGACY_SCHEMA_IDS[schemaName] || []).forEach(legacyId => { + schemaNamesById[legacyId] = schemaName; + }); }); /** @@ -332,15 +342,16 @@ function getSchemas() { } /** - * Get a specific schema by name - * @param {string} schemaName - Name of the schema to retrieve + * Get a specific schema by name or `$id` + * @param {string} schemaName - Schema name (`app-definition`), its `$id`, or a legacy unversioned id * @returns {object} - Schema object */ function getSchema(schemaName) { - if (!schemas[schemaName]) { + const name = schemas[schemaName] ? schemaName : schemaNamesById[schemaName]; + if (!name) { throw new Error(`Schema '${schemaName}' not found. Available schemas: ${Object.keys(schemas).join(', ')}`); } - return schemas[schemaName]; + return schemas[name]; } /** @@ -401,6 +412,11 @@ module.exports = { schemas, ajv, + // Schema identifiers (https://friggframework.org/schemas//v.json) + SCHEMA_ID_PATTERN: schemaIds.SCHEMA_ID_PATTERN, + LEGACY_SCHEMA_IDS: schemaIds.LEGACY_SCHEMA_IDS, + parseSchemaId: schemaIds.parseSchemaId, + // Middleware exports middleware: schemaValidationMiddleware, validateBody: schemaValidationMiddleware.validateBody, diff --git a/packages/schemas/middleware/schema-validation.js b/packages/schemas/middleware/schema-validation.js index 84e7cb92b..fbeb048ea 100644 --- a/packages/schemas/middleware/schema-validation.js +++ b/packages/schemas/middleware/schema-validation.js @@ -9,6 +9,7 @@ const Ajv = require('ajv'); const addFormats = require('ajv-formats'); const fs = require('fs'); const path = require('path'); +const { addSchemaWithAliases } = require('../schema-ids'); // Initialize AJV with formats const ajv = new Ajv({ @@ -33,7 +34,7 @@ schemaFiles.forEach(file => { if (fs.existsSync(schemaPath)) { const schemaContent = JSON.parse(fs.readFileSync(schemaPath, 'utf8')); const schemaName = file.replace('.schema.json', ''); - ajv.addSchema(schemaContent, schemaName); + addSchemaWithAliases(ajv, schemaContent, schemaName); } }); diff --git a/packages/schemas/package.json b/packages/schemas/package.json index fb7d3ac27..413c275ec 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -19,7 +19,9 @@ "scripts": { "test": "jest", "validate": "node scripts/validate-schemas.js", - "build": "node scripts/build-schemas.js" + "build": "node scripts/build-schemas.js", + "sync:hosted": "node scripts/sync-hosted-schemas.js", + "check:hosted": "node scripts/sync-hosted-schemas.js --check" }, "keywords": [ "frigg", @@ -40,6 +42,7 @@ "mocks/", "middleware/", "app-definition.js", + "schema-ids.js", "index.js" ] } \ No newline at end of file diff --git a/packages/schemas/schema-ids.js b/packages/schemas/schema-ids.js new file mode 100644 index 000000000..9e9af4095 --- /dev/null +++ b/packages/schemas/schema-ids.js @@ -0,0 +1,69 @@ +/** + * Schema identifiers (ADR-051, "Hosted schemas" amendment). + * + * Every Frigg JSON Schema has the `$id` + * https://friggframework.org/schemas//v..json + * and friggframework.org serves the file at exactly that URL. The `$id` is a + * name, not a location the runtime reads: validation always resolves it from + * this package and never fetches it. + */ + +const SCHEMA_BASE_URL = 'https://friggframework.org/'; + +const SCHEMA_ID_PATTERN = + /^https:\/\/friggframework\.org\/schemas\/([a-z0-9]+(?:-[a-z0-9]+)*)\/v(0|[1-9]\d*)\.(0|[1-9]\d*)\.json$/; + +/** + * Unversioned ids the schemas carried before 2.0.1. They never resolved over + * HTTP, but code may pass them to `validate()`/`getSchema()` or Ajv, so the + * package still registers them as aliases of the current schema. + * + * @deprecated Remove in the release after 2.0.1. + */ +const LEGACY_SCHEMA_IDS = Object.freeze({ + 'api-authorization': ['https://friggframework.org/schemas/api-authorization.json'], + 'api-credentials': ['https://friggframework.org/schemas/api-credentials.json'], + 'api-entities': ['https://friggframework.org/schemas/api-entities.json'], + 'api-proxy': ['https://friggframework.org/schemas/api-proxy.json'], + 'api-module-definition': ['https://schemas.friggframework.org/api-module-definition.schema.json'], + 'app-definition': ['https://schemas.friggframework.org/app-definition.schema.json'], + 'core-models': ['https://schemas.friggframework.org/core-models.schema.json'], + 'environment-config': ['https://schemas.friggframework.org/environment-config.schema.json'], + 'integration-definition': ['https://schemas.friggframework.org/integration-definition.schema.json'], + 'serverless-config': ['https://schemas.friggframework.org/serverless-config.schema.json'], +}); + +/** + * Parse a schema `$id`. + * @param {string} id + * @returns {{ name: string, version: string, hostedPath: string } | null} + * `hostedPath` is the path under the website root (`schemas//v1.0.json`). + */ +function parseSchemaId(id) { + const match = typeof id === 'string' ? SCHEMA_ID_PATTERN.exec(id) : null; + if (!match) return null; + const [, name, major, minor] = match; + const version = `${major}.${minor}`; + return { name, version, hostedPath: `schemas/${name}/v${version}.json` }; +} + +/** + * Register a schema with Ajv under its file name, its `$id`, and its legacy ids. + * @param {import('ajv').default} ajv + * @param {object} schema - parsed schema (its `$id` registers automatically) + * @param {string} name - file name without `.schema.json` + */ +function addSchemaWithAliases(ajv, schema, name) { + ajv.addSchema(schema, name); + for (const legacyId of LEGACY_SCHEMA_IDS[name] || []) { + ajv.addSchema({ ...schema, $id: legacyId }); + } +} + +module.exports = { + SCHEMA_BASE_URL, + SCHEMA_ID_PATTERN, + LEGACY_SCHEMA_IDS, + parseSchemaId, + addSchemaWithAliases, +}; diff --git a/packages/schemas/schemas/api-authorization.schema.json b/packages/schemas/schemas/api-authorization.schema.json index 5552e9e46..97304c392 100644 --- a/packages/schemas/schemas/api-authorization.schema.json +++ b/packages/schemas/schemas/api-authorization.schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://friggframework.org/schemas/api-authorization.json", + "$id": "https://friggframework.org/schemas/api-authorization/v1.0.json", "title": "Frigg Authorization API Schemas", "description": "JSON schemas for authorization and authentication API requests and responses", "definitions": { diff --git a/packages/schemas/schemas/api-credentials.schema.json b/packages/schemas/schemas/api-credentials.schema.json index fda153faf..111edd72b 100644 --- a/packages/schemas/schemas/api-credentials.schema.json +++ b/packages/schemas/schemas/api-credentials.schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://friggframework.org/schemas/api-credentials.json", + "$id": "https://friggframework.org/schemas/api-credentials/v1.0.json", "title": "Frigg Credentials API Schemas", "description": "JSON schemas for credentials API requests and responses", "definitions": { diff --git a/packages/schemas/schemas/api-entities.schema.json b/packages/schemas/schemas/api-entities.schema.json index 429086393..7bad04a6d 100644 --- a/packages/schemas/schemas/api-entities.schema.json +++ b/packages/schemas/schemas/api-entities.schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://friggframework.org/schemas/api-entities.json", + "$id": "https://friggframework.org/schemas/api-entities/v1.0.json", "title": "Frigg Entities API Schemas", "description": "JSON schemas for entities API requests and responses", "definitions": { diff --git a/packages/schemas/schemas/api-module-definition.schema.json b/packages/schemas/schemas/api-module-definition.schema.json index 0a3d71a57..0db52f71f 100644 --- a/packages/schemas/schemas/api-module-definition.schema.json +++ b/packages/schemas/schemas/api-module-definition.schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://schemas.friggframework.org/api-module-definition.schema.json", + "$id": "https://friggframework.org/schemas/api-module-definition/v1.0.json", "title": "Frigg API Module Definition", "description": "Schema for defining a Frigg API module including authentication, API configuration, and required methods.", "type": "object", diff --git a/packages/schemas/schemas/api-proxy.schema.json b/packages/schemas/schemas/api-proxy.schema.json index 6541ab636..e0692b71c 100644 --- a/packages/schemas/schemas/api-proxy.schema.json +++ b/packages/schemas/schemas/api-proxy.schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://friggframework.org/schemas/api-proxy.json", + "$id": "https://friggframework.org/schemas/api-proxy/v1.0.json", "title": "Frigg Proxy API Schemas", "description": "JSON schemas for proxy API requests and responses that forward calls to external APIs", "definitions": { diff --git a/packages/schemas/schemas/app-definition.schema.json b/packages/schemas/schemas/app-definition.schema.json index dc4f170bc..8276a78bb 100644 --- a/packages/schemas/schemas/app-definition.schema.json +++ b/packages/schemas/schemas/app-definition.schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://schemas.friggframework.org/app-definition.schema.json", + "$id": "https://friggframework.org/schemas/app-definition/v1.0.json", "title": "Frigg Application Definition", "description": "Schema for defining a Frigg application configuration including integrations, infrastructure, security settings, and deployment options.", "type": "object", diff --git a/packages/schemas/schemas/core-models.schema.json b/packages/schemas/schemas/core-models.schema.json index 757d2f1d4..f287c0bc4 100644 --- a/packages/schemas/schemas/core-models.schema.json +++ b/packages/schemas/schemas/core-models.schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://schemas.friggframework.org/core-models.schema.json", + "$id": "https://friggframework.org/schemas/core-models/v1.0.json", "title": "Frigg Core Data Models", "description": "Schema definitions for core Frigg data models including User, Entity, and Credential.", "definitions": { diff --git a/packages/schemas/schemas/environment-config.schema.json b/packages/schemas/schemas/environment-config.schema.json index e62703495..b22371cbb 100644 --- a/packages/schemas/schemas/environment-config.schema.json +++ b/packages/schemas/schemas/environment-config.schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://schemas.friggframework.org/environment-config.schema.json", + "$id": "https://friggframework.org/schemas/environment-config/v1.0.json", "title": "Frigg Environment Configuration", "description": "Schema for environment variable configuration and validation in Frigg applications.", "type": "object", diff --git a/packages/schemas/schemas/integration-definition.schema.json b/packages/schemas/schemas/integration-definition.schema.json index b34baa597..dae3c3b4c 100644 --- a/packages/schemas/schemas/integration-definition.schema.json +++ b/packages/schemas/schemas/integration-definition.schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://schemas.friggframework.org/integration-definition.schema.json", + "$id": "https://friggframework.org/schemas/integration-definition/v1.0.json", "title": "Frigg Integration Definition", "description": "Schema for defining a Frigg integration including configuration, options, and display properties.", "type": "object", diff --git a/packages/schemas/schemas/serverless-config.schema.json b/packages/schemas/schemas/serverless-config.schema.json index ace675792..730c6b920 100644 --- a/packages/schemas/schemas/serverless-config.schema.json +++ b/packages/schemas/schemas/serverless-config.schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://schemas.friggframework.org/serverless-config.schema.json", + "$id": "https://friggframework.org/schemas/serverless-config/v1.0.json", "title": "Frigg Serverless Configuration", "description": "Schema for Serverless Framework configuration used in Frigg applications for AWS deployment.", "type": "object", diff --git a/packages/schemas/scripts/sync-hosted-schemas.js b/packages/schemas/scripts/sync-hosted-schemas.js new file mode 100644 index 000000000..09fd064b9 --- /dev/null +++ b/packages/schemas/scripts/sync-hosted-schemas.js @@ -0,0 +1,102 @@ +#!/usr/bin/env node + +/** + * Copy the Frigg JSON Schemas into the website, at the path their `$id` names. + * + * https://friggframework.org/schemas//v.json + * -> website/schemas//v.json + * + * The copies are committed: friggframework.org is published from website/ as + * static files, and a file stays in website/schemas after its schema moves to + * a new version, so every published version keeps resolving. + * + * node scripts/sync-hosted-schemas.js write the copies + * node scripts/sync-hosted-schemas.js --check exit 1 when a copy is missing or stale + * + * See ADR-051, "Hosted schemas" amendment. + */ + +const fs = require('fs'); +const path = require('path'); +const { parseSchemaId } = require('../schema-ids'); + +const REPO_ROOT = path.resolve(__dirname, '..', '..', '..'); +const WEBSITE_ROOT = path.join(REPO_ROOT, 'website'); + +// Directories whose *.schema.json files are published. A directory that does +// not exist on this branch is skipped. +const SOURCE_DIRS = [ + 'packages/schemas/schemas', + 'packages/core/integrations/definitions/schema', +]; + +/** + * @returns {Array<{ source: string, id: string, hostedPath: string, target: string, content: string }>} + * @throws when a schema has no `$id` on the pattern, or two schemas claim one URL + */ +function planHostedSchemas() { + const plan = []; + const byHostedPath = new Map(); + + for (const dir of SOURCE_DIRS) { + const absDir = path.join(REPO_ROOT, dir); + if (!fs.existsSync(absDir)) continue; + + for (const file of fs.readdirSync(absDir).filter(f => f.endsWith('.schema.json')).sort()) { + const source = path.posix.join(dir, file); + const content = fs.readFileSync(path.join(absDir, file), 'utf8'); + const id = JSON.parse(content).$id; + const parsed = parseSchemaId(id); + if (!parsed) { + throw new Error( + `${source}: $id ${JSON.stringify(id)} is not https://friggframework.org/schemas//v..json` + ); + } + if (byHostedPath.has(parsed.hostedPath)) { + throw new Error( + `${source} and ${byHostedPath.get(parsed.hostedPath)} both have the $id ${id}` + ); + } + byHostedPath.set(parsed.hostedPath, source); + plan.push({ + source, + id, + hostedPath: parsed.hostedPath, + target: path.join(WEBSITE_ROOT, parsed.hostedPath), + content, + }); + } + } + return plan; +} + +function main() { + const check = process.argv.includes('--check'); + const plan = planHostedSchemas(); + const stale = []; + + for (const entry of plan) { + const current = fs.existsSync(entry.target) ? fs.readFileSync(entry.target, 'utf8') : null; + if (current === entry.content) continue; + if (check) { + stale.push(entry); + continue; + } + fs.mkdirSync(path.dirname(entry.target), { recursive: true }); + fs.writeFileSync(entry.target, entry.content); + console.log(`wrote website/${entry.hostedPath} (from ${entry.source})`); + } + + if (check && stale.length > 0) { + for (const entry of stale) { + console.error(`website/${entry.hostedPath} is missing or differs from ${entry.source}`); + } + console.error('Run `npm run sync:hosted --workspace=packages/schemas` and commit the result.'); + process.exit(1); + } + if (check) console.log(`${plan.length} hosted schemas in sync.`); +} + +if (require.main === module) main(); + +module.exports = { planHostedSchemas, REPO_ROOT, WEBSITE_ROOT, SOURCE_DIRS }; diff --git a/packages/schemas/tests/hosted-schemas.test.js b/packages/schemas/tests/hosted-schemas.test.js new file mode 100644 index 000000000..122f1e526 --- /dev/null +++ b/packages/schemas/tests/hosted-schemas.test.js @@ -0,0 +1,177 @@ +/** + * Hosted schema identifiers (ADR-051, "Hosted schemas" amendment). + * + * - Every schema's `$id` is https://friggframework.org/schemas//v.json. + * - website/ serves each schema at exactly that URL, byte for byte. + * - Validation resolves every `$id` (and every legacy id) from this package, + * without network access. + */ + +const fs = require('fs'); +const path = require('path'); +const http = require('http'); +const https = require('https'); +const { SCHEMA_ID_PATTERN, parseSchemaId, LEGACY_SCHEMA_IDS } = require('../schema-ids'); +const { + planHostedSchemas, + WEBSITE_ROOT, +} = require('../scripts/sync-hosted-schemas'); + +const SCHEMA_DIR = path.join(__dirname, '..', 'schemas'); +const packageSchemaFiles = fs.readdirSync(SCHEMA_DIR).filter(f => f.endsWith('.schema.json')); +const readSchema = file => JSON.parse(fs.readFileSync(path.join(SCHEMA_DIR, file), 'utf8')); + +function listFiles(dir) { + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => { + const full = path.join(dir, entry.name); + return entry.isDirectory() ? listFiles(full) : [full]; + }); +} + +function collectRefs(node, refs = []) { + if (Array.isArray(node)) node.forEach(item => collectRefs(item, refs)); + else if (node && typeof node === 'object') { + for (const [key, value] of Object.entries(node)) { + if (key === '$ref' && typeof value === 'string') refs.push(value); + else collectRefs(value, refs); + } + } + return refs; +} + +describe('schema ids', () => { + test.each(packageSchemaFiles)('%s has a versioned friggframework.org $id named after the file', file => { + const schema = readSchema(file); + expect(schema.$id).toMatch(SCHEMA_ID_PATTERN); + expect(parseSchemaId(schema.$id).name).toBe(file.replace('.schema.json', '')); + }); + + test('parseSchemaId maps an $id to its hosted path', () => { + expect(parseSchemaId('https://friggframework.org/schemas/app-definition/v1.0.json')).toEqual({ + name: 'app-definition', + version: '1.0', + hostedPath: 'schemas/app-definition/v1.0.json', + }); + expect(parseSchemaId('https://friggframework.org/schemas/app-definition.json')).toBeNull(); + expect(parseSchemaId('https://schemas.friggframework.org/app-definition/v1.0.json')).toBeNull(); + expect(parseSchemaId('https://friggframework.org/schemas/app-definition/v1.json')).toBeNull(); + expect(parseSchemaId('https://friggframework.org/schemas/App/v1.0.json')).toBeNull(); + }); + + test('every legacy id belongs to a schema in the package', () => { + const names = packageSchemaFiles.map(f => f.replace('.schema.json', '')); + for (const name of Object.keys(LEGACY_SCHEMA_IDS)) expect(names).toContain(name); + }); +}); + +describe('hosted copies in website/schemas', () => { + const plan = planHostedSchemas(); + + test('the plan covers every package schema', () => { + const sources = plan.map(entry => path.basename(entry.source)); + for (const file of packageSchemaFiles) expect(sources).toContain(file); + }); + + test.each(plan.map(entry => [entry.hostedPath, entry]))( + 'website/%s is an exact copy of its source (run `npm run sync:hosted` if not)', + (hostedPath, entry) => { + expect(fs.existsSync(entry.target)).toBe(true); + expect(fs.readFileSync(entry.target, 'utf8')).toBe(entry.content); + expect(`https://friggframework.org/${hostedPath}`).toBe(entry.id); + } + ); + + test('every file in website/schemas is served at the URL its $id names', () => { + const files = listFiles(path.join(WEBSITE_ROOT, 'schemas')); + expect(files.length).toBeGreaterThan(0); + for (const file of files) { + const relative = path.relative(WEBSITE_ROOT, file).split(path.sep).join('/'); + const { $id } = JSON.parse(fs.readFileSync(file, 'utf8')); + expect(parseSchemaId($id)).not.toBeNull(); + expect($id).toBe(`https://friggframework.org/${relative}`); + } + }); + + test('website/_headers serves /schemas/* as JSON Schema with open CORS', () => { + const headers = fs.readFileSync(path.join(WEBSITE_ROOT, '_headers'), 'utf8'); + const block = headers.split(/^\/schemas\/\*\s*$/m)[1]; + expect(block).toBeDefined(); + expect(block).toMatch(/^\s+Content-Type: application\/schema\+json/m); + expect(block).toMatch(/^\s+Access-Control-Allow-Origin: \*$/m); + }); +}); + +describe('offline resolution', () => { + let networkCalls; + let restore; + + beforeEach(() => { + networkCalls = []; + const spies = [ + jest.spyOn(http, 'request'), + jest.spyOn(http, 'get'), + jest.spyOn(https, 'request'), + jest.spyOn(https, 'get'), + ]; + spies.forEach(spy => + spy.mockImplementation((...args) => { + networkCalls.push(args[0]); + throw new Error('network access during schema validation'); + }) + ); + const originalFetch = global.fetch; + global.fetch = jest.fn(url => { + networkCalls.push(url); + throw new Error('network access during schema validation'); + }); + restore = () => { + spies.forEach(spy => spy.mockRestore()); + global.fetch = originalFetch; + }; + }); + + afterEach(() => restore()); + + test('every $id and legacy id resolves from the package', () => { + jest.isolateModules(() => { + const { ajv, getSchema, validate } = require('../index'); + for (const file of packageSchemaFiles) { + const name = file.replace('.schema.json', ''); + const schema = readSchema(file); + const ids = [schema.$id, ...(LEGACY_SCHEMA_IDS[name] || [])]; + for (const id of ids) { + expect(typeof ajv.getSchema(id)).toBe('function'); + expect(getSchema(id)).toBe(getSchema(name)); + expect(() => validate(id, {})).not.toThrow(); + } + } + }); + expect(networkCalls).toEqual([]); + }); + + test('the middleware registers the same ids', () => { + jest.isolateModules(() => { + const Ajv = require('ajv'); + const addSpy = jest.spyOn(Ajv.prototype, 'addSchema'); + require('../middleware/schema-validation'); + const registered = addSpy.mock.calls.map(([schema, key]) => key || schema.$id); + addSpy.mockRestore(); + for (const name of ['api-entities', 'api-credentials', 'api-proxy', 'api-authorization']) { + expect(registered).toContain(name); + for (const legacyId of LEGACY_SCHEMA_IDS[name]) expect(registered).toContain(legacyId); + } + }); + expect(networkCalls).toEqual([]); + }); + + test('every non-local $ref points at a schema in the package', () => { + const ids = new Set(packageSchemaFiles.map(file => readSchema(file).$id)); + for (const file of packageSchemaFiles) { + for (const ref of collectRefs(readSchema(file))) { + if (ref.startsWith('#')) continue; + expect(ids).toContain(ref.split('#')[0]); + } + } + }); +}); diff --git a/website/_headers b/website/_headers new file mode 100644 index 000000000..afdc97ef1 --- /dev/null +++ b/website/_headers @@ -0,0 +1,12 @@ +# Netlify response headers for friggframework.org (publish directory: website/). + +# JSON Schemas: https://friggframework.org/schemas//v..json +# Copied from the packages by packages/schemas/scripts/sync-hosted-schemas.js. +# CORS is open so editors and browser tools can fetch them; the Frigg runtime +# never does (ADR-051, "Hosted schemas"). +/schemas/* + Content-Type: application/schema+json; charset=utf-8 + Access-Control-Allow-Origin: * + Access-Control-Allow-Methods: GET, HEAD, OPTIONS + X-Content-Type-Options: nosniff + Cache-Control: public, max-age=3600 diff --git a/website/schemas/api-authorization/v1.0.json b/website/schemas/api-authorization/v1.0.json new file mode 100644 index 000000000..97304c392 --- /dev/null +++ b/website/schemas/api-authorization/v1.0.json @@ -0,0 +1,302 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://friggframework.org/schemas/api-authorization/v1.0.json", + "title": "Frigg Authorization API Schemas", + "description": "JSON schemas for authorization and authentication API requests and responses", + "definitions": { + "authorizationRequirements": { + "type": "object", + "description": "Authorization requirements response from GET /api/authorize", + "required": ["type", "step", "totalSteps", "isMultiStep"], + "properties": { + "type": { + "type": "string", + "enum": ["oauth2", "form", "api-key", "basic"], + "description": "Type of authentication required" + }, + "step": { + "type": "integer", + "minimum": 1, + "description": "Current step number (1-indexed)" + }, + "totalSteps": { + "type": "integer", + "minimum": 1, + "description": "Total number of steps in the auth flow" + }, + "isMultiStep": { + "type": "boolean", + "description": "Whether this is a multi-step authentication flow" + }, + "sessionId": { + "type": "string", + "description": "Session ID for multi-step flows (required after step 1)" + }, + "data": { + "type": "object", + "description": "Step-specific data (schema varies by type)", + "oneOf": [ + { "$ref": "#/definitions/oauth2Requirements" }, + { "$ref": "#/definitions/formRequirements" }, + { "$ref": "#/definitions/apiKeyRequirements" } + ] + } + } + }, + "oauth2Requirements": { + "type": "object", + "description": "OAuth2 authentication requirements", + "required": ["url"], + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "OAuth2 authorization URL to redirect user to" + }, + "scopes": { + "type": "array", + "items": { "type": "string" }, + "description": "Requested OAuth scopes" + } + } + }, + "formRequirements": { + "type": "object", + "description": "Form-based authentication requirements", + "required": ["jsonSchema"], + "properties": { + "jsonSchema": { + "type": "object", + "description": "JSON Schema for the form fields", + "required": ["type", "properties"], + "properties": { + "title": { "type": "string" }, + "description": { "type": "string" }, + "type": { "const": "object" }, + "required": { + "type": "array", + "items": { "type": "string" } + }, + "properties": { + "type": "object", + "additionalProperties": true + } + } + }, + "uiSchema": { + "type": "object", + "description": "UI schema for form rendering (react-jsonschema-form format)", + "additionalProperties": true + } + } + }, + "apiKeyRequirements": { + "type": "object", + "description": "API key authentication requirements", + "required": ["fields"], + "properties": { + "fields": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "type"], + "properties": { + "name": { "type": "string" }, + "type": { "type": "string", "enum": ["api_key", "secret", "token"] }, + "label": { "type": "string" }, + "required": { "type": "boolean" } + } + } + } + } + }, + "getEntityTypeRequirementsResponse": { + "type": "object", + "description": "Authorization requirements response from GET /api/entities/types/:typeName/requirements", + "required": ["type", "step", "totalSteps", "isMultiStep"], + "properties": { + "type": { + "type": "string", + "enum": ["oauth2", "form", "api-key", "basic"], + "description": "Type of authentication required" + }, + "step": { + "type": "integer", + "minimum": 1, + "description": "Current step number (1-indexed)" + }, + "totalSteps": { + "type": "integer", + "minimum": 1, + "description": "Total number of steps in the auth flow" + }, + "isMultiStep": { + "type": "boolean", + "description": "Whether this is a multi-step authentication flow" + }, + "sessionId": { + "type": "string", + "description": "Session ID for multi-step flows (required after step 1)" + }, + "data": { + "type": "object", + "description": "Step-specific data (schema varies by type)", + "oneOf": [ + { "$ref": "#/definitions/oauth2Requirements" }, + { "$ref": "#/definitions/formRequirements" }, + { "$ref": "#/definitions/apiKeyRequirements" } + ] + } + } + }, + "authorizationRequest": { + "type": "object", + "description": "Authorization request to POST /api/authorize", + "required": ["entityType"], + "properties": { + "entityType": { + "type": "string", + "description": "Type of entity being authorized (module name)" + }, + "moduleType": { + "type": "string", + "description": "Module type (v2 API) - same as entityType" + }, + "data": { + "type": "object", + "description": "Authentication credentials or OAuth callback data", + "additionalProperties": true + }, + "step": { + "type": "integer", + "minimum": 1, + "default": 1, + "description": "Current step number for multi-step flows" + }, + "sessionId": { + "type": "string", + "description": "Session ID from previous step (required for step > 1)" + }, + "connectingEntityType": { + "type": "string", + "description": "Type of entity being connected to (for paired integrations)" + } + } + }, + "authorizationResponse": { + "type": "object", + "description": "Authorization response (success or next step)", + "oneOf": [ + { "$ref": "#/definitions/authorizationSuccess" }, + { "$ref": "#/definitions/authorizationNextStep" } + ] + }, + "authorizationSuccess": { + "type": "object", + "description": "Successful authorization completion", + "required": ["entity_id", "credential_id", "type"], + "properties": { + "entity_id": { + "type": "string", + "description": "ID of created/updated entity" + }, + "credential_id": { + "type": "string", + "description": "ID of created/updated credential" + }, + "type": { + "type": "string", + "description": "Entity type (module name)" + }, + "display": { + "type": "string", + "description": "Display name for the entity" + } + } + }, + "authorizationNextStep": { + "type": "object", + "description": "Next step in multi-step authorization", + "required": ["nextStep", "sessionId", "requirements"], + "properties": { + "nextStep": { + "type": "integer", + "minimum": 2, + "description": "Next step number" + }, + "sessionId": { + "type": "string", + "description": "Session ID to use for next step" + }, + "requirements": { + "$ref": "#/definitions/authorizationRequirements", + "description": "Requirements for the next step" + }, + "message": { + "type": "string", + "description": "Message to display to user (e.g., 'OTP sent to your email')" + } + } + }, + "authorizationSession": { + "type": "object", + "description": "Authorization session stored in database", + "required": ["sessionId", "userId", "entityType", "currentStep", "maxSteps", "expiresAt"], + "properties": { + "id": { + "type": "string", + "description": "Database ID (ObjectId for MongoDB, integer for PostgreSQL)" + }, + "sessionId": { + "type": "string", + "format": "uuid", + "description": "Unique session identifier" + }, + "userId": { + "type": "string", + "description": "User ID initiating the auth flow" + }, + "entityType": { + "type": "string", + "description": "Type of entity being authorized" + }, + "currentStep": { + "type": "integer", + "minimum": 1, + "default": 1, + "description": "Current step in the flow" + }, + "maxSteps": { + "type": "integer", + "minimum": 1, + "description": "Total number of steps" + }, + "stepData": { + "type": "object", + "description": "Data collected from previous steps", + "additionalProperties": true + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "description": "Session expiration timestamp" + }, + "completed": { + "type": "boolean", + "default": false, + "description": "Whether the session has completed successfully" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Session creation timestamp" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Last update timestamp" + } + } + } + } +} diff --git a/website/schemas/api-credentials/v1.0.json b/website/schemas/api-credentials/v1.0.json new file mode 100644 index 000000000..111edd72b --- /dev/null +++ b/website/schemas/api-credentials/v1.0.json @@ -0,0 +1,176 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://friggframework.org/schemas/api-credentials/v1.0.json", + "title": "Frigg Credentials API Schemas", + "description": "JSON schemas for credentials API requests and responses", + "definitions": { + "credential": { + "type": "object", + "description": "A credential object representing authentication data for an external system. Tokens are masked in responses.", + "required": ["id", "type", "userId", "authIsValid"], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the credential" + }, + "type": { + "type": "string", + "description": "Module type (e.g., 'hubspot', 'salesforce', 'slack')" + }, + "externalId": { + "type": "string", + "description": "ID from the external system (e.g., account ID, organization ID)" + }, + "userId": { + "type": "string", + "description": "ID of the user who owns this credential" + }, + "authIsValid": { + "type": "boolean", + "description": "Whether the authentication is currently valid" + }, + "entityCount": { + "type": "integer", + "minimum": 0, + "description": "Number of entities using this credential" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the credential was created" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the credential was last updated" + } + } + }, + "listCredentialsResponse": { + "type": "object", + "description": "Response from GET /api/credentials - returns list of credentials for the authenticated user", + "required": ["credentials"], + "properties": { + "credentials": { + "type": "array", + "items": { + "$ref": "#/definitions/credential" + }, + "description": "Array of credential objects (tokens masked)" + } + } + }, + "getCredentialResponse": { + "description": "Response from GET /api/credentials/:id - returns a single credential", + "allOf": [ + { + "$ref": "#/definitions/credential" + } + ] + }, + "deleteCredentialResponse": { + "type": "object", + "description": "Response from DELETE /api/credentials/:id", + "required": ["success"], + "properties": { + "success": { + "type": "boolean", + "description": "Whether the deletion was successful" + }, + "message": { + "type": "string", + "description": "Optional message providing additional context" + } + } + }, + "reauthorizeCredentialRequest": { + "type": "object", + "description": "Request to POST /api/credentials/:id/reauthorize - refresh or update credential authentication", + "required": ["data"], + "properties": { + "data": { + "type": "object", + "description": "Authentication data (OAuth code, API keys, form fields, etc.)", + "additionalProperties": true + }, + "step": { + "type": "integer", + "minimum": 1, + "default": 1, + "description": "Current step number for multi-step reauthorization flows" + }, + "sessionId": { + "type": "string", + "description": "Session ID from previous step (required for step > 1)" + } + } + }, + "reauthorizeCredentialResponse": { + "type": "object", + "description": "Response from POST /api/credentials/:id/reauthorize - either success or next step", + "oneOf": [ + { + "$ref": "#/definitions/reauthorizeCredentialSuccess" + }, + { + "$ref": "#/definitions/reauthorizeCredentialNextStep" + } + ] + }, + "reauthorizeCredentialSuccess": { + "type": "object", + "description": "Successful reauthorization completion", + "required": ["success", "credential_id", "authIsValid"], + "properties": { + "success": { + "type": "boolean", + "const": true, + "description": "Indicates successful reauthorization" + }, + "credential_id": { + "type": "string", + "description": "ID of the reauthorized credential" + }, + "authIsValid": { + "type": "boolean", + "const": true, + "description": "Authentication is now valid" + }, + "message": { + "type": "string", + "description": "Optional success message" + } + } + }, + "reauthorizeCredentialNextStep": { + "type": "object", + "description": "Next step in multi-step reauthorization flow", + "required": ["step", "totalSteps", "sessionId", "requirements"], + "properties": { + "step": { + "type": "integer", + "minimum": 2, + "description": "Next step number" + }, + "totalSteps": { + "type": "integer", + "minimum": 2, + "description": "Total number of steps in the flow" + }, + "sessionId": { + "type": "string", + "description": "Session ID to use for the next step" + }, + "requirements": { + "type": "object", + "description": "Requirements for the next step (form fields, OAuth URL, etc.)", + "additionalProperties": true + }, + "message": { + "type": "string", + "description": "Message to display to user (e.g., 'OTP sent to your email')" + } + } + } + } +} diff --git a/website/schemas/api-entities/v1.0.json b/website/schemas/api-entities/v1.0.json new file mode 100644 index 000000000..7bad04a6d --- /dev/null +++ b/website/schemas/api-entities/v1.0.json @@ -0,0 +1,292 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://friggframework.org/schemas/api-entities/v1.0.json", + "title": "Frigg Entities API Schemas", + "description": "JSON schemas for entities API requests and responses", + "definitions": { + "entity": { + "type": "object", + "description": "A connected account/entity object representing an external integration", + "required": ["id", "type", "credentialId", "userId"], + "properties": { + "id": { + "type": "string", + "description": "Unique entity identifier" + }, + "type": { + "type": "string", + "description": "Module/entity type name (e.g., 'hubspot', 'salesforce')" + }, + "name": { + "type": "string", + "description": "Display name for the entity" + }, + "externalId": { + "type": "string", + "description": "ID from the external system (e.g., HubSpot portal ID)" + }, + "credentialId": { + "type": "string", + "description": "ID of the linked credential" + }, + "userId": { + "type": "string", + "description": "ID of the user who owns this entity" + }, + "authIsValid": { + "type": "boolean", + "description": "Whether authentication is currently valid" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Entity creation timestamp" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Last update timestamp" + } + } + }, + "listEntitiesResponse": { + "type": "object", + "description": "Response from GET /api/entities", + "required": ["entities"], + "properties": { + "entities": { + "type": "array", + "items": { + "$ref": "#/definitions/entity" + }, + "description": "List of entities for the authenticated user" + } + } + }, + "createEntityRequest": { + "type": "object", + "description": "Request to POST /api/entities", + "required": ["entityType", "data"], + "properties": { + "entityType": { + "type": "string", + "description": "Type of entity to create (module name)" + }, + "data": { + "type": "object", + "description": "Entity creation data (must contain credential_id)", + "required": ["credential_id"], + "properties": { + "credential_id": { + "type": "string", + "description": "ID of the credential to link to this entity" + } + }, + "additionalProperties": true + } + } + }, + "createEntityResponse": { + "type": "object", + "description": "Response from POST /api/entities", + "required": ["entity_id", "credential_id", "type"], + "properties": { + "entity_id": { + "type": "string", + "description": "ID of the created entity" + }, + "credential_id": { + "type": "string", + "description": "ID of the linked credential" + }, + "type": { + "type": "string", + "description": "Entity type (module name)" + } + } + }, + "entityType": { + "type": "object", + "description": "Metadata about an available entity type", + "required": ["type", "name"], + "properties": { + "type": { + "type": "string", + "description": "Module name (e.g., 'hubspot', 'salesforce')" + }, + "name": { + "type": "string", + "description": "Display name for the entity type" + }, + "description": { + "type": "string", + "description": "Description of the entity type" + }, + "authType": { + "type": "string", + "enum": ["oauth2", "form", "api-key", "basic"], + "description": "Type of authentication required" + }, + "isMultiStep": { + "type": "boolean", + "description": "Whether this entity type uses multi-step authentication" + }, + "stepCount": { + "type": "integer", + "minimum": 1, + "description": "Number of steps in the authentication flow" + }, + "capabilities": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of capabilities this entity type supports" + } + } + }, + "listEntityTypesResponse": { + "type": "object", + "description": "Response from GET /api/entities/types", + "required": ["types"], + "properties": { + "types": { + "type": "array", + "items": { + "$ref": "#/definitions/entityType" + }, + "description": "List of available entity types" + } + } + }, + "getEntityTypeResponse": { + "type": "object", + "description": "Response from GET /api/entities/types/:typeName", + "required": ["type", "name"], + "properties": { + "type": { + "type": "string", + "description": "Module name (e.g., 'hubspot', 'salesforce')" + }, + "name": { + "type": "string", + "description": "Display name for the entity type" + }, + "description": { + "type": "string", + "description": "Description of the entity type" + }, + "authType": { + "type": "string", + "enum": ["oauth2", "form", "api-key", "basic"], + "description": "Type of authentication required" + }, + "isMultiStep": { + "type": "boolean", + "description": "Whether this entity type uses multi-step authentication" + }, + "stepCount": { + "type": "integer", + "minimum": 1, + "description": "Number of steps in the authentication flow" + }, + "capabilities": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of capabilities this entity type supports" + } + } + }, + "reauthorizeEntityRequest": { + "type": "object", + "description": "Request to POST /api/entities/:id/reauthorize", + "required": ["data"], + "properties": { + "data": { + "type": "object", + "description": "Authentication data (OAuth code, form fields, API keys, etc.)", + "additionalProperties": true + }, + "step": { + "type": "integer", + "minimum": 1, + "default": 1, + "description": "Current step number for multi-step flows" + }, + "sessionId": { + "type": "string", + "description": "Session ID from previous step (required for step > 1)" + } + } + }, + "reauthorizeEntityResponse": { + "type": "object", + "description": "Response from POST /api/entities/:id/reauthorize (success or next step)", + "oneOf": [ + { + "$ref": "#/definitions/reauthorizeEntitySuccess" + }, + { + "$ref": "#/definitions/reauthorizeEntityNextStep" + } + ] + }, + "reauthorizeEntitySuccess": { + "type": "object", + "description": "Successful reauthorization completion", + "required": ["success", "credential_id", "entity_id", "authIsValid"], + "properties": { + "success": { + "type": "boolean", + "const": true, + "description": "Indicates successful reauthorization" + }, + "credential_id": { + "type": "string", + "description": "ID of the updated credential" + }, + "entity_id": { + "type": "string", + "description": "ID of the reauthorized entity" + }, + "authIsValid": { + "type": "boolean", + "const": true, + "description": "Confirms authentication is now valid" + } + } + }, + "reauthorizeEntityNextStep": { + "type": "object", + "description": "Next step in multi-step reauthorization", + "required": ["step", "totalSteps", "sessionId", "requirements"], + "properties": { + "step": { + "type": "integer", + "minimum": 2, + "description": "Next step number" + }, + "totalSteps": { + "type": "integer", + "minimum": 2, + "description": "Total number of steps in the flow" + }, + "sessionId": { + "type": "string", + "description": "Session ID to use for next step" + }, + "requirements": { + "type": "object", + "description": "Requirements for the next step (varies by auth type)", + "additionalProperties": true + }, + "message": { + "type": "string", + "description": "Message to display to user (e.g., 'OTP sent to your email')" + } + } + } + } +} diff --git a/website/schemas/api-module-definition/v1.0.json b/website/schemas/api-module-definition/v1.0.json new file mode 100644 index 000000000..0db52f71f --- /dev/null +++ b/website/schemas/api-module-definition/v1.0.json @@ -0,0 +1,379 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://friggframework.org/schemas/api-module-definition/v1.0.json", + "title": "Frigg API Module Definition", + "description": "Schema for defining a Frigg API module including authentication, API configuration, and required methods.", + "type": "object", + "required": ["moduleName", "getName", "requiredAuthMethods"], + "properties": { + "API": { + "type": "object", + "description": "API class or configuration object for the module" + }, + "getName": { + "type": "object", + "description": "Function that returns the module name", + "properties": { + "type": { + "type": "string", + "enum": ["function"] + } + } + }, + "moduleName": { + "type": "string", + "description": "Module name identifier", + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", + "minLength": 1, + "maxLength": 100 + }, + "requiredAuthMethods": { + "type": "object", + "description": "Required authentication methods and configuration", + "properties": { + "getToken": { + "type": "object", + "description": "OAuth token retrieval method", + "properties": { + "type": {"type": "string", "enum": ["function"]}, + "async": {"type": "boolean", "default": true} + } + }, + "apiPropertiesToPersist": { + "type": "object", + "description": "Properties to persist for credentials and entities", + "required": ["credential", "entity"], + "properties": { + "credential": { + "type": "array", + "description": "Credential properties to persist", + "items": { + "type": "string", + "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$" + }, + "uniqueItems": true, + "examples": [["access_token", "refresh_token", "expires_at"]] + }, + "entity": { + "type": "array", + "description": "Entity properties to persist", + "items": { + "type": "string", + "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$" + }, + "uniqueItems": true, + "examples": [["external_id", "name", "type"]] + } + }, + "additionalProperties": false + }, + "getCredentialDetails": { + "type": "object", + "description": "Method to retrieve credential details", + "properties": { + "type": {"type": "string", "enum": ["function"]}, + "async": {"type": "boolean", "default": true} + } + }, + "getEntityDetails": { + "type": "object", + "description": "Method to retrieve entity details", + "properties": { + "type": {"type": "string", "enum": ["function"]}, + "async": {"type": "boolean", "default": true} + } + }, + "testAuthRequest": { + "type": "object", + "description": "Method to test authentication", + "properties": { + "type": {"type": "string", "enum": ["function"]}, + "async": {"type": "boolean", "default": true} + } + } + }, + "additionalProperties": false + }, + "env": { + "type": "object", + "description": "Environment variable configuration", + "properties": { + "client_id": { + "type": "string", + "description": "OAuth client ID from environment variable" + }, + "client_secret": { + "type": "string", + "description": "OAuth client secret from environment variable" + }, + "scope": { + "type": "string", + "description": "OAuth scope from environment variable" + }, + "redirect_uri": { + "type": "string", + "description": "OAuth redirect URI from environment variable", + "format": "uri" + }, + "api_key": { + "type": "string", + "description": "API key from environment variable" + }, + "base_url": { + "type": "string", + "description": "Base API URL from environment variable", + "format": "uri" + } + }, + "patternProperties": { + "^[A-Z][A-Z0-9_]*$": { + "type": "string", + "description": "Custom environment variable" + } + }, + "additionalProperties": false + }, + "config": { + "type": "object", + "description": "Module configuration", + "properties": { + "name": { + "type": "string", + "description": "Configuration name" + }, + "version": { + "type": "string", + "description": "Module version", + "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$" + }, + "authType": { + "type": "string", + "description": "Authentication type", + "enum": ["oauth2", "api-key", "basic", "bearer", "custom"] + }, + "baseURL": { + "type": "string", + "description": "Base API URL", + "format": "uri" + }, + "rateLimit": { + "type": "object", + "description": "Rate limiting configuration", + "properties": { + "requests": { + "type": "integer", + "description": "Maximum requests", + "minimum": 1 + }, + "period": { + "type": "string", + "description": "Time period for rate limit", + "enum": ["second", "minute", "hour", "day"] + } + }, + "additionalProperties": false + }, + "timeout": { + "type": "integer", + "description": "Request timeout in milliseconds", + "minimum": 1000, + "maximum": 300000, + "default": 30000 + }, + "retries": { + "type": "integer", + "description": "Number of retries for failed requests", + "minimum": 0, + "maximum": 10, + "default": 3 + } + }, + "additionalProperties": true + }, + "manager": { + "type": "object", + "description": "Module manager configuration", + "properties": { + "Entity": { + "type": "object", + "description": "Entity model class" + }, + "Credential": { + "type": "object", + "description": "Credential model class" + }, + "methods": { + "type": "object", + "description": "Available manager methods", + "properties": { + "getInstance": { + "type": "object", + "description": "Get module instance method" + }, + "getEntitiesForUserId": { + "type": "object", + "description": "Get entities for user method" + }, + "validateAuthorizationRequirements": { + "type": "object", + "description": "Validate auth requirements method" + }, + "getAuthorizationRequirements": { + "type": "object", + "description": "Get auth requirements method" + }, + "testAuth": { + "type": "object", + "description": "Test authentication method" + }, + "processAuthorizationCallback": { + "type": "object", + "description": "Process auth callback method" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "webhooks": { + "type": "object", + "description": "Webhook configuration", + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether webhooks are enabled", + "default": false + }, + "endpoints": { + "type": "array", + "description": "Webhook endpoint configurations", + "items": { + "type": "object", + "required": ["path", "events"], + "properties": { + "path": { + "type": "string", + "description": "Webhook endpoint path", + "pattern": "^/[a-zA-Z0-9/_-]*$" + }, + "events": { + "type": "array", + "description": "Events handled by this endpoint", + "items": {"type": "string"}, + "uniqueItems": true + }, + "method": { + "type": "string", + "description": "HTTP method", + "enum": ["POST", "PUT", "PATCH"], + "default": "POST" + }, + "authentication": { + "type": "object", + "description": "Webhook authentication requirements", + "properties": { + "type": { + "type": "string", + "enum": ["signature", "token", "none"], + "default": "signature" + }, + "secret": { + "type": "string", + "description": "Webhook secret for signature verification" + } + } + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "encryption": { + "type": "object", + "description": "Module-specific encryption configuration for credential fields", + "properties": { + "credentialFields": { + "type": "array", + "description": "Credential field paths to encrypt (in addition to core schema). Use snake_case for consistency.", + "items": { + "type": "string", + "pattern": "^[a-zA-Z_][a-zA-Z0-9_.]*$", + "minLength": 1 + }, + "uniqueItems": true, + "examples": [ + ["api_key", "custom_token"], + ["webhook_secret", "signing_key"] + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "examples": [ + { + "moduleName": "hubspot", + "getName": { + "type": "function" + }, + "requiredAuthMethods": { + "getToken": { + "type": "function", + "async": true + }, + "apiPropertiesToPersist": { + "credential": ["access_token", "refresh_token", "expires_at"], + "entity": ["external_id", "portal_id"] + }, + "getCredentialDetails": { + "type": "function", + "async": true + }, + "getEntityDetails": { + "type": "function", + "async": true + }, + "testAuthRequest": { + "type": "function", + "async": true + } + }, + "env": { + "client_id": "HUBSPOT_CLIENT_ID", + "client_secret": "HUBSPOT_CLIENT_SECRET", + "scope": "HUBSPOT_SCOPE", + "redirect_uri": "https://api.myapp.com/auth/hubspot/callback" + }, + "config": { + "name": "HubSpot", + "version": "2.0.0", + "authType": "oauth2", + "baseURL": "https://api.hubapi.com", + "rateLimit": { + "requests": 100, + "period": "minute" + }, + "timeout": 30000, + "retries": 3 + }, + "webhooks": { + "enabled": true, + "endpoints": [ + { + "path": "/webhooks/hubspot", + "events": ["contact.creation", "deal.propertyChange"], + "method": "POST", + "authentication": { + "type": "signature", + "secret": "HUBSPOT_WEBHOOK_SECRET" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/website/schemas/api-proxy/v1.0.json b/website/schemas/api-proxy/v1.0.json new file mode 100644 index 000000000..e0692b71c --- /dev/null +++ b/website/schemas/api-proxy/v1.0.json @@ -0,0 +1,251 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://friggframework.org/schemas/api-proxy/v1.0.json", + "title": "Frigg Proxy API Schemas", + "description": "JSON schemas for proxy API requests and responses that forward calls to external APIs", + "definitions": { + "proxyRequest": { + "type": "object", + "description": "Proxy request to POST /api/entities/:id/proxy or POST /api/credentials/:id/proxy", + "required": ["method", "path"], + "properties": { + "method": { + "type": "string", + "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"], + "description": "HTTP method to use for the upstream API request" + }, + "path": { + "type": "string", + "description": "API path to call on the upstream service (e.g., '/v3/contacts' or '/api/users')", + "pattern": "^/", + "minLength": 1 + }, + "query": { + "type": "object", + "description": "Query parameters to include in the request as key-value pairs", + "additionalProperties": { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" }, + { + "type": "array", + "items": { "type": "string" } + } + ] + } + }, + "headers": { + "type": "object", + "description": "Additional headers to include in the upstream request (authorization headers are added automatically)", + "additionalProperties": { "type": "string" } + }, + "body": { + "description": "Request body for POST, PUT, or PATCH requests (can be object, array, string, or null)", + "oneOf": [ + { "type": "object" }, + { "type": "array" }, + { "type": "string" }, + { "type": "null" } + ] + } + }, + "examples": [ + { + "method": "GET", + "path": "/v3/contacts", + "query": { + "limit": "10", + "archived": "false" + } + }, + { + "method": "POST", + "path": "/v3/contacts", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "properties": { + "email": "contact@example.com", + "firstname": "John", + "lastname": "Doe" + } + } + }, + { + "method": "PATCH", + "path": "/api/v1/users/12345", + "body": { + "status": "active" + } + } + ] + }, + "proxyResponse": { + "type": "object", + "description": "Successful proxy response with data from upstream API", + "required": ["success", "status", "data"], + "properties": { + "success": { + "type": "boolean", + "const": true, + "description": "Indicates successful proxy operation" + }, + "status": { + "type": "integer", + "minimum": 200, + "maximum": 299, + "description": "HTTP status code from the upstream API response" + }, + "headers": { + "type": "object", + "description": "Response headers from the upstream API", + "additionalProperties": { "type": "string" } + }, + "data": { + "description": "Response body from the upstream API (can be any valid JSON type)", + "oneOf": [ + { "type": "object" }, + { "type": "array" }, + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" }, + { "type": "null" } + ] + } + }, + "examples": [ + { + "success": true, + "status": 200, + "headers": { + "content-type": "application/json", + "x-rate-limit-remaining": "998" + }, + "data": { + "results": [ + { + "id": "123", + "properties": { + "email": "contact@example.com", + "firstname": "John", + "lastname": "Doe" + } + } + ] + } + }, + { + "success": true, + "status": 201, + "data": { + "id": "456", + "created_at": "2025-01-15T10:30:00Z", + "status": "active" + } + } + ] + }, + "proxyErrorResponse": { + "type": "object", + "description": "Error response from proxy operation", + "required": ["success", "status", "error"], + "properties": { + "success": { + "type": "boolean", + "const": false, + "description": "Indicates failed proxy operation" + }, + "status": { + "type": "integer", + "minimum": 400, + "maximum": 599, + "description": "HTTP status code (either from upstream API or Frigg proxy error)" + }, + "error": { + "type": "object", + "required": ["code", "message"], + "properties": { + "code": { + "type": "string", + "description": "Error code identifying the type of failure", + "enum": [ + "INVALID_AUTH", + "EXPIRED_TOKEN", + "UPSTREAM_ERROR", + "TIMEOUT", + "NETWORK_ERROR", + "RATE_LIMITED", + "INVALID_REQUEST", + "NOT_FOUND", + "PERMISSION_DENIED", + "INVALID_CREDENTIALS", + "SERVICE_UNAVAILABLE" + ] + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "description": "Additional error details from upstream API or internal error information", + "oneOf": [ + { "type": "object" }, + { "type": "array" }, + { "type": "string" }, + { "type": "null" } + ] + }, + "upstreamStatus": { + "type": "integer", + "description": "Original HTTP status code from upstream API (if available)" + } + } + } + }, + "examples": [ + { + "success": false, + "status": 401, + "error": { + "code": "INVALID_AUTH", + "message": "Authentication credentials are invalid or expired", + "details": { + "category": "INVALID_AUTHENTICATION", + "message": "The access token provided is invalid or has expired" + }, + "upstreamStatus": 401 + } + }, + { + "success": false, + "status": 429, + "error": { + "code": "RATE_LIMITED", + "message": "Rate limit exceeded for this API", + "details": { + "retry_after": 60, + "limit": "100 requests per minute" + } + } + }, + { + "success": false, + "status": 504, + "error": { + "code": "TIMEOUT", + "message": "Request to upstream API timed out after 30 seconds" + } + } + ] + }, + "proxyResponseUnion": { + "description": "Union type representing either success or error proxy response", + "oneOf": [ + { "$ref": "#/definitions/proxyResponse" }, + { "$ref": "#/definitions/proxyErrorResponse" } + ] + } + } +} diff --git a/website/schemas/app-definition/v1.0.json b/website/schemas/app-definition/v1.0.json new file mode 100644 index 000000000..f975abd9a --- /dev/null +++ b/website/schemas/app-definition/v1.0.json @@ -0,0 +1,761 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://friggframework.org/schemas/app-definition/v1.0.json", + "title": "Frigg Application Definition", + "description": "Schema for defining a Frigg application configuration including integrations, infrastructure, security settings, and deployment options.", + "type": "object", + "required": ["integrations"], + "properties": { + "name": { + "type": "string", + "description": "Service name for deployment (used as CloudFormation stack name prefix)", + "pattern": "^[a-zA-Z][a-zA-Z0-9-]*$", + "minLength": 1, + "maxLength": 128, + "examples": ["my-frigg-app", "customer-integrations"] + }, + "provider": { + "type": "string", + "description": "Cloud provider for deployment", + "enum": ["aws"], + "default": "aws" + }, + "label": { + "type": "string", + "description": "Human-readable display name for the application", + "minLength": 1, + "maxLength": 200, + "examples": ["My Frigg Application", "Customer Portal Integrations"] + }, + "managementMode": { + "type": "string", + "description": "Infrastructure management mode: 'managed' lets Frigg manage resources, 'discover' uses existing resources, 'custom' for manual configuration", + "enum": ["managed", "discover", "custom"], + "default": "managed" + }, + "vpcIsolation": { + "type": "string", + "description": "VPC isolation strategy: 'isolated' creates separate VPC per stage, 'shared' reuses VPC across stages", + "enum": ["isolated", "shared"], + "default": "isolated" + }, + "environment": { + "type": "object", + "description": "Environment variable configuration. true includes the variable in the Lambda environment; 'ssm' offloads it to SSM Parameter Store (requires ssm.enable) so it never counts against the Lambda 4KB env limit", + "patternProperties": { + "^[A-Z][A-Z0-9_]*$": { + "oneOf": [ + { + "type": "boolean", + "description": "Set to true to include this environment variable from process.env" + }, + { + "type": "string", + "const": "ssm", + "description": "Offload this variable to SSM Parameter Store; fetched at runtime instead of stored in the Lambda environment" + } + ] + } + }, + "additionalProperties": false, + "examples": [ + { + "NODE_ENV": true, + "API_KEY": "ssm", + "DATABASE_URL": true + } + ] + }, + "integrations": { + "type": "array", + "description": "Array of integration classes to enable in the application", + "items": { + "type": "object", + "description": "Integration class or configuration object" + }, + "default": [] + }, + "adminScripts": { + "type": "array", + "description": "Admin script classes (extending AdminScriptBase from @friggframework/admin-scripts) served under /admin/scripts", + "items": { + "description": "Admin script class" + }, + "default": [] + }, + "reports": { + "type": "array", + "description": "Report classes served by the admin report runner", + "items": { + "description": "Report class" + }, + "default": [] + }, + "admin": { + "type": "object", + "description": "Admin script and report runner options", + "properties": { + "enableScheduling": { + "type": "boolean", + "description": "Provision the EventBridge Scheduler group and role so admin scripts and reports can run on a schedule", + "default": false + }, + "includeBuiltinReports": { + "type": "boolean", + "description": "Serve the built-in reports alongside the app's own reports", + "default": false + } + }, + "additionalProperties": false + }, + "user": { + "type": "object", + "description": "User management and authentication configuration", + "properties": { + "usePassword": { + "type": "boolean", + "description": "Enable password-based authentication for user login", + "default": true + }, + "password": { + "type": "boolean", + "description": "DEPRECATED: Use 'usePassword' instead. Enable password-based authentication", + "default": true + }, + "primary": { + "type": "string", + "description": "Primary user type for authentication", + "enum": ["individual", "organization"], + "default": "individual" + }, + "individualUserRequired": { + "type": "boolean", + "description": "Require individual user for authentication", + "default": true + }, + "organizationUserRequired": { + "type": "boolean", + "description": "Require organization user for authentication", + "default": false + }, + "authModes": { + "type": "object", + "description": "Authentication mode configuration - enables multiple authentication strategies. x-frigg-appUserId and x-frigg-appOrgId headers are automatically supported for user identification with any auth mode.", + "properties": { + "friggToken": { + "type": "boolean", + "description": "Enable Frigg native bearer token authentication (from /user/login)", + "default": true + }, + "sharedSecret": { + "type": "boolean", + "description": "Enable shared secret authentication for backend-to-backend communication (requires FRIGG_API_KEY env var and x-frigg-api-key header)", + "default": false + }, + "adopterJwt": { + "type": "boolean", + "description": "Enable custom JWT authentication from adopter's auth system (requires jwtConfig)", + "default": false + } + }, + "additionalProperties": false + }, + "jwtConfig": { + "type": "object", + "description": "JWT configuration for adopter JWT authentication mode (when authModes.adopterJwt is true)", + "properties": { + "secret": { + "type": "string", + "description": "JWT secret key for signature validation (should be set via environment variable)" + }, + "userIdClaim": { + "type": "string", + "description": "JWT claim name containing the user ID", + "default": "sub" + }, + "orgIdClaim": { + "type": "string", + "description": "JWT claim name containing the organization ID", + "default": "org_id" + }, + "algorithm": { + "type": "string", + "description": "JWT signing algorithm", + "enum": ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512"], + "default": "HS256" + } + }, + "required": ["secret"], + "additionalProperties": false + }, + "fields": { + "type": "array", + "description": "Additional user fields", + "items": { + "type": "string" + }, + "examples": [["email", "firstName", "lastName"]] + }, + "model": { + "type": "object", + "description": "Custom user model class" + } + }, + "additionalProperties": false + }, + "auth": { + "type": "object", + "description": "Authentication configuration for integrations", + "properties": { + "jwtSecret": { + "type": "string", + "description": "JWT secret key (should be set via environment variable)" + }, + "tokenExpiry": { + "type": "string", + "description": "Token expiration time", + "pattern": "^\\d+[smhd]$", + "default": "24h" + }, + "refreshTokenExpiry": { + "type": "string", + "description": "Refresh token expiration time", + "pattern": "^\\d+[smhd]$", + "default": "7d" + } + }, + "additionalProperties": false + }, + "database": { + "type": "object", + "description": "Database configuration for MongoDB, PostgreSQL, or DocumentDB", + "properties": { + "uri": { + "type": "string", + "description": "Database connection URI (should be set via environment variable)", + "format": "uri" + }, + "mongoDB": { + "type": "object", + "description": "MongoDB configuration", + "properties": { + "enable": { + "type": "boolean", + "description": "Enable MongoDB database", + "default": false + }, + "uri": { + "type": "string", + "description": "MongoDB connection URI" + } + }, + "additionalProperties": true + }, + "documentDB": { + "type": "object", + "description": "AWS DocumentDB configuration (MongoDB-compatible)", + "properties": { + "enable": { + "type": "boolean", + "description": "Enable DocumentDB database", + "default": false + }, + "tlsCAFile": { + "type": "string", + "description": "Path to TLS CA certificate file for DocumentDB", + "examples": ["./security/global-bundle.pem"] + } + }, + "additionalProperties": true + }, + "postgres": { + "type": "object", + "description": "PostgreSQL/Aurora configuration", + "properties": { + "enable": { + "type": "boolean", + "description": "Enable PostgreSQL database", + "default": false + }, + "management": { + "type": "string", + "description": "How to manage the PostgreSQL database", + "enum": ["discover", "create-new", "existing"], + "default": "discover" + }, + "endpoint": { + "type": "string", + "description": "Existing database endpoint (when management='existing')", + "examples": ["mydb.cluster-abc123.us-east-1.rds.amazonaws.com"] + }, + "port": { + "type": "integer", + "description": "Database port", + "default": 5432 + }, + "database": { + "type": "string", + "description": "Database name", + "default": "frigg" + }, + "username": { + "type": "string", + "description": "Database username", + "default": "postgres" + }, + "selfHeal": { + "type": "boolean", + "description": "Automatically fix database configuration issues", + "default": true + }, + "publiclyAccessible": { + "type": "boolean", + "description": "Make database publicly accessible (not recommended for production)", + "default": false + }, + "autoCreateCredentials": { + "type": "boolean", + "description": "Automatically create Secrets Manager secret and rotate Aurora password when discovering existing cluster (only applies when management='discover')", + "default": false + }, + "minCapacity": { + "type": "number", + "description": "Minimum Aurora Serverless capacity units (0.5-128)", + "minimum": 0.5, + "maximum": 128, + "default": 0.5 + }, + "maxCapacity": { + "type": "number", + "description": "Maximum Aurora Serverless capacity units (0.5-128)", + "minimum": 0.5, + "maximum": 128, + "default": 1 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": true + }, + "encryption": { + "type": "object", + "description": "Encryption settings for sensitive data", + "properties": { + "fieldLevelEncryptionMethod": { + "type": "string", + "description": "Method for field-level encryption", + "enum": ["kms", "aes"], + "default": "aes" + }, + "createResourceIfNoneFound": { + "type": "boolean", + "description": "Create KMS key if none found during discovery", + "default": false + }, + "schema": { + "type": "object", + "description": "Custom encryption schema defining additional fields to encrypt beyond core schema", + "patternProperties": { + "^[A-Z][a-zA-Z0-9]*$": { + "type": "object", + "description": "Model name (PascalCase Prisma model)", + "required": ["fields"], + "properties": { + "fields": { + "type": "array", + "description": "Array of field paths to encrypt (e.g., 'fieldName' or 'nested.field')", + "items": { + "type": "string", + "pattern": "^[a-zA-Z_][a-zA-Z0-9_.]*$", + "minLength": 1 + }, + "minItems": 1, + "uniqueItems": true + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "examples": [ + { + "Credential": { + "fields": ["data.customToken", "data.proprietaryKey"] + }, + "MyCustomModel": { + "fields": ["secretData", "nested.apiKey"] + } + } + ] + } + }, + "additionalProperties": false + }, + "vpc": { + "type": "object", + "description": "Virtual Private Cloud settings for Lambda functions", + "properties": { + "enable": { + "type": "boolean", + "description": "Enable VPC for Lambda functions", + "default": false + }, + "management": { + "type": "string", + "description": "How to manage VPC infrastructure", + "enum": ["discover", "create-new", "existing"], + "default": "discover" + }, + "vpcId": { + "type": "string", + "description": "Existing VPC ID (when management='existing')", + "pattern": "^vpc-[a-z0-9]+$", + "examples": ["vpc-abc123"] + }, + "cidrBlock": { + "type": "string", + "description": "CIDR block for VPC (when management='create-new')", + "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$", + "default": "10.0.0.0/16" + }, + "securityGroupIds": { + "type": "array", + "description": "Security group IDs for Lambda functions", + "items": { + "type": "string", + "pattern": "^sg-[a-z0-9]+$" + }, + "examples": [["sg-abc123", "sg-def456"]] + }, + "subnets": { + "type": "object", + "description": "Subnet configuration", + "properties": { + "management": { + "type": "string", + "description": "How to manage subnets", + "enum": ["discover", "create-new", "existing"], + "default": "discover" + }, + "ids": { + "type": "array", + "description": "Existing subnet IDs", + "items": { + "type": "string", + "pattern": "^subnet-[a-z0-9]+$" + }, + "minItems": 2, + "examples": [["subnet-abc123", "subnet-def456"]] + } + }, + "additionalProperties": false + }, + "natGateway": { + "type": "object", + "description": "NAT Gateway configuration for private subnets", + "properties": { + "management": { + "type": "string", + "description": "How to manage NAT Gateway", + "enum": ["discover", "create-new", "existing"], + "default": "discover" + }, + "id": { + "type": "string", + "description": "Existing NAT Gateway ID", + "pattern": "^nat-[a-z0-9]+$", + "examples": ["nat-abc123"] + } + }, + "additionalProperties": false + }, + "enableVPCEndpoints": { + "type": "boolean", + "description": "Create VPC endpoints for AWS services (S3, DynamoDB, KMS, Secrets Manager)", + "default": true + }, + "selfHeal": { + "type": "boolean", + "description": "Automatically fix VPC configuration issues", + "default": false + } + }, + "additionalProperties": false + }, + "ssm": { + "type": "object", + "description": "AWS Systems Manager Parameter Store configuration", + "properties": { + "enable": { + "type": "boolean", + "description": "Enable SSM Parameter Store for configuration", + "default": false + }, + "parameterPrefix": { + "type": "string", + "description": "Override the default parameter path prefix (/frigg/${service}/${stage}). Non-default prefixes may require widening the *frigg*-scoped deployment IAM policies" + }, + "kmsKeyArn": { + "type": "string", + "description": "Customer-managed KMS key for SecureString parameters. When set, the Lambda execution role is granted kms:Decrypt on this key. Defaults to the AWS-managed aws/ssm key" + }, + "restrictIamToPrefix": { + "type": "boolean", + "description": "Drop the legacy broad parameter/* read grant and only allow reads under the parameter prefix", + "default": false + }, + "parameters": { + "type": "object", + "description": "SSM parameter definitions", + "patternProperties": { + "^[A-Z][A-Z0-9_]*$": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["String", "StringList", "SecureString"], + "default": "String" + }, + "tier": { + "type": "string", + "description": "Parameter Store tier for this key: 'standard' caps the value at 4KB, 'advanced' at 8KB (billed by AWS)", + "enum": ["standard", "advanced"], + "default": "standard" + }, + "description": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "secretsManager": { + "type": "object", + "description": "AWS Secrets Manager configuration", + "properties": { + "enable": { + "type": "boolean", + "description": "Enable AWS Secrets Manager for secure credential storage", + "default": false + } + }, + "additionalProperties": false + }, + "lambda": { + "type": "object", + "description": "Lambda function generation options", + "properties": { + "scopedEnvironment": { + "type": "boolean", + "description": "Scope framework-generated environment variables (queue URLs, migration/scheduler vars) to the functions that consume them instead of broadcasting to every function. Reduces per-function env size against the Lambda 4KB limit", + "default": false + } + }, + "additionalProperties": false + }, + "websockets": { + "type": "object", + "description": "WebSocket API Gateway configuration", + "properties": { + "enable": { + "type": "boolean", + "description": "Enable WebSocket API Gateway", + "default": false + }, + "routeSelectionExpression": { + "type": "string", + "description": "Route selection expression for WebSocket", + "default": "$request.body.action" + } + }, + "additionalProperties": false + }, + "security": { + "type": "object", + "description": "Security settings for the application", + "properties": { + "cors": { + "type": "object", + "description": "CORS (Cross-Origin Resource Sharing) settings", + "properties": { + "origin": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}}, + {"type": "boolean"} + ], + "description": "Allowed origins for CORS requests", + "default": "http://localhost:3000" + }, + "credentials": { + "type": "boolean", + "description": "Allow credentials in CORS requests", + "default": true + }, + "methods": { + "type": "array", + "description": "Allowed HTTP methods", + "items": { + "type": "string", + "enum": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"] + } + }, + "allowedHeaders": { + "type": "array", + "description": "Allowed request headers", + "items": {"type": "string"} + } + }, + "additionalProperties": false + }, + "rateLimit": { + "type": "object", + "description": "Rate limiting configuration", + "properties": { + "windowMs": { + "type": "integer", + "description": "Time window in milliseconds", + "minimum": 1000, + "default": 900000 + }, + "max": { + "type": "integer", + "description": "Maximum requests per window", + "minimum": 1, + "default": 100 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "webhooks": { + "type": "object", + "description": "Webhook handling configuration", + "properties": { + "secret": { + "type": "string", + "description": "Webhook secret for signature verification" + }, + "prefix": { + "type": "string", + "description": "Webhook endpoint prefix", + "pattern": "^/[a-zA-Z0-9/_-]*$", + "default": "/webhooks" + } + }, + "additionalProperties": false + }, + "logging": { + "type": "object", + "description": "Logging configuration (ADR-048). An absent block changes nothing in the deployment.", + "properties": { + "level": { + "type": "string", + "description": "Minimum log level, in lower or upper case. Sets FRIGG_LOG_LEVEL (when not info) and the Lambda application log level.", + "enum": ["trace", "debug", "info", "warn", "error", "fatal", "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], + "default": "info" + }, + "retentionInDays": { + "type": "integer", + "description": "CloudWatch log retention for every Lambda log group, in days", + "enum": [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653] + }, + "format": { + "type": "string", + "description": "Log format. Records are always JSON lines.", + "enum": ["json"], + "default": "json" + } + }, + "additionalProperties": false + }, + "custom": { + "type": "object", + "description": "Custom application-specific configuration", + "properties": { + "appName": { + "type": "string", + "description": "Application display name", + "default": "My Frigg Application" + }, + "version": { + "type": "string", + "description": "Application version", + "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "default": "1.0.0" + }, + "environment": { + "type": "string", + "description": "Deployment environment/stage", + "enum": ["development", "staging", "production", "test"], + "default": "development" + }, + "features": { + "type": "object", + "description": "Feature flags and toggles", + "patternProperties": { + "^[a-zA-Z][a-zA-Z0-9_]*$": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": true + } + }, + "additionalProperties": false, + "examples": [ + { + "name": "my-frigg-app", + "provider": "aws", + "environment": { + "NODE_ENV": true, + "API_KEY": true + }, + "integrations": [], + "user": { + "password": true + }, + "database": { + "postgres": { + "enable": true, + "management": "create-new" + } + }, + "encryption": { + "fieldLevelEncryptionMethod": "kms", + "createResourceIfNoneFound": true + }, + "vpc": { + "enable": true, + "management": "create-new", + "cidrBlock": "10.0.0.0/16", + "enableVPCEndpoints": true + }, + "ssm": { + "enable": true + }, + "websockets": { + "enable": true + }, + "security": { + "cors": { + "origin": "http://localhost:3000", + "credentials": true + } + }, + "logging": { + "level": "info", + "retentionInDays": 30 + } + } + ] +} diff --git a/website/schemas/core-models/v1.0.json b/website/schemas/core-models/v1.0.json new file mode 100644 index 000000000..f287c0bc4 --- /dev/null +++ b/website/schemas/core-models/v1.0.json @@ -0,0 +1,492 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://friggframework.org/schemas/core-models/v1.0.json", + "title": "Frigg Core Data Models", + "description": "Schema definitions for core Frigg data models including User, Entity, and Credential.", + "definitions": { + "objectId": { + "type": "string", + "description": "MongoDB ObjectId", + "pattern": "^[a-f0-9]{24}$" + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp", + "format": "date-time" + }, + "userModel": { + "type": "object", + "description": "Frigg User model schema", + "required": [ + "id" + ], + "properties": { + "id": { + "$ref": "#/definitions/objectId", + "description": "Unique user identifier (maps to _id in MongoDB)" + }, + "email": { + "type": "string", + "description": "User email address", + "format": "email", + "maxLength": 255 + }, + "firstName": { + "type": "string", + "description": "User first name", + "maxLength": 100 + }, + "lastName": { + "type": "string", + "description": "User last name", + "maxLength": 100 + }, + "organization": { + "type": "string", + "description": "User organization", + "maxLength": 200 + }, + "role": { + "type": "string", + "description": "User role", + "enum": [ + "admin", + "user", + "developer", + "viewer" + ], + "default": "user" + }, + "permissions": { + "type": "array", + "description": "User permissions", + "items": { + "type": "string", + "enum": [ + "integrations:read", + "integrations:write", + "integrations:delete", + "entities:read", + "entities:write", + "entities:delete", + "credentials:read", + "credentials:write", + "credentials:delete", + "users:read", + "users:write", + "users:delete", + "admin:all" + ] + }, + "uniqueItems": true + }, + "preferences": { + "type": "object", + "description": "User preferences and settings", + "properties": { + "theme": { + "type": "string", + "enum": [ + "light", + "dark", + "auto" + ], + "default": "light" + }, + "language": { + "type": "string", + "pattern": "^[a-z]{2}(-[A-Z]{2})?$", + "default": "en" + }, + "timezone": { + "type": "string", + "description": "User timezone", + "default": "UTC" + }, + "notifications": { + "type": "object", + "properties": { + "email": { + "type": "boolean", + "default": true + }, + "push": { + "type": "boolean", + "default": false + }, + "integration_errors": { + "type": "boolean", + "default": true + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "isActive": { + "type": "boolean", + "description": "Whether user account is active", + "default": true + }, + "lastLogin": { + "$ref": "#/definitions/timestamp", + "description": "Last login timestamp" + }, + "createdAt": { + "$ref": "#/definitions/timestamp", + "description": "User creation timestamp" + }, + "updatedAt": { + "$ref": "#/definitions/timestamp", + "description": "Last update timestamp" + } + }, + "additionalProperties": false + }, + "credentialModel": { + "type": "object", + "description": "Frigg Credential model schema", + "required": [ + "id", + "userId" + ], + "properties": { + "id": { + "$ref": "#/definitions/objectId", + "description": "Unique credential identifier (maps to _id in MongoDB)" + }, + "userId": { + "$ref": "#/definitions/objectId", + "description": "Associated user ID" + }, + "externalId": { + "type": "string", + "description": "External system identifier", + "maxLength": 255 + }, + "authIsValid": { + "type": "boolean", + "description": "Whether authentication is currently valid", + "default": false + }, + "data": { + "type": "object", + "description": "Encrypted authentication data", + "properties": { + "access_token": { + "type": "string", + "description": "Encrypted access token" + }, + "refresh_token": { + "type": "string", + "description": "Encrypted refresh token" + }, + "token_type": { + "type": "string", + "description": "Token type", + "enum": [ + "Bearer", + "Basic", + "API-Key" + ], + "default": "Bearer" + }, + "expires_at": { + "$ref": "#/definitions/timestamp", + "description": "Token expiration time" + }, + "scope": { + "type": "string", + "description": "OAuth scope granted" + } + }, + "additionalProperties": true + }, + "metadata": { + "type": "object", + "description": "Additional credential metadata", + "properties": { + "integration_version": { + "type": "string", + "description": "Version of integration used" + }, + "last_validated": { + "$ref": "#/definitions/timestamp", + "description": "Last validation timestamp" + }, + "validation_errors": { + "type": "array", + "description": "Recent validation errors", + "items": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + }, + "timestamp": { + "$ref": "#/definitions/timestamp", + "description": "Error timestamp" + }, + "code": { + "type": "string", + "description": "Error code" + } + }, + "required": [ + "error", + "timestamp" + ] + } + } + }, + "additionalProperties": true + }, + "isActive": { + "type": "boolean", + "description": "Whether credential is active", + "default": true + }, + "createdAt": { + "$ref": "#/definitions/timestamp", + "description": "Credential creation timestamp" + }, + "updatedAt": { + "$ref": "#/definitions/timestamp", + "description": "Last update timestamp" + } + }, + "additionalProperties": false + }, + "entityModel": { + "type": "object", + "description": "Frigg Entity model schema", + "required": [ + "id", + "credentialId", + "userId" + ], + "properties": { + "id": { + "$ref": "#/definitions/objectId", + "description": "Unique entity identifier (maps to _id in MongoDB)" + }, + "credentialId": { + "$ref": "#/definitions/objectId", + "description": "Associated credential ID" + }, + "userId": { + "$ref": "#/definitions/objectId", + "description": "Associated user ID" + }, + "moduleName": { + "type": "string", + "description": "Module type identifier (e.g., 'salesforce', 'hubspot', 'attio'). Represents the API/service being integrated. Historically mapped from Mongoose discriminator (__t).", + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", + "maxLength": 100 + }, + "name": { + "type": "string", + "description": "Entity display name", + "maxLength": 255 + }, + "externalId": { + "type": "string", + "description": "External system identifier", + "maxLength": 255 + }, + "data": { + "type": "object", + "description": "Entity-specific data", + "additionalProperties": true + }, + "config": { + "type": "object", + "description": "Entity configuration settings", + "properties": { + "sync_enabled": { + "type": "boolean", + "description": "Whether synchronization is enabled", + "default": true + }, + "sync_direction": { + "type": "string", + "description": "Synchronization direction", + "enum": [ + "incoming", + "outgoing", + "bidirectional" + ], + "default": "bidirectional" + }, + "sync_frequency": { + "type": "string", + "description": "Synchronization frequency", + "enum": [ + "real-time", + "hourly", + "daily", + "weekly", + "manual" + ], + "default": "hourly" + }, + "field_mappings": { + "type": "object", + "description": "Field mapping configuration", + "patternProperties": { + "^[a-zA-Z][a-zA-Z0-9_]*$": { + "type": "string", + "description": "Target field mapping" + } + } + } + }, + "additionalProperties": false + }, + "status": { + "type": "string", + "description": "Entity status", + "enum": [ + "active", + "inactive", + "error", + "syncing", + "pending" + ], + "default": "active" + }, + "lastSync": { + "$ref": "#/definitions/timestamp", + "description": "Last synchronization timestamp" + }, + "syncErrors": { + "type": "array", + "description": "Recent synchronization errors", + "items": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + }, + "timestamp": { + "$ref": "#/definitions/timestamp", + "description": "Error timestamp" + }, + "code": { + "type": "string", + "description": "Error code" + }, + "retryable": { + "type": "boolean", + "description": "Whether error is retryable", + "default": true + } + }, + "required": [ + "error", + "timestamp" + ] + } + }, + "isActive": { + "type": "boolean", + "description": "Whether entity is active", + "default": true + }, + "createdAt": { + "$ref": "#/definitions/timestamp", + "description": "Entity creation timestamp" + }, + "updatedAt": { + "$ref": "#/definitions/timestamp", + "description": "Last update timestamp" + } + }, + "additionalProperties": false + } + }, + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/userModel" + }, + "credential": { + "$ref": "#/definitions/credentialModel" + }, + "entity": { + "$ref": "#/definitions/entityModel" + } + }, + "examples": [ + { + "user": { + "id": "507f1f77bcf86cd799439011", + "email": "user@example.com", + "firstName": "John", + "lastName": "Doe", + "organization": "Example Corp", + "role": "user", + "permissions": [ + "integrations:read", + "integrations:write" + ], + "preferences": { + "theme": "dark", + "language": "en", + "notifications": { + "email": true, + "integration_errors": true + } + }, + "isActive": true, + "createdAt": "2023-01-01T00:00:00Z", + "updatedAt": "2023-01-01T00:00:00Z" + }, + "credential": { + "id": "507f1f77bcf86cd799439012", + "userId": "507f1f77bcf86cd799439011", + "externalId": "12345", + "authIsValid": true, + "data": { + "access_token": "encrypted_access_token", + "refresh_token": "encrypted_refresh_token", + "token_type": "Bearer", + "expires_at": "2023-12-31T23:59:59Z", + "scope": "contacts companies deals" + }, + "metadata": { + "integration_version": "2.0.0", + "last_validated": "2023-01-01T00:00:00Z" + }, + "isActive": true, + "createdAt": "2023-01-01T00:00:00Z", + "updatedAt": "2023-01-01T00:00:00Z" + }, + "entity": { + "id": "507f1f77bcf86cd799439013", + "credentialId": "507f1f77bcf86cd799439012", + "userId": "507f1f77bcf86cd799439011", + "name": "HubSpot Contacts", + "externalId": "hubspot_contacts_12345", + "config": { + "sync_enabled": true, + "sync_direction": "bidirectional", + "sync_frequency": "hourly", + "field_mappings": { + "email": "email", + "firstname": "first_name", + "lastname": "last_name" + } + }, + "status": "active", + "lastSync": "2023-01-01T00:00:00Z", + "isActive": true, + "createdAt": "2023-01-01T00:00:00Z", + "updatedAt": "2023-01-01T00:00:00Z" + } + } + ] +} \ No newline at end of file diff --git a/website/schemas/environment-config/v1.0.json b/website/schemas/environment-config/v1.0.json new file mode 100644 index 000000000..b22371cbb --- /dev/null +++ b/website/schemas/environment-config/v1.0.json @@ -0,0 +1,387 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://friggframework.org/schemas/environment-config/v1.0.json", + "title": "Frigg Environment Configuration", + "description": "Schema for environment variable configuration and validation in Frigg applications.", + "type": "object", + "properties": { + "environments": { + "type": "object", + "description": "Environment-specific configurations", + "patternProperties": { + "^(development|staging|production|test)$": { + "type": "object", + "description": "Environment configuration", + "properties": { + "variables": { + "type": "object", + "description": "Environment variables for this environment", + "patternProperties": { + "^[A-Z][A-Z0-9_]*$": { + "type": "object", + "description": "Environment variable configuration", + "required": ["value"], + "properties": { + "value": { + "type": "string", + "description": "Variable value" + }, + "required": { + "type": "boolean", + "description": "Whether this variable is required", + "default": true + }, + "sensitive": { + "type": "boolean", + "description": "Whether this variable contains sensitive data", + "default": false + }, + "description": { + "type": "string", + "description": "Human-readable description of the variable", + "maxLength": 500 + }, + "validation": { + "type": "object", + "description": "Validation rules for the variable", + "properties": { + "pattern": { + "type": "string", + "description": "Regex pattern for validation" + }, + "minLength": { + "type": "integer", + "description": "Minimum length", + "minimum": 0 + }, + "maxLength": { + "type": "integer", + "description": "Maximum length", + "minimum": 1 + }, + "enum": { + "type": "array", + "description": "Allowed values", + "items": {"type": "string"}, + "uniqueItems": true + }, + "format": { + "type": "string", + "description": "Expected format", + "enum": ["url", "email", "uuid", "base64", "jwt", "arn"] + } + }, + "additionalProperties": false + }, + "defaultValue": { + "type": "string", + "description": "Default value if not provided" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "integrations": { + "type": "object", + "description": "Integration-specific environment requirements", + "patternProperties": { + "^[a-zA-Z][a-zA-Z0-9_-]*$": { + "type": "object", + "description": "Integration environment configuration", + "properties": { + "required": { + "type": "array", + "description": "Required environment variables for this integration", + "items": { + "type": "string", + "pattern": "^[A-Z][A-Z0-9_]*$" + }, + "uniqueItems": true + }, + "optional": { + "type": "array", + "description": "Optional environment variables for this integration", + "items": { + "type": "string", + "pattern": "^[A-Z][A-Z0-9_]*$" + }, + "uniqueItems": true + }, + "prefix": { + "type": "string", + "description": "Common prefix for integration variables", + "pattern": "^[A-Z][A-Z0-9_]*$" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "validation": { + "type": "object", + "description": "Environment-level validation rules", + "properties": { + "strict": { + "type": "boolean", + "description": "Whether to enforce strict validation", + "default": false + }, + "allowUndefined": { + "type": "boolean", + "description": "Whether to allow undefined variables", + "default": true + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "global": { + "type": "object", + "description": "Global environment configuration", + "properties": { + "maskingPatterns": { + "type": "array", + "description": "Patterns for masking sensitive values in logs", + "items": { + "type": "object", + "required": ["pattern", "replacement"], + "properties": { + "pattern": { + "type": "string", + "description": "Regex pattern to match sensitive data" + }, + "replacement": { + "type": "string", + "description": "Replacement string for matched patterns", + "default": "***" + }, + "description": { + "type": "string", + "description": "Description of what this pattern matches" + } + }, + "additionalProperties": false + } + }, + "encryptionKeys": { + "type": "object", + "description": "Encryption key configuration", + "properties": { + "kmsKeyId": { + "type": "string", + "description": "AWS KMS key ID for encryption", + "pattern": "^[a-f0-9-]+$" + }, + "algorithm": { + "type": "string", + "description": "Encryption algorithm", + "enum": ["AES-256-GCM", "AES-256-CBC"], + "default": "AES-256-GCM" + } + }, + "additionalProperties": false + }, + "auditLog": { + "type": "object", + "description": "Audit logging configuration", + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether audit logging is enabled", + "default": true + }, + "logChanges": { + "type": "boolean", + "description": "Whether to log environment variable changes", + "default": true + }, + "logAccess": { + "type": "boolean", + "description": "Whether to log environment variable access", + "default": false + }, + "retention": { + "type": "integer", + "description": "Audit log retention period in days", + "minimum": 1, + "maximum": 2557, + "default": 90 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "templates": { + "type": "object", + "description": "Environment variable templates for different integration types", + "patternProperties": { + "^[a-zA-Z][a-zA-Z0-9_-]*$": { + "type": "object", + "description": "Template for specific integration type", + "properties": { + "variables": { + "type": "array", + "description": "Required environment variables for this template", + "items": { + "type": "object", + "required": ["name", "description"], + "properties": { + "name": { + "type": "string", + "description": "Variable name", + "pattern": "^[A-Z][A-Z0-9_]*$" + }, + "description": { + "type": "string", + "description": "Variable description" + }, + "required": { + "type": "boolean", + "description": "Whether variable is required", + "default": true + }, + "sensitive": { + "type": "boolean", + "description": "Whether variable is sensitive", + "default": false + }, + "example": { + "type": "string", + "description": "Example value for documentation" + } + }, + "additionalProperties": false + } + }, + "description": { + "type": "string", + "description": "Template description" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "examples": [ + { + "environments": { + "development": { + "variables": { + "DATABASE_URL": { + "value": "mongodb://localhost:27017/frigg-dev", + "required": true, + "sensitive": true, + "description": "MongoDB connection string", + "validation": { + "format": "url", + "pattern": "^mongodb://" + } + }, + "JWT_SECRET": { + "value": "dev-secret-key", + "required": true, + "sensitive": true, + "description": "JWT signing secret", + "validation": { + "minLength": 32 + } + }, + "FRIGG_LOG_LEVEL": { + "value": "DEBUG", + "required": false, + "description": "Minimum log level (ADR-048)", + "validation": { + "enum": ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"] + }, + "defaultValue": "INFO" + } + }, + "integrations": { + "hubspot": { + "required": ["HUBSPOT_CLIENT_ID", "HUBSPOT_CLIENT_SECRET"], + "optional": ["HUBSPOT_SCOPE"], + "prefix": "HUBSPOT" + } + } + }, + "production": { + "variables": { + "DATABASE_URL": { + "value": "${AWS_SSM:/frigg/prod/database-url}", + "required": true, + "sensitive": true, + "description": "MongoDB connection string from SSM" + }, + "JWT_SECRET": { + "value": "${AWS_SSM:/frigg/prod/jwt-secret}", + "required": true, + "sensitive": true, + "description": "JWT signing secret from SSM" + } + }, + "validation": { + "strict": true, + "allowUndefined": false + } + } + }, + "global": { + "maskingPatterns": [ + { + "pattern": "(?i)(password|secret|key|token)=([^\\s&]+)", + "replacement": "$1=***", + "description": "Mask password and secret parameters" + } + ], + "encryptionKeys": { + "kmsKeyId": "12345678-1234-1234-1234-123456789abc", + "algorithm": "AES-256-GCM" + }, + "auditLog": { + "enabled": true, + "logChanges": true, + "retention": 90 + } + }, + "templates": { + "oauth2": { + "variables": [ + { + "name": "CLIENT_ID", + "description": "OAuth2 client identifier", + "required": true, + "example": "your-client-id" + }, + { + "name": "CLIENT_SECRET", + "description": "OAuth2 client secret", + "required": true, + "sensitive": true, + "example": "your-client-secret" + }, + { + "name": "REDIRECT_URI", + "description": "OAuth2 redirect URI", + "required": true, + "example": "https://your-app.com/auth/callback" + } + ], + "description": "Standard OAuth2 integration template" + } + } + } + ] +} \ No newline at end of file diff --git a/website/schemas/integration-definition/v1.0.json b/website/schemas/integration-definition/v1.0.json new file mode 100644 index 000000000..dae3c3b4c --- /dev/null +++ b/website/schemas/integration-definition/v1.0.json @@ -0,0 +1,389 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://friggframework.org/schemas/integration-definition/v1.0.json", + "title": "Frigg Integration Definition", + "description": "Schema for defining a Frigg integration including configuration, options, and display properties.", + "type": "object", + "required": ["name", "version"], + "properties": { + "name": { + "type": "string", + "description": "Integration name identifier", + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", + "minLength": 1, + "maxLength": 100 + }, + "version": { + "type": "string", + "description": "Integration version following semantic versioning", + "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$" + }, + "supportedVersions": { + "type": "array", + "description": "List of supported Frigg framework versions", + "items": { + "type": "string", + "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$" + }, + "uniqueItems": true + }, + "events": { + "type": "array", + "description": "List of events this integration can emit or handle", + "items": { + "type": "string", + "pattern": "^[a-zA-Z][a-zA-Z0-9._-]*$" + }, + "uniqueItems": true + }, + "options": { + "type": "object", + "description": "Integration configuration options", + "properties": { + "type": { + "type": "string", + "description": "Integration type classification", + "enum": ["api", "webhook", "sync", "transform", "custom"] + }, + "hasUserConfig": { + "type": "boolean", + "description": "Whether integration requires user-specific configuration", + "default": false + }, + "isMany": { + "type": "boolean", + "description": "Whether multiple instances can exist per user", + "default": false + }, + "requiresNewEntity": { + "type": "boolean", + "description": "Whether integration requires creating new entities", + "default": true + }, + "display": { + "type": "object", + "description": "Display properties for UI representation", + "required": ["name", "description"], + "properties": { + "name": { + "type": "string", + "description": "Human-readable display name", + "minLength": 1, + "maxLength": 200 + }, + "description": { + "type": "string", + "description": "Integration description", + "minLength": 1, + "maxLength": 1000 + }, + "detailsUrl": { + "type": "string", + "description": "URL to integration documentation or details", + "format": "uri" + }, + "icon": { + "type": "string", + "description": "Icon identifier or URL for the integration", + "minLength": 1 + }, + "category": { + "type": "string", + "description": "Integration category for grouping", + "enum": [ + "CRM", "Marketing", "Communication", "ECommerce", + "Finance", "Analytics", "Storage", "Development", + "Productivity", "Social", "Other" + ] + }, + "tags": { + "type": "array", + "description": "Tags for filtering and search", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + "uniqueItems": true + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "model": { + "type": "object", + "description": "Integration data model configuration", + "properties": { + "entities": { + "type": "array", + "description": "Entity types managed by this integration", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "userId": { + "type": "string", + "description": "User identifier field" + }, + "status": { + "type": "string", + "description": "Integration status", + "enum": ["active", "inactive", "error", "pending", "disabled"] + }, + "config": { + "type": "object", + "description": "Integration-specific configuration object" + }, + "messages": { + "type": "object", + "description": "Integration messaging configuration", + "properties": { + "errors": { + "type": "array", + "description": "Error messages", + "items": {"type": "string"} + }, + "warnings": { + "type": "array", + "description": "Warning messages", + "items": {"type": "string"} + }, + "info": { + "type": "array", + "description": "Informational messages", + "items": {"type": "string"} + }, + "logs": { + "type": "array", + "description": "Log messages", + "items": {"type": "string"} + } + }, + "additionalProperties": false + } + }, + "additionalProperties": true + }, + "capabilities": { + "type": "object", + "description": "Integration capabilities and features", + "properties": { + "auth": { + "type": "array", + "description": "Supported authentication methods", + "items": { + "type": "string", + "enum": ["oauth2", "api-key", "basic", "token", "custom"] + }, + "uniqueItems": true + }, + "webhooks": { + "type": "boolean", + "description": "Supports webhook functionality", + "default": false + }, + "realtime": { + "type": "boolean", + "description": "Supports real-time data updates", + "default": false + }, + "sync": { + "type": "object", + "description": "Data synchronization capabilities", + "properties": { + "bidirectional": { + "type": "boolean", + "description": "Supports bidirectional sync", + "default": false + }, + "incremental": { + "type": "boolean", + "description": "Supports incremental sync", + "default": false + }, + "batchSize": { + "type": "integer", + "description": "Maximum batch size for sync operations", + "minimum": 1, + "maximum": 10000 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "requirements": { + "type": "object", + "description": "Integration requirements and dependencies", + "properties": { + "environment": { + "type": "object", + "description": "Required environment variables", + "patternProperties": { + "^[A-Z][A-Z0-9_]*$": { + "type": "object", + "properties": { + "required": {"type": "boolean", "default": true}, + "description": {"type": "string"}, + "example": {"type": "string"} + }, + "required": ["description"] + } + } + }, + "permissions": { + "type": "array", + "description": "Required permissions or scopes", + "items": {"type": "string"}, + "uniqueItems": true + }, + "dependencies": { + "type": "object", + "description": "Package dependencies", + "patternProperties": { + "^[a-zA-Z0-9@/_-]+$": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + "modules": { + "type": "object", + "description": "API modules used by this integration. Keys are reference names, values contain module definitions.", + "additionalProperties": { + "type": "object", + "description": "Module configuration with its definition", + "required": ["definition"], + "properties": { + "definition": { + "type": "object", + "description": "The API module definition (e.g., from @friggframework/api-module-*)" + } + }, + "additionalProperties": true + } + }, + "hasUserConfig": { + "type": "boolean", + "description": "Whether integration requires user-specific configuration", + "default": false + }, + "webhooks": { + "type": "object", + "description": "Webhook configuration for the integration", + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether webhooks are enabled", + "default": false + } + }, + "additionalProperties": true + }, + "display": { + "type": "object", + "description": "Display properties for UI representation", + "properties": { + "label": { + "type": "string", + "description": "Human-readable display label" + }, + "description": { + "type": "string", + "description": "Integration description" + }, + "category": { + "type": "string", + "description": "Integration category for grouping" + }, + "detailsUrl": { + "type": "string", + "description": "URL to integration documentation or details", + "format": "uri" + }, + "icon": { + "type": "string", + "description": "Icon identifier or URL for the integration" + } + }, + "additionalProperties": true + }, + "routes": { + "type": "array", + "description": "Custom routes exposed by this integration", + "items": { + "type": "object", + "required": ["path", "method", "event"], + "properties": { + "path": { + "type": "string", + "description": "Route path pattern" + }, + "method": { + "type": "string", + "description": "HTTP method", + "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"] + }, + "event": { + "type": "string", + "description": "Event name triggered by this route" + } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true, + "examples": [ + { + "name": "hubspot", + "version": "2.0.0", + "supportedVersions": ["2.0.0", "1.9.0"], + "events": ["contact.created", "deal.updated"], + "options": { + "type": "api", + "hasUserConfig": true, + "isMany": false, + "requiresNewEntity": true, + "display": { + "name": "HubSpot CRM", + "description": "Integrate with HubSpot CRM for contacts, deals, and marketing automation", + "detailsUrl": "https://docs.frigg.dev/integrations/hubspot", + "icon": "hubspot", + "category": "CRM", + "tags": ["crm", "marketing", "sales"] + } + }, + "capabilities": { + "auth": ["oauth2"], + "webhooks": true, + "realtime": false, + "sync": { + "bidirectional": true, + "incremental": true, + "batchSize": 100 + } + }, + "requirements": { + "environment": { + "HUBSPOT_CLIENT_ID": { + "required": true, + "description": "HubSpot OAuth client ID", + "example": "your-client-id" + }, + "HUBSPOT_CLIENT_SECRET": { + "required": true, + "description": "HubSpot OAuth client secret", + "example": "your-client-secret" + } + }, + "permissions": ["contacts", "deals", "companies"] + } + } + ] +} \ No newline at end of file diff --git a/website/schemas/serverless-config/v1.0.json b/website/schemas/serverless-config/v1.0.json new file mode 100644 index 000000000..730c6b920 --- /dev/null +++ b/website/schemas/serverless-config/v1.0.json @@ -0,0 +1,398 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://friggframework.org/schemas/serverless-config/v1.0.json", + "title": "Frigg Serverless Configuration", + "description": "Schema for Serverless Framework configuration used in Frigg applications for AWS deployment.", + "type": "object", + "required": ["service", "provider"], + "properties": { + "frameworkVersion": { + "type": "string", + "description": "Serverless Framework version constraint", + "pattern": "^[><=]*\\d+\\.\\d+\\.\\d+$", + "examples": [">=3.17.0", "3.38.0"] + }, + "service": { + "type": "string", + "description": "Service name for the Serverless application", + "pattern": "^[a-zA-Z][a-zA-Z0-9-]*$", + "minLength": 1, + "maxLength": 128 + }, + "provider": { + "type": "object", + "description": "Cloud provider configuration", + "required": ["name", "runtime"], + "properties": { + "name": { + "type": "string", + "description": "Cloud provider name", + "enum": ["aws", "azure", "gcp"] + }, + "runtime": { + "type": "string", + "description": "Runtime environment", + "enum": ["nodejs20.x", "nodejs22.x", "python3.9", "python3.10", "python3.11", "python3.12"] + }, + "timeout": { + "type": "integer", + "description": "Function timeout in seconds", + "minimum": 1, + "maximum": 900, + "default": 30 + }, + "region": { + "type": "string", + "description": "AWS region for deployment", + "enum": [ + "us-east-1", "us-east-2", "us-west-1", "us-west-2", + "eu-west-1", "eu-west-2", "eu-central-1", "ap-southeast-1", + "ap-southeast-2", "ap-northeast-1" + ], + "default": "us-east-1" + }, + "stage": { + "type": "string", + "description": "Deployment stage", + "enum": ["dev", "staging", "prod", "test"], + "default": "dev" + }, + "environment": { + "type": "object", + "description": "Environment variables for all functions", + "patternProperties": { + "^[A-Z][A-Z0-9_]*$": { + "type": "string" + } + }, + "additionalProperties": false + }, + "vpc": { + "type": "object", + "description": "VPC configuration", + "properties": { + "securityGroupIds": { + "type": "array", + "description": "Security group IDs", + "items": { + "type": "string", + "pattern": "^sg-[a-z0-9]+$" + } + }, + "subnetIds": { + "type": "array", + "description": "Subnet IDs", + "items": { + "type": "string", + "pattern": "^subnet-[a-z0-9]+$" + } + } + }, + "additionalProperties": false + }, + "iam": { + "type": "object", + "description": "IAM role configuration", + "properties": { + "role": { + "type": "string", + "description": "IAM role ARN or role statements" + }, + "roleStatements": { + "type": "array", + "description": "IAM policy statements", + "items": { + "type": "object", + "required": ["Effect", "Action"], + "properties": { + "Effect": { + "type": "string", + "enum": ["Allow", "Deny"] + }, + "Action": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "Resource": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "Condition": { + "type": "object" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "functions": { + "type": "object", + "description": "Lambda function definitions", + "patternProperties": { + "^[a-zA-Z][a-zA-Z0-9_-]*$": { + "type": "object", + "required": ["handler"], + "properties": { + "handler": { + "type": "string", + "description": "Function handler path", + "pattern": "^[./a-zA-Z0-9_-]+\\.[a-zA-Z][a-zA-Z0-9_]*$" + }, + "description": { + "type": "string", + "description": "Function description", + "maxLength": 256 + }, + "timeout": { + "type": "integer", + "description": "Function-specific timeout", + "minimum": 1, + "maximum": 900 + }, + "memorySize": { + "type": "integer", + "description": "Memory allocation in MB", + "minimum": 128, + "maximum": 10240, + "multipleOf": 64 + }, + "environment": { + "type": "object", + "description": "Function-specific environment variables", + "patternProperties": { + "^[A-Z][A-Z0-9_]*$": { + "type": "string" + } + } + }, + "events": { + "type": "array", + "description": "Function event triggers", + "items": { + "type": "object", + "properties": { + "http": { + "type": "object", + "description": "HTTP API Gateway event", + "required": ["path", "method"], + "properties": { + "path": { + "type": "string", + "description": "API path", + "pattern": "^/[a-zA-Z0-9/_{}+-]*$" + }, + "method": { + "type": "string", + "description": "HTTP method", + "enum": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "ANY"] + }, + "cors": { + "type": "boolean", + "description": "Enable CORS", + "default": true + }, + "authorizer": { + "type": "string", + "description": "Authorizer function name" + } + }, + "additionalProperties": false + }, + "sqs": { + "type": "object", + "description": "SQS queue event", + "required": ["arn"], + "properties": { + "arn": { + "type": "string", + "description": "SQS queue ARN", + "pattern": "^arn:aws:sqs:[a-z0-9-]+:\\d{12}:[a-zA-Z0-9_-]+$" + }, + "batchSize": { + "type": "integer", + "description": "Batch size for SQS processing", + "minimum": 1, + "maximum": 10 + }, + "functionResponseType": { + "type": "string", + "description": "Function response type for partial batch failure reporting", + "enum": ["ReportBatchItemFailures"] + } + }, + "additionalProperties": false + }, + "schedule": { + "type": "object", + "description": "Scheduled event", + "required": ["rate"], + "properties": { + "rate": { + "type": "string", + "description": "Schedule expression", + "pattern": "^(rate|cron)\\(.+\\)$" + }, + "enabled": { + "type": "boolean", + "description": "Whether schedule is enabled", + "default": true + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "resources": { + "type": "object", + "description": "CloudFormation resources", + "properties": { + "Resources": { + "type": "object", + "description": "CloudFormation resource definitions", + "patternProperties": { + "^[a-zA-Z][a-zA-Z0-9]*$": { + "type": "object", + "required": ["Type"], + "properties": { + "Type": { + "type": "string", + "description": "AWS resource type", + "pattern": "^AWS::[a-zA-Z0-9]+::[a-zA-Z0-9]+$" + }, + "Properties": { + "type": "object", + "description": "Resource properties" + }, + "DependsOn": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ], + "description": "Resource dependencies" + } + }, + "additionalProperties": false + } + } + }, + "Outputs": { + "type": "object", + "description": "CloudFormation outputs", + "patternProperties": { + "^[a-zA-Z][a-zA-Z0-9]*$": { + "type": "object", + "required": ["Value"], + "properties": { + "Value": { + "type": "string", + "description": "Output value" + }, + "Description": { + "type": "string", + "description": "Output description" + }, + "Export": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Export name" + } + } + } + }, + "additionalProperties": false + } + } + } + }, + "additionalProperties": false + }, + "plugins": { + "type": "array", + "description": "Serverless plugins", + "items": { + "type": "string", + "description": "Plugin name" + }, + "uniqueItems": true + }, + "custom": { + "type": "object", + "description": "Custom configuration for plugins and other settings", + "properties": { + "webpack": { + "type": "object", + "description": "Webpack configuration for serverless-webpack plugin" + }, + "dotenv": { + "type": "object", + "description": "Environment file configuration" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": false, + "examples": [ + { + "frameworkVersion": ">=3.17.0", + "service": "frigg-backend", + "provider": { + "name": "aws", + "runtime": "nodejs22.x", + "timeout": 30, + "region": "us-east-1", + "environment": { + "STAGE": "${opt:stage}", + "NODE_ENV": "production" + } + }, + "functions": { + "api": { + "handler": "./src/api.handler", + "events": [ + { + "http": { + "path": "/api/{proxy+}", + "method": "ANY", + "cors": true + } + } + ] + } + }, + "resources": { + "Resources": { + "ErrorQueue": { + "Type": "AWS::SQS::Queue", + "Properties": { + "QueueName": "frigg-errors-${opt:stage}" + } + } + } + }, + "plugins": [ + "serverless-webpack", + "serverless-offline" + ] + } + ] +} \ No newline at end of file From f070722ad939d3221446e24653352373a5f4e0f6 Mon Sep 17 00:00:00 2001 From: Sean Matthews Date: Sat, 24 Oct 2026 08:03:29 -0400 Subject: [PATCH 2/2] chore(website): re-sync the hosted app-definition schema after #666 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013b2UUFaQ5Lk6TFPqzRr1j7 --- website/schemas/app-definition/v1.0.json | 699 +++++++++++++++++------ 1 file changed, 534 insertions(+), 165 deletions(-) diff --git a/website/schemas/app-definition/v1.0.json b/website/schemas/app-definition/v1.0.json index f975abd9a..8276a78bb 100644 --- a/website/schemas/app-definition/v1.0.json +++ b/website/schemas/app-definition/v1.0.json @@ -22,22 +22,35 @@ }, "label": { "type": "string", - "description": "Human-readable display name for the application", + "description": "Human-readable display name for the application. Not read by the Frigg runtime: it has no effect. Kept so existing definitions stay valid; `frigg validate` warns when it is set.", "minLength": 1, "maxLength": 200, - "examples": ["My Frigg Application", "Customer Portal Integrations"] + "examples": ["My Frigg Application", "Customer Portal Integrations"], + "deprecated": true + }, + "region": { + "type": "string", + "description": "AWS region. The deploy region comes from AWS_REGION (default us-east-1), not from this key. Not read by the Frigg runtime: it has no effect. Kept so existing definitions stay valid; `frigg validate` warns when it is set.", + "deprecated": true + }, + "stage": { + "type": "string", + "description": "Stage name used only to infer the KMS key alias. The deployed stage comes from `--stage`." }, "managementMode": { "type": "string", - "description": "Infrastructure management mode: 'managed' lets Frigg manage resources, 'discover' uses existing resources, 'custom' for manual configuration", - "enum": ["managed", "discover", "custom"], - "default": "managed" + "description": "Infrastructure management mode. 'managed': Frigg creates and owns the VPC, database and KMS key it needs (with vpcIsolation 'isolated', per stage). 'existing': use existing resources given by id. 'discover' (also the behaviour when omitted): find existing resources and create what is missing. 'custom' is accepted for compatibility and behaves like 'discover'.", + "enum": ["managed", "discover", "existing", "custom"] }, "vpcIsolation": { "type": "string", - "description": "VPC isolation strategy: 'isolated' creates separate VPC per stage, 'shared' reuses VPC across stages", - "enum": ["isolated", "shared"], - "default": "isolated" + "description": "With managementMode 'managed': 'isolated' gives each stage its own VPC, database and key; 'shared' reuses them across stages. Set it explicitly: when it is omitted the builders do not agree on a default (see ADR-051).", + "enum": ["isolated", "shared"] + }, + "usePrismaLambdaLayer": { + "type": "boolean", + "description": "Ship Prisma in a shared Lambda layer. false bundles Prisma into each function instead.", + "default": true }, "environment": { "type": "object", @@ -68,93 +81,72 @@ }, "integrations": { "type": "array", - "description": "Array of integration classes to enable in the application", + "description": "The IntegrationBase subclasses this application exposes. JSON Schema cannot describe a JavaScript class, so validateAppDefinition checks a serialisable view in which each class is replaced by { Definition: }; `frigg validate` checks each Definition structurally (name, version, modules and their API module definitions).", "items": { "type": "object", - "description": "Integration class or configuration object" - }, - "default": [] - }, - "adminScripts": { - "type": "array", - "description": "Admin script classes (extending AdminScriptBase from @friggframework/admin-scripts) served under /admin/scripts", - "items": { - "description": "Admin script class" - }, - "default": [] - }, - "reports": { - "type": "array", - "description": "Report classes served by the admin report runner", - "items": { - "description": "Report class" - }, - "default": [] - }, - "admin": { - "type": "object", - "description": "Admin script and report runner options", - "properties": { - "enableScheduling": { - "type": "boolean", - "description": "Provision the EventBridge Scheduler group and role so admin scripts and reports can run on a schedule", - "default": false - }, - "includeBuiltinReports": { - "type": "boolean", - "description": "Serve the built-in reports alongside the app's own reports", - "default": false + "description": "An integration class, seen as { Definition } (see the integrations description)", + "required": ["Definition"], + "properties": { + "Definition": { + "type": "object", + "description": "The integration's static Definition (see integration-definition.schema.json)" + } } }, - "additionalProperties": false + "default": [] }, "user": { "type": "object", - "description": "User management and authentication configuration", + "description": "User management and authentication. Read by core when it loads the app definition; an omitted block gets the defaults below (ADR-051).", "properties": { "usePassword": { "type": "boolean", - "description": "Enable password-based authentication for user login", - "default": true + "description": "Users log in with username and password (POST /user/login). false: users are identified by appUserId (x-frigg-appuserid) instead.", + "default": false }, "password": { "type": "boolean", - "description": "DEPRECATED: Use 'usePassword' instead. Enable password-based authentication", - "default": true + "description": "DEPRECATED alias of usePassword: when usePassword is not set, the loader copies this value into it.", + "deprecated": true }, "primary": { "type": "string", - "description": "Primary user type for authentication", + "description": "Primary user type. Any value other than 'organization' is treated as 'individual'.", "enum": ["individual", "organization"], "default": "individual" }, "individualUserRequired": { "type": "boolean", - "description": "Require individual user for authentication", + "description": "Require an individual user. POST /user/login logs in individual users only when this is true.", "default": true }, "organizationUserRequired": { "type": "boolean", - "description": "Require organization user for authentication", + "description": "Require an organization user (looked up by appOrgId).", + "default": false + }, + "strictUserValidation": { + "type": "boolean", + "description": "With x-frigg headers, reject a user id that does not exist instead of creating and linking it.", "default": false }, "authModes": { "type": "object", - "description": "Authentication mode configuration - enables multiple authentication strategies. x-frigg-appUserId and x-frigg-appOrgId headers are automatically supported for user identification with any auth mode.", + "description": "Authentication modes. When the block is omitted, Frigg tokens and the shared secret are accepted. x-frigg-appUserId and x-frigg-appOrgId headers identify the user with any mode.", "properties": { "friggToken": { "type": "boolean", - "description": "Enable Frigg native bearer token authentication (from /user/login)", + "description": "Accept Frigg bearer tokens from /user/login. Enabled unless false.", "default": true }, "sharedSecret": { "type": "boolean", - "description": "Enable shared secret authentication for backend-to-backend communication (requires FRIGG_API_KEY env var and x-frigg-api-key header)", - "default": false + "description": "Accept the x-frigg-api-key header (checked against FRIGG_API_KEY) for backend-to-backend calls. Enabled unless false.", + "default": true }, "adopterJwt": { "type": "boolean", - "description": "Enable custom JWT authentication from adopter's auth system (requires jwtConfig)", + "description": "Accept JWTs from the adopter's own auth system (requires jwtConfig). Enabled only when true.", "default": false } }, @@ -166,7 +158,7 @@ "properties": { "secret": { "type": "string", - "description": "JWT secret key for signature validation (should be set via environment variable)" + "description": "JWT secret key for signature validation, usually `process.env.JWT_SECRET`. Required when authModes.adopterJwt is true; `frigg validate` errors when the key is missing from the definition and warns when its value is empty in the shell that runs it (build and deploy validate before `.env` is loaded)." }, "userIdClaim": { "type": "string", @@ -185,27 +177,29 @@ "default": "HS256" } }, - "required": ["secret"], "additionalProperties": false }, "fields": { "type": "array", - "description": "Additional user fields", - "items": { - "type": "string" - }, - "examples": [["email", "firstName", "lastName"]] + "description": "Additional user fields. Not read by the Frigg runtime: it has no effect. Kept so existing definitions stay valid; `frigg validate` warns when it is set.", + "items": {"type": "string"}, + "examples": [ + ["email", "firstName", "lastName"] + ], + "deprecated": true }, "model": { "type": "object", - "description": "Custom user model class" + "description": "Custom user model class. Not read by the Frigg runtime: it has no effect. Kept so existing definitions stay valid; `frigg validate` warns when it is set.", + "deprecated": true } }, - "additionalProperties": false + "additionalProperties": false, + "default": {} }, "auth": { "type": "object", - "description": "Authentication configuration for integrations", + "description": "Authentication configuration. Not read by the Frigg runtime: it has no effect. Kept so existing definitions stay valid; `frigg validate` warns when it is set.", "properties": { "jwtSecret": { "type": "string", @@ -214,17 +208,16 @@ "tokenExpiry": { "type": "string", "description": "Token expiration time", - "pattern": "^\\d+[smhd]$", - "default": "24h" + "pattern": "^\\d+[smhd]$" }, "refreshTokenExpiry": { "type": "string", - "description": "Refresh token expiration time", - "pattern": "^\\d+[smhd]$", - "default": "7d" + "description": "Refresh token expiration time", + "pattern": "^\\d+[smhd]$" } }, - "additionalProperties": false + "additionalProperties": false, + "deprecated": true }, "database": { "type": "object", @@ -232,8 +225,8 @@ "properties": { "uri": { "type": "string", - "description": "Database connection URI (should be set via environment variable)", - "format": "uri" + "description": "Database connection URI (should be set via environment variable). Not read by the Frigg runtime: it has no effect. Kept so existing definitions stay valid; `frigg validate` warns when it is set.", + "deprecated": true }, "mongoDB": { "type": "object", @@ -246,7 +239,8 @@ }, "uri": { "type": "string", - "description": "MongoDB connection URI" + "description": "MongoDB connection URI. Not read by the Frigg runtime: it has no effect. Kept so existing definitions stay valid; `frigg validate` warns when it is set. The connection string comes from DATABASE_URL.", + "deprecated": true } }, "additionalProperties": true @@ -274,18 +268,16 @@ "properties": { "enable": { "type": "boolean", - "description": "Enable PostgreSQL database", - "default": false + "description": "Use PostgreSQL. true provisions or discovers Aurora (unless management says otherwise). When omitted, Aurora is not provisioned, but migrations and the Prisma layer still include PostgreSQL; set false for a MongoDB/DocumentDB app." }, "management": { "type": "string", - "description": "How to manage the PostgreSQL database", - "enum": ["discover", "create-new", "existing"], - "default": "discover" + "description": "How Frigg gets the Aurora cluster (ignored when managementMode is 'managed' or 'existing'). 'discover' (also when omitted): find one, else create one in the stack. 'managed': create it in the stack. 'use-existing': connect to `endpoint` without creating anything. 'external': Frigg provisions and discovers no database (no RDS/Aurora, no VPC requirement); the app connects to any PostgreSQL through the DATABASE_URL environment variable.", + "enum": ["discover", "managed", "use-existing", "external"] }, "endpoint": { "type": "string", - "description": "Existing database endpoint (when management='existing')", + "description": "Existing database endpoint (required when management='use-existing')", "examples": ["mydb.cluster-abc123.us-east-1.rds.amazonaws.com"] }, "port": { @@ -303,10 +295,15 @@ "description": "Database username", "default": "postgres" }, + "engineVersion": { + "type": "string", + "description": "Aurora PostgreSQL engine version for a cluster Frigg creates", + "default": "15.13" + }, "selfHeal": { "type": "boolean", - "description": "Automatically fix database configuration issues", - "default": true + "description": "Automatically fix database configuration issues. Not read by the Frigg runtime: it has no effect. Kept so existing definitions stay valid; `frigg validate` warns when it is set.", + "deprecated": true }, "publiclyAccessible": { "type": "boolean", @@ -315,7 +312,7 @@ }, "autoCreateCredentials": { "type": "boolean", - "description": "Automatically create Secrets Manager secret and rotate Aurora password when discovering existing cluster (only applies when management='discover')", + "description": "Create a Secrets Manager secret and rotate the Aurora password when a discovered cluster has none (management='discover' only)", "default": false }, "minCapacity": { @@ -330,7 +327,65 @@ "description": "Maximum Aurora Serverless capacity units (0.5-128)", "minimum": 0.5, "maximum": 128, - "default": 1 + "default": 4 + }, + "clusterId": { + "type": "string", + "description": "Only discover the Aurora cluster with this identifier" + }, + "instanceId": { + "type": "string", + "description": "Only discover the Aurora instance with this identifier" + }, + "ownership": { + "type": "object", + "description": "Per-resource ownership. When set, `management` is not translated.", + "properties": { + "cluster": { + "type": "string", + "enum": ["stack", "external", "auto"], + "description": "Aurora cluster. Who owns the resource: 'stack' (this stack creates it), 'external' (use the id in `external`), 'auto' (reuse what discovery finds, else create)" + }, + "instance": { + "type": "string", + "enum": ["stack", "external", "auto"], + "description": "Aurora instance. Who owns the resource: 'stack' (this stack creates it), 'external' (use the id in `external`), 'auto' (reuse what discovery finds, else create)" + }, + "subnetGroup": { + "type": "string", + "enum": ["stack", "external", "auto"], + "description": "DB subnet group. Who owns the resource: 'stack' (this stack creates it), 'external' (use the id in `external`), 'auto' (reuse what discovery finds, else create)" + }, + "secret": { + "type": "string", + "enum": ["stack", "external", "auto"], + "description": "Secrets Manager credentials secret. Who owns the resource: 'stack' (this stack creates it), 'external' (use the id in `external`), 'auto' (reuse what discovery finds, else create)" + } + }, + "additionalProperties": false + }, + "external": { + "type": "object", + "description": "Ids of externally owned resources (ownership 'external')", + "properties": { + "clusterIdentifier": {"type": "string"}, + "instanceIdentifier": {"type": "string"}, + "subnetGroupName": {"type": "string"}, + "secretArn": {"type": "string"} + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "dynamodb": { + "type": "object", + "description": "DynamoDB access from inside the VPC", + "properties": { + "enable": { + "type": "boolean", + "description": "Add a DynamoDB VPC endpoint", + "default": false } }, "additionalProperties": false @@ -344,14 +399,12 @@ "properties": { "fieldLevelEncryptionMethod": { "type": "string", - "description": "Method for field-level encryption", - "enum": ["kms", "aes"], - "default": "aes" + "description": "Field-level encryption key. 'kms': Frigg creates or discovers a KMS key and sets KMS_KEY_ARN. 'aes': the app supplies AES_KEY_ID and AES_KEY (list both in `environment`). 'none': no field-level encryption, chosen explicitly; no key is provisioned and deployed stages store sensitive fields unencrypted. Omitted: no key is provisioned and deployed stages store sensitive fields unencrypted (core logs a warning). Stages dev, test and local never encrypt.", + "enum": ["kms", "aes", "none"] }, "createResourceIfNoneFound": { "type": "boolean", - "description": "Create KMS key if none found during discovery", - "default": false + "description": "With 'kms' outside managementMode 'managed': create a KMS key when discovery finds none. When omitted, KMS_KEY_ARN falls back to the AWS_DISCOVERY_KMS_KEY_ID environment variable." }, "schema": { "type": "object", @@ -388,6 +441,48 @@ } } ] + }, + "ownership": { + "type": "object", + "description": "Per-resource ownership. When set, createResourceIfNoneFound is not translated.", + "properties": { + "key": { + "type": "string", + "enum": ["stack", "external", "auto"], + "description": "KMS key. Who owns the resource: 'stack' (this stack creates it), 'external' (use the id in `external`), 'auto' (reuse what discovery finds, else create)" + } + }, + "additionalProperties": false + }, + "kmsKeyAlias": { + "type": "boolean", + "description": "Create an alias for a KMS key Frigg creates", + "default": false + }, + "keyAlias": { + "type": "string", + "description": "Only discover the KMS key with this alias", + "pattern": "^alias/", + "examples": ["alias/my-app-key"] + }, + "disable": { + "type": "object", + "description": "Opt specific core-encrypted fields out of encryption: { Model: ['field.path'] }. Existing encrypted values still decrypt.", + "patternProperties": { + "^[A-Z][a-zA-Z0-9]*$": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false + }, + "enable": { + "type": "boolean", + "description": "Encryption is controlled by fieldLevelEncryptionMethod. Not read by the Frigg runtime: it has no effect. Kept so existing definitions stay valid; `frigg validate` warns when it is set.", + "deprecated": true } }, "additionalProperties": false @@ -398,24 +493,23 @@ "properties": { "enable": { "type": "boolean", - "description": "Enable VPC for Lambda functions", + "description": "Run the Lambda functions in a VPC", "default": false }, "management": { "type": "string", - "description": "How to manage VPC infrastructure", - "enum": ["discover", "create-new", "existing"], - "default": "discover" + "description": "How Frigg gets the VPC. 'discover' (also when omitted): find one (discovery fails if none exists). 'create-new': create VPC, security group and subnets in the stack. 'use-existing': use `vpcId` (and securityGroupIds / subnets.ids when given). Ignored when `ownership` is set.", + "enum": ["discover", "create-new", "use-existing"] }, "vpcId": { "type": "string", - "description": "Existing VPC ID (when management='existing')", + "description": "Existing VPC ID (required when management='use-existing')", "pattern": "^vpc-[a-z0-9]+$", "examples": ["vpc-abc123"] }, "cidrBlock": { "type": "string", - "description": "CIDR block for VPC (when management='create-new')", + "description": "CIDR block for a VPC Frigg creates", "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$", "default": "10.0.0.0/16" }, @@ -426,7 +520,9 @@ "type": "string", "pattern": "^sg-[a-z0-9]+$" }, - "examples": [["sg-abc123", "sg-def456"]] + "examples": [ + ["sg-abc123", "sg-def456"] + ] }, "subnets": { "type": "object", @@ -434,19 +530,20 @@ "properties": { "management": { "type": "string", - "description": "How to manage subnets", - "enum": ["discover", "create-new", "existing"], - "default": "discover" + "description": "'create': create the subnets in the stack. 'use-existing': use `ids` (at least 2). 'discover', 'create-new' and 'existing' are accepted for compatibility but have no effect (`frigg validate` warns).", + "enum": ["create", "use-existing", "discover", "create-new", "existing"] }, "ids": { "type": "array", - "description": "Existing subnet IDs", + "description": "Existing subnet IDs (management='use-existing')", "items": { "type": "string", "pattern": "^subnet-[a-z0-9]+$" }, "minItems": 2, - "examples": [["subnet-abc123", "subnet-def456"]] + "examples": [ + ["subnet-abc123", "subnet-def456"] + ] } }, "additionalProperties": false @@ -457,13 +554,12 @@ "properties": { "management": { "type": "string", - "description": "How to manage NAT Gateway", - "enum": ["discover", "create-new", "existing"], - "default": "discover" + "description": "'createAndManage': reuse a well-placed NAT gateway or create one. 'discover', 'create-new' and 'existing' are accepted for compatibility but have no effect (`frigg validate` warns). Set `id` to use an existing NAT gateway.", + "enum": ["createAndManage", "discover", "create-new", "existing"] }, "id": { "type": "string", - "description": "Existing NAT Gateway ID", + "description": "Existing NAT Gateway ID (makes the NAT gateway external)", "pattern": "^nat-[a-z0-9]+$", "examples": ["nat-abc123"] } @@ -472,13 +568,112 @@ }, "enableVPCEndpoints": { "type": "boolean", - "description": "Create VPC endpoints for AWS services (S3, DynamoDB, KMS, Secrets Manager)", + "description": "Create VPC endpoints for AWS services (S3, DynamoDB, KMS, Secrets Manager). Enabled unless false.", "default": true }, "selfHeal": { "type": "boolean", - "description": "Automatically fix VPC configuration issues", - "default": false + "description": "Repair VPC configuration issues (route table associations) during discovery. When omitted, self-healing of stack-owned subnets is on." + }, + "shareAcrossStages": { + "type": "boolean", + "description": "Legacy: true reuses (auto) the VPC and subnets across stages; false creates a VPC, subnets and NAT gateway per stage. Ignored when `ownership` is set." + }, + "ownership": { + "type": "object", + "description": "Per-resource ownership. When set, management, subnets, natGateway, shareAcrossStages, enableVPCEndpoints and selfHeal are not translated; use `config` instead.", + "properties": { + "vpc": { + "type": "string", + "enum": ["stack", "external", "auto"], + "description": "VPC. Who owns the resource: 'stack' (this stack creates it), 'external' (use the id in `external`), 'auto' (reuse what discovery finds, else create)" + }, + "securityGroup": { + "type": "string", + "enum": ["stack", "external", "auto"], + "description": "Lambda security group. Who owns the resource: 'stack' (this stack creates it), 'external' (use the id in `external`), 'auto' (reuse what discovery finds, else create)" + }, + "subnets": { + "type": "string", + "enum": ["stack", "external", "auto"], + "description": "Private subnets. Who owns the resource: 'stack' (this stack creates it), 'external' (use the id in `external`), 'auto' (reuse what discovery finds, else create)" + }, + "natGateway": { + "type": "string", + "enum": ["stack", "external", "auto"], + "description": "NAT gateway. Who owns the resource: 'stack' (this stack creates it), 'external' (use the id in `external`), 'auto' (reuse what discovery finds, else create)" + }, + "vpcEndpoints": { + "type": "string", + "enum": ["stack", "external", "auto"], + "description": "VPC endpoints. Who owns the resource: 'stack' (this stack creates it), 'external' (use the id in `external`), 'auto' (reuse what discovery finds, else create)" + } + }, + "additionalProperties": false + }, + "external": { + "type": "object", + "description": "Ids of externally owned resources (ownership 'external'); discovered ids are used when omitted", + "properties": { + "vpcId": { + "type": "string", + "pattern": "^vpc-[a-z0-9]+$" + }, + "securityGroupIds": { + "type": "array", + "items": { + "type": "string", + "pattern": "^sg-[a-z0-9]+$" + } + }, + "subnetIds": { + "type": "array", + "items": { + "type": "string", + "pattern": "^subnet-[a-z0-9]+$" + } + }, + "natGatewayId": { + "type": "string", + "pattern": "^nat-[a-z0-9]+$" + }, + "vpcEndpointIds": { + "type": "object", + "description": "Endpoint ids by type (s3, dynamodb, kms, secretsManager, sqs)", + "additionalProperties": {"type": "string"} + } + }, + "additionalProperties": false + }, + "config": { + "type": "object", + "description": "Settings for the ownership-based VPC configuration", + "properties": { + "cidrBlock": { + "type": "string", + "description": "CIDR block for a VPC Frigg creates (wins over vpc.cidrBlock)", + "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$" + }, + "selfHeal": { + "type": "boolean", + "description": "Recreate missing stack-owned subnets. Enabled unless false." + }, + "natGateway": { + "type": "object", + "properties": { + "enable": { + "type": "boolean", + "description": "Resolve a NAT gateway. Enabled unless false." + } + }, + "additionalProperties": false + }, + "enableVpcEndpoints": { + "type": "boolean", + "description": "Create VPC endpoints. Enabled unless false." + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -523,9 +718,7 @@ "enum": ["standard", "advanced"], "default": "standard" }, - "description": { - "type": "string" - } + "description": {"type": "string"} } } }, @@ -536,15 +729,15 @@ }, "secretsManager": { "type": "object", - "description": "AWS Secrets Manager configuration", + "description": "AWS Secrets Manager configuration. Not read by the Frigg runtime: it has no effect. Kept so existing definitions stay valid; `frigg validate` warns when it is set.", "properties": { "enable": { "type": "boolean", - "description": "Enable AWS Secrets Manager for secure credential storage", - "default": false + "description": "Enable AWS Secrets Manager for secure credential storage" } }, - "additionalProperties": false + "additionalProperties": false, + "deprecated": true }, "lambda": { "type": "object", @@ -554,6 +747,11 @@ "type": "boolean", "description": "Scope framework-generated environment variables (queue URLs, migration/scheduler vars) to the functions that consume them instead of broadcasting to every function. Reduces per-function env size against the Lambda 4KB limit", "default": false + }, + "keepNestedNodeModules": { + "type": "boolean", + "description": "Keep nested node_modules in function packages (nested Frigg, AWS SDK and Prisma packages are still excluded)", + "default": false } }, "additionalProperties": false @@ -569,15 +767,15 @@ }, "routeSelectionExpression": { "type": "string", - "description": "Route selection expression for WebSocket", - "default": "$request.body.action" + "description": "Route selection expression for WebSocket. Not read by the Frigg runtime: it has no effect. Kept so existing definitions stay valid; `frigg validate` warns when it is set.", + "deprecated": true } }, "additionalProperties": false }, "security": { "type": "object", - "description": "Security settings for the application", + "description": "Security settings for the application. Not read by the Frigg runtime: it has no effect. Kept so existing definitions stay valid; `frigg validate` warns when it is set.", "properties": { "cors": { "type": "object", @@ -586,16 +784,17 @@ "origin": { "oneOf": [ {"type": "string"}, - {"type": "array", "items": {"type": "string"}}, + { + "type": "array", + "items": {"type": "string"} + }, {"type": "boolean"} ], - "description": "Allowed origins for CORS requests", - "default": "http://localhost:3000" + "description": "Allowed origins for CORS requests" }, "credentials": { "type": "boolean", - "description": "Allow credentials in CORS requests", - "default": true + "description": "Allow credentials in CORS requests" }, "methods": { "type": "array", @@ -620,24 +819,23 @@ "windowMs": { "type": "integer", "description": "Time window in milliseconds", - "minimum": 1000, - "default": 900000 + "minimum": 1000 }, "max": { - "type": "integer", + "type": "integer", "description": "Maximum requests per window", - "minimum": 1, - "default": 100 + "minimum": 1 } }, "additionalProperties": false } }, - "additionalProperties": false + "additionalProperties": false, + "deprecated": true }, "webhooks": { "type": "object", - "description": "Webhook handling configuration", + "description": "Webhook handling configuration. Not read by the Frigg runtime: it has no effect. Kept so existing definitions stay valid; `frigg validate` warns when it is set. Webhooks are declared per integration (Definition.webhooks).", "properties": { "secret": { "type": "string", @@ -646,11 +844,11 @@ "prefix": { "type": "string", "description": "Webhook endpoint prefix", - "pattern": "^/[a-zA-Z0-9/_-]*$", - "default": "/webhooks" + "pattern": "^/[a-zA-Z0-9/_-]*$" } }, - "additionalProperties": false + "additionalProperties": false, + "deprecated": true }, "logging": { "type": "object", @@ -658,9 +856,8 @@ "properties": { "level": { "type": "string", - "description": "Minimum log level, in lower or upper case. Sets FRIGG_LOG_LEVEL (when not info) and the Lambda application log level.", - "enum": ["trace", "debug", "info", "warn", "error", "fatal", "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], - "default": "info" + "description": "Minimum log level, in lower or upper case. Sets FRIGG_LOG_LEVEL (when not info) and the Lambda application log level. When omitted, core logs at info and no Lambda log level is set.", + "enum": ["trace", "debug", "info", "warn", "error", "fatal", "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"] }, "retentionInDays": { "type": "integer", @@ -669,46 +866,225 @@ }, "format": { "type": "string", - "description": "Log format. Records are always JSON lines.", - "enum": ["json"], - "default": "json" + "description": "Log format. Records are always JSON lines; this key is informational and has no default.", + "enum": ["json"] + } + }, + "additionalProperties": false + }, + "scheduler": { + "type": "object", + "description": "EventBridge Scheduler resources for one-time jobs. Also enabled automatically when an integration declares webhooks.", + "properties": { + "enable": { + "type": "boolean", + "description": "Provision the scheduler role and dead-letter queue", + "default": false + } + }, + "additionalProperties": false + }, + "aws": { + "type": "object", + "description": "AWS options for the build", + "properties": { + "discovery": { + "type": "object", + "description": "AWS resource discovery during build/deploy", + "properties": { + "enabled": { + "type": "boolean", + "description": "Force discovery on or off. When omitted, discovery runs when a feature needs it (VPC, KMS, SSM, PostgreSQL) and FRIGG_SKIP_AWS_DISCOVERY is not set." + }, + "failOnError": { + "type": "boolean", + "description": "Fail the build when discovery fails", + "default": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "migration": { + "type": "object", + "description": "Database migration resources", + "properties": { + "ownership": { + "type": "object", + "properties": { + "bucket": { + "type": "string", + "enum": ["stack", "external", "auto"], + "description": "Migration S3 bucket. Who owns the resource: 'stack' (this stack creates it), 'external' (use the id in `external`), 'auto' (reuse what discovery finds, else create)" + }, + "queue": { + "type": "string", + "enum": ["stack", "external", "auto"], + "description": "Migration SQS queue. Who owns the resource: 'stack' (this stack creates it), 'external' (use the id in `external`), 'auto' (reuse what discovery finds, else create)" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "telemetry": { + "type": "object", + "description": "Telemetry and usage tracking (ADR-011). An invalid block disables telemetry at runtime instead of failing.", + "properties": { + "exporter": { + "type": "object", + "description": "Where traces and metrics go. When omitted: 'console' for STAGE=local, else 'none'.", + "properties": { + "type": { + "type": "string", + "enum": ["none", "noop", "console", "otlp", "honeycomb", "datadog"] + }, + "endpoint": { + "type": "string", + "description": "OTLP endpoint (honeycomb defaults to https://api.honeycomb.io)" + }, + "headers": { + "type": "object", + "additionalProperties": {"type": "string"} + }, + "apiKey": { + "type": "string", + "description": "Honeycomb API key (sent as x-honeycomb-team)" + }, + "traceExporter": { + "description": "A pre-built OTel trace exporter instance (advanced)" + }, + "metricExporter": { + "description": "A pre-built OTel metric exporter instance (advanced)" + } + }, + "additionalProperties": false + }, + "sampleRatio": { + "type": "number", + "description": "Fraction of traces exported", + "minimum": 0, + "maximum": 1, + "default": 1 + }, + "northStar": { + "type": "object", + "description": "The usage counter that measures each integration type's value: { default: { name }, byType: { : { name } } }", + "properties": { + "default": { + "type": "object", + "required": ["name"], + "properties": { + "name": {"type": "string"} + } + }, + "byType": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["name"], + "properties": { + "name": {"type": "string"} + } + } + } + }, + "additionalProperties": false + }, + "subscribers": { + "type": "array", + "description": "Telemetry event subscribers: functions or { handler } objects", + "items": { + "description": "A function or { handler }" + } + } + }, + "additionalProperties": false + }, + "admin": { + "type": "object", + "description": "Admin Script Runner options (ADR-005, ADR-010)", + "properties": { + "includeBuiltinReports": { + "type": "boolean", + "description": "Register Frigg's built-in reports", + "default": false + }, + "enableScheduling": { + "type": "boolean", + "description": "Provision scheduling for admin scripts" + } + }, + "additionalProperties": false + }, + "adminScripts": { + "type": "array", + "description": "Admin script classes (each with a static Definition). Validators see each class as { Definition }.", + "items": { + "type": "object", + "required": ["Definition"], + "properties": { + "Definition": {"type": "object"} + } + }, + "default": [] + }, + "reports": { + "type": "array", + "description": "Report classes (each with a static Definition). Validators see each class as { Definition }.", + "items": { + "type": "object", + "required": ["Definition"], + "properties": { + "Definition": {"type": "object"} + } + }, + "default": [] + }, + "deployment": { + "type": "object", + "description": "`frigg deploy` options", + "properties": { + "skipPostDeploymentHealthCheck": { + "type": "boolean", + "description": "Skip `frigg doctor` after a deploy", + "default": false } }, "additionalProperties": false }, "custom": { "type": "object", - "description": "Custom application-specific configuration", + "description": "Custom application-specific configuration. Not read by the Frigg runtime: it has no effect. Kept so existing definitions stay valid; `frigg validate` warns when it is set.", "properties": { "appName": { "type": "string", - "description": "Application display name", - "default": "My Frigg Application" + "description": "Application display name" }, "version": { "type": "string", "description": "Application version", - "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", - "default": "1.0.0" + "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$" }, "environment": { "type": "string", "description": "Deployment environment/stage", - "enum": ["development", "staging", "production", "test"], - "default": "development" + "enum": ["development", "staging", "production", "test"] }, "features": { "type": "object", "description": "Feature flags and toggles", "patternProperties": { - "^[a-zA-Z][a-zA-Z0-9_]*$": { - "type": "boolean" - } + "^[a-zA-Z][a-zA-Z0-9_]*$": {"type": "boolean"} }, "additionalProperties": false } }, - "additionalProperties": true + "additionalProperties": true, + "deprecated": true } }, "additionalProperties": false, @@ -722,12 +1098,15 @@ }, "integrations": [], "user": { - "password": true + "usePassword": true, + "primary": "individual", + "individualUserRequired": true, + "organizationUserRequired": false }, "database": { "postgres": { "enable": true, - "management": "create-new" + "management": "managed" } }, "encryption": { @@ -740,18 +1119,8 @@ "cidrBlock": "10.0.0.0/16", "enableVPCEndpoints": true }, - "ssm": { - "enable": true - }, - "websockets": { - "enable": true - }, - "security": { - "cors": { - "origin": "http://localhost:3000", - "credentials": true - } - }, + "ssm": {"enable": true}, + "websockets": {"enable": true}, "logging": { "level": "info", "retentionInDays": 30