@augmenting-integrations/create-spoke 8.3.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.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # @augmenting-integrations/create-spoke
2
+
3
+ Scaffold a new product subdomain (spoke) for an augint-\* tenant.
4
+
5
+ ```bash
6
+ pnpm dlx @augmenting-integrations/create-spoke my-spoke
7
+ pnpm dlx @augmenting-integrations/create-spoke my-spoke --subdomain=foo --slug=bar
8
+ ```
9
+
10
+ ## What gets generated
11
+
12
+ A Next.js 16 + Auth.js v5 app pre-wired for the augint tenant ecosystem:
13
+
14
+ - `loadTenantConfig({ role: "spoke" })` in `src/lib/auth.ts` (single env-var read)
15
+ - `<TenantBootScript>` + `<TenantProvider>` mounted in the root layout
16
+ - Library-owned route handlers for `/api/auth/me`, `/api/billing/*`,
17
+ `/api/invitations/[token]`, and `/api/admin/users/[id]/impersonate` —
18
+ each is a 2-line re-export from `@augmenting-integrations/auth/server` or
19
+ `@augmenting-integrations/billing/server`
20
+ - Canonical Prisma fragments (`User`, `Invitation`, `PaymentMethod`,
21
+ `CreditTransaction`, `ActivityLog`) in `prisma/schema.prisma` — your
22
+ product models live below the marker comment
23
+ - `prisma/seed.mjs` with the `STAGE=staging` gate so seeds never fire
24
+ against a production database
25
+
26
+ ## What you still have to do per-tenant
27
+
28
+ The generated app is portable; the AWS infra around it is per-tenant:
29
+
30
+ - Provision your tenant's Cognito user pool, hosted zone, app registry
31
+ table, secrets, GitHub OIDC role (copy `template.yaml` from an existing
32
+ spoke in the same tenant — see `augint-example-leads-marketplace` for
33
+ the reference).
34
+ - Register this spoke in the apex's DynamoDB app registry (slug, subdomain,
35
+ displayName, navOrder) so it shows up in every other spoke's AppShell.
36
+
37
+ ## Bringing your product
38
+
39
+ The kernel ships only auth/billing/admin route handlers. Product routes
40
+ (`/api/leads`, `/api/quotes`, `/api/widgets`) are yours to design under
41
+ `src/app/api/`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@augmenting-integrations/create-spoke",
3
- "version": "8.3.0",
3
+ "version": "8.5.0",
4
4
  "description": "Scaffold a new product subdomain (spoke) for an augint-* tenant. Generates a Next 16 + Auth.js v5 app with TenantConfig wired up, library-owned route handlers, a Prisma canonical schema fragment, and a deployable template.yaml + GitHub workflow. Single command: pnpm dlx @augmenting-integrations/create-spoke my-spoke.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -15,15 +15,15 @@
15
15
  "templates",
16
16
  "README.md"
17
17
  ],
18
- "scripts": {
19
- "build": "tsup",
20
- "clean": "rm -rf dist",
21
- "test": "vitest run --passWithNoTests"
22
- },
23
18
  "devDependencies": {
24
19
  "@types/node": "^22.0.0",
25
20
  "tsup": "^8.3.5",
26
21
  "typescript": "^5.7.2",
27
22
  "vitest": "^4.1.5"
23
+ },
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "clean": "rm -rf dist",
27
+ "test": "vitest run --passWithNoTests"
28
28
  }
29
- }
29
+ }
@@ -1,17 +1,18 @@
1
1
  # __SPOKE_NAME__
2
2
 
3
3
  Generated by `@augmenting-integrations/create-spoke`. This is a Next.js 16
4
- product subdomain ("spoke") inside an augint tenant. Auth, billing, and
5
- admin route handlers come from `@augmenting-integrations/*` libraries —
6
- your job is the product surface in `src/app/` plus any product-specific
7
- Prisma models.
4
+ product subdomain ("spoke") inside an augint tenant. Auth, billing,
5
+ settings, invitations, and admin route handlers all come from
6
+ `@augmenting-integrations/*` libraries — your job is the product surface
7
+ in `src/app/` plus any product-specific Prisma models.
8
8
 
9
9
  ## Local dev
10
10
 
11
11
  ```bash
12
12
  cp .env.example .env
13
- # fill in the tenant identity values; AWS creds may stay blank for local dev
13
+ # fill in tenant identity values; AWS creds may stay blank for local dev
14
14
  pnpm install
15
+ pnpm exec augint validate-spoke # validates prisma/schema.prisma against app.manifest.json
15
16
  pnpm dev
16
17
  ```
