create-ailk 0.15.0 → 0.16.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 +124 -0
- package/dist/add-module.js +52 -7
- package/dist/add-route.js +8 -0
- package/dist/adopt.d.ts +74 -0
- package/dist/adopt.js +314 -0
- package/dist/cli.js +82 -2
- package/dist/derive-architecture.d.ts +22 -0
- package/dist/derive-architecture.js +58 -1
- package/dist/lib/declared-env.d.ts +142 -0
- package/dist/lib/declared-env.js +985 -0
- package/dist/lib/env-catalog.d.ts +17 -0
- package/dist/lib/env-catalog.js +234 -0
- package/dist/lib/extract-module-bundle.d.ts +9 -0
- package/dist/lib/extract-module-bundle.js +18 -1
- package/dist/lib/module-tier.d.ts +2 -0
- package/dist/lib/module-tier.js +4 -0
- package/dist/module-architecture.js +76 -0
- package/dist/parse-args.d.ts +5 -0
- package/dist/parse-args.js +17 -1
- package/dist/programmatic.d.ts +8 -0
- package/dist/programmatic.js +78 -9
- package/dist/seed-flows.d.ts +65 -0
- package/dist/seed-flows.js +350 -0
- package/dist/site-manifest.d.ts +43 -0
- package/dist/site-manifest.js +98 -3
- package/dist/site-roster.d.ts +13 -0
- package/dist/site-roster.js +22 -8
- package/dist/spoke-checkout.d.ts +42 -0
- package/dist/spoke-checkout.js +359 -0
- package/dist/surfaces.js +7 -0
- package/dist/sync-routes.js +23 -8
- package/dist/topology.d.ts +124 -9
- package/dist/topology.js +449 -40
- package/package.json +3 -3
- package/templates/.env.example +10 -0
- package/templates/apps/api/CLAUDE.md +19 -4
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +95 -16
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +122 -0
- package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +362 -0
- package/templates/apps/api/src/bin/spokes.ts +62 -1
- package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +75 -0
- package/templates/apps/api/src/config/index.ts +35 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +89 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +66 -5
- package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +209 -0
- package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +20 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +24 -5
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +419 -0
- package/templates/apps/api/src/lib/spoke-store.ts +25 -3
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +310 -0
- package/templates/apps/api/src/middleware/auth.ts +126 -0
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +483 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +201 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +7 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +105 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +72 -1
- package/templates/apps/api/src/server.ts +105 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +4 -4
- package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +854 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +293 -0
- package/templates/apps/api/src/services/__tests__/person.test.ts +553 -0
- package/templates/apps/api/src/services/hub-sign-in.ts +471 -0
- package/templates/apps/api/src/services/lead-routing.ts +131 -2
- package/templates/apps/api/src/services/person.ts +541 -0
- package/templates/apps/web/.env.example +58 -5
- package/templates/apps/web/app/[locale]/(auth)/sign-in/next-path.ts +0 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +206 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +256 -3
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +5 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +205 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +98 -29
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +6 -3
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -101
- package/templates/apps/web/app/[locale]/(authed)/signed-out-gate.generated.tsx +90 -0
- package/templates/apps/web/app/[locale]/layout.tsx +28 -15
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +295 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +156 -0
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +341 -0
- package/templates/apps/web/app/api/hub-session/callback/route.ts +168 -0
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +76 -0
- package/templates/apps/web/app/api/hub-session/sign-out/route.ts +63 -0
- package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +15 -0
- package/templates/apps/web/app/openapi.json/route.ts +3 -6
- package/templates/apps/web/auth.ts +29 -2
- package/templates/apps/web/components/sign-out-trigger.tsx +20 -1
- package/templates/apps/web/components/signed-in-nav.tsx +17 -3
- package/templates/apps/web/lib/__tests__/account-config.test.ts +5 -2
- package/templates/apps/web/lib/__tests__/listings-config.test.ts +83 -0
- package/templates/apps/web/lib/__tests__/listings.test.tsx +49 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +49 -1
- package/templates/apps/web/lib/__tests__/relay.test.ts +27 -0
- package/templates/apps/web/lib/account-config.ts +130 -1
- package/templates/apps/web/lib/api-origin.generated.ts +11 -0
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +262 -0
- package/templates/apps/web/lib/hub-session/config.ts +104 -0
- package/templates/apps/web/lib/hub-session/index.ts +23 -0
- package/templates/apps/web/lib/hub-session/seal.ts +82 -0
- package/templates/apps/web/lib/hub-session/session.ts +180 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +79 -0
- package/templates/apps/web/lib/listings-config.ts +31 -0
- package/templates/apps/web/lib/listings.tsx +7 -3
- package/templates/apps/web/lib/meeting-slugs.ts +11 -1
- package/templates/apps/web/lib/next-path.ts +35 -0
- package/templates/apps/web/lib/relay.ts +3 -1
- package/templates/apps/web/lib/server-api.ts +11 -1
- package/templates/apps/web/lib/topology.generated.ts +15 -5
- package/templates/apps/web/messages/en.json +32 -3
- package/templates/database/CHANGELOG.md +26 -0
- package/templates/database/__tests__/agents.db.test.ts +25 -5
- package/templates/database/__tests__/fixtures/person-backfill.json +264 -0
- package/templates/database/__tests__/member-area.db.test.ts +245 -0
- package/templates/database/__tests__/person.db.test.ts +584 -0
- package/templates/database/auth/schema.prisma +3 -2
- package/templates/database/config/schema.prisma +13 -0
- package/templates/database/inbox/schema.prisma +98 -0
- package/templates/database/migrations/20261010120000_person/migration.sql +106 -0
- package/templates/database/migrations/20261010121000_person_backfill/migration.sql +132 -0
- package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +20 -0
- package/templates/database/migrations/20261011120000_member_area/migration.sql +158 -0
- package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +21 -0
- package/templates/database/package.json +1 -1
- package/templates/database/scripts/person-backfill.sql +132 -0
- package/templates/database/tenancy.ts +53 -0
- package/templates/tools/check-declared-env.ts +37 -0
- package/templates/tools/declared-env.ts +1163 -0
- package/versions.json +35 -35
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
-- #852 — the Person backfill (spec
|
|
2
|
+
-- docs/specs/852-person-the-per-workspace-client-spec.md D2, D4).
|
|
3
|
+
--
|
|
4
|
+
-- Fills "people" from the rows the hub already holds and stamps
|
|
5
|
+
-- leads.person_id and appointments.person_id, so the People list is not
|
|
6
|
+
-- empty on the day it ships. Set-based and idempotent: every insert is
|
|
7
|
+
-- ON CONFLICT DO NOTHING and every update is guarded by IS NULL, so running
|
|
8
|
+
-- this text twice changes nothing the first run wrote.
|
|
9
|
+
--
|
|
10
|
+
-- THE SAME TEXT, byte for byte, is database/scripts/person-backfill.sql. The
|
|
11
|
+
-- previous deployment keeps writing leads and appointments without a
|
|
12
|
+
-- person_id between this migration and the cutover to the new code; run
|
|
13
|
+
-- that script ONCE against the hub database after the cutover to stamp them.
|
|
14
|
+
-- database/__tests__/person.db.test.ts asserts the two files are identical
|
|
15
|
+
-- and that a second run changes no row.
|
|
16
|
+
--
|
|
17
|
+
-- Normalization — the SQL twin of apps/api/src/services/person.ts's
|
|
18
|
+
-- normalizePersonEmail / isStorablePersonEmail:
|
|
19
|
+
-- N(x) = lower(btrim(x, E' \t\n\r\f\x0B') COLLATE "C")
|
|
20
|
+
-- (\x0B is vertical tab; an E-string has no \v escape — E'\v'
|
|
21
|
+
-- is the letter v, which would strip the v of every .dev)
|
|
22
|
+
-- VALID(e) = e ~ '^[!-?A-~]+@[!-?A-~]+$' AND length(e) <= 254
|
|
23
|
+
-- NAME(x) = C0/C1 controls and bidi controls removed, trimmed, cut to
|
|
24
|
+
-- 200 characters; empty → NULL
|
|
25
|
+
-- A row whose email is not VALID makes no Person and keeps person_id NULL.
|
|
26
|
+
--
|
|
27
|
+
-- Sources, in order: leads (oldest first), then completed hub FlowCheckouts
|
|
28
|
+
-- through their lead (the customer id, first writer wins), then appointments
|
|
29
|
+
-- by attendee email. Subscriber is NOT a source; FlowSession, ProjectListing
|
|
30
|
+
-- and DeliverableGrant reach a Person through their lead at read time.
|
|
31
|
+
-- The join to "users" reads the auth namespace inside this one-off
|
|
32
|
+
-- statement only; it adds no foreign key (ADR 0009 invariant 1).
|
|
33
|
+
--
|
|
34
|
+
-- Rollback: nothing to undo on its own — dropping 20261010120000_person's
|
|
35
|
+
-- table and columns removes everything this wrote.
|
|
36
|
+
|
|
37
|
+
-- 1. Leads, oldest first: one Person per (workspace, normalized email). The
|
|
38
|
+
-- name is the oldest lead's payload.name when it is a JSON string.
|
|
39
|
+
INSERT INTO "people" ("workspace_id", "email", "name", "created_at", "updated_at")
|
|
40
|
+
SELECT DISTINCT ON (l."workspace_id", lower(btrim(l."email", E' \t\n\r\f\x0B') COLLATE "C"))
|
|
41
|
+
l."workspace_id",
|
|
42
|
+
lower(btrim(l."email", E' \t\n\r\f\x0B') COLLATE "C"),
|
|
43
|
+
CASE WHEN jsonb_typeof(l."payload" -> 'name') = 'string'
|
|
44
|
+
THEN NULLIF(left(btrim(regexp_replace(l."payload" ->> 'name', '[\x01-\x1F\x7F-\x9F\u200E\u200F\u202A-\u202E\u2066-\u2069]', '', 'g')), 200), '')
|
|
45
|
+
END,
|
|
46
|
+
l."created_at",
|
|
47
|
+
now()
|
|
48
|
+
FROM "leads" l
|
|
49
|
+
WHERE lower(btrim(l."email", E' \t\n\r\f\x0B') COLLATE "C") ~ '^[!-?A-~]+@[!-?A-~]+$'
|
|
50
|
+
AND length(lower(btrim(l."email", E' \t\n\r\f\x0B') COLLATE "C")) <= 254
|
|
51
|
+
ORDER BY l."workspace_id", lower(btrim(l."email", E' \t\n\r\f\x0B') COLLATE "C"), l."created_at" ASC, l."id" ASC
|
|
52
|
+
ON CONFLICT ("workspace_id", "email") DO NOTHING;
|
|
53
|
+
|
|
54
|
+
-- 2. Hub FlowCheckouts, through their lead (same workspace): the oldest
|
|
55
|
+
-- completed checkout with a customer id writes it onto a Person that has
|
|
56
|
+
-- none. First writer wins — oldest by completed_at, then created_at, then id.
|
|
57
|
+
UPDATE "people" p
|
|
58
|
+
SET "billing_provider" = 'stripe',
|
|
59
|
+
"billing_customer_id" = c."stripe_customer_id",
|
|
60
|
+
"updated_at" = now()
|
|
61
|
+
FROM (
|
|
62
|
+
SELECT DISTINCT ON (fc."workspace_id", lower(btrim(l."email", E' \t\n\r\f\x0B') COLLATE "C"))
|
|
63
|
+
fc."workspace_id",
|
|
64
|
+
lower(btrim(l."email", E' \t\n\r\f\x0B') COLLATE "C") AS "email",
|
|
65
|
+
fc."stripe_customer_id"
|
|
66
|
+
FROM "flow_checkouts" fc
|
|
67
|
+
JOIN "leads" l ON l."id" = fc."lead_id" AND l."workspace_id" = fc."workspace_id"
|
|
68
|
+
WHERE fc."status" = 'completed'
|
|
69
|
+
AND fc."stripe_customer_id" IS NOT NULL
|
|
70
|
+
AND fc."stripe_customer_id" <> ''
|
|
71
|
+
AND length(fc."stripe_customer_id") <= 255
|
|
72
|
+
ORDER BY fc."workspace_id", lower(btrim(l."email", E' \t\n\r\f\x0B') COLLATE "C"),
|
|
73
|
+
fc."completed_at" ASC NULLS LAST, fc."created_at" ASC, fc."id" ASC
|
|
74
|
+
) c
|
|
75
|
+
WHERE p."workspace_id" = c."workspace_id"
|
|
76
|
+
AND p."email" = c."email"
|
|
77
|
+
AND p."billing_customer_id" IS NULL;
|
|
78
|
+
|
|
79
|
+
-- 3. Appointments, by attendee email: a Person a lead already made keeps its
|
|
80
|
+
-- lead-derived name and created_at (ON CONFLICT DO NOTHING).
|
|
81
|
+
INSERT INTO "people" ("workspace_id", "email", "name", "created_at", "updated_at")
|
|
82
|
+
SELECT DISTINCT ON (a."workspace_id", lower(btrim(a."attendee_email", E' \t\n\r\f\x0B') COLLATE "C"))
|
|
83
|
+
a."workspace_id",
|
|
84
|
+
lower(btrim(a."attendee_email", E' \t\n\r\f\x0B') COLLATE "C"),
|
|
85
|
+
NULLIF(left(btrim(regexp_replace(a."attendee_name", '[\x01-\x1F\x7F-\x9F\u200E\u200F\u202A-\u202E\u2066-\u2069]', '', 'g')), 200), ''),
|
|
86
|
+
a."created_at",
|
|
87
|
+
now()
|
|
88
|
+
FROM "appointments" a
|
|
89
|
+
WHERE lower(btrim(a."attendee_email", E' \t\n\r\f\x0B') COLLATE "C") ~ '^[!-?A-~]+@[!-?A-~]+$'
|
|
90
|
+
AND length(lower(btrim(a."attendee_email", E' \t\n\r\f\x0B') COLLATE "C")) <= 254
|
|
91
|
+
ORDER BY a."workspace_id", lower(btrim(a."attendee_email", E' \t\n\r\f\x0B') COLLATE "C"), a."created_at" ASC, a."id" ASC
|
|
92
|
+
ON CONFLICT ("workspace_id", "email") DO NOTHING;
|
|
93
|
+
|
|
94
|
+
-- 4. Stamps: every lead and appointment with a storable email points at its
|
|
95
|
+
-- workspace's Person; a row already stamped is left alone.
|
|
96
|
+
UPDATE "leads"
|
|
97
|
+
SET "person_id" = p."id"
|
|
98
|
+
FROM "people" p
|
|
99
|
+
WHERE "leads"."person_id" IS NULL
|
|
100
|
+
AND p."workspace_id" = "leads"."workspace_id"
|
|
101
|
+
AND p."email" = lower(btrim("leads"."email", E' \t\n\r\f\x0B') COLLATE "C");
|
|
102
|
+
|
|
103
|
+
UPDATE "appointments"
|
|
104
|
+
SET "person_id" = p."id"
|
|
105
|
+
FROM "people" p
|
|
106
|
+
WHERE "appointments"."person_id" IS NULL
|
|
107
|
+
AND p."workspace_id" = "appointments"."workspace_id"
|
|
108
|
+
AND p."email" = lower(btrim("appointments"."attendee_email", E' \t\n\r\f\x0B') COLLATE "C");
|
|
109
|
+
|
|
110
|
+
-- 5. user_id, verified only: the oldest promoted lead (by promoted_at, then
|
|
111
|
+
-- created_at, then id) whose user is email-verified and whose verified
|
|
112
|
+
-- address normalizes to the Person's. An unverified or differing user is
|
|
113
|
+
-- never copied — the rule stampPersonUser applies to new rows.
|
|
114
|
+
UPDATE "people" p
|
|
115
|
+
SET "user_id" = c."user_id",
|
|
116
|
+
"updated_at" = now()
|
|
117
|
+
FROM (
|
|
118
|
+
SELECT DISTINCT ON (l."workspace_id", lower(btrim(l."email", E' \t\n\r\f\x0B') COLLATE "C"))
|
|
119
|
+
l."workspace_id",
|
|
120
|
+
lower(btrim(l."email", E' \t\n\r\f\x0B') COLLATE "C") AS "email",
|
|
121
|
+
l."promoted_user_id" AS "user_id"
|
|
122
|
+
FROM "leads" l
|
|
123
|
+
JOIN "users" u ON u."id" = l."promoted_user_id"
|
|
124
|
+
WHERE l."promoted_user_id" IS NOT NULL
|
|
125
|
+
AND u."email_verified" = true
|
|
126
|
+
AND lower(btrim(u."email", E' \t\n\r\f\x0B') COLLATE "C") = lower(btrim(l."email", E' \t\n\r\f\x0B') COLLATE "C")
|
|
127
|
+
ORDER BY l."workspace_id", lower(btrim(l."email", E' \t\n\r\f\x0B') COLLATE "C"),
|
|
128
|
+
l."promoted_at" ASC NULLS LAST, l."created_at" ASC, l."id" ASC
|
|
129
|
+
) c
|
|
130
|
+
WHERE p."workspace_id" = c."workspace_id"
|
|
131
|
+
AND p."email" = c."email"
|
|
132
|
+
AND p."user_id" IS NULL;
|
|
@@ -128,3 +128,56 @@ export async function resolveSentinelWorkspaceRow(
|
|
|
128
128
|
}
|
|
129
129
|
return { id: DEFAULT_WORKSPACE_ID, organizationId: workspace.organizationId };
|
|
130
130
|
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// #852 D7 — the Person timeline's pre-migration email fallback.
|
|
134
|
+
//
|
|
135
|
+
// A FlowSession or ProjectListing written before the Person backfill and
|
|
136
|
+
// never tied to a lead (`lead_id IS NULL`) reaches its Person only by email.
|
|
137
|
+
// These three SQL texts are the ONLY way that match is made: run by
|
|
138
|
+
// `apps/api/src/lib/tenant-db.ts`'s `people.timeline` through
|
|
139
|
+
// `$queryRawUnsafe(SQL, email, workspaceId, cutoff)`, and executed verbatim
|
|
140
|
+
// against live Postgres by `database/__tests__/person.db.test.ts` — one
|
|
141
|
+
// string, proven where it runs. `$n` placeholders only, never interpolation.
|
|
142
|
+
//
|
|
143
|
+
// The match is EXACT equality of the normalized column against the Person's
|
|
144
|
+
// stored (already-normalized) email — never Prisma's `mode: "insensitive"`,
|
|
145
|
+
// which compiles to ILIKE: `_`/`%` would act as wildcards and non-ASCII case
|
|
146
|
+
// would fold, joining another client's rows (SEC-852-B). The normalization is
|
|
147
|
+
// the backfill's N(x) (database/migrations/20261010121000_person_backfill).
|
|
148
|
+
//
|
|
149
|
+
// `$3` is the cutoff: `finished_at` of the backfill migration's
|
|
150
|
+
// `_prisma_migrations` row (`PERSON_CUTOFF_SQL`). That column is timestamptz;
|
|
151
|
+
// the inbox `created_at` columns are timestamp(3) holding UTC (Prisma's
|
|
152
|
+
// convention), so the comparison converts the cutoff to UTC wall time. Only
|
|
153
|
+
// rows strictly older than the cutoff qualify. Each returns ids only, at most
|
|
154
|
+
// 50, newest first; the caller re-reads them through its scoped findMany.
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
/** The `_prisma_migrations` key whose `finished_at` is the fallback cutoff. */
|
|
158
|
+
export const PERSON_BACKFILL_MIGRATION = "20261010121000_person_backfill";
|
|
159
|
+
|
|
160
|
+
/** `$1` = the Person's email, `$2` = workspace id, `$3` = the cutoff. */
|
|
161
|
+
export const PERSON_FALLBACK_FLOW_SESSIONS_SQL = String.raw`SELECT "id" FROM "flow_sessions"
|
|
162
|
+
WHERE "workspace_id" = $2::text
|
|
163
|
+
AND "lead_id" IS NULL
|
|
164
|
+
AND "created_at" < ($3::timestamptz AT TIME ZONE 'UTC')
|
|
165
|
+
AND lower(btrim("email", E' \t\n\r\f\x0B') COLLATE "C") = $1::text
|
|
166
|
+
ORDER BY "created_at" DESC, "id" DESC
|
|
167
|
+
LIMIT 50`;
|
|
168
|
+
|
|
169
|
+
/** `$1` = the Person's email, `$2` = workspace id, `$3` = the cutoff. */
|
|
170
|
+
export const PERSON_FALLBACK_PROJECT_LISTINGS_SQL = String.raw`SELECT "id" FROM "project_listings"
|
|
171
|
+
WHERE "workspace_id" = $2::text
|
|
172
|
+
AND "lead_id" IS NULL
|
|
173
|
+
AND "created_at" < ($3::timestamptz AT TIME ZONE 'UTC')
|
|
174
|
+
AND lower(btrim("founder_email", E' \t\n\r\f\x0B') COLLATE "C") = $1::text
|
|
175
|
+
ORDER BY "created_at" DESC, "id" DESC
|
|
176
|
+
LIMIT 50`;
|
|
177
|
+
|
|
178
|
+
/** The cutoff read: the backfill's `finished_at`, or no row when it has not run. */
|
|
179
|
+
export const PERSON_CUTOFF_SQL = `SELECT "finished_at" FROM "_prisma_migrations"
|
|
180
|
+
WHERE "migration_name" = '20261010121000_person_backfill'
|
|
181
|
+
AND "finished_at" IS NOT NULL
|
|
182
|
+
AND "rolled_back_at" IS NULL
|
|
183
|
+
LIMIT 1`;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Declared-env check — fails when this site's code reads an environment
|
|
4
|
+
* variable that `project.yaml` does not declare.
|
|
5
|
+
*
|
|
6
|
+
* Every literal `process.env` read in the site's source (tests, stories and
|
|
7
|
+
* this `tools/` directory excepted) needs a row in `project.yaml`'s
|
|
8
|
+
* `env_contract` block: a `name`, a one-line `description`, and
|
|
9
|
+
* `secret: true` or `false`. Platform names (`NODE_ENV`, `CI`, `VERCEL_*`, …)
|
|
10
|
+
* need none. On a spoke (`.ailk-site-manifest.json` records
|
|
11
|
+
* `"topology": "spoke"`), `NEXT_PUBLIC_AILK_API_URL` is refused outright:
|
|
12
|
+
* a spoke's hub origin is server-only, so read `AILK_API_URL` instead.
|
|
13
|
+
*
|
|
14
|
+
* The scanner is `tools/declared-env.ts`, a copy of create-ailk's own, which
|
|
15
|
+
* writes this site's rows when it adds a module or a route. Add a row by hand
|
|
16
|
+
* for a variable you add; create-ailk never edits or removes one.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* pnpm env:declared # or: tsx tools/check-declared-env.ts
|
|
20
|
+
*
|
|
21
|
+
* Exit codes:
|
|
22
|
+
* 0 — every read is declared
|
|
23
|
+
* 1 — an undeclared read, a malformed row, a refused name, or a missing file
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { dirname, resolve } from "node:path";
|
|
27
|
+
|
|
28
|
+
import { checkDeclaredEnv } from "./declared-env";
|
|
29
|
+
|
|
30
|
+
const ROOT = resolve(dirname(process.argv[1] ?? "."), "..");
|
|
31
|
+
|
|
32
|
+
const { findings, summary } = checkDeclaredEnv(ROOT);
|
|
33
|
+
if (findings.length > 0) {
|
|
34
|
+
for (const finding of findings) console.error(finding);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
console.log(summary);
|