@starci/hfs 4.2.13 → 4.2.15

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.
Files changed (203) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +7 -3
  3. package/emit/db-types.mjs +2 -1
  4. package/package.json +1 -1
  5. package/runtime/knowledge/hfs/canon-pins.yaml +200 -8
  6. package/runtime/knowledge/hfs/fe-stack.yaml +221 -0
  7. package/runtime/knowledge/hfs/rules.yaml +50 -47
  8. package/runtime/knowledge/hfs/slots.yaml +307 -179
  9. package/runtime/knowledge/patterns/be/event-bus.yaml +15 -0
  10. package/runtime/knowledge/patterns/be/jobs.yaml +10 -0
  11. package/runtime/knowledge/patterns/be/projections.yaml +10 -0
  12. package/runtime/knowledge/patterns/be/queues.yaml +15 -0
  13. package/runtime/knowledge/patterns/be/reactors.yaml +8 -0
  14. package/runtime/knowledge/patterns/be/realtime.yaml +17 -0
  15. package/runtime/knowledge/patterns/be/saga.yaml +13 -0
  16. package/runtime/knowledge/patterns/fe/folder.yaml +24 -18
  17. package/runtime/knowledge/sonar-gate.yaml +10 -2
  18. package/runtime/modules/kernel/failure-codes.yaml +40 -60
  19. package/runtime/modules/kernel/removed-vocabulary.yaml +43 -0
  20. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +0 -2
  21. package/runtime/scripts/hfs/architecture/config.mjs +2 -1
  22. package/runtime/scripts/hfs/architecture/dead-exports.mjs +6 -2
  23. package/runtime/scripts/hfs/architecture/error-masked.mjs +10 -1
  24. package/runtime/scripts/hfs/architecture/frontend.mjs +10 -3
  25. package/runtime/scripts/hfs/architecture/hfs.mjs +6 -3
  26. package/runtime/scripts/hfs/architecture/index.mjs +2 -0
  27. package/runtime/scripts/hfs/architecture/owners.mjs +19 -16
  28. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +4 -1
  29. package/runtime/scripts/hfs/architecture/server-action.mjs +75 -20
  30. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +2 -2
  31. package/runtime/scripts/hfs/architecture/supabase.mjs +36 -210
  32. package/runtime/scripts/hfs/architecture/tiers.mjs +1 -33
  33. package/runtime/scripts/hfs/architecture/transport-owner.mjs +110 -54
  34. package/runtime/scripts/hfs/architecture/type-context.mjs +4 -1
  35. package/runtime/scripts/hfs/check.mjs +10 -6
  36. package/runtime/scripts/hfs/edition-slots.mjs +13 -4
  37. package/runtime/scripts/hfs/manifest-shape.mjs +3 -3
  38. package/runtime/scripts/hfs/rule-catalog.mjs +21 -1
  39. package/runtime/scripts/hfs/rules/database-constants.mjs +4 -0
  40. package/runtime/scripts/hfs/rules/database-migrations.mjs +12 -7
  41. package/runtime/scripts/hfs/rules/database.mjs +4 -3
  42. package/runtime/scripts/hfs/rules/edition.mjs +49 -37
  43. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +4 -5
  44. package/runtime/scripts/hfs/rules/fe-stack.mjs +71 -0
  45. package/scaffold/add-app.mjs +24 -18
  46. package/scaffold/add-cli-lite.mjs +33 -3
  47. package/scaffold/add-inbox-lite.mjs +24 -8
  48. package/scaffold/add-table.mjs +222 -74
  49. package/scaffold/add.mjs +85 -35
  50. package/scaffold/app.mjs +140 -36
  51. package/scaffold/edition-gate.mjs +21 -2
  52. package/scaffold/lite-exports.mjs +30 -1
  53. package/scaffold/service.mjs +5 -8
  54. package/src/main.mjs +2 -2
  55. package/sync/index.mjs +19 -15
  56. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +14 -0
  57. package/templates/app/codegen-graphql.mjs.partial +34 -0
  58. package/templates/app/codegen-lite/scripts/codegen.mjs +18 -13
  59. package/templates/app/package-scripts/core.json +1 -0
  60. package/templates/app/package-scripts-lite/package.json +4 -0
  61. package/templates/app/skeleton/README.md +1 -1
  62. package/templates/app/skeleton/scripts/codegen.mjs +4 -1
  63. package/templates/app/skeleton-lite/README.md +7 -4
  64. package/templates/app/skeleton-lite/supabase/config.toml +1 -1
  65. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +13 -14
  66. package/templates/be/inbox/database-client.spec.ts.tpl +47 -0
  67. package/templates/be/inbox/service.spec.ts.tpl +46 -0
  68. package/templates/be/inbox/webhook-door.spec.ts.tpl +76 -0
  69. package/templates/be/patterns/api/graphql.module.ts.tpl +2 -2
  70. package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +6 -3
  71. package/templates/be/platform/errors-graphql/graphql-error.mapper.spec.ts +52 -0
  72. package/templates/be/platform/errors-graphql/graphql-error.mapper.ts +31 -0
  73. package/templates/be/platform/graphql/graphql-depth.policy.spec.ts +43 -0
  74. package/templates/be/platform/graphql/graphql-depth.policy.ts +21 -0
  75. package/templates/be/platform/graphql/graphql.contracts.ts +12 -0
  76. package/templates/be/platform/graphql/graphql.module-definition.ts +7 -0
  77. package/templates/be/platform/graphql/graphql.module.ts +49 -0
  78. package/templates/be/platform/graphql/graphql.options.ts +2 -0
  79. package/templates/be/platform/graphql/index.ts +1 -0
  80. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +114 -0
  81. package/templates/be/platform/graphql/localize-errors.mapper.ts +51 -0
  82. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +11 -0
  83. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +8 -3
  84. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +13 -1
  85. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.ts +11 -2
  86. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +21 -0
  87. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +94 -0
  88. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +2 -4
  89. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +10 -0
  90. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +2 -2
  91. package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +2 -6
  92. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +15 -0
  93. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  94. package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +1 -1
  95. package/templates/be/table/api/create.command.ts.tpl +10 -0
  96. package/templates/be/table/api/create.contracts.ts.tpl +8 -0
  97. package/templates/be/table/api/create.handler.ts.tpl +22 -0
  98. package/templates/be/table/api/create.mapper.ts.tpl +5 -0
  99. package/templates/be/table/api/create.resolver.ts.tpl +21 -0
  100. package/templates/be/table/api/create.type.ts.tpl +9 -0
  101. package/templates/be/table/api/feature.index.ts.tpl +1 -0
  102. package/templates/be/table/api/feature.module.ts.tpl +7 -0
  103. package/templates/be/table/api/get.contracts.ts.tpl +11 -0
  104. package/templates/be/table/api/get.handler.ts.tpl +22 -0
  105. package/templates/be/table/api/get.input.ts.tpl +12 -0
  106. package/templates/be/table/api/get.mapper.ts.tpl +9 -0
  107. package/templates/be/table/api/get.query.ts.tpl +10 -0
  108. package/templates/be/table/api/get.resolver.ts.tpl +27 -0
  109. package/templates/be/table/api/get.type.ts.tpl +9 -0
  110. package/templates/be/table/api/graphql.module.ts.tpl +8 -0
  111. package/templates/be/table/error.api.ts.tpl +19 -0
  112. package/templates/be/table/migration.sql.tpl +13 -21
  113. package/templates/be/table/service.api.ts.tpl +30 -0
  114. package/templates/be/table/sql.api.ts.tpl +7 -0
  115. package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignInForm/index.tsx +16 -18
  116. package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignOutButton/component.tsx +7 -3
  117. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignOutButton/index.tsx +23 -0
  118. package/templates/fe/skeleton/apps/__app__/src/components/providers/AppProviders/index.tsx +49 -0
  119. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/index.ts +4 -0
  120. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -0
  121. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignIn.ts +29 -0
  122. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +27 -0
  123. package/templates/fe/skeleton/apps/__app__/src/hooks/effects/index.ts +1 -0
  124. package/templates/fe/skeleton/apps/__app__/src/hooks/effects/useAuthSessionEffect.ts +14 -0
  125. package/templates/fe/skeleton/apps/__app__/src/hooks/swr/SwrSideEffects.tsx +8 -0
  126. package/templates/fe/skeleton/apps/__app__/src/hooks/swr/index.ts +1 -0
  127. package/templates/fe/skeleton/apps/__app__/src/modules/api/graphql/index.ts +1 -0
  128. package/templates/fe/skeleton/apps/__app__/src/modules/api/graphql/types.ts +26 -0
  129. package/templates/fe/skeleton/apps/__app__/src/modules/api/index.ts +1 -0
  130. package/templates/fe/skeleton/apps/__app__/src/modules/auth/client.ts +21 -0
  131. package/templates/fe/skeleton/apps/__app__/src/modules/auth/contracts.ts +27 -0
  132. package/templates/fe/skeleton/apps/__app__/src/modules/auth/index.ts +2 -0
  133. package/templates/fe/skeleton/apps/__app__/src/modules/toast/api.ts +52 -0
  134. package/templates/fe/skeleton/apps/__app__/src/modules/toast/hooks.ts +33 -0
  135. package/templates/fe/skeleton/apps/__app__/src/modules/toast/index.ts +1 -0
  136. package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +32 -0
  137. package/templates/fe/skeleton/apps/__app__/src/modules/types/ambient/styles.d.ts +2 -0
  138. package/templates/fe/skeleton/apps/__app__/src/redux/ReduxProvider.tsx +11 -0
  139. package/templates/fe/skeleton/apps/__app__/src/redux/hooks.ts +8 -0
  140. package/templates/fe/skeleton/apps/__app__/src/redux/index.ts +3 -0
  141. package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +26 -0
  142. package/templates/fe/skeleton/apps/__app__/src/redux/store.ts +14 -0
  143. package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +5 -2
  144. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/component.tsx +10 -2
  145. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +1 -1
  146. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +23 -0
  147. package/templates/fe/skeleton/apps/landing/src/modules/types/ambient/styles.d.ts +2 -0
  148. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +51 -0
  149. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/providers.tsx +10 -13
  150. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +8 -5
  151. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +2 -17
  152. package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +3 -10
  153. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +0 -6
  154. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +11 -8
  155. package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +0 -3
  156. package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +9 -25
  157. package/templates/fe/table/graphql/mutation.ts.tpl +25 -0
  158. package/templates/fe/table/graphql/mutation.types.ts.tpl +7 -0
  159. package/templates/fe/table/graphql/query.ts.tpl +25 -0
  160. package/templates/fe/table/graphql/query.types.ts.tpl +7 -0
  161. package/templates/fe/table/graphql/useMutateSwr.ts.tpl +21 -0
  162. package/templates/fe/table/graphql/useQuerySwr.ts.tpl +17 -0
  163. package/templates/fe/transport/graphql/clients/clients/auth.ts +39 -0
  164. package/templates/fe/transport/graphql/clients/clients/index.ts +2 -0
  165. package/templates/fe/transport/graphql/clients/clients/no-auth.ts +33 -0
  166. package/templates/fe/transport/graphql/clients/index.ts +2 -0
  167. package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +9 -0
  168. package/templates/fe/transport/graphql/clients/links/error.ts +30 -0
  169. package/templates/fe/transport/graphql/clients/links/http.ts +16 -0
  170. package/templates/fe/transport/graphql/clients/links/index.ts +8 -0
  171. package/templates/fe/transport/graphql/clients/links/locale.ts +13 -0
  172. package/templates/fe/transport/graphql/clients/links/options.ts +17 -0
  173. package/templates/fe/transport/graphql/clients/links/refresh-token.ts +36 -0
  174. package/templates/fe/transport/graphql/clients/links/retry.ts +16 -0
  175. package/templates/fe/transport/graphql/clients/links/timeout.ts +28 -0
  176. package/templates/fe/transport/graphql/clients/response.ts +43 -0
  177. package/upgrade/check.mjs +1 -1
  178. package/upgrade/index.mjs +3 -2
  179. package/runtime/scripts/hfs/architecture/supabase-results.mjs +0 -182
  180. package/templates/app/quality-config-lite/sonar-project.properties +0 -5
  181. package/templates/be/patterns/api/feature.index.http.ts.tpl +0 -1
  182. package/templates/be/patterns/api/http.controller.ts.tpl +0 -27
  183. package/templates/be/patterns/api/http.mapper.ts.tpl +0 -9
  184. package/templates/be/patterns/api/http.module.ts.tpl +0 -7
  185. package/templates/be/patterns/api/http.request.ts.tpl +0 -10
  186. package/templates/be/patterns/api/http.response.ts.tpl +0 -5
  187. package/templates/fe/skeleton-lite/apps/web/src/app/auth/callback/route.ts +0 -24
  188. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/index.tsx +0 -38
  189. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/read-session.ts +0 -25
  190. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-in.ts +0 -25
  191. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +0 -17
  192. package/templates/fe/skeleton-lite/apps/web/src/modules/db/browser.ts +0 -14
  193. package/templates/fe/skeleton-lite/apps/web/src/modules/db/index.ts +0 -3
  194. package/templates/fe/skeleton-lite/apps/web/src/modules/db/outcome.ts +0 -59
  195. package/templates/fe/skeleton-lite/apps/web/src/modules/db/principal.ts +0 -29
  196. package/templates/fe/skeleton-lite/apps/web/src/modules/db/server.ts +0 -70
  197. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +0 -58
  198. package/templates/fe/skeleton-lite/apps/web/src/modules/routes/safe-next-path.ts +0 -26
  199. package/templates/fe/table/read.ts.tpl +0 -16
  200. package/templates/fe/table/write.ts.tpl +0 -16
  201. /package/templates/be/patterns/api/{application.command.http.ts.tpl → application.command.principal.ts.tpl} +0 -0
  202. /package/templates/be/patterns/api/{application.handler.http.ts.tpl → application.handler.principal.ts.tpl} +0 -0
  203. /package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignInForm/component.tsx +0 -0