17
18
 
@@ -19,29 +20,63 @@ You should see `Not signed in.` on `/`. Sign-in flow requires a working
19
20
  apex (the apex app is the only OAuth callback in the tenant — it sets the
20
21
  shared cookie and redirects you back).
21
22
 
23
+ ## The manifest is the source of truth
24
+
25
+ Edit `app.manifest.json` to change the spoke's identity, access policy,
26
+ feature enablement, or data plane. The manifest is read at build time by
27
+ `src/lib/auth.ts` (drives the access policy) and at deploy time by the
28
+ shared deploy tooling (drives the registry registration). You should not
29
+ need to thread the same values through `.env` + workflow files + Studio
30
+ forms — that drift is the bug class this contract closes.
31
+
22
32
  ## Per-tenant adoption checklist
23
33
 
24
- 1. **Tenant identity** in `.env` — replace `tenant.example.com` with your real apex.
25
- 2. **AWS resources** — `AUTH_SECRET_ARN`, `DB_SECRET_ARN`, Cognito ids, etc. come from your tenant infra stack (the same SSM params the apex reads).
26
- 3. **Prisma** — `prisma/schema.prisma` ships with canonical User/Invitation/PaymentMethod/CreditTransaction. Add your product models below the marker comment.
27
- 4. **Brand**drop a `config/brand.json` in if you want this spoke's name in the chrome.
28
- 5. **App registry** register this spoke in your apex's DynamoDB app registry (slug `__SPOKE_SLUG__`, subdomain `__SPOKE_SUBDOMAIN__`).
34
+ 1. **Manifest** (`app.manifest.json`) edit `tenantSlug`, `appSlug`,
35
+ `subdomain`, `displayName`, `navOrder`, `access.requiredIdentityGroups`,
36
+ and `features.*`.
37
+ 2. **`.env`**fill in `AUTH_SECRET_ARN`, `DB_SECRET_ARN`, Cognito ids,
38
+ etc. These come from your tenant infra stack.
39
+ 3. **Prisma** — `prisma/schema.prisma` ships with canonical User /
40
+ Invitation / PaymentMethod / CreditTransaction / ActivityLog. Add
41
+ your product-domain models below the marker comment. Run
42
+ `pnpm exec augint validate-spoke` to confirm the canonical fragments
43
+ satisfy the contract implied by your manifest's `features.*`.
44
+ 4. **Brand** — drop a `config/brand.json` in if you want this spoke's
45
+ name in the chrome.
29
46
 
30
47
  ## Deploy
31
48
 
32
- This scaffold ships only the application code. Infrastructure (CloudFront
33
- distribution, Lambda runtime, Aurora cluster + RDS Proxy, migrate-runner
34
- Lambda) is per-tenant and varies; copy the `template.yaml` from your
35
- tenant's other spokes (e.g. the example leads-marketplace) and edit the
36
- slug + subdomain.
49
+ The shared workflow + `template.yaml` provision the Lambda + CloudFront +
50
+ DNS + IAM. The deploy step invokes:
51
+
52
+ ```bash
53
+ pnpm exec augint validate-spoke
54
+ pnpm build
55
+ pnpm exec augint package-next-lambda
56
+ ```
57
+
58
+ The `package-next-lambda` command does the Next standalone staging, the
59
+ pnpm symlink flatten, the Prisma `.prisma` hoist, and the `.pnpm` purge
60
+ that every spoke needs. This logic lives in the shared
61
+ `@augmenting-integrations/deploy-tools` package -- do NOT copy it back
62
+ into your `.github/workflows/deploy.yaml`.
63
+
64
+ Spoke kernel infra (Lambda + CloudFront + DNS + IAM + optional Aurora
65
+ data plane) is documented in
66
+ `augint-common-web/docs/spoke-kernel.md`. The current generated
67
+ `template.yaml` mirrors the leads-marketplace reference; over time it
68
+ will be replaced by a nested-stack consumer of the shared kernel
69
+ provisioned in `<tenant>-infra`.
37
70
 
38
71
  ## What you can NOT customize without diverging from the library
39
72
 
40
73
  - Canonical schema fields (`User.credit_balance`, `CreditTransaction.type`,
41
74
  `PaymentMethod.stripe_payment_method_id`, etc.). The library's billing
