@augmenting-integrations/platform 8.4.0 → 8.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Augmenting Integrations LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1 +1 @@
1
- {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/manifest/load.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAEjE,MAAM,MAAM,mBAAmB,GAAG;IAChC,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,GAAE,mBAAwB,GAAG,WAAW,CAyBxE;AAED,OAAO,EAAE,gBAAgB,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EAAE,uBAAuB,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/manifest/load.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAEjE,MAAM,MAAM,mBAAmB,GAAG;IAChC,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,GAAE,mBAAwB,GAAG,WAAW,CA6BxE;AAED,OAAO,EAAE,gBAAgB,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EAAE,uBAAuB,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
package/dist/manifest.cjs CHANGED
@@ -107,13 +107,17 @@ function loadManifest(opts = {}) {
107
107
  try {
108
108
  raw = (0, import_node_fs.readFileSync)(file, "utf8");
109
109
  } catch (err) {
110
- throw new Error(`loadManifest: cannot read ${file}: ${err.message}`);
110
+ throw new Error(`loadManifest: cannot read ${file}: ${err.message}`, {
111
+ cause: err
112
+ });
111
113
  }
112
114
  let parsed;
113
115
  try {
114
116
  parsed = JSON.parse(raw);
115
117
  } catch (err) {
116
- throw new Error(`loadManifest: invalid JSON in ${file}: ${err.message}`);
118
+ throw new Error(`loadManifest: invalid JSON in ${file}: ${err.message}`, {
119
+ cause: err
120
+ });
117
121
  }
118
122
  const result = validateManifest(parsed);
119
123
  if (!result.ok) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/manifest.ts","../src/manifest/load.ts","../src/manifest/schema.ts"],"sourcesContent":["export {\n loadManifest,\n validateManifest,\n type LoadManifestOptions,\n type AppManifest,\n type ManifestValidationError,\n type AppRole,\n type DataPlaneType,\n} from \"./manifest/load.js\";\n","import { readFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { validateManifest, type AppManifest } from \"./schema.js\";\n\nexport type LoadManifestOptions = {\n /** Defaults to process.cwd(). */\n cwd?: string;\n /** Manifest filename. Defaults to \"app.manifest.json\". */\n filename?: string;\n};\n\n/**\n * Read and validate an app.manifest.json. Throws with a consolidated error\n * message listing every validation failure. The deploy fails loudly instead\n * of substituting defaults that mask drift.\n */\nexport function loadManifest(opts: LoadManifestOptions = {}): AppManifest {\n const cwd = opts.cwd ?? process.cwd();\n const filename = opts.filename ?? \"app.manifest.json\";\n const file = resolve(join(cwd, filename));\n\n let raw: string;\n try {\n raw = readFileSync(file, \"utf8\");\n } catch (err) {\n throw new Error(`loadManifest: cannot read ${file}: ${(err as Error).message}`);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(`loadManifest: invalid JSON in ${file}: ${(err as Error).message}`);\n }\n\n const result = validateManifest(parsed);\n if (!result.ok) {\n const lines = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n throw new Error(`loadManifest: ${file} failed validation:\\n${lines}`);\n }\n return result.value;\n}\n\nexport { validateManifest, type AppManifest } from \"./schema.js\";\nexport type { ManifestValidationError, AppRole, DataPlaneType } from \"./schema.js\";\n","// =============================================================================\n// app.manifest.json -- per-app declaration of slug, role, subdomain, access\n// policy, feature enablement, and data plane choice. Every spoke and apex\n// app ships one of these at the repo root.\n//\n// The manifest is the local source of truth for the deploy pipeline, the\n// registry registration call, the schema validator, the runtime app-access\n// enforcement, and the spoke kernel infra wiring. Product developers should\n// only have to edit this file (not env vars, workflow files, or registry\n// rows) to change their app's identity or feature set.\n// =============================================================================\n\nexport type AppRole = \"apex\" | \"spoke\";\n\nexport type DataPlaneType = \"app-aurora\" | \"tenant-postgres\" | \"dynamodb\" | \"none\";\n\nexport type AppManifest = {\n /** Schema version. Always 1 for this generation. */\n schemaVersion: 1;\n /** Tenant slug, e.g. \"agency\". Matches the tenant repo. */\n tenantSlug: string;\n /** App slug. PK in the registry. Stable identifier. */\n appSlug: string;\n /** \"apex\" (the auth broker) or \"spoke\" (a product app). */\n role: AppRole;\n /** Subdomain label. \"\" for apex. */\n subdomain: string;\n /** Human-friendly name for nav + admin UI. */\n displayName: string;\n /** Sort order in the cross-app nav. Lower = first. */\n navOrder: number;\n access: {\n /**\n * Cognito identity groups required to see AND enter this app. Empty =\n * all authenticated users. This is NOT product-level authorization;\n * it gates entry to the entire app surface.\n */\n requiredIdentityGroups: string[];\n };\n features: {\n /** Has billing surface (Stripe, credit balance, cart). */\n billing: boolean;\n /** Has settings surface (password, MFA, profile). */\n settings: boolean;\n /** Sends invitations (Invitation table required). */\n invitations: boolean;\n /** Supports admin impersonation. */\n impersonation: boolean;\n };\n dataPlane: {\n /**\n * \"app-aurora\" = per-app Aurora Serverless v2 + RDS Proxy.\n * \"tenant-postgres\" = shared tenant DB (rare).\n * \"dynamodb\" = the app only uses DynamoDB tables it provisions itself.\n * \"none\" = no app-owned data plane (apex auth-broker).\n */\n type: DataPlaneType;\n /** True if Prisma migrations should run on deploy. */\n migrations: boolean;\n };\n};\n\nexport type ManifestValidationError = {\n path: string;\n message: string;\n};\n\n/**\n * Pure validator. Returns the typed manifest on success, or an array of\n * errors on failure. No throws -- the loader wraps this and throws with\n * a consolidated error message.\n */\nexport function validateManifest(\n raw: unknown,\n): { ok: true; value: AppManifest } | { ok: false; errors: ManifestValidationError[] } {\n const errors: ManifestValidationError[] = [];\n if (typeof raw !== \"object\" || raw === null) {\n return { ok: false, errors: [{ path: \"\", message: \"manifest must be an object\" }] };\n }\n const m = raw as Record<string, unknown>;\n\n const requireString = (path: string, value: unknown) => {\n if (typeof value !== \"string\") errors.push({ path, message: \"expected string\" });\n };\n const requireNumber = (path: string, value: unknown) => {\n if (typeof value !== \"number\") errors.push({ path, message: \"expected number\" });\n };\n const requireBool = (path: string, value: unknown) => {\n if (typeof value !== \"boolean\") errors.push({ path, message: \"expected boolean\" });\n };\n const requireOneOf = (path: string, value: unknown, options: readonly string[]) => {\n if (typeof value !== \"string\" || !options.includes(value)) {\n errors.push({ path, message: `expected one of: ${options.join(\", \")}` });\n }\n };\n const requireStringArray = (path: string, value: unknown) => {\n if (!Array.isArray(value) || value.some((v) => typeof v !== \"string\")) {\n errors.push({ path, message: \"expected string[]\" });\n }\n };\n\n if (m.schemaVersion !== 1) {\n errors.push({ path: \"schemaVersion\", message: \"expected literal 1\" });\n }\n requireString(\"tenantSlug\", m.tenantSlug);\n requireString(\"appSlug\", m.appSlug);\n requireOneOf(\"role\", m.role, [\"apex\", \"spoke\"]);\n requireString(\"subdomain\", m.subdomain);\n requireString(\"displayName\", m.displayName);\n requireNumber(\"navOrder\", m.navOrder);\n\n const access = m.access as Record<string, unknown> | undefined;\n if (!access || typeof access !== \"object\") {\n errors.push({ path: \"access\", message: \"expected object\" });\n } else {\n requireStringArray(\"access.requiredIdentityGroups\", access.requiredIdentityGroups);\n }\n\n const features = m.features as Record<string, unknown> | undefined;\n if (!features || typeof features !== \"object\") {\n errors.push({ path: \"features\", message: \"expected object\" });\n } else {\n requireBool(\"features.billing\", features.billing);\n requireBool(\"features.settings\", features.settings);\n requireBool(\"features.invitations\", features.invitations);\n requireBool(\"features.impersonation\", features.impersonation);\n }\n\n const dataPlane = m.dataPlane as Record<string, unknown> | undefined;\n if (!dataPlane || typeof dataPlane !== \"object\") {\n errors.push({ path: \"dataPlane\", message: \"expected object\" });\n } else {\n requireOneOf(\"dataPlane.type\", dataPlane.type, [\n \"app-aurora\",\n \"tenant-postgres\",\n \"dynamodb\",\n \"none\",\n ]);\n requireBool(\"dataPlane.migrations\", dataPlane.migrations);\n }\n\n // Apex consistency: subdomain must be empty.\n if (m.role === \"apex\" && m.subdomain !== \"\") {\n errors.push({ path: \"subdomain\", message: \"apex apps must have empty subdomain\" });\n }\n\n if (errors.length > 0) return { ok: false, errors };\n return { ok: true, value: m as unknown as AppManifest };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/manifest.ts","../src/manifest/load.ts","../src/manifest/schema.ts"],"sourcesContent":["export {\n loadManifest,\n validateManifest,\n type LoadManifestOptions,\n type AppManifest,\n type ManifestValidationError,\n type AppRole,\n type DataPlaneType,\n} from \"./manifest/load.js\";\n","import { readFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { validateManifest, type AppManifest } from \"./schema.js\";\n\nexport type LoadManifestOptions = {\n /** Defaults to process.cwd(). */\n cwd?: string;\n /** Manifest filename. Defaults to \"app.manifest.json\". */\n filename?: string;\n};\n\n/**\n * Read and validate an app.manifest.json. Throws with a consolidated error\n * message listing every validation failure. The deploy fails loudly instead\n * of substituting defaults that mask drift.\n */\nexport function loadManifest(opts: LoadManifestOptions = {}): AppManifest {\n const cwd = opts.cwd ?? process.cwd();\n const filename = opts.filename ?? \"app.manifest.json\";\n const file = resolve(join(cwd, filename));\n\n let raw: string;\n try {\n raw = readFileSync(file, \"utf8\");\n } catch (err) {\n throw new Error(`loadManifest: cannot read ${file}: ${(err as Error).message}`, {\n cause: err,\n });\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(`loadManifest: invalid JSON in ${file}: ${(err as Error).message}`, {\n cause: err,\n });\n }\n\n const result = validateManifest(parsed);\n if (!result.ok) {\n const lines = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n throw new Error(`loadManifest: ${file} failed validation:\\n${lines}`);\n }\n return result.value;\n}\n\nexport { validateManifest, type AppManifest } from \"./schema.js\";\nexport type { ManifestValidationError, AppRole, DataPlaneType } from \"./schema.js\";\n","// =============================================================================\n// app.manifest.json -- per-app declaration of slug, role, subdomain, access\n// policy, feature enablement, and data plane choice. Every spoke and apex\n// app ships one of these at the repo root.\n//\n// The manifest is the local source of truth for the deploy pipeline, the\n// registry registration call, the schema validator, the runtime app-access\n// enforcement, and the spoke kernel infra wiring. Product developers should\n// only have to edit this file (not env vars, workflow files, or registry\n// rows) to change their app's identity or feature set.\n// =============================================================================\n\nexport type AppRole = \"apex\" | \"spoke\";\n\nexport type DataPlaneType = \"app-aurora\" | \"tenant-postgres\" | \"dynamodb\" | \"none\";\n\nexport type AppManifest = {\n /** Schema version. Always 1 for this generation. */\n schemaVersion: 1;\n /** Tenant slug, e.g. \"agency\". Matches the tenant repo. */\n tenantSlug: string;\n /** App slug. PK in the registry. Stable identifier. */\n appSlug: string;\n /** \"apex\" (the auth broker) or \"spoke\" (a product app). */\n role: AppRole;\n /** Subdomain label. \"\" for apex. */\n subdomain: string;\n /** Human-friendly name for nav + admin UI. */\n displayName: string;\n /** Sort order in the cross-app nav. Lower = first. */\n navOrder: number;\n access: {\n /**\n * Cognito identity groups required to see AND enter this app. Empty =\n * all authenticated users. This is NOT product-level authorization;\n * it gates entry to the entire app surface.\n */\n requiredIdentityGroups: string[];\n };\n features: {\n /** Has billing surface (Stripe, credit balance, cart). */\n billing: boolean;\n /** Has settings surface (password, MFA, profile). */\n settings: boolean;\n /** Sends invitations (Invitation table required). */\n invitations: boolean;\n /** Supports admin impersonation. */\n impersonation: boolean;\n };\n dataPlane: {\n /**\n * \"app-aurora\" = per-app Aurora Serverless v2 + RDS Proxy.\n * \"tenant-postgres\" = shared tenant DB (rare).\n * \"dynamodb\" = the app only uses DynamoDB tables it provisions itself.\n * \"none\" = no app-owned data plane (apex auth-broker).\n */\n type: DataPlaneType;\n /** True if Prisma migrations should run on deploy. */\n migrations: boolean;\n };\n};\n\nexport type ManifestValidationError = {\n path: string;\n message: string;\n};\n\n/**\n * Pure validator. Returns the typed manifest on success, or an array of\n * errors on failure. No throws -- the loader wraps this and throws with\n * a consolidated error message.\n */\nexport function validateManifest(\n raw: unknown,\n): { ok: true; value: AppManifest } | { ok: false; errors: ManifestValidationError[] } {\n const errors: ManifestValidationError[] = [];\n if (typeof raw !== \"object\" || raw === null) {\n return { ok: false, errors: [{ path: \"\", message: \"manifest must be an object\" }] };\n }\n const m = raw as Record<string, unknown>;\n\n const requireString = (path: string, value: unknown) => {\n if (typeof value !== \"string\") errors.push({ path, message: \"expected string\" });\n };\n const requireNumber = (path: string, value: unknown) => {\n if (typeof value !== \"number\") errors.push({ path, message: \"expected number\" });\n };\n const requireBool = (path: string, value: unknown) => {\n if (typeof value !== \"boolean\") errors.push({ path, message: \"expected boolean\" });\n };\n const requireOneOf = (path: string, value: unknown, options: readonly string[]) => {\n if (typeof value !== \"string\" || !options.includes(value)) {\n errors.push({ path, message: `expected one of: ${options.join(\", \")}` });\n }\n };\n const requireStringArray = (path: string, value: unknown) => {\n if (!Array.isArray(value) || value.some((v) => typeof v !== \"string\")) {\n errors.push({ path, message: \"expected string[]\" });\n }\n };\n\n if (m.schemaVersion !== 1) {\n errors.push({ path: \"schemaVersion\", message: \"expected literal 1\" });\n }\n requireString(\"tenantSlug\", m.tenantSlug);\n requireString(\"appSlug\", m.appSlug);\n requireOneOf(\"role\", m.role, [\"apex\", \"spoke\"]);\n requireString(\"subdomain\", m.subdomain);\n requireString(\"displayName\", m.displayName);\n requireNumber(\"navOrder\", m.navOrder);\n\n const access = m.access as Record<string, unknown> | undefined;\n if (!access || typeof access !== \"object\") {\n errors.push({ path: \"access\", message: \"expected object\" });\n } else {\n requireStringArray(\"access.requiredIdentityGroups\", access.requiredIdentityGroups);\n }\n\n const features = m.features as Record<string, unknown> | undefined;\n if (!features || typeof features !== \"object\") {\n errors.push({ path: \"features\", message: \"expected object\" });\n } else {\n requireBool(\"features.billing\", features.billing);\n requireBool(\"features.settings\", features.settings);\n requireBool(\"features.invitations\", features.invitations);\n requireBool(\"features.impersonation\", features.impersonation);\n }\n\n const dataPlane = m.dataPlane as Record<string, unknown> | undefined;\n if (!dataPlane || typeof dataPlane !== \"object\") {\n errors.push({ path: \"dataPlane\", message: \"expected object\" });\n } else {\n requireOneOf(\"dataPlane.type\", dataPlane.type, [\n \"app-aurora\",\n \"tenant-postgres\",\n \"dynamodb\",\n \"none\",\n ]);\n requireBool(\"dataPlane.migrations\", dataPlane.migrations);\n }\n\n // Apex consistency: subdomain must be empty.\n if (m.role === \"apex\" && m.subdomain !== \"\") {\n errors.push({ path: \"subdomain\", message: \"apex apps must have empty subdomain\" });\n }\n\n if (errors.length > 0) return { ok: false, errors };\n return { ok: true, value: m as unknown as AppManifest };\n}\n"],"mappings":"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","names":[]}
