@augmenting-integrations/create-tenant 8.6.0 → 8.8.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/README.md CHANGED
@@ -9,12 +9,15 @@ pnpm dlx @augmenting-integrations/create-tenant acme --apex=acme.com
9
9
  ## What gets generated
10
10
 
11
11
  The Next.js 16 apex for a new tenant — the single OAuth broker and the
12
- home of the app registry:
12
+ runtime serving point for the tenant app roster:
13
13
 
14
14
  - `/api/auth/[...nextauth]` — the ONLY Cognito callback in the tenant
15
15
  ecosystem; sets the parent-domain session cookie
16
- - `/api/apps` — registry auto-discovery handler that every spoke's AppShell
17
- fetches to render cross-app navigation
16
+ - `/api/apps` — static-roster handler reading `config/apps.json`,
17
+ filtered by user identity groups. Spokes proxy here for cross-app nav.
18
+ - `app.manifest.json` declaring this apex
19
+ - `config/apps.json` seeded with just the apex entry (add spokes as you
20
+ scaffold them)
18
21
  - `/login` — Cognito sign-in entry point (spokes redirect here)
19
22
  - `loadTenantConfig({ role: "apex" })` + `<TenantBootScript>` +
20
23
  `<TenantProvider>` so spokes see the same tenant struct
@@ -27,11 +30,12 @@ The generated app is portable; the AWS infra is not:
27
30
  2. Cognito user pool + one App Client with ONE callback URL:
28
31
  `https://<your-apex>/api/auth/callback/cognito`
29
32
  3. Hosted zone + cert for `<your-apex>` and `*.<your-apex>`