42
- factory assumes these.
43
- - `/api/billing/*` URL paths (the client widgets bake them in).
75
+ factory assumes these. The validator catches drift before deploy.
76
+ - `/api/billing/*`, `/api/auth/me`, `/api/invitations/[token]`,
77
+ `/api/settings/*` URL paths -- the library and the client widgets bake
78
+ them in.
44
79
  - Auth.js v5 + Cognito as the provider chain.
45
80
 
46
- For product-specific routes (`/api/leads`, `/api/quotes`, `/api/products`),
81
+ For product-specific routes (`/api/leads`, `/api/quotes`, `/api/widgets`),
47
82
  add them under `src/app/api/` — they're yours to design.
@@ -0,0 +1,22 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "tenantSlug": "TENANT_PLACEHOLDER",
4
+ "appSlug": "__SPOKE_SLUG__",
5
+ "role": "spoke",
6
+ "subdomain": "__SPOKE_SUBDOMAIN__",
7
+ "displayName": "__SPOKE_NAME__",
8
+ "navOrder": 10,
9
+ "access": {
10
+ "requiredIdentityGroups": []
11
+ },
12
+ "features": {
13
+ "billing": true,
14
+ "settings": true,
15
+ "invitations": true,
16
+ "impersonation": true
17
+ },
18
+ "dataPlane": {
19
+ "type": "app-aurora",
20
+ "migrations": true
21
+ }
22
+ }
@@ -16,6 +16,7 @@
16
16
  "@augmenting-integrations/billing": "^8.0.0",
17
17
  "@augmenting-integrations/brand": "^8.0.0",
18
18
  "@augmenting-integrations/db-secret-loader": "^8.0.0",
19
+ "@augmenting-integrations/platform": "^8.0.0",
19
20
  "@augmenting-integrations/themes": "^8.0.0",
20
21
  "@augmenting-integrations/ui": "^8.0.0",
21
22
  "@prisma/client": "^6.0.0",
@@ -6,7 +6,7 @@ import { SessionProvider } from "@augmenting-integrations/ui";
6
6
  import {
7
7
  TenantProvider,
8
8
  type TenantPublicConfig,
9
- } from "@augmenting-integrations/auth/client";
9
+ } from "@augmenting-integrations/platform/client";
10
10
 
