@augmenting-integrations/create-spoke 8.4.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@augmenting-integrations/create-spoke",
3
- "version": "8.4.1",
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": {
@@ -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>;