@starci/hfs 2.0.2 → 4.0.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/CHANGELOG.md +25 -0
- package/README.md +33 -33
- package/bin/hfs.mjs +94 -68
- package/lint/run.mjs +161 -0
- package/package.json +3 -2
- package/report/sonar.mjs +14 -29
- package/runtime/engine/admission.mjs +3 -3
- package/runtime/engine/ledger-db.mjs +2 -2
- package/runtime/engine/machine-db.mjs +90 -9
- package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +13 -0
- package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +93 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +32 -13
- package/runtime/knowledge/hfs/peer-integrations.yaml +18 -0
- package/runtime/knowledge/hfs/slots.yaml +193 -128
- package/runtime/knowledge/patterns/fe/folder.yaml +36 -36
- package/runtime/knowledge/sonar-gate.yaml +8 -7
- package/runtime/modules/kernel/failure-codes.yaml +31 -52
- package/runtime/scripts/checks/architecture/backend.mjs +1 -1
- package/runtime/scripts/checks/architecture/config.mjs +31 -11
- package/runtime/scripts/checks/architecture/contracts.mjs +4 -4
- package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +7 -3
- package/runtime/scripts/checks/architecture/framework-pinned.mjs +5 -47
- package/runtime/scripts/checks/architecture/frontend.mjs +6 -4
- package/runtime/scripts/checks/architecture/hfs-graph.mjs +1 -1
- package/runtime/scripts/checks/architecture/hfs.mjs +105 -67
- package/runtime/scripts/checks/architecture/index.mjs +18 -13
- package/runtime/scripts/checks/architecture/next-data.mjs +3 -2
- package/runtime/scripts/checks/architecture/owners.mjs +12 -6
- package/runtime/scripts/checks/architecture/registration.mjs +1 -1
- package/runtime/scripts/checks/architecture/surface.mjs +91 -0
- package/runtime/scripts/checks/architecture/symbols.mjs +13 -2
- package/runtime/scripts/checks/architecture/test-world-files.mjs +83 -45
- package/runtime/scripts/checks/architecture/typescript.mjs +127 -29
- package/runtime/scripts/checks/typescript-programs.mjs +2 -2
- package/runtime/scripts/lib/hfs-check.mjs +160 -209
- package/runtime/scripts/lib/hfs-path-findings.mjs +95 -0
- package/runtime/scripts/lib/hfs-rules/contract.mjs +15 -42
- package/runtime/scripts/lib/hfs-rules/frontend.mjs +37 -36
- package/runtime/scripts/lib/hfs-rules/peer-integrations.mjs +44 -0
- package/runtime/scripts/lib/hfs-rules/pipeline.mjs +12 -13
- package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +11 -8
- package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +1 -3
- package/runtime/scripts/lib/hfs-slots.mjs +244 -61
- package/runtime/scripts/lib/hfs-view.mjs +9 -7
- package/runtime/scripts/lib/language.mjs +11 -1
- package/runtime/scripts/lib/safe-remove.mjs +95 -10
- package/scaffold/app.mjs +179 -0
- package/scaffold/service.mjs +26 -16
- package/sync/cli.mjs +1 -1
- package/sync/hygiene.mjs +11 -8
- package/sync/index.mjs +109 -111
- package/sync/managed.mjs +9 -8
- package/sync/sonar-key.mjs +20 -22
- package/templates/{be → app}/ci-workflows/github/workflows/ci.yml +5 -11
- package/templates/app/gitignore +6 -0
- package/templates/app/hooks/husky/pre-commit +25 -0
- package/templates/app/hooks/husky/pre-push +7 -0
- package/templates/app/package-scripts/package.json +22 -0
- package/templates/{be → app}/quality-config/sonar-project.properties +4 -3
- package/templates/app/skeleton/.editorconfig +15 -0
- package/templates/app/skeleton/.gitattributes +2 -0
- package/templates/app/skeleton/.nvmrc +1 -0
- package/templates/app/skeleton/.starciwork/features/index.yaml +7 -0
- package/templates/app/skeleton/.starciwork/workspace.yaml +9 -0
- package/templates/app/skeleton/README.md +36 -0
- package/templates/app/skeleton/scripts/codegen.mjs +4 -0
- package/templates/{fe → app}/tool-config/prettierignore +4 -1
- package/templates/be/skeleton/.sops.yaml +2 -0
- package/templates/be/skeleton/.starcistacks/application-stacks.yaml +10 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +27 -6
- package/templates/be/skeleton/apps/__app__/src/main.ts +4 -1
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +11 -0
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +25 -0
- package/templates/be/skeleton/src/modules/domain/identity/errors/identity.error.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +11 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +14 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/identity/index.ts +6 -0
- package/templates/be/skeleton/src/modules/domain/identity/messages/identity.messages.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -1
- package/templates/be/skeleton/src/modules/platform/composition/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +71 -23
- package/templates/be/skeleton/src/modules/platform/config/errors/config.error.ts +14 -16
- package/templates/be/skeleton/src/modules/platform/config/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/config/server.config.ts +2 -12
- package/templates/be/skeleton/src/modules/platform/errors/domain.error.ts +16 -7
- package/templates/be/skeleton/src/modules/platform/errors/errors/errors.error.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.contracts.ts +33 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.decorators.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +32 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.options.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.service.spec.ts +94 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.service.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/errors/http-status.policy.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/errors/index.ts +4 -1
- package/templates/be/skeleton/src/modules/platform/errors/messages/errors.messages.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.decorators.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +17 -0
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +31 -0
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.ts +68 -0
- package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.spec.ts +66 -0
- package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.contracts.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.decorators.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module.ts +24 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.options.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.port.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/i18n/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.spec.ts +45 -0
- package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +85 -68
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +14 -10
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +0 -1
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.contracts.ts +25 -0
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.ts +24 -0
- package/templates/fe/skeleton/apps/__app__/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/error.tsx +5 -17
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +11 -22
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +3 -12
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +6 -24
- package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +4 -18
- package/templates/fe/skeleton/apps/__app__/src/app/globals.css +5 -0
- package/templates/fe/skeleton/apps/__app__/src/components/composites/FailureScreen/index.tsx +28 -0
- package/templates/fe/skeleton/apps/__app__/src/features/layouts/LocaleShell/index.tsx +35 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +21 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +17 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +26 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +25 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +27 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +32 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/config/index.ts +12 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/index.ts +2 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +6 -0
- package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/request.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/routes/index.ts +4 -0
- package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/proxy.ts +1 -1
- package/runtime/scripts/checks/architecture/size-growth.mjs +0 -73
- package/sync/skeleton.mjs +0 -76
- package/templates/be/gitignore +0 -2
- package/templates/be/hooks/husky/pre-commit +0 -13
- package/templates/be/hooks/husky/pre-push +0 -7
- package/templates/be/package-scripts/package.json +0 -22
- package/templates/be/skeleton/scripts/.gitkeep +0 -0
- package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +0 -9
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +0 -20
- package/templates/be/tool-config/prettierignore +0 -8
- package/templates/fe/ci-workflows/github/workflows/ci.yml +0 -54
- package/templates/fe/gitignore +0 -3
- package/templates/fe/hooks/husky/pre-commit +0 -16
- package/templates/fe/hooks/husky/pre-push +0 -6
- package/templates/fe/package-scripts/package.json +0 -17
- package/templates/fe/parts/api-client.ts +0 -44
- package/templates/fe/parts/api-outcome.ts +0 -7
- package/templates/fe/quality-config/sonar-project.properties +0 -8
- package/templates/fe/skeleton/scripts/.gitkeep +0 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +0 -1
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +0 -3
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +0 -1
- package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +0 -4
- package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/navigation.ts +0 -5
- package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +0 -12
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +0 -2
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +0 -9
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +0 -5
- package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +0 -5
- package/templates/fe/skeleton-shared/packages/__family__-api/package.json +0 -12
- package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +0 -1
- package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +0 -3
- package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +0 -1
- package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +0 -5
- package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +0 -18
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +0 -19
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +0 -2
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +0 -12
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +0 -15
- package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +0 -5
- package/templates/fe/tool-config/prettierrc +0 -1
- /package/templates/{be → app}/ci-workflows/github/workflows/e2e.yml +0 -0
- /package/templates/{be → app}/starciwork.gitignore +0 -0
- /package/templates/{be → app}/tool-config/prettierrc +0 -0
- /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/next.config.ts +0 -0
- /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/config.ts +0 -0
- /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/routing.ts +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# {{project}}
|
|
2
|
+
|
|
3
|
+
One StarCi app: its back end in be/, its front end in fe/, one install at the root.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The app starts with one api app (be/apps/api), one Next app (fe/apps/web) and the liveness probe both serve.
|
|
8
|
+
|
|
9
|
+
## Stack
|
|
10
|
+
|
|
11
|
+
TypeScript everywhere; NestJS in be/, Next.js with next-intl in fe/; npm with one package.json and one lockfile at the root.
|
|
12
|
+
|
|
13
|
+
## Repository layout
|
|
14
|
+
|
|
15
|
+
- `be/`: the back end, `apps/<app>/src` composes `src/features` and `src/modules`; its tests live under `src/tests`.
|
|
16
|
+
- `fe/`: the front end, one Next app per `apps/<app>`.
|
|
17
|
+
- `scripts/`: the app's operational scripts; `hfs.json` declares both sides.
|
|
18
|
+
|
|
19
|
+
## Development
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install
|
|
23
|
+
npm run typecheck
|
|
24
|
+
npm run lint
|
|
25
|
+
npm test
|
|
26
|
+
npm run build:be
|
|
27
|
+
npm run build:fe
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The api app reads `PORT` and `HTTP_SECURITY_ALLOWED_ORIGINS` (comma-separated origins allowed to send state-changing browser
|
|
31
|
+
requests) at boot; every door is closed until it is marked public or sign-in is designed. The web app reads
|
|
32
|
+
`NEXT_PUBLIC_SITE_URL`. A missing key stops the process with an error that names it.
|
|
33
|
+
|
|
34
|
+
## Work
|
|
35
|
+
|
|
36
|
+
The product's Work records live in `.starciwork`.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// codegen.mjs - the app's generated code, the one step behind `npm run codegen` (lint, typecheck, dev and build run it first).
|
|
2
|
+
// The front end reads the back end's committed contracts in place (be/contracts/, hfs.json sides.fe.reads) and generates its
|
|
3
|
+
// client from them into a gitignored __generated__/ folder. A new app has no contract yet, so there is nothing to generate.
|
|
4
|
+
process.stdout.write("codegen: no contract under be/contracts yet\n")
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import type { HttpSecurityOptions } from "@modules/platform/http-security"
|
|
1
2
|
import type { ServerOptions } from "@modules/platform/config"
|
|
2
3
|
|
|
3
4
|
/** Everything the {{app}} app needs from its environment, parsed once by `main.ts` and handed to `AppModule.register`. */
|
|
4
5
|
export interface {{appPascal}}Options {
|
|
5
6
|
/** The HTTP listener. */
|
|
6
7
|
readonly server: ServerOptions
|
|
8
|
+
/** The origin allowlist and rate limits. */
|
|
9
|
+
readonly httpSecurity: HttpSecurityOptions
|
|
7
10
|
}
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { Module } from "@nestjs/common"
|
|
2
2
|
import type { DynamicModule } from "@nestjs/common"
|
|
3
|
-
import { APP_FILTER } from "@nestjs/core"
|
|
3
|
+
import { APP_FILTER, APP_GUARD } from "@nestjs/core"
|
|
4
4
|
import { SystemHealthHttpModule } from "@features/system-health"
|
|
5
|
+
import { AuthGuard, IDENTITY_ERROR_KINDS, IDENTITY_MESSAGES, IdentityModule } from "@modules/domain/identity"
|
|
5
6
|
import { LivenessModule } from "@modules/domain/liveness"
|
|
6
7
|
import { ClockModule } from "@modules/platform/clock"
|
|
7
|
-
import {
|
|
8
|
+
import { CONFIG_ERROR_KINDS } from "@modules/platform/config"
|
|
8
9
|
import { CqrsModule } from "@modules/platform/cqrs"
|
|
9
|
-
import {
|
|
10
|
+
import { ERRORS_MESSAGES, ErrorsFilter, ErrorsModule } from "@modules/platform/errors"
|
|
11
|
+
import {
|
|
12
|
+
HTTP_SECURITY_ERROR_KINDS,
|
|
13
|
+
HTTP_SECURITY_MESSAGES,
|
|
14
|
+
HttpSecurityModule,
|
|
15
|
+
OriginGuard,
|
|
16
|
+
RateLimitGuard,
|
|
17
|
+
} from "@modules/platform/http-security"
|
|
18
|
+
import { I18nModule } from "@modules/platform/i18n"
|
|
10
19
|
import { LoggingModule } from "@modules/platform/logging"
|
|
11
20
|
import type { {{appPascal}}Options } from "./{{app}}.options"
|
|
12
21
|
|
|
13
22
|
@Module({})
|
|
14
|
-
/** Composition root of the {{app}} app: every capability is registered once, app-wide, the
|
|
23
|
+
/** Composition root of the {{app}} app: every capability is registered once, app-wide, and the three guards run in a fixed order. */
|
|
15
24
|
export class AppModule {
|
|
16
25
|
/** Builds the module from options parsed once at boot; nothing here reads the environment. */
|
|
17
26
|
static register(options: {{appPascal}}Options): DynamicModule {
|
|
@@ -20,13 +29,25 @@ export class AppModule {
|
|
|
20
29
|
imports: [
|
|
21
30
|
ClockModule.register({ isGlobal: true }),
|
|
22
31
|
LoggingModule.register({ isGlobal: true }),
|
|
32
|
+
I18nModule.register({
|
|
33
|
+
isGlobal: true,
|
|
34
|
+
bundles: [ERRORS_MESSAGES, HTTP_SECURITY_MESSAGES, IDENTITY_MESSAGES],
|
|
35
|
+
}),
|
|
36
|
+
ErrorsModule.register({
|
|
37
|
+
isGlobal: true,
|
|
38
|
+
kinds: [CONFIG_ERROR_KINDS, HTTP_SECURITY_ERROR_KINDS, IDENTITY_ERROR_KINDS],
|
|
39
|
+
}),
|
|
23
40
|
CqrsModule.register({ isGlobal: true }),
|
|
41
|
+
HttpSecurityModule.register({ isGlobal: true, ...options.httpSecurity }),
|
|
42
|
+
IdentityModule.register({ isGlobal: true }),
|
|
24
43
|
LivenessModule.register({ isGlobal: true }),
|
|
25
44
|
SystemHealthHttpModule,
|
|
26
45
|
],
|
|
27
46
|
providers: [
|
|
28
|
-
{ provide:
|
|
29
|
-
{ provide:
|
|
47
|
+
{ provide: APP_FILTER, useClass: ErrorsFilter },
|
|
48
|
+
{ provide: APP_GUARD, useClass: RateLimitGuard },
|
|
49
|
+
{ provide: APP_GUARD, useClass: OriginGuard },
|
|
50
|
+
{ provide: APP_GUARD, useClass: AuthGuard },
|
|
30
51
|
],
|
|
31
52
|
}
|
|
32
53
|
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { NestFactory } from "@nestjs/core"
|
|
2
2
|
import { SystemClockService } from "@modules/platform/clock"
|
|
3
3
|
import { EnvSource, parseServerConfig } from "@modules/platform/config"
|
|
4
|
+
import { parseHttpSecurityConfig } from "@modules/platform/http-security"
|
|
4
5
|
import { createJsonLogger, LoggingLogEvent } from "@modules/platform/logging"
|
|
5
6
|
import { AppModule } from "./app.module"
|
|
6
7
|
import type { {{appPascal}}Options } from "./{{app}}.options"
|
|
7
8
|
|
|
8
9
|
/** Reads the environment once, composes the app from the parsed options and serves until a shutdown signal. */
|
|
9
10
|
const bootstrap = async (): Promise<void> => {
|
|
10
|
-
const
|
|
11
|
+
const env = EnvSource.fromProcess()
|
|
12
|
+
const options: {{appPascal}}Options = { server: parseServerConfig(env), httpSecurity: parseHttpSecurityConfig(env) }
|
|
11
13
|
const app = await NestFactory.create(AppModule.register(options))
|
|
14
|
+
app.enableCors({ origin: [...options.httpSecurity.allowedOrigins] })
|
|
12
15
|
app.enableShutdownHooks()
|
|
13
16
|
await app.listen(options.server.port)
|
|
14
17
|
createJsonLogger(new SystemClockService()).info(LoggingLogEvent.ServerStarted, {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Controller, Get } from "@nestjs/common"
|
|
2
2
|
import type { QueryBus } from "@nestjs/cqrs"
|
|
3
|
+
import { Public, PublicReason } from "@modules/domain/identity"
|
|
3
4
|
import type { LivenessReport } from "@modules/domain/liveness"
|
|
4
5
|
import { InjectQueryBus } from "@modules/platform/cqrs"
|
|
5
6
|
import { CheckLivenessQuery } from "../../application/check-liveness.query"
|
|
@@ -11,6 +12,7 @@ export class LiveController {
|
|
|
11
12
|
|
|
12
13
|
/** Reports the process alive while its event loop still answers requests. */
|
|
13
14
|
@Get("live")
|
|
15
|
+
@Public({ reason: PublicReason.Health })
|
|
14
16
|
live(): Promise<LivenessReport> {
|
|
15
17
|
return this.queryBus.execute(new CheckLivenessQuery({ request: {} }))
|
|
16
18
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ok, refused } from "@modules/platform/primitives"
|
|
2
|
+
import type { Outcome } from "@modules/platform/primitives"
|
|
3
|
+
import { IdentityErrorCode } from "./errors/identity.error"
|
|
4
|
+
import type { PublicMetadata, PublicReason } from "./identity.contracts"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The default-deny decision of one door: a door that states why it is public is admitted; every other door is refused
|
|
8
|
+
* until the design that adds sign-in establishes a caller here.
|
|
9
|
+
*/
|
|
10
|
+
export const admit = (metadata: PublicMetadata | undefined): Outcome<PublicReason, IdentityErrorCode> =>
|
|
11
|
+
metadata === undefined ? refused(IdentityErrorCode.Unauthenticated) : ok(metadata.reason)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Injectable } from "@nestjs/common"
|
|
2
|
+
import type { CanActivate, ExecutionContext } from "@nestjs/common"
|
|
3
|
+
import type { Reflector } from "@nestjs/core"
|
|
4
|
+
import { InjectReflector } from "@modules/platform/composition"
|
|
5
|
+
import { unwrapOutcome } from "@modules/platform/primitives"
|
|
6
|
+
import { admit } from "./admission.policy"
|
|
7
|
+
import { IdentityError } from "./errors/identity.error"
|
|
8
|
+
import type { PublicMetadata } from "./identity.contracts"
|
|
9
|
+
import { PUBLIC_KEY } from "./identity.decorators"
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
/** The third app guard and the default-deny gate: a door is open only when it says `@Public({ reason })`. */
|
|
13
|
+
export class AuthGuard implements CanActivate {
|
|
14
|
+
constructor(@InjectReflector() private readonly reflector: Reflector) {}
|
|
15
|
+
|
|
16
|
+
/** Lets public doors through and refuses every other door. */
|
|
17
|
+
canActivate(context: ExecutionContext): boolean {
|
|
18
|
+
const metadata = this.reflector.getAllAndOverride<PublicMetadata | undefined>(PUBLIC_KEY, [
|
|
19
|
+
context.getHandler(),
|
|
20
|
+
context.getClass(),
|
|
21
|
+
])
|
|
22
|
+
unwrapOutcome(admit(metadata), IdentityError)
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DomainError } from "@modules/platform/errors"
|
|
2
|
+
import type { ErrorKind } from "@modules/platform/errors"
|
|
3
|
+
|
|
4
|
+
/** Codes of the identity capability. */
|
|
5
|
+
export enum IdentityErrorCode {
|
|
6
|
+
/** The door needs a signed-in caller and the request establishes none. */
|
|
7
|
+
Unauthenticated = "IDENTITY_UNAUTHENTICATED",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** How each identity code travels. */
|
|
11
|
+
export const IDENTITY_ERROR_KINDS: Record<IdentityErrorCode, ErrorKind> = {
|
|
12
|
+
[IdentityErrorCode.Unauthenticated]: "unauthenticated",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The one error class of the identity capability. */
|
|
16
|
+
export class IdentityError extends DomainError<IdentityErrorCode> {}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Why a door is open to anonymous callers; the vocabulary is closed. */
|
|
2
|
+
export enum PublicReason {
|
|
3
|
+
/** Liveness and readiness probes. */
|
|
4
|
+
Health = "health",
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** The metadata `@Public` attaches to a door. */
|
|
8
|
+
export interface PublicMetadata {
|
|
9
|
+
/** Why the door is anonymous. */
|
|
10
|
+
readonly reason: PublicReason
|
|
11
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SetMetadata } from "@nestjs/common"
|
|
2
|
+
import type { PublicMetadata } from "./identity.contracts"
|
|
3
|
+
|
|
4
|
+
/** Metadata key of `@Public`. */
|
|
5
|
+
export const PUBLIC_KEY = "domain.identity.public"
|
|
6
|
+
|
|
7
|
+
/** Opens a door to anonymous callers, stating why; every other door needs a signed-in caller. */
|
|
8
|
+
export const Public = (metadata: PublicMetadata): ReturnType<typeof SetMetadata> => SetMetadata(PUBLIC_KEY, metadata)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ConfigurableModuleBuilder } from "@nestjs/common"
|
|
2
|
+
import type { IdentityOptions } from "./identity.options"
|
|
3
|
+
|
|
4
|
+
/** The configurable-module base of the identity capability; `isGlobal` is decided by the app root. */
|
|
5
|
+
export const { ConfigurableModuleClass, OPTIONS_TYPE } = new ConfigurableModuleBuilder<IdentityOptions>()
|
|
6
|
+
.setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
|
|
7
|
+
.build()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common"
|
|
2
|
+
import type { DynamicModule } from "@nestjs/common"
|
|
3
|
+
import { AuthGuard } from "./auth.guard"
|
|
4
|
+
import { ConfigurableModuleClass, OPTIONS_TYPE } from "./identity.module-definition"
|
|
5
|
+
|
|
6
|
+
@Module({})
|
|
7
|
+
/** The identity capability: the guard that closes every door by default. The app registers AuthGuard itself after the throttler and the origin guard. */
|
|
8
|
+
export class IdentityModule extends ConfigurableModuleClass {
|
|
9
|
+
/** Registers the capability once per app. */
|
|
10
|
+
static register(options: typeof OPTIONS_TYPE): DynamicModule {
|
|
11
|
+
const base = super.register(options)
|
|
12
|
+
return { ...base, providers: [...(base.providers ?? []), AuthGuard], exports: [AuthGuard] }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { AuthGuard } from "./auth.guard"
|
|
2
|
+
export { IDENTITY_ERROR_KINDS } from "./errors/identity.error"
|
|
3
|
+
export { IDENTITY_MESSAGES } from "./messages/identity.messages"
|
|
4
|
+
export { PublicReason } from "./identity.contracts"
|
|
5
|
+
export { Public } from "./identity.decorators"
|
|
6
|
+
export { IdentityModule } from "./identity.module"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MessageBundle } from "@modules/platform/i18n"
|
|
2
|
+
|
|
3
|
+
/** Display text of the identity codes, Vietnamese and English. */
|
|
4
|
+
export const IDENTITY_MESSAGES: MessageBundle = {
|
|
5
|
+
vi: {
|
|
6
|
+
"errors.IDENTITY_UNAUTHENTICATED": "Bạn cần đăng nhập để thực hiện thao tác này.",
|
|
7
|
+
},
|
|
8
|
+
en: {
|
|
9
|
+
"errors.IDENTITY_UNAUTHENTICATED": "You need to sign in to do this.",
|
|
10
|
+
},
|
|
11
|
+
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { Inject } from "@nestjs/common"
|
|
2
2
|
import type { InjectionToken } from "@nestjs/common"
|
|
3
|
+
import { Reflector } from "@nestjs/core"
|
|
3
4
|
|
|
4
5
|
/** A parameter decorator that remembers the type `T` it injects, so a lint rule can compare it with the parameter annotation. */
|
|
5
|
-
export type TypedParameterDecorator<T> = ParameterDecorator & {
|
|
6
|
+
export type TypedParameterDecorator<T> = ParameterDecorator & {
|
|
7
|
+
/** Never set at run time: it carries `T` so the lint rule can compare it with the parameter annotation. */
|
|
8
|
+
readonly __injects?: T
|
|
9
|
+
}
|
|
6
10
|
|
|
7
11
|
/** Builds the parameter decorator that injects `token`, typed with the value it injects. Every `Inject<Thing>()` is built with it. */
|
|
8
12
|
export const injector = <T>(token: InjectionToken): TypedParameterDecorator<T> => Inject(token)
|
|
13
|
+
|
|
14
|
+
/** Injects the framework Reflector that guards read handler metadata with. Parameter type: Reflector. */
|
|
15
|
+
export const InjectReflector = (): TypedParameterDecorator<Reflector> => injector<Reflector>(Reflector)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { injector } from "./composition.decorators"
|
|
1
|
+
export { InjectReflector, injector } from "./composition.decorators"
|
|
2
2
|
export type { TypedParameterDecorator } from "./composition.decorators"
|
|
@@ -1,40 +1,88 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs"
|
|
2
|
-
import { ConfigError } from "./errors/config.error"
|
|
2
|
+
import { ConfigError, ConfigErrorCode } from "./errors/config.error"
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const FILE_SUFFIX = "_FILE"
|
|
5
|
+
const DURATION_UNITS: Readonly<Record<string, number>> = { ms: 1, s: 1000, m: 60_000, h: 3_600_000 }
|
|
6
|
+
const DURATION_PATTERN = /^(\d+)(ms|s|m|h)?$/
|
|
5
7
|
|
|
6
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* The only reader of the process environment. Typed readers name the missing or malformed key in the error; a key
|
|
10
|
+
* `<KEY>_FILE` supplies `<KEY>` from a file, so secrets can be mounted instead of exported.
|
|
11
|
+
*/
|
|
7
12
|
export class EnvSource {
|
|
8
|
-
|
|
13
|
+
constructor(private readonly values: Readonly<Record<string, string | undefined>>) {}
|
|
9
14
|
|
|
10
|
-
/**
|
|
15
|
+
/** Reads the process environment once, resolving every `<KEY>_FILE` into `<KEY>`. */
|
|
11
16
|
static fromProcess(): EnvSource {
|
|
12
|
-
|
|
17
|
+
const values: Record<string, string | undefined> = { ...process.env }
|
|
18
|
+
for (const [key, path] of Object.entries(process.env)) {
|
|
19
|
+
if (key.endsWith(FILE_SUFFIX) && path)
|
|
20
|
+
values[key.slice(0, -FILE_SUFFIX.length)] = readFileSync(path, "utf8").trim()
|
|
21
|
+
}
|
|
22
|
+
return new EnvSource(values)
|
|
13
23
|
}
|
|
14
24
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
return
|
|
25
|
+
/** True when the key is declared and not empty. */
|
|
26
|
+
has(key: string): boolean {
|
|
27
|
+
return Boolean(this.values[key])
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** True when at least one of the keys is declared: the switch of an all-or-nothing optional integration. */
|
|
31
|
+
anyDeclared(keys: ReadonlyArray<string>): boolean {
|
|
32
|
+
return keys.some((key) => this.has(key))
|
|
18
33
|
}
|
|
19
34
|
|
|
20
|
-
/** The value of
|
|
35
|
+
/** The declared value of an optional key, or undefined. */
|
|
21
36
|
optional(key: string): string | undefined {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
throw new ConfigError("CONFIG_FILE_UNREADABLE", `${key}_FILE`, { cause })
|
|
28
|
-
}
|
|
29
|
-
}
|
|
37
|
+
return this.values[key] || undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A required string. */
|
|
41
|
+
string(key: string): string {
|
|
30
42
|
const value = this.values[key]
|
|
31
|
-
|
|
43
|
+
if (!value) throw new ConfigError({ code: ConfigErrorCode.KeyMissing, params: { key } })
|
|
44
|
+
return value
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
/** A required integer, or the literal `fallback` when the key is not declared (tunables only). */
|
|
48
|
+
int(key: string, fallback?: number): number {
|
|
49
|
+
if (!this.has(key) && fallback !== undefined) return fallback
|
|
50
|
+
const value = Number(this.string(key))
|
|
51
|
+
if (!Number.isInteger(value)) throw new ConfigError({ code: ConfigErrorCode.KeyInvalid, params: { key } })
|
|
38
52
|
return value
|
|
39
53
|
}
|
|
54
|
+
|
|
55
|
+
/** A required boolean (`true` or `false`), or the literal `fallback` when not declared (tunables only). */
|
|
56
|
+
bool(key: string, fallback?: boolean): boolean {
|
|
57
|
+
if (!this.has(key) && fallback !== undefined) return fallback
|
|
58
|
+
const value = this.string(key)
|
|
59
|
+
if (value !== "true" && value !== "false")
|
|
60
|
+
throw new ConfigError({ code: ConfigErrorCode.KeyInvalid, params: { key } })
|
|
61
|
+
return value === "true"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A required absolute URL, returned as written. */
|
|
65
|
+
url(key: string): string {
|
|
66
|
+
const value = this.string(key)
|
|
67
|
+
if (!URL.canParse(value)) throw new ConfigError({ code: ConfigErrorCode.KeyInvalid, params: { key } })
|
|
68
|
+
return value
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A required value that must be one of `allowed`. */
|
|
72
|
+
enum<T extends string>(key: string, allowed: ReadonlyArray<T>): T {
|
|
73
|
+
const value = this.string(key)
|
|
74
|
+
const found = allowed.find((candidate) => candidate === value)
|
|
75
|
+
if (found === undefined) throw new ConfigError({ code: ConfigErrorCode.KeyInvalid, params: { key } })
|
|
76
|
+
return found
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A duration in milliseconds: `250`, `250ms`, `30s`, `5m` or `2h`; the literal `fallback` applies when not declared (tunables only). */
|
|
80
|
+
duration(key: string, fallback?: number): number {
|
|
81
|
+
if (!this.has(key) && fallback !== undefined) return fallback
|
|
82
|
+
const match = DURATION_PATTERN.exec(this.string(key))
|
|
83
|
+
const unit = DURATION_UNITS[match?.[2] ?? "ms"]
|
|
84
|
+
if (!match?.[1] || unit === undefined)
|
|
85
|
+
throw new ConfigError({ code: ConfigErrorCode.KeyInvalid, params: { key } })
|
|
86
|
+
return Number(match[1]) * unit
|
|
87
|
+
}
|
|
40
88
|
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import { DomainError } from "@modules/platform/errors"
|
|
2
|
+
import type { ErrorKind } from "@modules/platform/errors"
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
CONFIG_FILE_UNREADABLE: "points at a file that cannot be read",
|
|
4
|
+
/** Codes of the config capability; both are boot failures that name the offending key in the params. */
|
|
5
|
+
export enum ConfigErrorCode {
|
|
6
|
+
/** A required key is not declared in the environment. */
|
|
7
|
+
KeyMissing = "CONFIG_KEY_MISSING",
|
|
8
|
+
/** A declared key does not parse as the type its reader expects. */
|
|
9
|
+
KeyInvalid = "CONFIG_KEY_INVALID",
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
constructor(code: ConfigErrorCode, key: string, options?: { readonly cause?: unknown }) {
|
|
18
|
-
super(code, `Configuration key ${key} ${REASONS[code]}.`, options)
|
|
19
|
-
this.key = key
|
|
20
|
-
}
|
|
12
|
+
/** How each config code travels. */
|
|
13
|
+
export const CONFIG_ERROR_KINDS: Record<ConfigErrorCode, ErrorKind> = {
|
|
14
|
+
[ConfigErrorCode.KeyMissing]: "internal",
|
|
15
|
+
[ConfigErrorCode.KeyInvalid]: "internal",
|
|
21
16
|
}
|
|
17
|
+
|
|
18
|
+
/** The one error class of the config capability. */
|
|
19
|
+
export class ConfigError extends DomainError<ConfigErrorCode> {}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { SERVER_OPTIONS } from "./config.decorators"
|
|
2
1
|
export { EnvSource } from "./env-source.config"
|
|
2
|
+
export { CONFIG_ERROR_KINDS } from "./errors/config.error"
|
|
3
3
|
export { parseServerConfig } from "./server.config"
|
|
4
4
|
export type { ServerOptions } from "./server.options"
|
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
import type { EnvSource } from "./env-source.config"
|
|
2
|
-
import { ConfigError } from "./errors/config.error"
|
|
3
2
|
import type { ServerOptions } from "./server.options"
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
/** Parses the server keys of an environment; a malformed PORT stops the boot. */
|
|
9
|
-
export const parseServerConfig = (env: EnvSource): ServerOptions => {
|
|
10
|
-
const raw = env.optional("PORT")
|
|
11
|
-
if (raw === undefined) return { port: DEFAULT_PORT }
|
|
12
|
-
const port = Number(raw)
|
|
13
|
-
if (!Number.isInteger(port) || port < 1 || port > MAX_PORT) throw new ConfigError("CONFIG_KEY_INVALID", "PORT")
|
|
14
|
-
return { port }
|
|
15
|
-
}
|
|
4
|
+
/** Parses the server keys of an environment; a missing or malformed PORT stops the boot. */
|
|
5
|
+
export const parseServerConfig = (env: EnvSource): ServerOptions => ({ port: env.int("PORT") })
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
export abstract class DomainError extends Error {
|
|
3
|
-
/** Stable machine code that transports map to a status and logs group by. */
|
|
4
|
-
readonly code: string
|
|
1
|
+
import type { DomainErrorInit, ErrorParams } from "./errors.contracts"
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The base of every capability error family: a stable code, text parameters and the cause. The message is the code
|
|
5
|
+
* itself; display text is resolved from the message catalog by code, so an error never carries prose.
|
|
6
|
+
*/
|
|
7
|
+
export abstract class DomainError<C extends string> extends Error {
|
|
8
|
+
/** The stable machine code, `<CAPABILITY>_<WHAT>`. */
|
|
9
|
+
readonly code: C
|
|
10
|
+
|
|
11
|
+
/** Values for the placeholders of the display text. */
|
|
12
|
+
readonly params: ErrorParams
|
|
13
|
+
|
|
14
|
+
constructor(init: DomainErrorInit<C>) {
|
|
15
|
+
super(init.code, init.cause === undefined ? undefined : { cause: init.cause })
|
|
8
16
|
this.name = new.target.name
|
|
9
|
-
this.code = code
|
|
17
|
+
this.code = init.code
|
|
18
|
+
this.params = init.params ?? {}
|
|
10
19
|
}
|
|
11
20
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DomainError } from "../domain.error"
|
|
2
|
+
import type { ErrorKind } from "../errors.contracts"
|
|
3
|
+
|
|
4
|
+
/** Codes of the errors capability: the answer it gives for a failure no capability declared. */
|
|
5
|
+
export enum ErrorsErrorCode {
|
|
6
|
+
/** A failure no capability declared; the transport masks its details. */
|
|
7
|
+
Internal = "ERRORS_INTERNAL",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** How each errors code travels. */
|
|
11
|
+
export const ERRORS_ERROR_KINDS: Record<ErrorsErrorCode, ErrorKind> = {
|
|
12
|
+
[ErrorsErrorCode.Internal]: "internal",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The one error class of the errors capability. */
|
|
16
|
+
export class ErrorsError extends DomainError<ErrorsErrorCode> {}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** How a failure travels: the transport maps each kind to one HTTP status. */
|
|
2
|
+
export type ErrorKind =
|
|
3
|
+
"invalid" | "unauthenticated" | "forbidden" | "not-found" | "conflict" | "rate-limited" | "unavailable" | "internal"
|
|
4
|
+
|
|
5
|
+
/** Values that fill the named placeholders of an error text and travel next to the code; never prose. */
|
|
6
|
+
export interface ErrorParams {
|
|
7
|
+
readonly [name: string]: string | number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** What a capability error is built from: its code, optional text parameters and the failure that caused it. */
|
|
11
|
+
export interface DomainErrorInit<C extends string> {
|
|
12
|
+
/** The capability code, `<CAPABILITY>_<WHAT>`. */
|
|
13
|
+
readonly code: C
|
|
14
|
+
/** Values for the placeholders of the display text. */
|
|
15
|
+
readonly params?: ErrorParams
|
|
16
|
+
/** The failure that caused this one, kept for the log. */
|
|
17
|
+
readonly cause?: unknown
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One capability's exhaustive code to kind table, as its `<C>_ERROR_KINDS` constant declares it. */
|
|
21
|
+
export type ErrorKindTable = Readonly<Record<string, ErrorKind>>
|
|
22
|
+
|
|
23
|
+
/** Everything a transport needs to answer one failure: the stable code, the kind, the HTTP status and the parameters. */
|
|
24
|
+
export interface ErrorDescription {
|
|
25
|
+
/** The stable machine code. */
|
|
26
|
+
readonly code: string
|
|
27
|
+
/** How the failure travels. */
|
|
28
|
+
readonly kind: ErrorKind
|
|
29
|
+
/** The HTTP status of the kind. */
|
|
30
|
+
readonly status: number
|
|
31
|
+
/** Values for the placeholders of the display text. */
|
|
32
|
+
readonly params: ErrorParams
|
|
33
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { injector } from "@modules/platform/composition"
|
|
2
|
+
import type { TypedParameterDecorator } from "@modules/platform/composition"
|
|
3
|
+
import type { ErrorsOptions } from "./errors.options"
|
|
4
|
+
import type { ErrorsService } from "./errors.service"
|
|
5
|
+
|
|
6
|
+
/** Token of the errors options, exported so a spec can provide it. */
|
|
7
|
+
export const ERRORS_OPTIONS: unique symbol = Symbol("platform.errors.options")
|
|
8
|
+
|
|
9
|
+
/** Token of the service that describes failures for the transports. */
|
|
10
|
+
export const ERRORS_SERVICE: unique symbol = Symbol("platform.errors.service")
|
|
11
|
+
|
|
12
|
+
/** Injects the options of the errors capability. Parameter type: ErrorsOptions. */
|
|
13
|
+
export const InjectErrorsOptions = (): TypedParameterDecorator<ErrorsOptions> => injector<ErrorsOptions>(ERRORS_OPTIONS)
|
|
14
|
+
|
|
15
|
+
/** Injects the service that describes failures for the transports. Parameter type: ErrorsService. */
|
|
16
|
+
export const InjectErrorsService = (): TypedParameterDecorator<ErrorsService> => injector<ErrorsService>(ERRORS_SERVICE)
|