@augmenting-integrations/create-spoke 8.4.1 → 8.6.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 +1 -1
- package/templates/README.md.tmpl +53 -18
- package/templates/app.manifest.json.tmpl +22 -0
- package/templates/package.json.tmpl +1 -0
- package/templates/src/app/Providers.tsx.tmpl +1 -1
- package/templates/src/app/api/admin/users/[id]/impersonate/route.ts.tmpl +4 -4
- package/templates/src/app/api/auth/me/route.ts.tmpl +4 -4
- package/templates/src/app/api/invitations/[token]/route.ts.tmpl +2 -2
- package/templates/src/app/layout.tsx.tmpl +1 -1
- package/templates/src/lib/auth.ts.tmpl +18 -1
- package/templates/src/lib/billing.ts.tmpl +7 -4
- package/templates/src/lib/users.ts.tmpl +11 -6
- package/templates/src/platform/app-user.ts.tmpl +22 -0
- package/templates/src/platform/repositories.ts.tmpl +31 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@augmenting-integrations/create-spoke",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.6.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": {
|
package/templates/README.md.tmpl
CHANGED
|
@@ -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,
|
|
5
|
-
admin route handlers come from
|
|
6
|
-
your job is the product surface
|
|
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
|
|
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. **
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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/
|
|
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/
|
|
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 {
|
|
4
|
-
import {
|
|
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:
|
|
12
|
-
getOrCreateAppUser:
|
|
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 {
|
|
4
|
-
import {
|
|
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:
|
|
12
|
-
getOrCreateAppUser:
|
|
11
|
+
getDb: getAuthHandlersDb,
|
|
12
|
+
getOrCreateAppUser: getMeAppUser,
|
|
13
13
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createInvitationHandlers } from "@augmenting-integrations/auth/server";
|
|
2
|
-
import {
|
|
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:
|
|
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/
|
|
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
|
|
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 {
|
|
4
|
-
import {
|
|
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:
|
|
10
|
-
getOrCreateAppUser:
|
|
12
|
+
getDb: getBillingDb,
|
|
13
|
+
getOrCreateAppUser: getBillingAppUser,
|
|
11
14
|
});
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
|
11
|
-
|
|
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>;
|