@starci/hfs 4.7.23 → 4.7.25
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/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/release/candidate-ci.mjs +20 -10
- package/release/gate.mjs +37 -19
- package/release/push.mjs +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/ci-standard.yaml +5 -5
- package/runtime/knowledge/hfs/rules.yaml +17 -17
- package/runtime/knowledge/hfs/slots.yaml +20 -1
- package/runtime/knowledge/patterns/be/saga.yaml +1 -1
- package/runtime/knowledge/sonar-gate.yaml +12 -12
- package/runtime/modules/kernel/failure-codes.yaml +4 -4
- package/runtime/modules/schemas/application-stacks.schema.yaml +0 -3
- package/runtime/packages/cli/src/catalog.generated.mjs +67 -4
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +1 -1
- package/runtime/scripts/hfs/ci-declaration.mjs +2 -2
- package/runtime/scripts/hfs/rules/ci-standard.mjs +24 -14
- package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +68 -2
- package/runtime/scripts/hfs/rules/unused-symbol-properties.mjs +1 -1
- package/runtime/scripts/hfs/rules/unused-symbol.mjs +21 -5
- package/runtime/scripts/hfs/slots.mjs +3 -2
- package/runtime/scripts/lib/gate-loosening.mjs +248 -0
- package/runtime/scripts/lib/managed-json.mjs +51 -0
- package/runtime/scripts/lib/uat-run-folder.mjs +118 -0
- package/stack/prepare.mjs +10 -3
- package/stack/release-store.mjs +3 -3
- package/stack/remote.mjs +9 -0
- package/sync/ci-commands.mjs +38 -90
- package/sync/index.mjs +12 -6
- package/templates/app/browser-config/browser/tsconfig.json +11 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +2 -3
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +2 -3
- package/templates/app/hooks/husky/pre-push +4 -2
- package/templates/app/package-scripts/package.json +0 -1
- package/templates/app/package-scripts-lite/package.json +0 -1
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +5 -6
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -6
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +5 -6
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +6 -2
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +4 -1
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +5 -2
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +1 -1
- package/templates/be/patterns/saga/saga.service.spec.ts.tpl +4 -1
- package/templates/be/platform/errors-graphql/graphql-error.mapper.spec.ts +15 -0
- package/templates/be/platform/errors-graphql/graphql-error.mapper.ts +4 -4
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +15 -12
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +6 -2
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +2 -6
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +1 -1
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/errors/domain.error.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.service.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +35 -9
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +14 -1
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.spec.ts +1 -3
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +5 -7
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +5 -2
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +4 -1
- package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.spec.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +8 -10
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +15 -7
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +2 -2
- package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +2 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +6 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +16 -10
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +5 -1
- package/templates/fe/auth/supabase-client.ts.tpl +11 -7
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/component.tsx +1 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +1 -1
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +1 -4
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +18 -3
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +4 -4
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +11 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +2 -2
- package/templates/fe/transport/graphql/clients/clients/no-auth.ts +2 -2
- package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +2 -1
- package/templates/fe/transport/graphql/clients/links/context-headers.ts +9 -0
- package/templates/fe/transport/graphql/clients/links/http.ts +3 -3
- package/templates/fe/transport/graphql/clients/links/locale.ts +2 -1
- package/templates/fe/transport/graphql/clients/response.ts +1 -1
|
@@ -4,7 +4,11 @@ const sessionOf = (session: Session | null): AuthSession | undefined =>
|
|
|
4
4
|
: { userId: session.user.id, ...(session.user.email === undefined ? {} : { email: session.user.email }) }
|
|
5
5
|
|
|
6
6
|
/** A Supabase Auth error as the provider reports it: its HTTP status, its code and its message. */
|
|
7
|
-
type ProviderError = {
|
|
7
|
+
type ProviderError = {
|
|
8
|
+
readonly status: number | undefined
|
|
9
|
+
readonly code: string | undefined
|
|
10
|
+
readonly message: string
|
|
11
|
+
}
|
|
8
12
|
|
|
9
13
|
/** The failure kind of a Supabase Auth error: refused credentials, rejected input, or the provider unavailable. */
|
|
10
14
|
const kindOf = (status: number | undefined): string => {
|
|
@@ -20,15 +24,15 @@ const failureOf = (error: ProviderError | null): AuthFailure | undefined =>
|
|
|
20
24
|
|
|
21
25
|
/** The Supabase Auth adapter shared by browser and cookie-backed PKCE flows. */
|
|
22
26
|
export const authClient: AuthClient = {
|
|
23
|
-
session: async () => sessionOf((await
|
|
24
|
-
accessToken: async () => (await
|
|
25
|
-
refreshAccessToken: async () => (await
|
|
27
|
+
session: async () => sessionOf((await authOf().getSession()).data.session),
|
|
28
|
+
accessToken: async () => (await authOf().getSession()).data.session?.access_token,
|
|
29
|
+
refreshAccessToken: async () => (await authOf().refreshSession()).data.session?.access_token,
|
|
26
30
|
signInWithPassword: async (email, password) =>
|
|
27
|
-
failureOf((await
|
|
28
|
-
signOut: async () => failureOf((await
|
|
31
|
+
failureOf((await authOf().signInWithPassword({ email, password })).error),
|
|
32
|
+
signOut: async () => failureOf((await authOf().signOut()).error),
|
|
29
33
|
// Supabase reports INITIAL_SESSION on subscribe, then every change: the subscription alone keeps the session current.
|
|
30
34
|
onChange: (listener) => {
|
|
31
|
-
const { data } =
|
|
35
|
+
const { data } = authOf().onAuthStateChange((_event, session) => listener(sessionOf(session)))
|
|
32
36
|
return () => data.subscription.unsubscribe()
|
|
33
37
|
},
|
|
34
38
|
}
|
|
@@ -8,7 +8,7 @@ export interface SignInFormBaseProps {
|
|
|
8
8
|
readonly emailLabel: string
|
|
9
9
|
readonly passwordLabel: string
|
|
10
10
|
readonly submitLabel: string
|
|
11
|
-
readonly failure
|
|
11
|
+
readonly failure: string | undefined
|
|
12
12
|
}
|
|
13
13
|
readonly on: { readonly submit: (input: FormData) => void }
|
|
14
14
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { createAppI18n } from "@{{project}}/i18n"
|
|
2
2
|
|
|
3
3
|
/** The product app's i18n: the product's one next-intl stack (the i18n package), with this app's own catalogs under `messages/`. */
|
|
4
|
-
export const appI18n = createAppI18n("app",
|
|
4
|
+
export const appI18n = createAppI18n("app", (locale) => import(`./messages/${locale}.json`))
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { createAppI18n } from "@{{project}}/i18n"
|
|
2
2
|
|
|
3
3
|
/** The landing's i18n: the product's one next-intl stack (the i18n package), with this app's own catalogs under `messages/`. */
|
|
4
|
-
export const landingI18n = createAppI18n(
|
|
5
|
-
"landing",
|
|
6
|
-
async (locale) => (await import(`./messages/${locale}.json`)).default,
|
|
7
|
-
)
|
|
4
|
+
export const landingI18n = createAppI18n("landing", (locale) => import(`./messages/${locale}.json`))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import "server-only"
|
|
2
2
|
import { hasLocale } from "next-intl"
|
|
3
|
+
import type { AbstractIntlMessages } from "next-intl"
|
|
3
4
|
import { createNavigation } from "next-intl/navigation"
|
|
4
5
|
import { getMessages, getRequestConfig } from "next-intl/server"
|
|
5
6
|
import { notFound } from "next/navigation"
|
|
@@ -7,11 +8,25 @@ import { locale as routeLocale } from "next/root-params"
|
|
|
7
8
|
import { PRODUCT_TIME_ZONE, routing } from "./routing"
|
|
8
9
|
|
|
9
10
|
/** Loads one app's own catalog for a served locale. */
|
|
10
|
-
type LoadMessages = (locale: string) => Promise<
|
|
11
|
+
type LoadMessages = (locale: string) => Promise<unknown>
|
|
11
12
|
|
|
12
13
|
/** The missing message next-intl asks a fallback for. */
|
|
13
14
|
type MissingMessage = { readonly namespace?: string; readonly key: string }
|
|
14
15
|
|
|
16
|
+
/** The message tree a catalog module exports as its default: the module is read as `unknown` and checked, never trusted as `any`. */
|
|
17
|
+
const isMessages = (value: unknown): value is AbstractIntlMessages => typeof value === "object" && value !== null
|
|
18
|
+
|
|
19
|
+
const messagesOf = (loaded: unknown): AbstractIntlMessages => {
|
|
20
|
+
const messages = typeof loaded === "object" && loaded !== null && "default" in loaded ? loaded.default : undefined
|
|
21
|
+
if (!isMessages(messages)) throw new TypeError("A message catalog module must export its messages as default")
|
|
22
|
+
return messages
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** `next/root-params` is typed only after Next generates its types, so the reader is checked before it is called. */
|
|
26
|
+
const isLocaleReader = (value: unknown): value is () => Promise<unknown> => typeof value === "function"
|
|
27
|
+
|
|
28
|
+
const readRouteLocale = async (): Promise<unknown> => (isLocaleReader(routeLocale) ? routeLocale() : undefined)
|
|
29
|
+
|
|
15
30
|
/**
|
|
16
31
|
* The next-intl stack of one app of the product, written once here and called once by each app's `modules/i18n` (R59): the
|
|
17
32
|
* product's one routing with its locale-aware navigation, the request config next-intl's plugin loads (the app's
|
|
@@ -27,12 +42,12 @@ export const createAppI18n = (app: string, loadMessages: LoadMessages) => ({
|
|
|
27
42
|
* missing catalog file is never imported.
|
|
28
43
|
*/
|
|
29
44
|
requestConfig: getRequestConfig(async () => {
|
|
30
|
-
const requested
|
|
45
|
+
const requested = await readRouteLocale()
|
|
31
46
|
const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale
|
|
32
47
|
return {
|
|
33
48
|
locale,
|
|
34
49
|
timeZone: PRODUCT_TIME_ZONE,
|
|
35
|
-
messages: await loadMessages(locale),
|
|
50
|
+
messages: messagesOf(await loadMessages(locale)),
|
|
36
51
|
getMessageFallback: ({ namespace, key }: MissingMessage) => `${app}:${namespace ?? ""}.${key}`,
|
|
37
52
|
}
|
|
38
53
|
}),
|
|
@@ -3,16 +3,16 @@ import type { Session, SupabaseClient } from "@supabase/supabase-js"
|
|
|
3
3
|
import { supabaseAnonKey, supabaseUrl } from "@/modules/config"
|
|
4
4
|
import type { AuthClient, AuthFailure, AuthSession } from "./contracts"
|
|
5
5
|
|
|
6
|
-
let
|
|
6
|
+
let authentication: SupabaseClient["auth"] | undefined
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* The one Supabase client of the front end, used for Supabase Auth only (the PKCE flow runs in the browser: a returning
|
|
10
10
|
* redirect's code is exchanged by `detectSessionInUrl`, no route handler). It never calls `.from()`, `.rpc()` or storage:
|
|
11
11
|
* data goes through modules/api to the back end, which verifies the access token.
|
|
12
12
|
*/
|
|
13
|
-
const
|
|
14
|
-
(
|
|
13
|
+
const authOf = (): SupabaseClient["auth"] =>
|
|
14
|
+
(authentication ??= createClient(supabaseUrl(), supabaseAnonKey(), {
|
|
15
15
|
auth: { flowType: "pkce", detectSessionInUrl: true, persistSession: true, autoRefreshToken: true },
|
|
16
|
-
}))
|
|
16
|
+
}).auth)
|
|
17
17
|
|
|
18
18
|
// {{> fe/auth/supabase-client.ts.tpl}}
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import "server-only"
|
|
2
2
|
import { hasLocale } from "next-intl"
|
|
3
|
+
import type { AbstractIntlMessages } from "next-intl"
|
|
3
4
|
import { getRequestConfig } from "next-intl/server"
|
|
4
5
|
import { headers } from "next/headers"
|
|
5
6
|
import { PRODUCT_TIME_ZONE, routing } from "./routing"
|
|
6
7
|
|
|
8
|
+
/** The message tree a catalog module exports as its default: the module is read as `unknown` and checked, never trusted as `any`. */
|
|
9
|
+
const isMessages = (value: unknown): value is AbstractIntlMessages => typeof value === "object" && value !== null
|
|
10
|
+
|
|
11
|
+
const messagesOf = (loaded: unknown): AbstractIntlMessages => {
|
|
12
|
+
const messages = typeof loaded === "object" && loaded !== null && "default" in loaded ? loaded.default : undefined
|
|
13
|
+
if (!isMessages(messages)) throw new TypeError("A message catalog module must export its messages as default")
|
|
14
|
+
return messages
|
|
15
|
+
}
|
|
16
|
+
|
|
7
17
|
/** The header the next-intl middleware sets on the request with the locale it negotiated. */
|
|
8
18
|
const LOCALE_HEADER = "x-next-intl-locale"
|
|
9
19
|
|
|
@@ -17,7 +27,7 @@ export default getRequestConfig(async () => {
|
|
|
17
27
|
return {
|
|
18
28
|
locale,
|
|
19
29
|
timeZone: PRODUCT_TIME_ZONE,
|
|
20
|
-
messages: (await import(`./messages/${locale}.json`))
|
|
30
|
+
messages: messagesOf(await import(`./messages/${locale}.json`)),
|
|
21
31
|
getMessageFallback: ({ namespace, key }) => `web:${namespace ?? ""}.${key}`,
|
|
22
32
|
}
|
|
23
33
|
})
|
|
@@ -14,7 +14,7 @@ import type { ErrorReporter } from "../links/error"
|
|
|
14
14
|
|
|
15
15
|
/** What the authenticated client takes: the abort signal of its operations and where the error link reports (none: it stays silent). */
|
|
16
16
|
export interface CreateAuthApolloClientOptions {
|
|
17
|
-
readonly signal
|
|
17
|
+
readonly signal: AbortSignal | undefined
|
|
18
18
|
readonly report?: ErrorReporter
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -23,7 +23,7 @@ export interface CreateAuthApolloClientOptions {
|
|
|
23
23
|
* retry (transient network failures) -> error (report) -> timeout -> refresh (one token refresh and replay on
|
|
24
24
|
* UNAUTHENTICATED) -> locale (`Accept-Language`) -> access token (`Authorization: Bearer`, from modules/auth) -> http (terminal).
|
|
25
25
|
*/
|
|
26
|
-
export const createAuthApolloClient = ({ signal, report }: CreateAuthApolloClientOptions
|
|
26
|
+
export const createAuthApolloClient = ({ signal, report }: CreateAuthApolloClientOptions) =>
|
|
27
27
|
new ApolloClient({
|
|
28
28
|
link: ApolloLink.from([
|
|
29
29
|
createRetryLink(),
|
|
@@ -11,7 +11,7 @@ import type { ErrorReporter } from "../links/error"
|
|
|
11
11
|
|
|
12
12
|
/** What the anonymous client takes: the abort signal of its operations and where the error link reports (none: it stays silent). */
|
|
13
13
|
export interface CreateNoAuthApolloClientOptions {
|
|
14
|
-
readonly signal
|
|
14
|
+
readonly signal: AbortSignal | undefined
|
|
15
15
|
readonly report?: ErrorReporter
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -19,7 +19,7 @@ export interface CreateNoAuthApolloClientOptions {
|
|
|
19
19
|
* The anonymous Apollo client for public operations: retry -> error -> timeout -> locale -> http. It never attaches a
|
|
20
20
|
* token, so a public operation stays public even for a signed-in reader.
|
|
21
21
|
*/
|
|
22
|
-
export const createNoAuthApolloClient = ({ signal, report }: CreateNoAuthApolloClientOptions
|
|
22
|
+
export const createNoAuthApolloClient = ({ signal, report }: CreateNoAuthApolloClientOptions) =>
|
|
23
23
|
new ApolloClient({
|
|
24
24
|
link: ApolloLink.from([
|
|
25
25
|
createRetryLink(),
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { SetContextLink } from "@apollo/client/link/context"
|
|
2
|
+
import { contextHeaders } from "./context-headers"
|
|
2
3
|
|
|
3
4
|
/** Attaches `Authorization: Bearer <access token>` from modules/auth when a session exists; an anonymous call goes out without it. */
|
|
4
5
|
export const createAttachAccessTokenLink = (accessToken: () => Promise<string | undefined>) =>
|
|
5
6
|
new SetContextLink(async (context) => {
|
|
6
7
|
const token = await accessToken()
|
|
7
8
|
if (token === undefined) return {}
|
|
8
|
-
return { headers: { ...context
|
|
9
|
+
return { headers: { ...contextHeaders(context), authorization: `Bearer ${token}` } }
|
|
9
10
|
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null
|
|
2
|
+
|
|
3
|
+
/** The string headers an earlier link put on the operation context. Apollo types the context `any`, so they are read as `unknown` and only strings are kept. */
|
|
4
|
+
export const contextHeaders = (context: Readonly<Record<string, unknown>>): Record<string, string> => {
|
|
5
|
+
const kept: Record<string, string> = {}
|
|
6
|
+
if (!isRecord(context.headers)) return kept
|
|
7
|
+
for (const [name, value] of Object.entries(context.headers)) if (typeof value === "string") kept[name] = value
|
|
8
|
+
return kept
|
|
9
|
+
}
|
|
@@ -4,13 +4,13 @@ import { graphqlUrl } from "@/modules/config"
|
|
|
4
4
|
/** What the terminal link takes: whether cookies travel and the abort signal of every operation of the client. */
|
|
5
5
|
export interface CreateHttpLinkParams {
|
|
6
6
|
readonly withCredentials?: boolean
|
|
7
|
-
readonly signal
|
|
7
|
+
readonly signal: AbortSignal | undefined
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/** The terminal link: POSTs the operation to the back end's GraphQL endpoint (modules/config). */
|
|
11
|
-
export const createHttpLink = ({ withCredentials = false, signal }: CreateHttpLinkParams
|
|
11
|
+
export const createHttpLink = ({ withCredentials = false, signal }: CreateHttpLinkParams) =>
|
|
12
12
|
new HttpLink({
|
|
13
13
|
uri: graphqlUrl(),
|
|
14
14
|
credentials: withCredentials ? "include" : "same-origin",
|
|
15
|
-
fetchOptions: { signal },
|
|
15
|
+
fetchOptions: signal === undefined ? {} : { signal },
|
|
16
16
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SetContextLink } from "@apollo/client/link/context"
|
|
2
|
+
import { contextHeaders } from "./context-headers"
|
|
2
3
|
|
|
3
4
|
/** The language of the document the reader is on (`<html lang>`, set by the locale layout); undefined outside a browser. */
|
|
4
5
|
const documentLocale = (): string | undefined =>
|
|
@@ -9,5 +10,5 @@ export const createAttachLocaleLink = (locale: () => string | undefined = docume
|
|
|
9
10
|
new SetContextLink((context) => {
|
|
10
11
|
const value = locale()
|
|
11
12
|
if (value === undefined) return {}
|
|
12
|
-
return { headers: { ...context
|
|
13
|
+
return { headers: { ...contextHeaders(context), "Accept-Language": value } }
|
|
13
14
|
})
|
|
@@ -6,7 +6,7 @@ const TRANSPORT_FAILED = "TRANSPORT_FAILED"
|
|
|
6
6
|
|
|
7
7
|
/** What an Apollo query or mutation resolves to under the `all` error policy: the data it got and the error it met. */
|
|
8
8
|
export interface ApolloOutcome<TData> {
|
|
9
|
-
readonly data
|
|
9
|
+
readonly data: TData | null | undefined
|
|
10
10
|
readonly error?: unknown
|
|
11
11
|
}
|
|
12
12
|
|