@@ -1,25 +0,0 @@
1
- "use server"
2
- import "server-only"
3
-
4
- import { getPrincipal } from "../principal"
5
- import { createServerDbClient } from "../server"
6
- import { dbFailure, dbOk, toOutcome } from "../outcome"
7
- import type { DbOutcome } from "../outcome"
8
- import { signInInputSchema } from "../validation/validation.mapper"
9
-
10
- /** Result exposed to the client boundary after sign-in. */
11
- export interface SignInResult {
12
- readonly userId: string
13
- }
14
-
15
- /** Schema-checks credentials, signs in through the server client and returns a typed outcome. */
16
- export const writeSignIn = async (input: FormData): Promise<DbOutcome<SignInResult>> => {
17
- const principal = await getPrincipal()
18
- if (principal.kind === "ok") return dbOk({ userId: principal.value.id })
19
- const parsed = signInInputSchema.safeParse(input)
20
- if (!parsed.success) return dbFailure("invalid", "sign-in-input")
21
- const client = await createServerDbClient()
22
- const signedIn = toOutcome(await client.auth.signInWithPassword(parsed.data))
23
- if (signedIn.kind !== "ok") return signedIn
24
- return signedIn.value.user === null ? dbFailure("refused", "user") : dbOk({ userId: signedIn.value.user.id })
25
- }
@@ -1,17 +0,0 @@
1
- "use server"
2
- import "server-only"
3
-
4
- import { getPrincipal } from "../principal"
5
- import { createServerDbClient } from "../server"
6
- import { dbFailure, dbOk } from "../outcome"
7
- import type { DbOutcome } from "../outcome"
8
-
9
- /** Ends the cookie-backed Supabase session, including an already-anonymous one. */
10
- export const writeSignOut = async (): Promise<DbOutcome<true>> => {
11
- const principal = await getPrincipal()
12
- const client = await createServerDbClient()
13
- const result = await client.auth.signOut()
14
- if (result.error === null) return dbOk(true)
15
- if (principal.kind !== "ok" && result.error.status === 401) return dbOk(true)
16
- return dbFailure(result.error.status === 401 ? "refused" : "unavailable", result.error.code ?? "sign-out")
17
- }
@@ -1,14 +0,0 @@
1
- import { createBrowserClient } from "@supabase/ssr"
2
- import type { SupabaseClient } from "@supabase/supabase-js"
3
- import type { Database } from "../../../../../../supabase/types/database.types"
4
- import { supabaseAnonKey, supabaseUrl } from "@/modules/config"
5
-
6
- /** Creates the typed public client used only by client-side auth lifecycle hooks. */
7
- const createBrowserDbClient = (): SupabaseClient<Database> =>
8
- createBrowserClient<Database>(supabaseUrl(), supabaseAnonKey())
9
-
10
- /** Owns the browser Auth subscription and exposes only its lifecycle to client hooks. */
11
- export const subscribeToAuthRefresh = (refresh: () => void): (() => void) => {
12
- const listener = createBrowserDbClient().auth.onAuthStateChange(refresh)
13
- return () => listener.data.subscription.unsubscribe()
14
- }
@@ -1,3 +0,0 @@
1
- import "server-only"
2
-
3
- export { exchangeAuthCode, readSession, refreshSession } from "./auth/read-session"
@@ -1,59 +0,0 @@
1
- /** Failure kinds exposed by the database boundary. */
2
- export type DbFailureKind = "refused" | "not-found" | "invalid" | "unavailable"
3
-
4
- /** A successful database or Auth operation. */
5
- export interface DbOk<T> {
6
- readonly kind: "ok"
7
- readonly value: T
8
- }
9
-
10
- /** An expected database or Auth failure. */
11
- export interface DbFailure {
12
- readonly kind: DbFailureKind
13
- readonly code: string
14
- }
15
-
16
- /** The app's one result vocabulary, owned by the lite database transport. */
17
- export type DbOutcome<T> = DbOk<T> | DbFailure
18
-
19
- /** The structural part of a Supabase error this boundary interprets. */
20
- interface SupabaseFailure {
21
- readonly code?: string
22
- readonly status?: number
23
- readonly message: string
24
- }
25
-
26
- /** The common structural pair returned by Supabase data and Auth calls. */
27
- export interface SupabaseResult {
28
- readonly data: unknown
29
- readonly error: SupabaseFailure | null
30
- }
31
-
32
- /** The non-null data carried by one concrete SDK result. */
33
- export type SupabaseData<T extends SupabaseResult> = NonNullable<T["data"]>
34
-
35
- /** Wraps a value as a successful outcome. */
36
- export const dbOk = <T>(value: T): DbOk<T> => ({ kind: "ok", value })
37
- /** Builds a refusal without throwing across a Server Action boundary. */
38
- export const dbFailure = (kind: DbFailureKind, code: string): DbFailure => ({ kind, code })
39
-
40
- const hasSignal = (error: SupabaseFailure, signals: ReadonlyArray<string>): boolean => {
41
- const values = [error.code, error.status === undefined ? undefined : String(error.status)]
42
- return values.some((value) => value !== undefined && signals.includes(value))
43
- }
44
-
45
- const hasData = <T>(value: T): value is NonNullable<T> => value !== null && value !== undefined
46
-
47
- /** Maps Supabase's data/error pair into the app's closed outcome vocabulary. */
48
- export const toOutcome = <T extends SupabaseResult>(result: T): DbOutcome<SupabaseData<T>> => {
49
- if (result.error === null) {
50
- return hasData(result.data) ? dbOk(result.data) : dbFailure("not-found", "empty")
51
- }
52
- const code = result.error.code ?? String(result.error.status ?? "transport")
53
- if (hasSignal(result.error, ["42501", "PGRST301", "401", "403", "invalid_credentials"])) {
54
- return dbFailure("refused", code)
55
- }
56
- if (hasSignal(result.error, ["PGRST116"])) return dbFailure("not-found", code)
57
- if (result.error.code?.startsWith("23") === true) return dbFailure("invalid", code)
58
- return dbFailure("unavailable", code)
59
- }
@@ -1,29 +0,0 @@
1
- import "server-only"
2
-
3
- import { cache } from "react"
4
- import { createServerDbClient } from "./server"
5
- import { dbFailure, dbOk, toOutcome } from "./outcome"
6
- import type { DbOutcome } from "./outcome"
7
-
8
- /** The trusted caller claims exposed to server readers and writers. */
9
- export interface Principal {
10
- readonly id: string
11
- readonly email?: string
12
- }
13
-
14
- /** Keeps only the claims a consumer may trust after `getClaims()` verified the token. */
15
- export const principalFromClaims = (claims: Readonly<Record<string, unknown>>): DbOutcome<Principal> => {
16
- if (typeof claims.sub !== "string") return dbFailure("refused", "claims")
17
- return dbOk({
18
- id: claims.sub,
19
- ...(typeof claims.email === "string" ? { email: claims.email } : {}),
20
- })
21
- }
22
-
23
- /** Verifies and memoizes the current request principal. */
24
- export const getPrincipal = cache(async (): Promise<DbOutcome<Principal>> => {
25
- const client = await createServerDbClient()
26
- const claims = toOutcome(await client.auth.getClaims())
27
- if (claims.kind !== "ok") return claims
28
- return principalFromClaims(claims.value.claims)
29
- })
@@ -1,70 +0,0 @@
1
- import "server-only"
2
-
3
- import { createServerClient } from "@supabase/ssr"
4
- import type { SupabaseClient } from "@supabase/supabase-js"
5
- import { cookies } from "next/headers"
6
- import type { NextRequest, NextResponse } from "next/server"
7
- import type { Database } from "../../../../../../supabase/types/database.types"
8
- import { sessionCookieOptions, supabaseAnonKey, supabaseUrl } from "@/modules/config"
9
- import { toOutcome } from "./outcome"
10
- import type { DbOutcome, SupabaseData, SupabaseResult } from "./outcome"
11
-
12
- interface OwnedRowIdentity {
13
- readonly id: string
14
- readonly owner_id: string
15
- }
16
-
17
- type InsertOwnedRow<T extends SupabaseResult> = (
18
- client: SupabaseClient<Database>,
19
- row: OwnedRowIdentity,
20
- ) => PromiseLike<T>
21
-
22
- /** Creates the cookie-backed server client; the public key keeps every database call under RLS. */
23
- export const createServerDbClient = async (): Promise<SupabaseClient<Database>> => {
24
- const store = await cookies()
25
- return createServerClient<Database>(supabaseUrl(), supabaseAnonKey(), {
26
- cookies: {
27
- getAll: () => store.getAll(),
28
- setAll: (changes) => {
29
- try {
30
- for (const change of changes) {
31
- store.set(change.name, change.value, {
32
- ...change.options,
33
- ...sessionCookieOptions,
34
- })
35
- }
36
- } catch (cause) {
37
- if (!(cause instanceof Error) || !cause.message.includes("Cookies can only be modified"))
38
- throw cause
39
- }
40
- },
41
- },
42
- })
43
- }
44
-
45
- /** Inserts one validated principal-owned row through the typed server client. */
46
- export const insertRow = async <T extends SupabaseResult>(
47
- id: string,
48
- ownerId: string,
49
- insert: InsertOwnedRow<T>,
50
- ): Promise<DbOutcome<SupabaseData<T>>> => {
51
- const client = await createServerDbClient()
52
- return toOutcome(await insert(client, { id, owner_id: ownerId }))
53
- }
54
-
55
- /** Creates the proxy client and writes every refreshed cookie onto the response that will be returned. */
56
- export const createRequestDbClient = (request: NextRequest, response: NextResponse): SupabaseClient<Database> =>
57
- createServerClient<Database>(supabaseUrl(), supabaseAnonKey(), {
58
- cookies: {
59
- getAll: () => request.cookies.getAll(),
60
- setAll: (changes) => {
61
- for (const change of changes) {
62
- request.cookies.set(change.name, change.value)
63
- response.cookies.set(change.name, change.value, {
64
- ...change.options,
65
- ...sessionCookieOptions,
66
- })
67
- }
68
- },
69
- },
70
- })
@@ -1,58 +0,0 @@
1
- interface OwnedRowInput {
2
- readonly id: string
3
- }
4
-
5
- type OwnedRowParse = { readonly success: true; readonly data: OwnedRowInput } | { readonly success: false }
6
-
7
- interface SignInInput {
8
- readonly email: string
9
- readonly password: string
10
- }
11
-
12
- type SignInParse = { readonly success: true; readonly data: SignInInput } | { readonly success: false }
13
-
14
- const UUID = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/u
15
- const ADDRESS = /^[^\s@]+@([^\s@]+)$/u
16
-
17
- /** Whether the text is one local part, one `@` and a domain with a dot that has a character on each side; linear in the length. */
18
- const isEmail = (value: string): boolean => {
19
- const domain = ADDRESS.exec(value)?.[1]
20
- if (domain === undefined) return false
21
- const dot = domain.indexOf(".", 1)
22
- return dot !== -1 && dot < domain.length - 1
23
- }
24
-
25
- /** The bounded UUID schema shared by generated table Server Actions. */
26
- export const rowSchema = {
27
- safeParse: (input: unknown): OwnedRowParse => {
28
- if (
29
- typeof input !== "object" ||
30
- input === null ||
31
- !("id" in input) ||
32
- typeof input.id !== "string" ||
33
- !UUID.test(input.id)
34
- ) {
35
- return { success: false }
36
- }
37
- return { success: true, data: { id: input.id } }
38
- },
39
- }
40
-
41
- /** Bounds credentials before a sign-in request reaches Supabase Auth. */
42
- export const signInInputSchema = {
43
- safeParse: (input: FormData): SignInParse => {
44
- const email = input.get("email")
45
- const password = input.get("password")
46
- if (
47
- typeof email !== "string" ||
48
- email.length > 254 ||
49
- !isEmail(email) ||
50
- typeof password !== "string" ||
51
- password.length < 8 ||
52
- password.length > 256
53
- ) {
54
- return { success: false }
55
- }
56
- return { success: true, data: { email, password } }
57
- },
58
- }
@@ -1,26 +0,0 @@
1
- const INVALID_PATH_CHARACTER = /[\\\u0000-\u001F\u007F]/u
2
-
3
- const isSingleSlashPath = (value: string): boolean => value.startsWith("/") && !value.startsWith("//")
4
-
5
- /** Normalizes a same-origin callback destination or falls back to the app root. */
6
- export const safeNextPath = (candidate: string | null, origin: string): string => {
7
- if (candidate === null) return "/"
8
- let destination: URL
9
- let decoded: string
10
- try {
11
- destination = new URL(candidate, origin)
12
- decoded = decodeURIComponent(candidate)
13
- } catch {
14
- return "/"
15
- }
16
- if (
17
- destination.origin !== origin ||
18
- !isSingleSlashPath(candidate) ||
19
- !isSingleSlashPath(decoded) ||
20
- INVALID_PATH_CHARACTER.test(candidate) ||
21
- INVALID_PATH_CHARACTER.test(decoded)
22
- ) {
23
- return "/"
24
- }
25
- return `${destination.pathname}${destination.search}${destination.hash}`
26
- }
@@ -1,16 +0,0 @@
1
- import "server-only"
2
-
3
- import { cache } from "react"
4
- import type { Database } from "../../../../../../../supabase/types/database.types"
5
- import { createServerDbClient } from "../server"
6
- import { toOutcome } from "../outcome"
7
- import type { DbOutcome } from "../outcome"
8
-
9
- /** One {{table}} row from the generated database contract. */
10
- type {{Name}}Row = Database["public"]["Tables"]["{{table}}"]["Row"]
11
-
12
- /** Reads a bounded page of {{table}} rows visible to the current RLS principal. */
13
- export const read{{Name}} = cache(async (): Promise<DbOutcome<ReadonlyArray<{{Name}}Row>>> => {
14
- const client = await createServerDbClient()
15
- return toOutcome(await client.from("{{table}}").select("*").limit(100))
16
- })
@@ -1,16 +0,0 @@
1
- "use server"
2
- import "server-only"
3
-
4
- import { getPrincipal } from "../principal"
5
- import { dbFailure } from "../outcome"
6
- import { insertRow } from "../server"
7
- import { rowSchema } from "../validation/validation.mapper"
8
-
9
- /** Writes one validated {{table}} row for the current principal. */
10
- export const write{{Name}} = async (input: unknown) => {
11
- const principal = await getPrincipal()
12
- if (principal.kind !== "ok") return dbFailure("refused", "principal")
13
- const parsed = rowSchema.safeParse(input)
14
- if (!parsed.success) return dbFailure("invalid", "{{table}}-input")
15
- return insertRow(parsed.data.id, principal.value.id, (db, row) => db.from("{{table}}").insert(row).select().single())
16
- }