11
11
  export function Providers({
12
12
  children,
@@ -1,13 +1,13 @@
1
1
  import { createImpersonateHandlers } from "@augmenting-integrations/auth/server";
2
2
  import { auth } from "@/lib/auth";
3
- import { getDb } from "@/lib/db";
4
- import { getOrCreateAppUser } from "@/lib/users";
3
+ import { getAuthHandlersDb } from "@/platform/repositories";
4
+ import { getMeAppUser } from "@/platform/app-user";
5
5
 
6
6
  export const runtime = "nodejs";
7
7
  export const dynamic = "force-dynamic";
8
8
 
9
9
  export const { POST, DELETE } = createImpersonateHandlers({
10
10
  auth,
11
- getDb: getDb as never,
12
- getOrCreateAppUser: getOrCreateAppUser as never,
11
+ getDb: getAuthHandlersDb,
12
+ getOrCreateAppUser: getMeAppUser,
13
13
  });
@@ -1,13 +1,13 @@
1
1
  import { createMeHandler } from "@augmenting-integrations/auth/server";
2
2
  import { auth } from "@/lib/auth";
3
- import { getDb } from "@/lib/db";
4
- import { getOrCreateAppUser } from "@/lib/users";
3
+ import { getAuthHandlersDb } from "@/platform/repositories";
4
+ import { getMeAppUser } from "@/platform/app-user";
5
5
 
6
6
  export const runtime = "nodejs";
7
7
  export const dynamic = "force-dynamic";
8
8
 
9
9
  export const { GET } = createMeHandler({
10
10
  auth,
11
- getDb: getDb as never,
12
- getOrCreateAppUser: getOrCreateAppUser as never,
11
+ getDb: getAuthHandlersDb,
12
+ getOrCreateAppUser: getMeAppUser,
13
13
  });
@@ -1,7 +1,7 @@
1
1
  import { createInvitationHandlers } from "@augmenting-integrations/auth/server";
2
- import { getDb } from "@/lib/db";
2
+ import { getAuthHandlersDb } from "@/platform/repositories";
3
3
 
4
4
  export const runtime = "nodejs";
5
5
  export const dynamic = "force-dynamic";
6
6
 
7
- export const { GET, POST } = createInvitationHandlers({ getDb: getDb as never });
7
+ export const { GET, POST } = createInvitationHandlers({ getDb: getAuthHandlersDb });
@@ -5,7 +5,7 @@ import {
5
5
  THEME_VARIANT_COOKIE_KEY,
6
6
  } from "@augmenting-integrations/themes";
7
7
  import { ThemeBootScript } from "@augmenting-integrations/ui";
8
- import { TenantBootScript, publicSubset } from "@augmenting-integrations/auth/server";
8
+ import { TenantBootScript, publicSubset } from "@augmenting-integrations/platform/server";
9
9
  import { auth, tenant } from "@/lib/auth";
10
10
  import { Providers } from "./Providers";
11
11
  import "./globals.css";
@@ -1,5 +1,19 @@
1
- import { createAuth, loadTenantConfig } from "@augmenting-integrations/auth/server";
1
+ import { createAuth } from "@augmenting-integrations/auth/server";
2
+ import { loadTenantConfig } from "@augmenting-integrations/platform/server";
3
+ import { validateManifest } from "@augmenting-integrations/platform/manifest";
2
4
  import { getSecret } from "@augmenting-integrations/aws/server";
5
+ import manifestJson from "../../app.manifest.json";
6
+
7
+ // app.manifest.json is the local source of truth for slug / subdomain /
8
+ // access policy / feature enablement. Validate at module init so a bad
9
+ // manifest fails the deploy loudly with the field-by-field error list.
10
+ const manifestResult = validateManifest(manifestJson);
11
+ if (!manifestResult.ok) {
12
+ throw new Error(
13
+ `app.manifest.json failed validation: ${manifestResult.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`,
14
+ );
15
+ }
16
+ export const manifest = manifestResult.value;
3
17
 
4
18
  // Single tenant configuration source.
5
19
  export const tenant = loadTenantConfig({ role: "spoke" });
@@ -12,4 +26,7 @@ export const { handlers, auth, signIn, signOut } = createAuth({
12
26
  tenant,
13
27
  authedRoutePrefixes: ["/"],
14
28
  authSecret,
29
+ appAccess: {
30
+ requiredIdentityGroups: manifest.access.requiredIdentityGroups,
31
+ },
15
32
  });
@@ -1,11 +1,14 @@
1
1
  import { createBillingHandlers } from "@augmenting-integrations/billing/server";
2
2
  import { auth } from "./auth.js";
3
- import { getDb } from "./db.js";
4
- import { getOrCreateAppUser } from "./users.js";
3
+ import { getBillingDb } from "../platform/repositories.js";
4
+ import { getBillingAppUser } from "../platform/app-user.js";
5
5
 
6
6
  // One factory call. Each /api/billing/* route file re-exports from this.
7
+ // The platform adapters keep this wiring free of `as never` casts; if the
8
+ // canonical schema drifts, the schema validator (augint validate-spoke)
9
+ // fails CI before this file is reached.
7
10
  export const billingHandlers = createBillingHandlers({
8
11
  auth,
9
- getDb: getDb as never,
10
- getOrCreateAppUser: getOrCreateAppUser as never,
12
+ getDb: getBillingDb,
13
+ getOrCreateAppUser: getBillingAppUser,
11
14
  });
@@ -1,5 +1,10 @@
1
- import { createGetOrCreateAppUser } from "@augmenting-integrations/auth/server";
2
- import { getDb } from "./db.js";
1
+ import {
2
+ createGetOrCreateAppUser,
3
+ type AppUserWithImpersonation,
4
+ } from "@augmenting-integrations/auth/server";
5
+ import type { User } from "@prisma/client";
6
+
7
+ import { getJitDb } from "../platform/repositories.js";
3
8
 
4
9
  // Default $500 for admins/owners, $100 for everyone else. Tune per spoke.
5
10
  function computeCreditBalance(role: string): number {
@@ -7,8 +12,10 @@ function computeCreditBalance(role: string): number {
7
12
  return 100;
8
13
  }
9
14
 
10
- export const getOrCreateAppUser = createGetOrCreateAppUser({
11
- db: getDb,
15
+ export type AppUser = AppUserWithImpersonation<User>;
16
+
17
+ export const getOrCreateAppUser = createGetOrCreateAppUser<User>({
18
+ db: getJitDb,
12
19
  defaultRole: "member",
13
20
  computeCreditBalance,
14
21
  adminEmails: (process.env.ADMIN_EMAILS ?? "")
@@ -17,5 +24,3 @@ export const getOrCreateAppUser = createGetOrCreateAppUser({
17
24
  .filter(Boolean),
18
25
  extraCreateFields: { is_active: true, must_change_password: false },
19
26
  });
20
-
21
- export type { AppUser } from "@augmenting-integrations/auth/server";
@@ -0,0 +1,22 @@
1
+ import "server-only";
2
+
3
+ import type { Session } from "next-auth";
4
+ import type { BillingUser } from "@augmenting-integrations/billing/server";
5
+ import type { MeAppUser } from "@augmenting-integrations/auth/server";
6
+
7
+ import { getOrCreateAppUser } from "../lib/users.js";
8
+
9
+ // Platform user adapter. Bridges the spoke's getOrCreateAppUser (which
10
+ // returns the full generated User row) to the narrow shapes library
11
+ // handler factories consume (MeAppUser, BillingUser).
12
+ //
13
+ // Split from src/platform/repositories.ts to avoid an import cycle with
14
+ // src/lib/users.ts (which imports the DB adapter). See repositories.ts
15
+ // for the schema-contract rationale.
16
+
17
+ export const getMeAppUser: (session: Session) => Promise<MeAppUser> = async (session) =>
18
+ (await getOrCreateAppUser(session)) as unknown as MeAppUser;
19
+
20
+ export const getBillingAppUser: (session: Session) => Promise<BillingUser> = async (
21
+ session,
22
+ ) => (await getOrCreateAppUser(session)) as unknown as BillingUser;
@@ -0,0 +1,31 @@
1
+ import "server-only";
2
+
3
+ import type { BillingDb } from "@augmenting-integrations/billing/server";
4
+ import type {
5
+ AuthHandlersDb,
6
+ PrismaLikeClient,
7
+ } from "@augmenting-integrations/auth/server";
8
+ import type { User } from "@prisma/client";
9
+
10
+ import { getDb } from "../lib/db.js";
11
+
12
+ // Platform DB adapter. Bridges the generated PrismaClient to the structural
13
+ // shapes library handler factories consume (BillingDb, AuthHandlersDb,
14
+ // PrismaLikeClient<User>). The boundary lives here once; route handlers and
15
+ // factory wiring stay free of `as never` and similar unsafe casts.
16
+ //
17
+ // The library cannot import generated Prisma types -- it would have to know
18
+ // every spoke's schema. Instead it declares minimal structural row shapes
19
+ // and method signatures. Both sides agree on the canonical contract
20
+ // (User / Invitation / PaymentMethod / CreditTransaction / ActivityLog).
21
+ // The schema validator (`pnpm exec augint validate-spoke`) checks it at
22
+ // CI time so the contract cannot silently drift.
23
+
24
+ export const getBillingDb: () => Promise<BillingDb> = async () =>
25
+ (await getDb()) as unknown as BillingDb;
26
+
27
+ export const getAuthHandlersDb: () => Promise<AuthHandlersDb> = async () =>
28
+ (await getDb()) as unknown as AuthHandlersDb;
29
+
30
+ export const getJitDb: () => Promise<PrismaLikeClient<User>> = async () =>
31
+ (await getDb()) as unknown as PrismaLikeClient<User>;
@@ -1,25 +0,0 @@
1
- # Tenant identity (everything else flows from these)
2
- APP_DOMAIN=__SPOKE_SUBDOMAIN__.tenant.example.com
3
- APP_SLUG=__SPOKE_SLUG__
4
- APEX_DOMAIN=tenant.example.com
5
- AUTH_COOKIE_DOMAIN=.tenant.example.com
6
- AUTH_ALLOWED_PARENT_DOMAIN=.tenant.example.com
7
-
8
- # AWS resources (typically pulled from SSM in deployed Lambda; local dev only)
9
- AWS_REGION=us-east-1
10
- AUTH_SECRET_ARN=
11
- AUTH_COGNITO_ID=
12
- AUTH_COGNITO_ISSUER=
13
- DB_SECRET_ARN=
14
- DB_HOST=
15
- DB_NAME=
16
- APP_REGISTRY_TABLE=
17
- STRIPE_SECRET_ARN=
18
- STRIPE_WEBHOOK_SECRET_ARN=
19
-
20
- # Comma-separated emails auto-promoted to role=admin on first sign-in
21
- ADMIN_EMAILS=
22
-
23
- # Local-dev fallback for testing without Secrets Manager
24
- AUTH_SECRET=dev-only-fallback-not-for-prod
25
- NODE_ENV=development