package/dist/manifest.js CHANGED
@@ -80,13 +80,17 @@ function loadManifest(opts = {}) {
80
80
  try {
81
81
  raw = readFileSync(file, "utf8");
82
82
  } catch (err) {
83
- throw new Error(`loadManifest: cannot read ${file}: ${err.message}`);
83
+ throw new Error(`loadManifest: cannot read ${file}: ${err.message}`, {
84
+ cause: err
85
+ });
84
86
  }
85
87
  let parsed;
86
88
  try {
87
89
  parsed = JSON.parse(raw);
88
90
  } catch (err) {
89
- throw new Error(`loadManifest: invalid JSON in ${file}: ${err.message}`);
91
+ throw new Error(`loadManifest: invalid JSON in ${file}: ${err.message}`, {
92
+ cause: err
93
+ });
90
94
  }
91
95
  const result = validateManifest(parsed);
92
96
  if (!result.ok) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/manifest/load.ts","../src/manifest/schema.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { validateManifest, type AppManifest } from \"./schema.js\";\n\nexport type LoadManifestOptions = {\n /** Defaults to process.cwd(). */\n cwd?: string;\n /** Manifest filename. Defaults to \"app.manifest.json\". */\n filename?: string;\n};\n\n/**\n * Read and validate an app.manifest.json. Throws with a consolidated error\n * message listing every validation failure. The deploy fails loudly instead\n * of substituting defaults that mask drift.\n */\nexport function loadManifest(opts: LoadManifestOptions = {}): AppManifest {\n const cwd = opts.cwd ?? process.cwd();\n const filename = opts.filename ?? \"app.manifest.json\";\n const file = resolve(join(cwd, filename));\n\n let raw: string;\n try {\n raw = readFileSync(file, \"utf8\");\n } catch (err) {\n throw new Error(`loadManifest: cannot read ${file}: ${(err as Error).message}`);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(`loadManifest: invalid JSON in ${file}: ${(err as Error).message}`);\n }\n\n const result = validateManifest(parsed);\n if (!result.ok) {\n const lines = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n throw new Error(`loadManifest: ${file} failed validation:\\n${lines}`);\n }\n return result.value;\n}\n\nexport { validateManifest, type AppManifest } from \"./schema.js\";\nexport type { ManifestValidationError, AppRole, DataPlaneType } from \"./schema.js\";\n","// =============================================================================\n// app.manifest.json -- per-app declaration of slug, role, subdomain, access\n// policy, feature enablement, and data plane choice. Every spoke and apex\n// app ships one of these at the repo root.\n//\n// The manifest is the local source of truth for the deploy pipeline, the\n// registry registration call, the schema validator, the runtime app-access\n// enforcement, and the spoke kernel infra wiring. Product developers should\n// only have to edit this file (not env vars, workflow files, or registry\n// rows) to change their app's identity or feature set.\n// =============================================================================\n\nexport type AppRole = \"apex\" | \"spoke\";\n\nexport type DataPlaneType = \"app-aurora\" | \"tenant-postgres\" | \"dynamodb\" | \"none\";\n\nexport type AppManifest = {\n /** Schema version. Always 1 for this generation. */\n schemaVersion: 1;\n /** Tenant slug, e.g. \"agency\". Matches the tenant repo. */\n tenantSlug: string;\n /** App slug. PK in the registry. Stable identifier. */\n appSlug: string;\n /** \"apex\" (the auth broker) or \"spoke\" (a product app). */\n role: AppRole;\n /** Subdomain label. \"\" for apex. */\n subdomain: string;\n /** Human-friendly name for nav + admin UI. */\n displayName: string;\n /** Sort order in the cross-app nav. Lower = first. */\n navOrder: number;\n access: {\n /**\n * Cognito identity groups required to see AND enter this app. Empty =\n * all authenticated users. This is NOT product-level authorization;\n * it gates entry to the entire app surface.\n */\n requiredIdentityGroups: string[];\n };\n features: {\n /** Has billing surface (Stripe, credit balance, cart). */\n billing: boolean;\n /** Has settings surface (password, MFA, profile). */\n settings: boolean;\n /** Sends invitations (Invitation table required). */\n invitations: boolean;\n /** Supports admin impersonation. */\n impersonation: boolean;\n };\n dataPlane: {\n /**\n * \"app-aurora\" = per-app Aurora Serverless v2 + RDS Proxy.\n * \"tenant-postgres\" = shared tenant DB (rare).\n * \"dynamodb\" = the app only uses DynamoDB tables it provisions itself.\n * \"none\" = no app-owned data plane (apex auth-broker).\n */\n type: DataPlaneType;\n /** True if Prisma migrations should run on deploy. */\n migrations: boolean;\n };\n};\n\nexport type ManifestValidationError = {\n path: string;\n message: string;\n};\n\n/**\n * Pure validator. Returns the typed manifest on success, or an array of\n * errors on failure. No throws -- the loader wraps this and throws with\n * a consolidated error message.\n */\nexport function validateManifest(\n raw: unknown,\n): { ok: true; value: AppManifest } | { ok: false; errors: ManifestValidationError[] } {\n const errors: ManifestValidationError[] = [];\n if (typeof raw !== \"object\" || raw === null) {\n return { ok: false, errors: [{ path: \"\", message: \"manifest must be an object\" }] };\n }\n const m = raw as Record<string, unknown>;\n\n const requireString = (path: string, value: unknown) => {\n if (typeof value !== \"string\") errors.push({ path, message: \"expected string\" });\n };\n const requireNumber = (path: string, value: unknown) => {\n if (typeof value !== \"number\") errors.push({ path, message: \"expected number\" });\n };\n const requireBool = (path: string, value: unknown) => {\n if (typeof value !== \"boolean\") errors.push({ path, message: \"expected boolean\" });\n };\n const requireOneOf = (path: string, value: unknown, options: readonly string[]) => {\n if (typeof value !== \"string\" || !options.includes(value)) {\n errors.push({ path, message: `expected one of: ${options.join(\", \")}` });\n }\n };\n const requireStringArray = (path: string, value: unknown) => {\n if (!Array.isArray(value) || value.some((v) => typeof v !== \"string\")) {\n errors.push({ path, message: \"expected string[]\" });\n }\n };\n\n if (m.schemaVersion !== 1) {\n errors.push({ path: \"schemaVersion\", message: \"expected literal 1\" });\n }\n requireString(\"tenantSlug\", m.tenantSlug);\n requireString(\"appSlug\", m.appSlug);\n requireOneOf(\"role\", m.role, [\"apex\", \"spoke\"]);\n requireString(\"subdomain\", m.subdomain);\n requireString(\"displayName\", m.displayName);\n requireNumber(\"navOrder\", m.navOrder);\n\n const access = m.access as Record<string, unknown> | undefined;\n if (!access || typeof access !== \"object\") {\n errors.push({ path: \"access\", message: \"expected object\" });\n } else {\n requireStringArray(\"access.requiredIdentityGroups\", access.requiredIdentityGroups);\n }\n\n const features = m.features as Record<string, unknown> | undefined;\n if (!features || typeof features !== \"object\") {\n errors.push({ path: \"features\", message: \"expected object\" });\n } else {\n requireBool(\"features.billing\", features.billing);\n requireBool(\"features.settings\", features.settings);\n requireBool(\"features.invitations\", features.invitations);\n requireBool(\"features.impersonation\", features.impersonation);\n }\n\n const dataPlane = m.dataPlane as Record<string, unknown> | undefined;\n if (!dataPlane || typeof dataPlane !== \"object\") {\n errors.push({ path: \"dataPlane\", message: \"expected object\" });\n } else {\n requireOneOf(\"dataPlane.type\", dataPlane.type, [\n \"app-aurora\",\n \"tenant-postgres\",\n \"dynamodb\",\n \"none\",\n ]);\n requireBool(\"dataPlane.migrations\", dataPlane.migrations);\n }\n\n // Apex consistency: subdomain must be empty.\n if (m.role === \"apex\" && m.subdomain !== \"\") {\n errors.push({ path: \"subdomain\", message: \"apex apps must have empty subdomain\" });\n }\n\n if (errors.length > 0) return { ok: false, errors };\n return { ok: true, value: m as unknown as AppManifest };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/manifest/load.ts","../src/manifest/schema.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { validateManifest, type AppManifest } from \"./schema.js\";\n\nexport type LoadManifestOptions = {\n /** Defaults to process.cwd(). */\n cwd?: string;\n /** Manifest filename. Defaults to \"app.manifest.json\". */\n filename?: string;\n};\n\n/**\n * Read and validate an app.manifest.json. Throws with a consolidated error\n * message listing every validation failure. The deploy fails loudly instead\n * of substituting defaults that mask drift.\n */\nexport function loadManifest(opts: LoadManifestOptions = {}): AppManifest {\n const cwd = opts.cwd ?? process.cwd();\n const filename = opts.filename ?? \"app.manifest.json\";\n const file = resolve(join(cwd, filename));\n\n let raw: string;\n try {\n raw = readFileSync(file, \"utf8\");\n } catch (err) {\n throw new Error(`loadManifest: cannot read ${file}: ${(err as Error).message}`, {\n cause: err,\n });\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(`loadManifest: invalid JSON in ${file}: ${(err as Error).message}`, {\n cause: err,\n });\n }\n\n const result = validateManifest(parsed);\n if (!result.ok) {\n const lines = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n throw new Error(`loadManifest: ${file} failed validation:\\n${lines}`);\n }\n return result.value;\n}\n\nexport { validateManifest, type AppManifest } from \"./schema.js\";\nexport type { ManifestValidationError, AppRole, DataPlaneType } from \"./schema.js\";\n","// =============================================================================\n// app.manifest.json -- per-app declaration of slug, role, subdomain, access\n// policy, feature enablement, and data plane choice. Every spoke and apex\n// app ships one of these at the repo root.\n//\n// The manifest is the local source of truth for the deploy pipeline, the\n// registry registration call, the schema validator, the runtime app-access\n// enforcement, and the spoke kernel infra wiring. Product developers should\n// only have to edit this file (not env vars, workflow files, or registry\n// rows) to change their app's identity or feature set.\n// =============================================================================\n\nexport type AppRole = \"apex\" | \"spoke\";\n\nexport type DataPlaneType = \"app-aurora\" | \"tenant-postgres\" | \"dynamodb\" | \"none\";\n\nexport type AppManifest = {\n /** Schema version. Always 1 for this generation. */\n schemaVersion: 1;\n /** Tenant slug, e.g. \"agency\". Matches the tenant repo. */\n tenantSlug: string;\n /** App slug. PK in the registry. Stable identifier. */\n appSlug: string;\n /** \"apex\" (the auth broker) or \"spoke\" (a product app). */\n role: AppRole;\n /** Subdomain label. \"\" for apex. */\n subdomain: string;\n /** Human-friendly name for nav + admin UI. */\n displayName: string;\n /** Sort order in the cross-app nav. Lower = first. */\n navOrder: number;\n access: {\n /**\n * Cognito identity groups required to see AND enter this app. Empty =\n * all authenticated users. This is NOT product-level authorization;\n * it gates entry to the entire app surface.\n */\n requiredIdentityGroups: string[];\n };\n features: {\n /** Has billing surface (Stripe, credit balance, cart). */\n billing: boolean;\n /** Has settings surface (password, MFA, profile). */\n settings: boolean;\n /** Sends invitations (Invitation table required). */\n invitations: boolean;\n /** Supports admin impersonation. */\n impersonation: boolean;\n };\n dataPlane: {\n /**\n * \"app-aurora\" = per-app Aurora Serverless v2 + RDS Proxy.\n * \"tenant-postgres\" = shared tenant DB (rare).\n * \"dynamodb\" = the app only uses DynamoDB tables it provisions itself.\n * \"none\" = no app-owned data plane (apex auth-broker).\n */\n type: DataPlaneType;\n /** True if Prisma migrations should run on deploy. */\n migrations: boolean;\n };\n};\n\nexport type ManifestValidationError = {\n path: string;\n message: string;\n};\n\n/**\n * Pure validator. Returns the typed manifest on success, or an array of\n * errors on failure. No throws -- the loader wraps this and throws with\n * a consolidated error message.\n */\nexport function validateManifest(\n raw: unknown,\n): { ok: true; value: AppManifest } | { ok: false; errors: ManifestValidationError[] } {\n const errors: ManifestValidationError[] = [];\n if (typeof raw !== \"object\" || raw === null) {\n return { ok: false, errors: [{ path: \"\", message: \"manifest must be an object\" }] };\n }\n const m = raw as Record<string, unknown>;\n\n const requireString = (path: string, value: unknown) => {\n if (typeof value !== \"string\") errors.push({ path, message: \"expected string\" });\n };\n const requireNumber = (path: string, value: unknown) => {\n if (typeof value !== \"number\") errors.push({ path, message: \"expected number\" });\n };\n const requireBool = (path: string, value: unknown) => {\n if (typeof value !== \"boolean\") errors.push({ path, message: \"expected boolean\" });\n };\n const requireOneOf = (path: string, value: unknown, options: readonly string[]) => {\n if (typeof value !== \"string\" || !options.includes(value)) {\n errors.push({ path, message: `expected one of: ${options.join(\", \")}` });\n }\n };\n const requireStringArray = (path: string, value: unknown) => {\n if (!Array.isArray(value) || value.some((v) => typeof v !== \"string\")) {\n errors.push({ path, message: \"expected string[]\" });\n }\n };\n\n if (m.schemaVersion !== 1) {\n errors.push({ path: \"schemaVersion\", message: \"expected literal 1\" });\n }\n requireString(\"tenantSlug\", m.tenantSlug);\n requireString(\"appSlug\", m.appSlug);\n requireOneOf(\"role\", m.role, [\"apex\", \"spoke\"]);\n requireString(\"subdomain\", m.subdomain);\n requireString(\"displayName\", m.displayName);\n requireNumber(\"navOrder\", m.navOrder);\n\n const access = m.access as Record<string, unknown> | undefined;\n if (!access || typeof access !== \"object\") {\n errors.push({ path: \"access\", message: \"expected object\" });\n } else {\n requireStringArray(\"access.requiredIdentityGroups\", access.requiredIdentityGroups);\n }\n\n const features = m.features as Record<string, unknown> | undefined;\n if (!features || typeof features !== \"object\") {\n errors.push({ path: \"features\", message: \"expected object\" });\n } else {\n requireBool(\"features.billing\", features.billing);\n requireBool(\"features.settings\", features.settings);\n requireBool(\"features.invitations\", features.invitations);\n requireBool(\"features.impersonation\", features.impersonation);\n }\n\n const dataPlane = m.dataPlane as Record<string, unknown> | undefined;\n if (!dataPlane || typeof dataPlane !== \"object\") {\n errors.push({ path: \"dataPlane\", message: \"expected object\" });\n } else {\n requireOneOf(\"dataPlane.type\", dataPlane.type, [\n \"app-aurora\",\n \"tenant-postgres\",\n \"dynamodb\",\n \"none\",\n ]);\n requireBool(\"dataPlane.migrations\", dataPlane.migrations);\n }\n\n // Apex consistency: subdomain must be empty.\n if (m.role === \"apex\" && m.subdomain !== \"\") {\n errors.push({ path: \"subdomain\", message: \"apex apps must have empty subdomain\" });\n }\n\n if (errors.length > 0) return { ok: false, errors };\n return { ok: true, value: m as unknown as AppManifest };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@augmenting-integrations/platform",
3
- "version": "8.4.0",
3
+ "version": "8.5.0",
4
4
  "description": "Tenant + app manifest contract for the augint platform. Owns TenantConfig (server-side env load + browser-safe public subset + TenantBootScript) and the app.manifest.json schema/loader. Every other @augmenting-integrations/* package consumes tenant context from here, so /auth no longer owns it.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -31,11 +31,6 @@
31
31
  "dist",
32
32
  "README.md"
33
33
  ],
34
- "scripts": {
35
- "build": "tsup",
36
- "clean": "rm -rf dist",
37
- "test": "vitest run --passWithNoTests"
38
- },
39
34
  "peerDependencies": {
40
35
  "next": "^16.0.0",
41
36
  "react": "^19.0.0"
@@ -48,5 +43,10 @@
48
43
  "tsup": "^8.3.5",
49
44
  "typescript": "^5.7.2",
50
45
  "vitest": "^4.1.5"
46
+ },
47
+ "scripts": {
48
+ "build": "tsup",
49
+ "clean": "rm -rf dist",
50
+ "test": "vitest run --passWithNoTests"
51
51
  }
52
- }
52
+ }