30
- 4. App registry DynamoDB table (PK = `slug`); set `APP_REGISTRY_TABLE`
31
- 5. Secrets Manager rows for `AUTH_SECRET`, `AUTH_COGNITO_SECRET`
32
- 6. GitHub OIDC role in each AWS account
33
- 7. SAM `template.yaml` (copy from an existing apex like
33
+ 4. Secrets Manager rows for `AUTH_SECRET`, `AUTH_COGNITO_SECRET`
34
+ 5. GitHub OIDC role in each AWS account
35
+ 6. SAM `template.yaml` (copy from an existing apex like
34
36
  `augint-example-web` and adapt)
37
+ 7. A central infra repo (`<tenant>-infra`) with `config/apps.yaml`
38
+ declaring this tenant's app roster
35
39
 
36
40
  ## Adding spokes after the apex exists
37
41
 
@@ -39,4 +43,7 @@ The generated app is portable; the AWS infra is not:
39
43
  pnpm dlx @augmenting-integrations/create-spoke my-product-spoke
40
44
  ```
41
45
 
42
- Then register the spoke in this apex's DynamoDB app registry table.
46
+ Then add the new spoke's entry to `<tenant>-infra/config/apps.yaml` AND
47
+ to this apex repo's `config/apps.json`. `pnpm exec augint validate-app-roster`
48
+ enforces the two files agree. The spoke's `/api/apps` proxies here, so
49
+ no per-spoke roster maintenance is needed.
package/dist/cli.js CHANGED
@@ -70,7 +70,7 @@ function main() {
70
70
  console.log(" pnpm install");
71
71
  console.log(" pnpm dev");
72
72
  console.log("");
73
- console.log("Then provision tenant infra (Cognito + DNS + registry table):");
73
+ console.log("Then provision tenant infra (Cognito + DNS + config/apps.yaml roster):");
74
74
  console.log(" see README.md");
75
75
  }
76
76
  main();
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport process from \"node:process\";\n\n// =============================================================================\n// create-tenant -- scaffold a new tenant apex app.\n//\n// Usage:\n// pnpm dlx @augmenting-integrations/create-tenant <name> [--apex=foo.com]\n//\n// Writes a minimal but deployable Next 16 apex to ./<name>/ that:\n// - Mounts /api/auth/[...nextauth] -- the ONLY Cognito OAuth callback in\n// the tenant ecosystem\n// - Exposes /api/apps (static-roster handler, read by every spoke's\n// AppShell for cross-app nav)\n// - Wires loadTenantConfig({ role: \"apex\" }) + TenantBootScript +\n// TenantProvider so spokes can read the same tenant struct\n// - Includes a /studio admin landing page stub\n//\n// Out of scope (manual follow-up, documented in the generated README):\n// - Cognito User Pool + App Client provisioning\n// - DNS hosted zone + cert\n// - Tenant app roster file (config/apps.yaml in <tenant>-infra and\n// config/apps.json mirror in the apex repo)\n// - GitHub OIDC role for deploys\n// - First spoke (use create-spoke for each product subdomain)\n// =============================================================================\n\ntype Flags = {\n name: string;\n apex: string;\n};\n\nfunction parseArgs(argv: string[]): Flags {\n const args = argv.slice(2);\n let name: string | undefined;\n let apex: string | undefined;\n for (const a of args) {\n if (a.startsWith(\"--apex=\")) apex = a.slice(\"--apex=\".length);\n else if (!a.startsWith(\"--\")) name = a;\n }\n if (!name) {\n console.error(\"Usage: create-tenant <name> [--apex=tenant.example.com]\");\n process.exit(1);\n }\n return { name, apex: apex ?? `${name}.example.com` };\n}\n\nfunction templatesDir(): string {\n const here = fileURLToPath(import.meta.url);\n return path.resolve(path.dirname(here), \"..\", \"templates\");\n}\n\nfunction ensureEmptyDir(target: string): void {\n if (fs.existsSync(target)) {\n const entries = fs.readdirSync(target);\n if (entries.length > 0) {\n console.error(`Refusing to write into non-empty directory: ${target}`);\n process.exit(1);\n }\n } else {\n fs.mkdirSync(target, { recursive: true });\n }\n}\n\nfunction replaceVars(content: string, flags: Flags): string {\n return content\n .replace(/__TENANT_NAME__/g, flags.name)\n .replace(/__TENANT_APEX__/g, flags.apex)\n .replace(/__TENANT_PARENT__/g, `.${flags.apex}`);\n}\n\nfunction copyTree(src: string, dst: string, flags: Flags): void {\n for (const entry of fs.readdirSync(src, { withFileTypes: true })) {\n const srcPath = path.join(src, entry.name);\n const baseName = entry.name.endsWith(\".tmpl\")\n ? entry.name.slice(0, -\".tmpl\".length)\n : entry.name;\n const dstPath = path.join(dst, replaceVars(baseName, flags));\n if (entry.isDirectory()) {\n fs.mkdirSync(dstPath, { recursive: true });\n copyTree(srcPath, dstPath, flags);\n } else {\n const raw = fs.readFileSync(srcPath, \"utf8\");\n fs.writeFileSync(dstPath, replaceVars(raw, flags), \"utf8\");\n }\n }\n}\n\nfunction main(): void {\n const flags = parseArgs(process.argv);\n const target = path.resolve(process.cwd(), flags.name);\n ensureEmptyDir(target);\n const templates = templatesDir();\n if (!fs.existsSync(templates)) {\n console.error(`Templates directory missing at ${templates}. Re-install the CLI.`);\n process.exit(1);\n }\n copyTree(templates, target, flags);\n console.log(`Created apex \"${flags.name}\" at ${target}`);\n console.log(`Apex domain: ${flags.apex}`);\n console.log(\"\");\n console.log(\"Next steps:\");\n console.log(` cd ${flags.name}`);\n console.log(\" cp .env.example .env # fill in Cognito ARNs + AWS resources\");\n console.log(\" pnpm install\");\n console.log(\" pnpm dev\");\n console.log(\"\");\n console.log(\"Then provision tenant infra (Cognito + DNS + registry table):\");\n console.log(\" see README.md\");\n}\n\nmain();\n"],"mappings":";;;AAAA,YAAY,QAAQ;AACpB,YAAY,UAAU;AACtB,SAAS,qBAAqB;AAC9B,OAAO,aAAa;AA+BpB,SAAS,UAAU,MAAuB;AACxC,QAAM,OAAO,KAAK,MAAM,CAAC;AACzB,MAAI;AACJ,MAAI;AACJ,aAAW,KAAK,MAAM;AACpB,QAAI,EAAE,WAAW,SAAS,EAAG,QAAO,EAAE,MAAM,UAAU,MAAM;AAAA,aACnD,CAAC,EAAE,WAAW,IAAI,EAAG,QAAO;AAAA,EACvC;AACA,MAAI,CAAC,MAAM;AACT,YAAQ,MAAM,yDAAyD;AACvE,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,SAAO,EAAE,MAAM,MAAM,QAAQ,GAAG,IAAI,eAAe;AACrD;AAEA,SAAS,eAAuB;AAC9B,QAAM,OAAO,cAAc,YAAY,GAAG;AAC1C,SAAY,aAAa,aAAQ,IAAI,GAAG,MAAM,WAAW;AAC3D;AAEA,SAAS,eAAe,QAAsB;AAC5C,MAAO,cAAW,MAAM,GAAG;AACzB,UAAM,UAAa,eAAY,MAAM;AACrC,QAAI,QAAQ,SAAS,GAAG;AACtB,cAAQ,MAAM,+CAA+C,MAAM,EAAE;AACrE,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF,OAAO;AACL,IAAG,aAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;AAAA,EAC1C;AACF;AAEA,SAAS,YAAY,SAAiB,OAAsB;AAC1D,SAAO,QACJ,QAAQ,oBAAoB,MAAM,IAAI,EACtC,QAAQ,oBAAoB,MAAM,IAAI,EACtC,QAAQ,sBAAsB,IAAI,MAAM,IAAI,EAAE;AACnD;AAEA,SAAS,SAAS,KAAa,KAAa,OAAoB;AAC9D,aAAW,SAAY,eAAY,KAAK,EAAE,eAAe,KAAK,CAAC,GAAG;AAChE,UAAM,UAAe,UAAK,KAAK,MAAM,IAAI;AACzC,UAAM,WAAW,MAAM,KAAK,SAAS,OAAO,IACxC,MAAM,KAAK,MAAM,GAAG,CAAC,QAAQ,MAAM,IACnC,MAAM;AACV,UAAM,UAAe,UAAK,KAAK,YAAY,UAAU,KAAK,CAAC;AAC3D,QAAI,MAAM,YAAY,GAAG;AACvB,MAAG,aAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AACzC,eAAS,SAAS,SAAS,KAAK;AAAA,IAClC,OAAO;AACL,YAAM,MAAS,gBAAa,SAAS,MAAM;AAC3C,MAAG,iBAAc,SAAS,YAAY,KAAK,KAAK,GAAG,MAAM;AAAA,IAC3D;AAAA,EACF;AACF;AAEA,SAAS,OAAa;AACpB,QAAM,QAAQ,UAAU,QAAQ,IAAI;AACpC,QAAM,SAAc,aAAQ,QAAQ,IAAI,GAAG,MAAM,IAAI;AACrD,iBAAe,MAAM;AACrB,QAAM,YAAY,aAAa;AAC/B,MAAI,CAAI,cAAW,SAAS,GAAG;AAC7B,YAAQ,MAAM,kCAAkC,SAAS,uBAAuB;AAChF,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,WAAS,WAAW,QAAQ,KAAK;AACjC,UAAQ,IAAI,iBAAiB,MAAM,IAAI,QAAQ,MAAM,EAAE;AACvD,UAAQ,IAAI,gBAAgB,MAAM,IAAI,EAAE;AACxC,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,aAAa;AACzB,UAAQ,IAAI,QAAQ,MAAM,IAAI,EAAE;AAChC,UAAQ,IAAI,gEAAgE;AAC5E,UAAQ,IAAI,gBAAgB;AAC5B,UAAQ,IAAI,YAAY;AACxB,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,+DAA+D;AAC3E,UAAQ,IAAI,iBAAiB;AAC/B;AAEA,KAAK;","names":[]}
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport process from \"node:process\";\n\n// =============================================================================\n// create-tenant -- scaffold a new tenant apex app.\n//\n// Usage:\n// pnpm dlx @augmenting-integrations/create-tenant <name> [--apex=foo.com]\n//\n// Writes a minimal but deployable Next 16 apex to ./<name>/ that:\n// - Mounts /api/auth/[...nextauth] -- the ONLY Cognito OAuth callback in\n// the tenant ecosystem\n// - Exposes /api/apps (static-roster handler, read by every spoke's\n// AppShell for cross-app nav)\n// - Wires loadTenantConfig({ role: \"apex\" }) + TenantBootScript +\n// TenantProvider so spokes can read the same tenant struct\n// - Includes a /studio admin landing page stub\n//\n// Out of scope (manual follow-up, documented in the generated README):\n// - Cognito User Pool + App Client provisioning\n// - DNS hosted zone + cert\n// - Tenant app roster file (config/apps.yaml in <tenant>-infra and\n// config/apps.json mirror in the apex repo)\n// - GitHub OIDC role for deploys\n// - First spoke (use create-spoke for each product subdomain)\n// =============================================================================\n\ntype Flags = {\n name: string;\n apex: string;\n};\n\nfunction parseArgs(argv: string[]): Flags {\n const args = argv.slice(2);\n let name: string | undefined;\n let apex: string | undefined;\n for (const a of args) {\n if (a.startsWith(\"--apex=\")) apex = a.slice(\"--apex=\".length);\n else if (!a.startsWith(\"--\")) name = a;\n }\n if (!name) {\n console.error(\"Usage: create-tenant <name> [--apex=tenant.example.com]\");\n process.exit(1);\n }\n return { name, apex: apex ?? `${name}.example.com` };\n}\n\nfunction templatesDir(): string {\n const here = fileURLToPath(import.meta.url);\n return path.resolve(path.dirname(here), \"..\", \"templates\");\n}\n\nfunction ensureEmptyDir(target: string): void {\n if (fs.existsSync(target)) {\n const entries = fs.readdirSync(target);\n if (entries.length > 0) {\n console.error(`Refusing to write into non-empty directory: ${target}`);\n process.exit(1);\n }\n } else {\n fs.mkdirSync(target, { recursive: true });\n }\n}\n\nfunction replaceVars(content: string, flags: Flags): string {\n return content\n .replace(/__TENANT_NAME__/g, flags.name)\n .replace(/__TENANT_APEX__/g, flags.apex)\n .replace(/__TENANT_PARENT__/g, `.${flags.apex}`);\n}\n\nfunction copyTree(src: string, dst: string, flags: Flags): void {\n for (const entry of fs.readdirSync(src, { withFileTypes: true })) {\n const srcPath = path.join(src, entry.name);\n const baseName = entry.name.endsWith(\".tmpl\")\n ? entry.name.slice(0, -\".tmpl\".length)\n : entry.name;\n const dstPath = path.join(dst, replaceVars(baseName, flags));\n if (entry.isDirectory()) {\n fs.mkdirSync(dstPath, { recursive: true });\n copyTree(srcPath, dstPath, flags);\n } else {\n const raw = fs.readFileSync(srcPath, \"utf8\");\n fs.writeFileSync(dstPath, replaceVars(raw, flags), \"utf8\");\n }\n }\n}\n\nfunction main(): void {\n const flags = parseArgs(process.argv);\n const target = path.resolve(process.cwd(), flags.name);\n ensureEmptyDir(target);\n const templates = templatesDir();\n if (!fs.existsSync(templates)) {\n console.error(`Templates directory missing at ${templates}. Re-install the CLI.`);\n process.exit(1);\n }\n copyTree(templates, target, flags);\n console.log(`Created apex \"${flags.name}\" at ${target}`);\n console.log(`Apex domain: ${flags.apex}`);\n console.log(\"\");\n console.log(\"Next steps:\");\n console.log(` cd ${flags.name}`);\n console.log(\" cp .env.example .env # fill in Cognito ARNs + AWS resources\");\n console.log(\" pnpm install\");\n console.log(\" pnpm dev\");\n console.log(\"\");\n console.log(\"Then provision tenant infra (Cognito + DNS + config/apps.yaml roster):\");\n console.log(\" see README.md\");\n}\n\nmain();\n"],"mappings":";;;AAAA,YAAY,QAAQ;AACpB,YAAY,UAAU;AACtB,SAAS,qBAAqB;AAC9B,OAAO,aAAa;AA+BpB,SAAS,UAAU,MAAuB;AACxC,QAAM,OAAO,KAAK,MAAM,CAAC;AACzB,MAAI;AACJ,MAAI;AACJ,aAAW,KAAK,MAAM;AACpB,QAAI,EAAE,WAAW,SAAS,EAAG,QAAO,EAAE,MAAM,UAAU,MAAM;AAAA,aACnD,CAAC,EAAE,WAAW,IAAI,EAAG,QAAO;AAAA,EACvC;AACA,MAAI,CAAC,MAAM;AACT,YAAQ,MAAM,yDAAyD;AACvE,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,SAAO,EAAE,MAAM,MAAM,QAAQ,GAAG,IAAI,eAAe;AACrD;AAEA,SAAS,eAAuB;AAC9B,QAAM,OAAO,cAAc,YAAY,GAAG;AAC1C,SAAY,aAAa,aAAQ,IAAI,GAAG,MAAM,WAAW;AAC3D;AAEA,SAAS,eAAe,QAAsB;AAC5C,MAAO,cAAW,MAAM,GAAG;AACzB,UAAM,UAAa,eAAY,MAAM;AACrC,QAAI,QAAQ,SAAS,GAAG;AACtB,cAAQ,MAAM,+CAA+C,MAAM,EAAE;AACrE,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF,OAAO;AACL,IAAG,aAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;AAAA,EAC1C;AACF;AAEA,SAAS,YAAY,SAAiB,OAAsB;AAC1D,SAAO,QACJ,QAAQ,oBAAoB,MAAM,IAAI,EACtC,QAAQ,oBAAoB,MAAM,IAAI,EACtC,QAAQ,sBAAsB,IAAI,MAAM,IAAI,EAAE;AACnD;AAEA,SAAS,SAAS,KAAa,KAAa,OAAoB;AAC9D,aAAW,SAAY,eAAY,KAAK,EAAE,eAAe,KAAK,CAAC,GAAG;AAChE,UAAM,UAAe,UAAK,KAAK,MAAM,IAAI;AACzC,UAAM,WAAW,MAAM,KAAK,SAAS,OAAO,IACxC,MAAM,KAAK,MAAM,GAAG,CAAC,QAAQ,MAAM,IACnC,MAAM;AACV,UAAM,UAAe,UAAK,KAAK,YAAY,UAAU,KAAK,CAAC;AAC3D,QAAI,MAAM,YAAY,GAAG;AACvB,MAAG,aAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AACzC,eAAS,SAAS,SAAS,KAAK;AAAA,IAClC,OAAO;AACL,YAAM,MAAS,gBAAa,SAAS,MAAM;AAC3C,MAAG,iBAAc,SAAS,YAAY,KAAK,KAAK,GAAG,MAAM;AAAA,IAC3D;AAAA,EACF;AACF;AAEA,SAAS,OAAa;AACpB,QAAM,QAAQ,UAAU,QAAQ,IAAI;AACpC,QAAM,SAAc,aAAQ,QAAQ,IAAI,GAAG,MAAM,IAAI;AACrD,iBAAe,MAAM;AACrB,QAAM,YAAY,aAAa;AAC/B,MAAI,CAAI,cAAW,SAAS,GAAG;AAC7B,YAAQ,MAAM,kCAAkC,SAAS,uBAAuB;AAChF,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,WAAS,WAAW,QAAQ,KAAK;AACjC,UAAQ,IAAI,iBAAiB,MAAM,IAAI,QAAQ,MAAM,EAAE;AACvD,UAAQ,IAAI,gBAAgB,MAAM,IAAI,EAAE;AACxC,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,aAAa;AACzB,UAAQ,IAAI,QAAQ,MAAM,IAAI,EAAE;AAChC,UAAQ,IAAI,gEAAgE;AAC5E,UAAQ,IAAI,gBAAgB;AAC5B,UAAQ,IAAI,YAAY;AACxB,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,wEAAwE;AACpF,UAAQ,IAAI,iBAAiB;AAC/B;AAEA,KAAK;","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@augmenting-integrations/create-tenant",
3
- "version": "8.6.0",
4
- "description": "Scaffold a new tenant apex app for an augint deployment. Generates a Next 16 + Auth.js v5 + Cognito apex with TenantConfig wired up, library-owned /api/apps registry handler, and /studio admin. Single command: pnpm dlx @augmenting-integrations/create-tenant my-tenant.",
3
+ "version": "8.8.0",
4
+ "description": "Scaffold a new tenant apex app for an augint deployment. Generates a Next 16 + Auth.js v5 + Cognito apex with TenantConfig wired up, library-owned /api/apps static-roster handler, and /studio admin. Single command: pnpm dlx @augmenting-integrations/create-tenant my-tenant.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -0,0 +1,20 @@
1
+ APP_DOMAIN=__TENANT_APEX__
2
+ APEX_DOMAIN=__TENANT_APEX__
3
+ AUTH_COOKIE_DOMAIN=__TENANT_PARENT__
4
+ AUTH_ALLOWED_PARENT_DOMAIN=__TENANT_PARENT__
5
+
6
+ # Cognito (apex-only; from your tenant infra stack)
7
+ AUTH_SECRET_ARN=
8
+ AUTH_COGNITO_SECRET_ARN=
9
+ AUTH_COGNITO_ID=
10
+ AUTH_COGNITO_ISSUER=
11
+
12
+ TENANT_SLUG=__TENANT_NAME__
13
+ STAGE=staging
14
+
15
+ AWS_REGION=us-east-1
16
+ ADMIN_EMAILS=
17
+
18
+ # Local-dev fallbacks
19
+ AUTH_SECRET=dev-only-fallback-not-for-prod
20
+ NODE_ENV=development
@@ -5,7 +5,8 @@ Next.js 16 app for a new augint tenant. It owns:
5
5
 
