@augmenting-integrations/platform 8.5.0 → 8.7.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.
@@ -0,0 +1,15 @@
1
+ import { type AppsRoster } from "./schema.js";
2
+ export type LoadAppsRosterOptions = {
3
+ /** Absolute path to a JSON apps-roster file. */
4
+ path: string;
5
+ };
6
+ /**
7
+ * Read + validate a JSON apps-roster file. Throws with a consolidated error
8
+ * message listing every validation failure.
9
+ *
10
+ * YAML parsing is intentionally not bundled in this package; use
11
+ * @augmenting-integrations/deploy-tools `augint validate-app-roster` for the
12
+ * cross-format check between the infra YAML source and the apex JSON mirror.
13
+ */
14
+ export declare function loadAppsRosterJson(opts: LoadAppsRosterOptions): AppsRoster;
15
+ //# sourceMappingURL=load.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/apps-roster/load.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAElE,MAAM,MAAM,qBAAqB,GAAG;IAClC,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,qBAAqB,GAAG,UAAU,CA0B1E"}
@@ -0,0 +1,53 @@
1
+ export type TenantAppRole = "apex" | "spoke";
2
+ export type TenantApp = {
3
+ /** Stable identifier. Matches the spoke's app.manifest.json#appSlug. */
4
+ slug: string;
5
+ /** "apex" (auth broker) or "spoke" (product app). */
6
+ role: TenantAppRole;
7
+ /** DNS label. Empty string for apex. */
8
+ subdomain: string;
9
+ /** Human-friendly name. Drives the shared nav. */
10
+ displayName: string;
11
+ /** Sort order. Lower comes first. */
12
+ navOrder: number;
13
+ /**
14
+ * Cognito identity groups required to see this app in cross-app nav
15
+ * AND to enter its routes (when the spoke's createAuth is wired to its
16
+ * own manifest's access policy). Empty = all authenticated users.
17
+ */
18
+ requiredIdentityGroups: string[];
19
+ /**
20
+ * Static feature toggle. Default true. Set false to hide an app from
21
+ * cross-app nav without removing the entry. Editing this requires a
22
+ * PR + redeploy -- this is NOT mutable runtime state.
23
+ */
24
+ enabled?: boolean;
25
+ };
26
+ export type AppsRoster = {
27
+ apps: TenantApp[];
28
+ };
29
+ export type RosterValidationError = {
30
+ path: string;
31
+ message: string;
32
+ };
33
+ /**
34
+ * Pure validator for the roster object (parsed from YAML or JSON). Returns
35
+ * the typed roster on success, or an array of errors on failure. No throws.
36
+ */
37
+ export declare function validateAppsRoster(raw: unknown): {
38
+ ok: true;
39
+ value: AppsRoster;
40
+ } | {
41
+ ok: false;
42
+ errors: RosterValidationError[];
43
+ };
44
+ /**
45
+ * Filter the roster by user identity groups. Apps with empty
46
+ * `requiredIdentityGroups` are visible to all authenticated users; otherwise
47
+ * the user must be in at least one of the listed groups. `enabled: false`
48
+ * apps are always filtered out.
49
+ */
50
+ export declare function filterAppsByIdentityGroups(apps: TenantApp[], userGroups: string[]): TenantApp[];
51
+ /** Sort apps by navOrder ASC, then slug. Mutates a copy, returns it. */
52
+ export declare function sortAppsByNavOrder<T extends Pick<TenantApp, "navOrder" | "slug">>(apps: T[]): T[];
53
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/apps-roster/schema.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,CAAC;AAE7C,MAAM,MAAM,SAAS,GAAG;IACtB,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,IAAI,EAAE,aAAa,CAAC;IACpB,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,SAAS,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAIF;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,OAAO,GACX;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,qBAAqB,EAAE,CAAA;CAAE,CAoHlF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,SAAS,EAAE,EACjB,UAAU,EAAE,MAAM,EAAE,GACnB,SAAS,EAAE,CAOb;AAED,wEAAwE;AACxE,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,IAAI,CAAC,SAAS,EAAE,UAAU,GAAG,MAAM,CAAC,EAC/E,IAAI,EAAE,CAAC,EAAE,GACR,CAAC,EAAE,CAIL"}
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/apps-roster.ts
21
+ var apps_roster_exports = {};
22
+ __export(apps_roster_exports, {
23
+ filterAppsByIdentityGroups: () => filterAppsByIdentityGroups,
24
+ loadAppsRosterJson: () => loadAppsRosterJson,
25
+ sortAppsByNavOrder: () => sortAppsByNavOrder,
26
+ validateAppsRoster: () => validateAppsRoster
27
+ });
28
+ module.exports = __toCommonJS(apps_roster_exports);
29
+
30
+ // src/apps-roster/schema.ts
31
+ var ROLES = ["apex", "spoke"];
32
+ function validateAppsRoster(raw) {
33
+ const errors = [];
34
+ if (typeof raw !== "object" || raw === null) {
35
+ return { ok: false, errors: [{ path: "", message: "roster must be an object" }] };
36
+ }
37
+ const m = raw;
38
+ if (!Array.isArray(m.apps)) {
39
+ return { ok: false, errors: [{ path: "apps", message: "expected array" }] };
40
+ }
41
+ const apps = m.apps;
42
+ const seenSlugs = /* @__PURE__ */ new Map();
43
+ const seenSubdomains = /* @__PURE__ */ new Map();
44
+ const seenNavOrder = /* @__PURE__ */ new Map();
45
+ let apexCount = 0;
46
+ apps.forEach((entryUnknown, i) => {
47
+ const path = `apps[${i}]`;
48
+ if (typeof entryUnknown !== "object" || entryUnknown === null) {
49
+ errors.push({ path, message: "expected object" });
50
+ return;
51
+ }
52
+ const entry = entryUnknown;
53
+ if (typeof entry.slug !== "string" || entry.slug === "") {
54
+ errors.push({ path: `${path}.slug`, message: "expected non-empty string" });
55
+ } else {
56
+ const prior = seenSlugs.get(entry.slug);
57
+ if (prior !== void 0) {
58
+ errors.push({
59
+ path: `${path}.slug`,
60
+ message: `duplicate slug ${JSON.stringify(entry.slug)} (also at apps[${prior}])`
61
+ });
62
+ } else {
63
+ seenSlugs.set(entry.slug, i);
64
+ }
65
+ }
66
+ if (typeof entry.role !== "string" || !ROLES.includes(entry.role)) {
67
+ errors.push({
68
+ path: `${path}.role`,
69
+ message: `expected one of: ${ROLES.join(", ")}`
70
+ });
71
+ } else if (entry.role === "apex") {
72
+ apexCount++;
73
+ }
74
+ if (typeof entry.subdomain !== "string") {
75
+ errors.push({ path: `${path}.subdomain`, message: "expected string" });
76
+ } else {
77
+ if (entry.role === "apex" && entry.subdomain !== "") {
78
+ errors.push({
79
+ path: `${path}.subdomain`,
80
+ message: "apex apps must have empty subdomain"
81
+ });
82
+ }
83
+ if (entry.subdomain !== "") {
84
+ const prior = seenSubdomains.get(entry.subdomain);
85
+ if (prior !== void 0) {
86
+ errors.push({
87
+ path: `${path}.subdomain`,
88
+ message: `duplicate subdomain ${JSON.stringify(entry.subdomain)} (also at apps[${prior}])`
89
+ });
90
+ } else {
91
+ seenSubdomains.set(entry.subdomain, i);
92
+ }
93
+ }
94
+ }
95
+ if (typeof entry.displayName !== "string" || entry.displayName === "") {
96
+ errors.push({
97
+ path: `${path}.displayName`,
98
+ message: "expected non-empty string"
99
+ });
100
+ }
101
+ if (typeof entry.navOrder !== "number" || !Number.isFinite(entry.navOrder)) {
102
+ errors.push({ path: `${path}.navOrder`, message: "expected number" });
103
+ } else {
104
+ const prior = seenNavOrder.get(entry.navOrder);
105
+ if (prior !== void 0) {
106
+ errors.push({
107
+ path: `${path}.navOrder`,
108
+ message: `duplicate navOrder ${entry.navOrder} (also at apps[${prior}])`
109
+ });
110
+ } else {
111
+ seenNavOrder.set(entry.navOrder, i);
112
+ }
113
+ }
114
+ if (!Array.isArray(entry.requiredIdentityGroups) || entry.requiredIdentityGroups.some((g) => typeof g !== "string")) {
115
+ errors.push({
116
+ path: `${path}.requiredIdentityGroups`,
117
+ message: "expected string[]"
118
+ });
119
+ }
120
+ if (entry.enabled !== void 0 && typeof entry.enabled !== "boolean") {
121
+ errors.push({ path: `${path}.enabled`, message: "expected boolean" });
122
+ }
123
+ });
124
+ if (apexCount === 0) {
125
+ errors.push({ path: "apps", message: "roster must contain exactly one apex entry" });
126
+ } else if (apexCount > 1) {
127
+ errors.push({
128
+ path: "apps",
129
+ message: `roster must contain exactly one apex entry, found ${apexCount}`
130
+ });
131
+ }
132
+ if (errors.length > 0) return { ok: false, errors };
133
+ return { ok: true, value: m };
134
+ }
135
+ function filterAppsByIdentityGroups(apps, userGroups) {
136
+ const lower = userGroups.map((g) => g.toLowerCase());
137
+ return apps.filter((a) => {
138
+ if (a.enabled === false) return false;
139
+ if (!a.requiredIdentityGroups || a.requiredIdentityGroups.length === 0) return true;
140
+ return a.requiredIdentityGroups.some((g) => lower.includes(g.toLowerCase()));
141
+ });
142
+ }
143
+ function sortAppsByNavOrder(apps) {
144
+ return [...apps].sort(
145
+ (a, b) => (a.navOrder ?? 0) - (b.navOrder ?? 0) || a.slug.localeCompare(b.slug)
146
+ );
147
+ }
148
+
149
+ // src/apps-roster/load.ts
150
+ var import_node_fs = require("fs");
151
+ var import_node_path = require("path");
152
+ function loadAppsRosterJson(opts) {
153
+ const file = (0, import_node_path.resolve)(opts.path);
154
+ let raw;
155
+ try {
156
+ raw = (0, import_node_fs.readFileSync)(file, "utf8");
157
+ } catch (err) {
158
+ throw new Error(
159
+ `loadAppsRosterJson: cannot read ${file}: ${err.message}`,
160
+ { cause: err }
161
+ );
162
+ }
163
+ let parsed;
164
+ try {
165
+ parsed = JSON.parse(raw);
166
+ } catch (err) {
167
+ throw new Error(
168
+ `loadAppsRosterJson: invalid JSON in ${file}: ${err.message}`,
169
+ { cause: err }
170
+ );
171
+ }
172
+ const result = validateAppsRoster(parsed);
173
+ if (!result.ok) {
174
+ const lines = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join("\n");
175
+ throw new Error(`loadAppsRosterJson: ${file} failed validation:
176
+ ${lines}`);
177
+ }
178
+ return result.value;
179
+ }
180
+ // Annotate the CommonJS export names for ESM import in node:
181
+ 0 && (module.exports = {
182
+ filterAppsByIdentityGroups,
183
+ loadAppsRosterJson,
184
+ sortAppsByNavOrder,
185
+ validateAppsRoster
186
+ });
187
+ //# sourceMappingURL=apps-roster.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/apps-roster.ts","../src/apps-roster/schema.ts","../src/apps-roster/load.ts"],"sourcesContent":["export {\n validateAppsRoster,\n filterAppsByIdentityGroups,\n sortAppsByNavOrder,\n type TenantApp,\n type TenantAppRole,\n type AppsRoster,\n type RosterValidationError,\n} from \"./apps-roster/schema.js\";\nexport { loadAppsRosterJson, type LoadAppsRosterOptions } from \"./apps-roster/load.js\";\n","// =============================================================================\n// Tenant app roster.\n//\n// One file per tenant that lists every app (apex + spokes) the tenant\n// ecosystem contains. Replaces the runtime DynamoDB registry. Stored as\n// YAML in <tenant>-infra/config/apps.yaml (canonical) and mirrored to\n// <tenant>-apex/config/apps.json for runtime serving by /api/apps.\n//\n// Adding a new spoke = a PR to the spoke repo (its app.manifest.json) +\n// a PR to <tenant>-infra/config/apps.yaml + a PR to <tenant>-apex/config/\n// apps.json. Validation catches drift.\n// =============================================================================\n\nexport type TenantAppRole = \"apex\" | \"spoke\";\n\nexport type TenantApp = {\n /** Stable identifier. Matches the spoke's app.manifest.json#appSlug. */\n slug: string;\n /** \"apex\" (auth broker) or \"spoke\" (product app). */\n role: TenantAppRole;\n /** DNS label. Empty string for apex. */\n subdomain: string;\n /** Human-friendly name. Drives the shared nav. */\n displayName: string;\n /** Sort order. Lower comes first. */\n navOrder: number;\n /**\n * Cognito identity groups required to see this app in cross-app nav\n * AND to enter its routes (when the spoke's createAuth is wired to its\n * own manifest's access policy). Empty = all authenticated users.\n */\n requiredIdentityGroups: string[];\n /**\n * Static feature toggle. Default true. Set false to hide an app from\n * cross-app nav without removing the entry. Editing this requires a\n * PR + redeploy -- this is NOT mutable runtime state.\n */\n enabled?: boolean;\n};\n\nexport type AppsRoster = {\n apps: TenantApp[];\n};\n\nexport type RosterValidationError = {\n path: string;\n message: string;\n};\n\nconst ROLES: readonly string[] = [\"apex\", \"spoke\"];\n\n/**\n * Pure validator for the roster object (parsed from YAML or JSON). Returns\n * the typed roster on success, or an array of errors on failure. No throws.\n */\nexport function validateAppsRoster(\n raw: unknown,\n): { ok: true; value: AppsRoster } | { ok: false; errors: RosterValidationError[] } {\n const errors: RosterValidationError[] = [];\n if (typeof raw !== \"object\" || raw === null) {\n return { ok: false, errors: [{ path: \"\", message: \"roster must be an object\" }] };\n }\n const m = raw as Record<string, unknown>;\n if (!Array.isArray(m.apps)) {\n return { ok: false, errors: [{ path: \"apps\", message: \"expected array\" }] };\n }\n const apps = m.apps as unknown[];\n\n const seenSlugs = new Map<string, number>();\n const seenSubdomains = new Map<string, number>();\n const seenNavOrder = new Map<number, number>();\n let apexCount = 0;\n\n apps.forEach((entryUnknown, i) => {\n const path = `apps[${i}]`;\n if (typeof entryUnknown !== \"object\" || entryUnknown === null) {\n errors.push({ path, message: \"expected object\" });\n return;\n }\n const entry = entryUnknown as Record<string, unknown>;\n\n if (typeof entry.slug !== \"string\" || entry.slug === \"\") {\n errors.push({ path: `${path}.slug`, message: \"expected non-empty string\" });\n } else {\n const prior = seenSlugs.get(entry.slug);\n if (prior !== undefined) {\n errors.push({\n path: `${path}.slug`,\n message: `duplicate slug ${JSON.stringify(entry.slug)} (also at apps[${prior}])`,\n });\n } else {\n seenSlugs.set(entry.slug, i);\n }\n }\n\n if (typeof entry.role !== \"string\" || !ROLES.includes(entry.role)) {\n errors.push({\n path: `${path}.role`,\n message: `expected one of: ${ROLES.join(\", \")}`,\n });\n } else if (entry.role === \"apex\") {\n apexCount++;\n }\n\n if (typeof entry.subdomain !== \"string\") {\n errors.push({ path: `${path}.subdomain`, message: \"expected string\" });\n } else {\n if (entry.role === \"apex\" && entry.subdomain !== \"\") {\n errors.push({\n path: `${path}.subdomain`,\n message: \"apex apps must have empty subdomain\",\n });\n }\n if (entry.subdomain !== \"\") {\n const prior = seenSubdomains.get(entry.subdomain);\n if (prior !== undefined) {\n errors.push({\n path: `${path}.subdomain`,\n message: `duplicate subdomain ${JSON.stringify(entry.subdomain)} (also at apps[${prior}])`,\n });\n } else {\n seenSubdomains.set(entry.subdomain, i);\n }\n }\n }\n\n if (typeof entry.displayName !== \"string\" || entry.displayName === \"\") {\n errors.push({\n path: `${path}.displayName`,\n message: \"expected non-empty string\",\n });\n }\n\n if (typeof entry.navOrder !== \"number\" || !Number.isFinite(entry.navOrder)) {\n errors.push({ path: `${path}.navOrder`, message: \"expected number\" });\n } else {\n const prior = seenNavOrder.get(entry.navOrder);\n if (prior !== undefined) {\n errors.push({\n path: `${path}.navOrder`,\n message: `duplicate navOrder ${entry.navOrder} (also at apps[${prior}])`,\n });\n } else {\n seenNavOrder.set(entry.navOrder, i);\n }\n }\n\n if (\n !Array.isArray(entry.requiredIdentityGroups) ||\n entry.requiredIdentityGroups.some((g) => typeof g !== \"string\")\n ) {\n errors.push({\n path: `${path}.requiredIdentityGroups`,\n message: \"expected string[]\",\n });\n }\n\n if (entry.enabled !== undefined && typeof entry.enabled !== \"boolean\") {\n errors.push({ path: `${path}.enabled`, message: \"expected boolean\" });\n }\n });\n\n if (apexCount === 0) {\n errors.push({ path: \"apps\", message: \"roster must contain exactly one apex entry\" });\n } else if (apexCount > 1) {\n errors.push({\n path: \"apps\",\n message: `roster must contain exactly one apex entry, found ${apexCount}`,\n });\n }\n\n if (errors.length > 0) return { ok: false, errors };\n return { ok: true, value: m as unknown as AppsRoster };\n}\n\n/**\n * Filter the roster by user identity groups. Apps with empty\n * `requiredIdentityGroups` are visible to all authenticated users; otherwise\n * the user must be in at least one of the listed groups. `enabled: false`\n * apps are always filtered out.\n */\nexport function filterAppsByIdentityGroups(\n apps: TenantApp[],\n userGroups: string[],\n): TenantApp[] {\n const lower = userGroups.map((g) => g.toLowerCase());\n return apps.filter((a) => {\n if (a.enabled === false) return false;\n if (!a.requiredIdentityGroups || a.requiredIdentityGroups.length === 0) return true;\n return a.requiredIdentityGroups.some((g) => lower.includes(g.toLowerCase()));\n });\n}\n\n/** Sort apps by navOrder ASC, then slug. Mutates a copy, returns it. */\nexport function sortAppsByNavOrder<T extends Pick<TenantApp, \"navOrder\" | \"slug\">>(\n apps: T[],\n): T[] {\n return [...apps].sort(\n (a, b) => (a.navOrder ?? 0) - (b.navOrder ?? 0) || a.slug.localeCompare(b.slug),\n );\n}\n","import { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nimport { validateAppsRoster, type AppsRoster } from \"./schema.js\";\n\nexport type LoadAppsRosterOptions = {\n /** Absolute path to a JSON apps-roster file. */\n path: string;\n};\n\n/**\n * Read + validate a JSON apps-roster file. Throws with a consolidated error\n * message listing every validation failure.\n *\n * YAML parsing is intentionally not bundled in this package; use\n * @augmenting-integrations/deploy-tools `augint validate-app-roster` for the\n * cross-format check between the infra YAML source and the apex JSON mirror.\n */\nexport function loadAppsRosterJson(opts: LoadAppsRosterOptions): AppsRoster {\n const file = resolve(opts.path);\n let raw: string;\n try {\n raw = readFileSync(file, \"utf8\");\n } catch (err) {\n throw new Error(\n `loadAppsRosterJson: cannot read ${file}: ${(err as Error).message}`,\n { cause: err },\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(\n `loadAppsRosterJson: invalid JSON in ${file}: ${(err as Error).message}`,\n { cause: err },\n );\n }\n const result = validateAppsRoster(parsed);\n if (!result.ok) {\n const lines = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n throw new Error(`loadAppsRosterJson: ${file} failed validation:\\n${lines}`);\n }\n return result.value;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,3 @@
1
+ export { validateAppsRoster, filterAppsByIdentityGroups, sortAppsByNavOrder, type TenantApp, type TenantAppRole, type AppsRoster, type RosterValidationError, } from "./apps-roster/schema.js";
2
+ export { loadAppsRosterJson, type LoadAppsRosterOptions } from "./apps-roster/load.js";
3
+ //# sourceMappingURL=apps-roster.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apps-roster.d.ts","sourceRoot":"","sources":["../src/apps-roster.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,0BAA0B,EAC1B,kBAAkB,EAClB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,qBAAqB,GAC3B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,kBAAkB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,44 @@
1
+ import {
2
+ filterAppsByIdentityGroups,
3
+ sortAppsByNavOrder,
4
+ validateAppsRoster
5
+ } from "./chunk-ZJFI7R4O.js";
6
+
7
+ // src/apps-roster/load.ts
8
+ import { readFileSync } from "fs";
9
+ import { resolve } from "path";
10
+ function loadAppsRosterJson(opts) {
11
+ const file = resolve(opts.path);
12
+ let raw;
13
+ try {
14
+ raw = readFileSync(file, "utf8");
15
+ } catch (err) {
16
+ throw new Error(
17
+ `loadAppsRosterJson: cannot read ${file}: ${err.message}`,
18
+ { cause: err }
19
+ );
20
+ }
21
+ let parsed;
22
+ try {
23
+ parsed = JSON.parse(raw);
24
+ } catch (err) {
25
+ throw new Error(
26
+ `loadAppsRosterJson: invalid JSON in ${file}: ${err.message}`,
27
+ { cause: err }
28
+ );
29
+ }
30
+ const result = validateAppsRoster(parsed);
31
+ if (!result.ok) {
32
+ const lines = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join("\n");
33
+ throw new Error(`loadAppsRosterJson: ${file} failed validation:
34
+ ${lines}`);
35
+ }
36
+ return result.value;
37
+ }
38
+ export {
39
+ filterAppsByIdentityGroups,
40
+ loadAppsRosterJson,
41
+ sortAppsByNavOrder,
42
+ validateAppsRoster
43
+ };
44
+ //# sourceMappingURL=apps-roster.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/apps-roster/load.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nimport { validateAppsRoster, type AppsRoster } from \"./schema.js\";\n\nexport type LoadAppsRosterOptions = {\n /** Absolute path to a JSON apps-roster file. */\n path: string;\n};\n\n/**\n * Read + validate a JSON apps-roster file. Throws with a consolidated error\n * message listing every validation failure.\n *\n * YAML parsing is intentionally not bundled in this package; use\n * @augmenting-integrations/deploy-tools `augint validate-app-roster` for the\n * cross-format check between the infra YAML source and the apex JSON mirror.\n */\nexport function loadAppsRosterJson(opts: LoadAppsRosterOptions): AppsRoster {\n const file = resolve(opts.path);\n let raw: string;\n try {\n raw = readFileSync(file, \"utf8\");\n } catch (err) {\n throw new Error(\n `loadAppsRosterJson: cannot read ${file}: ${(err as Error).message}`,\n { cause: err },\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(\n `loadAppsRosterJson: invalid JSON in ${file}: ${(err as Error).message}`,\n { cause: err },\n );\n }\n const result = validateAppsRoster(parsed);\n if (!result.ok) {\n const lines = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n throw new Error(`loadAppsRosterJson: ${file} failed validation:\\n${lines}`);\n }\n return result.value;\n}\n"],"mappings":";;;;;;;AAAA,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AAiBjB,SAAS,mBAAmB,MAAyC;AAC1E,QAAM,OAAO,QAAQ,KAAK,IAAI;AAC9B,MAAI;AACJ,MAAI;AACF,UAAM,aAAa,MAAM,MAAM;AAAA,EACjC,SAAS,KAAK;AACZ,UAAM,IAAI;AAAA,MACR,mCAAmC,IAAI,KAAM,IAAc,OAAO;AAAA,MAClE,EAAE,OAAO,IAAI;AAAA,IACf;AAAA,EACF;AACA,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,GAAG;AAAA,EACzB,SAAS,KAAK;AACZ,UAAM,IAAI;AAAA,MACR,uCAAuC,IAAI,KAAM,IAAc,OAAO;AAAA,MACtE,EAAE,OAAO,IAAI;AAAA,IACf;AAAA,EACF;AACA,QAAM,SAAS,mBAAmB,MAAM;AACxC,MAAI,CAAC,OAAO,IAAI;AACd,UAAM,QAAQ,OAAO,OAAO,IAAI,CAAC,MAAM,OAAO,EAAE,IAAI,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI;AAC/E,UAAM,IAAI,MAAM,uBAAuB,IAAI;AAAA,EAAwB,KAAK,EAAE;AAAA,EAC5E;AACA,SAAO,OAAO;AAChB;","names":[]}
@@ -0,0 +1,125 @@
1
+ // src/apps-roster/schema.ts
2
+ var ROLES = ["apex", "spoke"];
3
+ function validateAppsRoster(raw) {
4
+ const errors = [];
5
+ if (typeof raw !== "object" || raw === null) {
6
+ return { ok: false, errors: [{ path: "", message: "roster must be an object" }] };
7
+ }
8
+ const m = raw;
9
+ if (!Array.isArray(m.apps)) {
10
+ return { ok: false, errors: [{ path: "apps", message: "expected array" }] };
11
+ }
12
+ const apps = m.apps;
13
+ const seenSlugs = /* @__PURE__ */ new Map();
14
+ const seenSubdomains = /* @__PURE__ */ new Map();
15
+ const seenNavOrder = /* @__PURE__ */ new Map();
16
+ let apexCount = 0;
17
+ apps.forEach((entryUnknown, i) => {
18
+ const path = `apps[${i}]`;
19
+ if (typeof entryUnknown !== "object" || entryUnknown === null) {
20
+ errors.push({ path, message: "expected object" });
21
+ return;
22
+ }
23
+ const entry = entryUnknown;
24
+ if (typeof entry.slug !== "string" || entry.slug === "") {
25
+ errors.push({ path: `${path}.slug`, message: "expected non-empty string" });
26
+ } else {
27
+ const prior = seenSlugs.get(entry.slug);
28
+ if (prior !== void 0) {
29
+ errors.push({
30
+ path: `${path}.slug`,
31
+ message: `duplicate slug ${JSON.stringify(entry.slug)} (also at apps[${prior}])`
32
+ });
33
+ } else {
34
+ seenSlugs.set(entry.slug, i);
35
+ }
36
+ }
37
+ if (typeof entry.role !== "string" || !ROLES.includes(entry.role)) {
38
+ errors.push({
39
+ path: `${path}.role`,
40
+ message: `expected one of: ${ROLES.join(", ")}`
41
+ });
42
+ } else if (entry.role === "apex") {
43
+ apexCount++;
44
+ }
45
+ if (typeof entry.subdomain !== "string") {
46
+ errors.push({ path: `${path}.subdomain`, message: "expected string" });
47
+ } else {
48
+ if (entry.role === "apex" && entry.subdomain !== "") {
49
+ errors.push({
50
+ path: `${path}.subdomain`,
51
+ message: "apex apps must have empty subdomain"
52
+ });
53
+ }
54
+ if (entry.subdomain !== "") {
55
+ const prior = seenSubdomains.get(entry.subdomain);
56
+ if (prior !== void 0) {
57
+ errors.push({
58
+ path: `${path}.subdomain`,
59
+ message: `duplicate subdomain ${JSON.stringify(entry.subdomain)} (also at apps[${prior}])`
60
+ });
61
+ } else {
62
+ seenSubdomains.set(entry.subdomain, i);
63
+ }
64
+ }
65
+ }
66
+ if (typeof entry.displayName !== "string" || entry.displayName === "") {
67
+ errors.push({
68
+ path: `${path}.displayName`,
69
+ message: "expected non-empty string"
70
+ });
71
+ }
72
+ if (typeof entry.navOrder !== "number" || !Number.isFinite(entry.navOrder)) {
73
+ errors.push({ path: `${path}.navOrder`, message: "expected number" });
74
+ } else {
75
+ const prior = seenNavOrder.get(entry.navOrder);
76
+ if (prior !== void 0) {
77
+ errors.push({
78
+ path: `${path}.navOrder`,
79
+ message: `duplicate navOrder ${entry.navOrder} (also at apps[${prior}])`
80
+ });
81
+ } else {
82
+ seenNavOrder.set(entry.navOrder, i);
83
+ }
84
+ }
85
+ if (!Array.isArray(entry.requiredIdentityGroups) || entry.requiredIdentityGroups.some((g) => typeof g !== "string")) {
86
+ errors.push({
87
+ path: `${path}.requiredIdentityGroups`,
88
+ message: "expected string[]"
89
+ });
90
+ }
91
+ if (entry.enabled !== void 0 && typeof entry.enabled !== "boolean") {
92
+ errors.push({ path: `${path}.enabled`, message: "expected boolean" });
93
+ }
94
+ });
95
+ if (apexCount === 0) {
96
+ errors.push({ path: "apps", message: "roster must contain exactly one apex entry" });
97
+ } else if (apexCount > 1) {
98
+ errors.push({
99
+ path: "apps",
100
+ message: `roster must contain exactly one apex entry, found ${apexCount}`
101
+ });
102
+ }
103
+ if (errors.length > 0) return { ok: false, errors };
104
+ return { ok: true, value: m };
105
+ }
106
+ function filterAppsByIdentityGroups(apps, userGroups) {
107
+ const lower = userGroups.map((g) => g.toLowerCase());
108
+ return apps.filter((a) => {
109
+ if (a.enabled === false) return false;
110
+ if (!a.requiredIdentityGroups || a.requiredIdentityGroups.length === 0) return true;
111
+ return a.requiredIdentityGroups.some((g) => lower.includes(g.toLowerCase()));
112
+ });
113
+ }
114
+ function sortAppsByNavOrder(apps) {
115
+ return [...apps].sort(
116
+ (a, b) => (a.navOrder ?? 0) - (b.navOrder ?? 0) || a.slug.localeCompare(b.slug)
117
+ );
118
+ }
119
+
120
+ export {
121
+ validateAppsRoster,
122
+ filterAppsByIdentityGroups,
123
+ sortAppsByNavOrder
124
+ };
125
+ //# sourceMappingURL=chunk-ZJFI7R4O.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/apps-roster/schema.ts"],"sourcesContent":["// =============================================================================\n// Tenant app roster.\n//\n// One file per tenant that lists every app (apex + spokes) the tenant\n// ecosystem contains. Replaces the runtime DynamoDB registry. Stored as\n// YAML in <tenant>-infra/config/apps.yaml (canonical) and mirrored to\n// <tenant>-apex/config/apps.json for runtime serving by /api/apps.\n//\n// Adding a new spoke = a PR to the spoke repo (its app.manifest.json) +\n// a PR to <tenant>-infra/config/apps.yaml + a PR to <tenant>-apex/config/\n// apps.json. Validation catches drift.\n// =============================================================================\n\nexport type TenantAppRole = \"apex\" | \"spoke\";\n\nexport type TenantApp = {\n /** Stable identifier. Matches the spoke's app.manifest.json#appSlug. */\n slug: string;\n /** \"apex\" (auth broker) or \"spoke\" (product app). */\n role: TenantAppRole;\n /** DNS label. Empty string for apex. */\n subdomain: string;\n /** Human-friendly name. Drives the shared nav. */\n displayName: string;\n /** Sort order. Lower comes first. */\n navOrder: number;\n /**\n * Cognito identity groups required to see this app in cross-app nav\n * AND to enter its routes (when the spoke's createAuth is wired to its\n * own manifest's access policy). Empty = all authenticated users.\n */\n requiredIdentityGroups: string[];\n /**\n * Static feature toggle. Default true. Set false to hide an app from\n * cross-app nav without removing the entry. Editing this requires a\n * PR + redeploy -- this is NOT mutable runtime state.\n */\n enabled?: boolean;\n};\n\nexport type AppsRoster = {\n apps: TenantApp[];\n};\n\nexport type RosterValidationError = {\n path: string;\n message: string;\n};\n\nconst ROLES: readonly string[] = [\"apex\", \"spoke\"];\n\n/**\n * Pure validator for the roster object (parsed from YAML or JSON). Returns\n * the typed roster on success, or an array of errors on failure. No throws.\n */\nexport function validateAppsRoster(\n raw: unknown,\n): { ok: true; value: AppsRoster } | { ok: false; errors: RosterValidationError[] } {\n const errors: RosterValidationError[] = [];\n if (typeof raw !== \"object\" || raw === null) {\n return { ok: false, errors: [{ path: \"\", message: \"roster must be an object\" }] };\n }\n const m = raw as Record<string, unknown>;\n if (!Array.isArray(m.apps)) {\n return { ok: false, errors: [{ path: \"apps\", message: \"expected array\" }] };\n }\n const apps = m.apps as unknown[];\n\n const seenSlugs = new Map<string, number>();\n const seenSubdomains = new Map<string, number>();\n const seenNavOrder = new Map<number, number>();\n let apexCount = 0;\n\n apps.forEach((entryUnknown, i) => {\n const path = `apps[${i}]`;\n if (typeof entryUnknown !== \"object\" || entryUnknown === null) {\n errors.push({ path, message: \"expected object\" });\n return;\n }\n const entry = entryUnknown as Record<string, unknown>;\n\n if (typeof entry.slug !== \"string\" || entry.slug === \"\") {\n errors.push({ path: `${path}.slug`, message: \"expected non-empty string\" });\n } else {\n const prior = seenSlugs.get(entry.slug);\n if (prior !== undefined) {\n errors.push({\n path: `${path}.slug`,\n message: `duplicate slug ${JSON.stringify(entry.slug)} (also at apps[${prior}])`,\n });\n } else {\n seenSlugs.set(entry.slug, i);\n }\n }\n\n if (typeof entry.role !== \"string\" || !ROLES.includes(entry.role)) {\n errors.push({\n path: `${path}.role`,\n message: `expected one of: ${ROLES.join(\", \")}`,\n });\n } else if (entry.role === \"apex\") {\n apexCount++;\n }\n\n if (typeof entry.subdomain !== \"string\") {\n errors.push({ path: `${path}.subdomain`, message: \"expected string\" });\n } else {\n if (entry.role === \"apex\" && entry.subdomain !== \"\") {\n errors.push({\n path: `${path}.subdomain`,\n message: \"apex apps must have empty subdomain\",\n });\n }\n if (entry.subdomain !== \"\") {\n const prior = seenSubdomains.get(entry.subdomain);\n if (prior !== undefined) {\n errors.push({\n path: `${path}.subdomain`,\n message: `duplicate subdomain ${JSON.stringify(entry.subdomain)} (also at apps[${prior}])`,\n });\n } else {\n seenSubdomains.set(entry.subdomain, i);\n }\n }\n }\n\n if (typeof entry.displayName !== \"string\" || entry.displayName === \"\") {\n errors.push({\n path: `${path}.displayName`,\n message: \"expected non-empty string\",\n });\n }\n\n if (typeof entry.navOrder !== \"number\" || !Number.isFinite(entry.navOrder)) {\n errors.push({ path: `${path}.navOrder`, message: \"expected number\" });\n } else {\n const prior = seenNavOrder.get(entry.navOrder);\n if (prior !== undefined) {\n errors.push({\n path: `${path}.navOrder`,\n message: `duplicate navOrder ${entry.navOrder} (also at apps[${prior}])`,\n });\n } else {\n seenNavOrder.set(entry.navOrder, i);\n }\n }\n\n if (\n !Array.isArray(entry.requiredIdentityGroups) ||\n entry.requiredIdentityGroups.some((g) => typeof g !== \"string\")\n ) {\n errors.push({\n path: `${path}.requiredIdentityGroups`,\n message: \"expected string[]\",\n });\n }\n\n if (entry.enabled !== undefined && typeof entry.enabled !== \"boolean\") {\n errors.push({ path: `${path}.enabled`, message: \"expected boolean\" });\n }\n });\n\n if (apexCount === 0) {\n errors.push({ path: \"apps\", message: \"roster must contain exactly one apex entry\" });\n } else if (apexCount > 1) {\n errors.push({\n path: \"apps\",\n message: `roster must contain exactly one apex entry, found ${apexCount}`,\n });\n }\n\n if (errors.length > 0) return { ok: false, errors };\n return { ok: true, value: m as unknown as AppsRoster };\n}\n\n/**\n * Filter the roster by user identity groups. Apps with empty\n * `requiredIdentityGroups` are visible to all authenticated users; otherwise\n * the user must be in at least one of the listed groups. `enabled: false`\n * apps are always filtered out.\n */\nexport function filterAppsByIdentityGroups(\n apps: TenantApp[],\n userGroups: string[],\n): TenantApp[] {\n const lower = userGroups.map((g) => g.toLowerCase());\n return apps.filter((a) => {\n if (a.enabled === false) return false;\n if (!a.requiredIdentityGroups || a.requiredIdentityGroups.length === 0) return true;\n return a.requiredIdentityGroups.some((g) => lower.includes(g.toLowerCase()));\n });\n}\n\n/** Sort apps by navOrder ASC, then slug. Mutates a copy, returns it. */\nexport function sortAppsByNavOrder<T extends Pick<TenantApp, \"navOrder\" | \"slug\">>(\n apps: T[],\n): T[] {\n return [...apps].sort(\n (a, b) => (a.navOrder ?? 0) - (b.navOrder ?? 0) || a.slug.localeCompare(b.slug),\n );\n}\n"],"mappings":"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","names":[]}
@@ -11,7 +11,7 @@ export type AppManifest = {
11
11
  role: AppRole;
12
12
  /** Subdomain label. "" for apex. */
13
13
  subdomain: string;
14
- /** Human-friendly name for nav + admin UI. */
14
+ /** Human-friendly name for the shared cross-app nav. */
15
15
  displayName: string;
16
16
  /** Sort order in the cross-app nav. Lower = first. */
17
17
  navOrder: number;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/manifest/schema.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAEvC,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,iBAAiB,GAAG,UAAU,GAAG,MAAM,CAAC;AAEnF,MAAM,MAAM,WAAW,GAAG;IACxB,oDAAoD;IACpD,aAAa,EAAE,CAAC,CAAC;IACjB,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,IAAI,EAAE,OAAO,CAAC;IACd,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE;QACN;;;;WAIG;QACH,sBAAsB,EAAE,MAAM,EAAE,CAAC;KAClC,CAAC;IACF,QAAQ,EAAE;QACR,0DAA0D;QAC1D,OAAO,EAAE,OAAO,CAAC;QACjB,qDAAqD;QACrD,QAAQ,EAAE,OAAO,CAAC;QAClB,qDAAqD;QACrD,WAAW,EAAE,OAAO,CAAC;QACrB,oCAAoC;QACpC,aAAa,EAAE,OAAO,CAAC;KACxB,CAAC;IACF,SAAS,EAAE;QACT;;;;;WAKG;QACH,IAAI,EAAE,aAAa,CAAC;QACpB,sDAAsD;QACtD,UAAU,EAAE,OAAO,CAAC;KACrB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,OAAO,GACX;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,uBAAuB,EAAE,CAAA;CAAE,CA0ErF"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/manifest/schema.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAEvC,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,iBAAiB,GAAG,UAAU,GAAG,MAAM,CAAC;AAEnF,MAAM,MAAM,WAAW,GAAG;IACxB,oDAAoD;IACpD,aAAa,EAAE,CAAC,CAAC;IACjB,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,IAAI,EAAE,OAAO,CAAC;IACd,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE;QACN;;;;WAIG;QACH,sBAAsB,EAAE,MAAM,EAAE,CAAC;KAClC,CAAC;IACF,QAAQ,EAAE;QACR,0DAA0D;QAC1D,OAAO,EAAE,OAAO,CAAC;QACjB,qDAAqD;QACrD,QAAQ,EAAE,OAAO,CAAC;QAClB,qDAAqD;QACrD,WAAW,EAAE,OAAO,CAAC;QACrB,oCAAoC;QACpC,aAAa,EAAE,OAAO,CAAC;KACxB,CAAC;IACF,SAAS,EAAE;QACT;;;;;WAKG;QACH,IAAI,EAAE,aAAa,CAAC;QACpB,sDAAsD;QACtD,UAAU,EAAE,OAAO,CAAC;KACrB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,OAAO,GACX;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,uBAAuB,EAAE,CAAA;CAAE,CA0ErF"}
@@ -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 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":[]}
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// schema validator, the runtime app-access enforcement, and the spoke\n// kernel infra wiring. Cross-tenant membership (which spokes belong to\n// this tenant) lives in the tenant roster at <tenant>-infra/config/\n// apps.yaml -- it's not duplicated here. Product developers should only\n// have to edit this file (not env vars, workflow files) to change their\n// 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 the shared cross-app nav. */\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 +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 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":[]}
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// schema validator, the runtime app-access enforcement, and the spoke\n// kernel infra wiring. Cross-tenant membership (which spokes belong to\n// this tenant) lives in the tenant roster at <tenant>-infra/config/\n// apps.yaml -- it's not duplicated here. Product developers should only\n// have to edit this file (not env vars, workflow files) to change their\n// 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 the shared cross-app nav. */\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":[]}