6
6
  - The OAuth callback (`/api/auth/[...nextauth]`) for the ENTIRE tenant
7
7
  - The session cookie scope (`Domain=__TENANT_PARENT__`)
8
- - The app registry (`/api/apps` for spoke auto-discovery)
8
+ - The runtime tenant app roster (`config/apps.json`, served by
9
+ `/api/apps`). Every spoke's `/api/apps` proxies to this endpoint.
9
10
  - The studio admin
10
11
 
11
12
  ## Local dev
@@ -25,20 +26,23 @@ The application code is portable; the AWS infra is tenant-specific:
25
26
  2. **Cognito User Pool** with one App Client + ONE callback URL:
26
27
  `https://__TENANT_APEX__/api/auth/callback/cognito`
27
28
  3. **Hosted zone + certs** for `__TENANT_APEX__` and `*.__TENANT_APEX__`.
28
- 4. **App registry DynamoDB table** (PK = `slug`). Apex owns CRUD; spokes
29
- read via `/api/apps`. Set `APP_REGISTRY_TABLE` env to the table name.
30
- 5. **Secrets Manager** rows for `AUTH_SECRET`, `AUTH_COGNITO_SECRET`.
31
- 6. **GitHub OIDC role** in each AWS account for CI deploys.
29
+ 4. **Secrets Manager** rows for `AUTH_SECRET`, `AUTH_COGNITO_SECRET`.
30
+ 5. **GitHub OIDC role** in each AWS account for CI deploys.
31
+ 6. **Tenant infra repo** (`__TENANT_NAME__-infra`) with `config/apps.yaml`
32
+ declaring this tenant's app roster (apex + every spoke). Mirror each
33
+ entry into this apex's `config/apps.json`.
32
34
 
33
35
  Copy `template.yaml` from an existing tenant (the example tenant ships one)
34
36
  and adapt the parameters.
35
37
 
36
- ## Adding spokes
38
+ ## Adding spokes after the apex exists
37
39
 
38
40
  ```bash
39
41
  pnpm dlx @augmenting-integrations/create-spoke my-product-spoke
40
42
  ```
41
43
 
42
- Then register the new spoke in this apex's app registry table (slug,
43
- subdomain, displayName, navOrder). The spoke shows up in every other
44
- spoke's AppShell ecosystem nav automatically.
44
+ Then add the new spoke's entry to `__TENANT_NAME__-infra/config/apps.yaml`
45
+ AND to this apex repo's `config/apps.json`. `pnpm exec augint
46
+ validate-app-roster` enforces the two files agree. The spoke's
47
+ `/api/apps` proxies here, so no per-spoke roster maintenance is needed
48
+ and existing spokes do NOT need to be redeployed.
@@ -0,0 +1,12 @@
1
+ {
2
+ "apps": [
3
+ {
4
+ "slug": "apex",
5
+ "role": "apex",
6
+ "subdomain": "",
7
+ "displayName": "__TENANT_NAME__ Portal",
8
+ "navOrder": 0,
9
+ "requiredIdentityGroups": []
10
+ }
11
+ ]
12
+ }
@@ -15,7 +15,6 @@
15
15
  "@augmenting-integrations/aws": "^8.0.0",
16
16
  "@augmenting-integrations/brand": "^8.0.0",
17
17
  "@augmenting-integrations/platform": "^8.0.0",
18
- "@augmenting-integrations/registry": "^8.0.0",
19
18
  "@augmenting-integrations/themes": "^8.0.0",
20
19
  "@augmenting-integrations/ui": "^8.0.0",
21
20
  "next": "^16.2.5",
@@ -24,6 +23,7 @@
24
23
  "react-dom": "^19.2.0"
25
24
  },
26
25
  "devDependencies": {
26
+ "@augmenting-integrations/deploy-tools": "^8.0.0",
27
27
  "@types/node": "^22.0.0",
28
28
  "@types/react": "^19.0.0",
29
29
  "@types/react-dom": "^19.0.0",
@@ -1,7 +1,18 @@
1
- // Auto-discovery endpoint consumed by every spoke's AppShell. Scans the
2
- // DynamoDB app registry table + filters by the caller's Cognito groups.
3
- import { createGetHandler } from "@augmenting-integrations/registry/api-route";
4
- import { auth } from "@/lib/auth";
1
+ // Apex-owned tenant app roster endpoint. Reads the static roster
2
+ // (config/apps.json), filters by the caller's Cognito identity groups,
3
+ // and returns the visible apps + their absolute URLs. Spokes proxy
4
+ // their /api/apps to this endpoint -- the apex is the single roster
5
+ // owner in the tenant ecosystem.
6
+
7
+ import { createAppsRouteHandler } from "@augmenting-integrations/platform/server";
8
+ import { auth, tenant } from "@/lib/auth";
9
+ import appsRoster from "../../../../config/apps.json";
5
10
 
6
- export const GET = createGetHandler({ authFn: auth });
7
11
  export const runtime = "nodejs";
12
+ export const dynamic = "force-dynamic";
13
+
14
+ export const { GET } = createAppsRouteHandler({
15
+ roster: appsRoster,
16
+ auth,
17
+ tenant,
18
+ });