@facilis.dev/core 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +18 -2
- package/dist/api-clients-config.d.ts +36 -0
- package/dist/api-clients-config.js +24 -0
- package/dist/config-validation.d.ts +4 -0
- package/dist/config-validation.js +68 -0
- package/dist/config.d.ts +264 -0
- package/dist/config.js +103 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +68 -0
- package/dist/event.d.ts +105 -0
- package/dist/event.js +1 -0
- package/dist/i18n.d.ts +22 -0
- package/dist/i18n.js +50 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +24 -0
- package/dist/jobs-config.d.ts +35 -0
- package/dist/jobs-config.js +23 -0
- package/dist/jobs.d.ts +152 -0
- package/dist/jobs.js +133 -0
- package/dist/logging-config.d.ts +27 -0
- package/dist/logging-config.js +18 -0
- package/dist/logging.d.ts +140 -0
- package/dist/logging.js +107 -0
- package/dist/manifest.d.ts +137 -0
- package/dist/manifest.js +51 -0
- package/dist/rate-limit.d.ts +38 -0
- package/dist/rate-limit.js +76 -0
- package/dist/release-api.d.ts +3 -0
- package/dist/release-api.js +74 -0
- package/dist/release-artifact.d.ts +109 -0
- package/dist/release-artifact.js +149 -0
- package/dist/release-breaking.d.ts +12 -0
- package/dist/release-breaking.js +50 -0
- package/dist/release-classify.d.ts +11 -0
- package/dist/release-classify.js +100 -0
- package/dist/release-preview.d.ts +55 -0
- package/dist/release-preview.js +109 -0
- package/dist/release-security.d.ts +9 -0
- package/dist/release-security.js +127 -0
- package/dist/release-sql.d.ts +8 -0
- package/dist/release-sql.js +107 -0
- package/dist/release.d.ts +119 -0
- package/dist/release.js +8 -0
- package/dist/request-input.d.ts +34 -0
- package/dist/request-input.js +256 -0
- package/dist/request-security.d.ts +18 -0
- package/dist/request-security.js +82 -0
- package/dist/revision.d.ts +31 -0
- package/dist/revision.js +47 -0
- package/dist/route-context.d.ts +61 -0
- package/dist/route-context.js +1 -0
- package/dist/route-logging.d.ts +20 -0
- package/dist/route-logging.js +43 -0
- package/dist/route-scope.d.ts +18 -0
- package/dist/route-scope.js +8 -0
- package/dist/route.d.ts +63 -0
- package/dist/route.js +172 -0
- package/dist/schema-plan/diff-constraints.d.ts +7 -0
- package/dist/schema-plan/diff-constraints.js +129 -0
- package/dist/schema-plan/diff-drops.d.ts +7 -0
- package/dist/schema-plan/diff-drops.js +219 -0
- package/dist/schema-plan/diff-helpers.d.ts +29 -0
- package/dist/schema-plan/diff-helpers.js +52 -0
- package/dist/schema-plan/diff-snapshot.d.ts +12 -0
- package/dist/schema-plan/diff-snapshot.js +201 -0
- package/dist/schema-plan/draft.d.ts +17 -0
- package/dist/schema-plan/draft.js +32 -0
- package/dist/schema-plan/fixtures.d.ts +20 -0
- package/dist/schema-plan/fixtures.js +80 -0
- package/dist/schema-plan/plan.d.ts +12 -0
- package/dist/schema-plan/plan.js +36 -0
- package/dist/schema-plan/problems.d.ts +14 -0
- package/dist/schema-plan/problems.js +13 -0
- package/dist/schema-plan/render.d.ts +49 -0
- package/dist/schema-plan/render.js +163 -0
- package/dist/schema-plan/types.d.ts +38 -0
- package/dist/schema-plan/types.js +1 -0
- package/dist/security-config.d.ts +11 -0
- package/dist/security-config.js +24 -0
- package/dist/sso-config.d.ts +132 -0
- package/dist/sso-config.js +155 -0
- package/dist/storage-config.d.ts +68 -0
- package/dist/storage-config.js +54 -0
- package/dist/storage.d.ts +66 -0
- package/dist/storage.js +29 -0
- package/dist/sudo.d.ts +60 -0
- package/dist/sudo.js +169 -0
- package/dist/tasks.d.ts +105 -0
- package/dist/tasks.js +65 -0
- package/dist/tenancy.d.ts +9 -0
- package/dist/tenancy.js +9 -0
- package/dist/verification-config.d.ts +15 -0
- package/dist/verification-config.js +32 -0
- package/dist/workflows-config.d.ts +29 -0
- package/dist/workflows-config.js +16 -0
- package/dist/workflows-validation.d.ts +14 -0
- package/dist/workflows-validation.js +120 -0
- package/dist/workflows.d.ts +159 -0
- package/dist/workflows.js +111 -0
- package/package.json +47 -4
- package/src/api-clients-config.ts +59 -0
- package/src/config-validation.ts +111 -0
- package/src/config.ts +277 -0
- package/src/errors.ts +81 -0
- package/src/event.ts +114 -0
- package/src/i18n.ts +59 -0
- package/src/index.ts +292 -0
- package/src/jobs-config.ts +51 -0
- package/src/jobs.ts +282 -0
- package/src/logging-config.ts +46 -0
- package/src/logging.ts +248 -0
- package/src/manifest.ts +204 -0
- package/src/rate-limit.ts +106 -0
- package/src/release-api.ts +91 -0
- package/src/release-artifact.ts +268 -0
- package/src/release-breaking.ts +83 -0
- package/src/release-classify.ts +227 -0
- package/src/release-preview.ts +206 -0
- package/src/release-security.ts +168 -0
- package/src/release-sql.ts +108 -0
- package/src/release.ts +143 -0
- package/src/request-input.ts +298 -0
- package/src/request-security.ts +92 -0
- package/src/revision.ts +96 -0
- package/src/route-context.ts +70 -0
- package/src/route-logging.ts +77 -0
- package/src/route-scope.ts +29 -0
- package/src/route.ts +263 -0
- package/src/schema-plan/diff-constraints.ts +160 -0
- package/src/schema-plan/diff-drops.ts +236 -0
- package/src/schema-plan/diff-helpers.ts +78 -0
- package/src/schema-plan/diff-snapshot.ts +229 -0
- package/src/schema-plan/draft.ts +45 -0
- package/src/schema-plan/fixtures.ts +104 -0
- package/src/schema-plan/plan.ts +48 -0
- package/src/schema-plan/problems.ts +24 -0
- package/src/schema-plan/render.ts +221 -0
- package/src/schema-plan/types.ts +46 -0
- package/src/security-config.ts +35 -0
- package/src/sso-config.ts +187 -0
- package/src/storage-config.ts +105 -0
- package/src/storage.ts +93 -0
- package/src/sudo.ts +297 -0
- package/src/tasks.ts +162 -0
- package/src/tenancy.ts +9 -0
- package/src/verification-config.ts +52 -0
- package/src/workflows-config.ts +40 -0
- package/src/workflows-validation.ts +138 -0
- package/src/workflows.ts +274 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 facilis-dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @facilis.dev/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Facilis framework core: configuration contract, authorized-handler types and manifest shapes.
|
|
4
|
+
|
|
5
|
+
Part of [Facilis](https://github.com/facilis-dev/facilis), a browser-first low-code framework: the Studio
|
|
6
|
+
runs a generated project in a WebContainer where vite dev is the only dev server
|
|
7
|
+
and Nitro v3 runs as a Vite plugin. Framework contracts live in
|
|
8
|
+
[docs/SPEC.md](https://github.com/facilis-dev/facilis/blob/main/docs/SPEC.md) and architectural decisions in
|
|
9
|
+
[docs/DECISIONS.md](https://github.com/facilis-dev/facilis/blob/main/docs/DECISIONS.md).
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @facilis.dev/core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The package publishes its TypeScript sources and its compiled dist/; the exports map
|
|
16
|
+
of the published manifest points at dist/, so Node and bundlers load JavaScript and
|
|
17
|
+
the bundled .d.ts files.
|
|
18
|
+
|
|
19
|
+
Licensed under the MIT License.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Programmatic API clients (V3a): `auth.apiClients`. Opt-in, because a project that never
|
|
4
|
+
* needs machine credentials should not expose an API-client surface at all.
|
|
5
|
+
*/
|
|
6
|
+
/** Rate limit of the framework token endpoint, per client id (D-143). */
|
|
7
|
+
export interface FacilisApiClientsRateLimitConfig {
|
|
8
|
+
/** Window the exchange count is measured over. */
|
|
9
|
+
readonly windowMs: number;
|
|
10
|
+
/** Token exchanges allowed per client id in one window. */
|
|
11
|
+
readonly maxRequests: number;
|
|
12
|
+
}
|
|
13
|
+
export interface FacilisApiClientsConfig {
|
|
14
|
+
readonly enabled: boolean;
|
|
15
|
+
/** Lifetime of an issued access token: a positive whole number of milliseconds. */
|
|
16
|
+
readonly tokenTtlMs: number;
|
|
17
|
+
/** Per-client limit of the token endpoint (D-143); in-process and per-window. */
|
|
18
|
+
readonly rateLimit: FacilisApiClientsRateLimitConfig;
|
|
19
|
+
}
|
|
20
|
+
/** Authored form of `auth.apiClients`: every key has the framework default. */
|
|
21
|
+
export interface FacilisApiClientsConfigInput {
|
|
22
|
+
readonly enabled?: boolean;
|
|
23
|
+
readonly tokenTtlMs?: number;
|
|
24
|
+
readonly rateLimit?: {
|
|
25
|
+
readonly windowMs?: number;
|
|
26
|
+
readonly maxRequests?: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare const apiClientsSchema: z.ZodDefault<z.ZodObject<{
|
|
30
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
31
|
+
tokenTtlMs: z.ZodDefault<z.ZodNumber>;
|
|
32
|
+
rateLimit: z.ZodDefault<z.ZodObject<{
|
|
33
|
+
windowMs: z.ZodDefault<z.ZodNumber>;
|
|
34
|
+
maxRequests: z.ZodDefault<z.ZodNumber>;
|
|
35
|
+
}, z.core.$strict>>;
|
|
36
|
+
}, z.core.$strict>>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const DEFAULT_API_TOKEN_TTL_MS = 1000 * 60 * 60;
|
|
3
|
+
// A client may exchange its secret 30 times a minute: token lifetimes are an hour, so a
|
|
4
|
+
// legitimate caller renews once, while a secret-guessing loop is throttled to a crawl.
|
|
5
|
+
const DEFAULT_API_CLIENT_RATE_LIMIT = { windowMs: 60_000, maxRequests: 30 };
|
|
6
|
+
// API clients are opt-in (disabled by default) and their token lifetime is a whole positive
|
|
7
|
+
// number of milliseconds: one hour, short enough that a leaked token ages out quickly.
|
|
8
|
+
const DEFAULT_API_CLIENTS_CONFIG = {
|
|
9
|
+
enabled: false,
|
|
10
|
+
tokenTtlMs: DEFAULT_API_TOKEN_TTL_MS,
|
|
11
|
+
rateLimit: DEFAULT_API_CLIENT_RATE_LIMIT,
|
|
12
|
+
};
|
|
13
|
+
export const apiClientsSchema = z
|
|
14
|
+
.strictObject({
|
|
15
|
+
enabled: z.boolean().default(false),
|
|
16
|
+
tokenTtlMs: z.number().int().positive().default(DEFAULT_API_TOKEN_TTL_MS),
|
|
17
|
+
rateLimit: z
|
|
18
|
+
.strictObject({
|
|
19
|
+
windowMs: z.number().int().positive().default(DEFAULT_API_CLIENT_RATE_LIMIT.windowMs),
|
|
20
|
+
maxRequests: z.number().int().positive().default(DEFAULT_API_CLIENT_RATE_LIMIT.maxRequests),
|
|
21
|
+
})
|
|
22
|
+
.default(() => DEFAULT_API_CLIENT_RATE_LIMIT),
|
|
23
|
+
})
|
|
24
|
+
.default(() => DEFAULT_API_CLIENTS_CONFIG);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { checkSsoConfig } from "./sso-config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Cross-section validation for `facilis.config.ts` (SPEC §7.1, §8.4.1).
|
|
5
|
+
*
|
|
6
|
+
* The per-section Zod schemas only see their own object; these checks need the whole config
|
|
7
|
+
* (a role names a declared permission, SSO names an existing role, S3 names its bucket). They
|
|
8
|
+
* live here so `config.ts` stays under the 300-line limit and so the auth/storage/SSO rules
|
|
9
|
+
* have one reviewable home.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Permissions are stable identifiers: a role may only reference permissions declared in
|
|
13
|
+
* `auth.permissions`, and the wildcard is a role-only shorthand (SPEC §8.4.1).
|
|
14
|
+
*/
|
|
15
|
+
const WILDCARD_PERMISSION = "*";
|
|
16
|
+
function addIssue(ctx, path, message) {
|
|
17
|
+
ctx.addIssue({ code: "custom", message, path: [...path] });
|
|
18
|
+
}
|
|
19
|
+
function reportDuplicates(values, path, ctx, label) {
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
for (const [index, value] of values.entries()) {
|
|
22
|
+
if (seen.has(value))
|
|
23
|
+
addIssue(ctx, [...path, index], `duplicate ${label} "${value}"`);
|
|
24
|
+
seen.add(value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function ownRole(config, role) {
|
|
28
|
+
// A record from JSON can inherit Object.prototype keys, so membership must be own-only.
|
|
29
|
+
return Object.hasOwn(config.auth.roles, role) ? config.auth.roles[role] : undefined;
|
|
30
|
+
}
|
|
31
|
+
/** Refinement run by `facilisConfigSchema`: every cross-section rule in one place. */
|
|
32
|
+
export function crossCheckConfig(config, ctx) {
|
|
33
|
+
const declared = new Set(config.auth.permissions);
|
|
34
|
+
if (declared.has(WILDCARD_PERMISSION)) {
|
|
35
|
+
addIssue(ctx, ["auth", "permissions"], `"${WILDCARD_PERMISSION}" is only valid inside auth.roles; declare permissions explicitly`);
|
|
36
|
+
}
|
|
37
|
+
reportDuplicates(config.auth.permissions, ["auth", "permissions"], ctx, "permission");
|
|
38
|
+
for (const [role, permissions] of Object.entries(config.auth.roles)) {
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
for (const [index, permission] of permissions.entries()) {
|
|
41
|
+
if (permission === WILDCARD_PERMISSION)
|
|
42
|
+
continue;
|
|
43
|
+
if (!declared.has(permission)) {
|
|
44
|
+
addIssue(ctx, ["auth", "roles", role, index], `role "${role}" references unknown permission "${permission}"; declare it in auth.permissions`);
|
|
45
|
+
}
|
|
46
|
+
if (seen.has(permission)) {
|
|
47
|
+
addIssue(ctx, ["auth", "roles", role, index], `role "${role}" repeats "${permission}"`);
|
|
48
|
+
}
|
|
49
|
+
seen.add(permission);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (ownRole(config, config.auth.defaultRole) === undefined) {
|
|
53
|
+
addIssue(ctx, ["auth", "defaultRole"], `defaultRole "${config.auth.defaultRole}" is not a key of auth.roles`);
|
|
54
|
+
}
|
|
55
|
+
if (config.storage.driver === "s3") {
|
|
56
|
+
if (config.storage.bucket === null) {
|
|
57
|
+
addIssue(ctx, ["storage", "bucket"], 'storage.bucket is required when storage.driver is "s3"');
|
|
58
|
+
}
|
|
59
|
+
if (config.storage.region === null) {
|
|
60
|
+
addIssue(ctx, ["storage", "region"], 'storage.region is required when storage.driver is "s3"');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!config.i18n.locales.includes(config.i18n.defaultLocale)) {
|
|
64
|
+
addIssue(ctx, ["i18n", "defaultLocale"], `defaultLocale "${config.i18n.defaultLocale}" must be listed in i18n.locales`);
|
|
65
|
+
}
|
|
66
|
+
// SSO provider ids are URL segments and their roles must exist in auth.roles (D-120).
|
|
67
|
+
checkSsoConfig(config.auth.sso, config.auth, ctx);
|
|
68
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type FacilisApiClientsConfig, type FacilisApiClientsConfigInput } from "./api-clients-config.js";
|
|
3
|
+
import { type FacilisJobsConfig, type FacilisJobsConfigInput } from "./jobs-config.js";
|
|
4
|
+
import { type FacilisWorkflowsConfig, type FacilisWorkflowsConfigInput } from "./workflows-config.js";
|
|
5
|
+
import { type FacilisLoggingConfig, type FacilisLoggingConfigInput } from "./logging-config.js";
|
|
6
|
+
import { type FacilisSsoConfig, type FacilisSsoConfigInput } from "./sso-config.js";
|
|
7
|
+
import { type FacilisEmailVerificationConfig, type FacilisEmailVerificationConfigInput } from "./verification-config.js";
|
|
8
|
+
import { type FacilisSecurityConfig } from "./security-config.js";
|
|
9
|
+
import { type FacilisStorageConfig, type FacilisStorageConfigInput } from "./storage-config.js";
|
|
10
|
+
export { DEFAULT_LOGGING_CONFIG, facilisLoggingConfigSchema } from "./logging-config.js";
|
|
11
|
+
export type { FacilisLoggingConfig, FacilisLoggingConfigInput } from "./logging-config.js";
|
|
12
|
+
export { DEFAULT_SSO_CONFIG, SSO_PROVIDER_KINDS, ssoConfigSchema, ssoProviderSchema, } from "./sso-config.js";
|
|
13
|
+
export type { FacilisSsoConfig, FacilisSsoConfigInput, FacilisSsoProvider, SsoProviderKind, } from "./sso-config.js";
|
|
14
|
+
export { DEFAULT_EMAIL_VERIFICATION_CONFIG, facilisEmailVerificationConfigSchema, } from "./verification-config.js";
|
|
15
|
+
export type { FacilisEmailVerificationConfig, FacilisEmailVerificationConfigInput, } from "./verification-config.js";
|
|
16
|
+
export type { FacilisSecurityConfig } from "./security-config.js";
|
|
17
|
+
export { DEFAULT_JOBS_CONFIG, facilisJobsConfigSchema } from "./jobs-config.js";
|
|
18
|
+
export type { FacilisJobsConfig, FacilisJobsConfigInput } from "./jobs-config.js";
|
|
19
|
+
export { DEFAULT_WORKFLOWS_CONFIG, facilisWorkflowsConfigSchema } from "./workflows-config.js";
|
|
20
|
+
export type { FacilisWorkflowsConfig, FacilisWorkflowsConfigInput } from "./workflows-config.js";
|
|
21
|
+
export { DEFAULT_STORAGE_CONFIG, STORAGE_DRIVERS } from "./storage-config.js";
|
|
22
|
+
export type { FacilisStorageConfig, FacilisStorageConfigInput, FacilisStorageEncryptionConfig, StorageDriverKind, } from "./storage-config.js";
|
|
23
|
+
/**
|
|
24
|
+
* The configuration contract of a Facilis project (SPEC §7.1).
|
|
25
|
+
*
|
|
26
|
+
* Everything here is declared in the project's `facilis.config.ts` and is therefore code:
|
|
27
|
+
* reviewable, diffable and the only source of truth for roles and permissions (SPEC §8.4.1).
|
|
28
|
+
*/
|
|
29
|
+
/** Database driver modes supported by `@facilis.dev/db` (SPEC §7.1). */
|
|
30
|
+
export declare const DB_MODES: readonly ["pglite", "postgres", "json"];
|
|
31
|
+
export type DbMode = (typeof DB_MODES)[number];
|
|
32
|
+
export interface FacilisReleaseConfig {
|
|
33
|
+
readonly ref: string;
|
|
34
|
+
}
|
|
35
|
+
export interface FacilisDbConfig {
|
|
36
|
+
readonly mode: DbMode;
|
|
37
|
+
readonly migrationsDir: string;
|
|
38
|
+
readonly tenantColumns: readonly string[];
|
|
39
|
+
readonly ownerColumns: readonly string[];
|
|
40
|
+
}
|
|
41
|
+
export interface FacilisI18nConfig {
|
|
42
|
+
readonly defaultLocale: string;
|
|
43
|
+
readonly locales: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
export interface FacilisAuthConfig {
|
|
46
|
+
readonly permissions: readonly string[];
|
|
47
|
+
/** Role name → permission names. `"*"` is only valid here, as "every declared permission". */
|
|
48
|
+
readonly roles: Readonly<Record<string, readonly string[]>>;
|
|
49
|
+
readonly defaultRole: string;
|
|
50
|
+
readonly sessionDurationMs: number;
|
|
51
|
+
/** External identity providers (D-120); `providers: []` when the app uses passwords only. */
|
|
52
|
+
readonly sso: FacilisSsoConfig;
|
|
53
|
+
/** Email verification for password accounts (D-156); disabled by default. */
|
|
54
|
+
readonly verification: FacilisEmailVerificationConfig;
|
|
55
|
+
/** Bearer-token API clients (V3a): opt-in, with a token lifetime. */
|
|
56
|
+
readonly apiClients: FacilisApiClientsConfig;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Nitro options owned by the project (D-034).
|
|
60
|
+
*
|
|
61
|
+
* Facilis validates only the shape — an object — and passes every key to Nitro untouched:
|
|
62
|
+
* these are Nitro's options, not the framework's. `@facilis.dev/nitro` merges them over the
|
|
63
|
+
* Facilis defaults (`serverDir: "./server"`, `experimental.openAPI: true`), so a project
|
|
64
|
+
* only declares what it changes.
|
|
65
|
+
*/
|
|
66
|
+
export type FacilisNitroOptions = Readonly<Record<string, unknown>>;
|
|
67
|
+
export interface FacilisConfig {
|
|
68
|
+
readonly release: FacilisReleaseConfig;
|
|
69
|
+
readonly db: FacilisDbConfig;
|
|
70
|
+
readonly i18n: FacilisI18nConfig;
|
|
71
|
+
readonly auth: FacilisAuthConfig;
|
|
72
|
+
/** Background-job queue settings (D-107, docs/JOBS.md §4). */
|
|
73
|
+
readonly jobs: FacilisJobsConfig;
|
|
74
|
+
/** Workflow runner settings (D-135, docs/WORKFLOWS.md §10). */
|
|
75
|
+
readonly workflows: FacilisWorkflowsConfig;
|
|
76
|
+
/** Binary uploads (D-053): driver, bucket and the framework limits. */
|
|
77
|
+
readonly storage: FacilisStorageConfig;
|
|
78
|
+
/** Request-origin policy; always present so the wrapper never guesses (SPEC §8). */
|
|
79
|
+
readonly security: FacilisSecurityConfig;
|
|
80
|
+
/** Operator logging defaults (D-119): level and what is collected; sinks are env-owned. */
|
|
81
|
+
readonly logging: FacilisLoggingConfig;
|
|
82
|
+
readonly nitro: FacilisNitroOptions;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Authored form of the config: everything has a default except the auth declaration, and
|
|
86
|
+
* arrays are readonly so a fully-typed `FacilisConfig` can be passed back in unchanged.
|
|
87
|
+
*/
|
|
88
|
+
export interface FacilisConfigInput {
|
|
89
|
+
readonly release?: {
|
|
90
|
+
readonly ref?: string;
|
|
91
|
+
};
|
|
92
|
+
readonly db?: {
|
|
93
|
+
readonly mode?: DbMode;
|
|
94
|
+
readonly migrationsDir?: string;
|
|
95
|
+
readonly tenantColumns?: readonly string[];
|
|
96
|
+
readonly ownerColumns?: readonly string[];
|
|
97
|
+
};
|
|
98
|
+
readonly i18n?: {
|
|
99
|
+
readonly defaultLocale?: string;
|
|
100
|
+
readonly locales?: readonly string[];
|
|
101
|
+
};
|
|
102
|
+
/** Passed to Nitro unchanged; `@facilis.dev/nitro` merges the framework defaults underneath. */
|
|
103
|
+
readonly nitro?: FacilisNitroOptions;
|
|
104
|
+
readonly auth: {
|
|
105
|
+
readonly permissions?: readonly string[];
|
|
106
|
+
readonly roles?: Readonly<Record<string, readonly string[]>>;
|
|
107
|
+
readonly defaultRole: string;
|
|
108
|
+
readonly sessionDurationMs?: number;
|
|
109
|
+
/** External identity providers (D-120); every key has a default. */
|
|
110
|
+
readonly sso?: FacilisSsoConfigInput;
|
|
111
|
+
/** Email verification for password accounts (D-156); every key has a default. */
|
|
112
|
+
readonly verification?: FacilisEmailVerificationConfigInput;
|
|
113
|
+
/** Bearer-token API clients (V3a); optional, disabled by default. */
|
|
114
|
+
readonly apiClients?: FacilisApiClientsConfigInput;
|
|
115
|
+
};
|
|
116
|
+
/** Background-job queue settings; every key has a default (D-107). */
|
|
117
|
+
readonly jobs?: FacilisJobsConfigInput;
|
|
118
|
+
/** Workflow runner settings (D-135); every key has a default. */
|
|
119
|
+
readonly workflows?: FacilisWorkflowsConfigInput;
|
|
120
|
+
/** Binary uploads (D-053, D-117); every key has a default. */
|
|
121
|
+
readonly storage?: FacilisStorageConfigInput;
|
|
122
|
+
/** Logging defaults (D-119); every key has a default and the environment overrides them. */
|
|
123
|
+
readonly logging?: FacilisLoggingConfigInput;
|
|
124
|
+
/** Request-origin allow-list; defaults to same-origin only. */
|
|
125
|
+
readonly security?: {
|
|
126
|
+
readonly trustedOrigins?: readonly string[];
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Zod schema for the complete `FacilisConfig` shape.
|
|
131
|
+
*
|
|
132
|
+
* Sections other than `auth` have documented defaults so a generated project can grow into
|
|
133
|
+
* them; the auth block is always explicit and cross-checked (SPEC §8.4.1).
|
|
134
|
+
*/
|
|
135
|
+
export declare const facilisConfigSchema: z.ZodObject<{
|
|
136
|
+
release: z.ZodDefault<z.ZodObject<{
|
|
137
|
+
ref: z.ZodString;
|
|
138
|
+
}, z.core.$strict>>;
|
|
139
|
+
db: z.ZodDefault<z.ZodObject<{
|
|
140
|
+
mode: z.ZodDefault<z.ZodEnum<{
|
|
141
|
+
json: "json";
|
|
142
|
+
pglite: "pglite";
|
|
143
|
+
postgres: "postgres";
|
|
144
|
+
}>>;
|
|
145
|
+
migrationsDir: z.ZodDefault<z.ZodString>;
|
|
146
|
+
tenantColumns: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
147
|
+
ownerColumns: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
148
|
+
}, z.core.$strict>>;
|
|
149
|
+
i18n: z.ZodDefault<z.ZodObject<{
|
|
150
|
+
defaultLocale: z.ZodDefault<z.ZodString>;
|
|
151
|
+
locales: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
152
|
+
}, z.core.$strict>>;
|
|
153
|
+
auth: z.ZodObject<{
|
|
154
|
+
permissions: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
155
|
+
roles: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
156
|
+
defaultRole: z.ZodString;
|
|
157
|
+
sessionDurationMs: z.ZodDefault<z.ZodNumber>;
|
|
158
|
+
sso: z.ZodDefault<z.ZodObject<{
|
|
159
|
+
providers: z.ZodDefault<z.ZodReadonly<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
160
|
+
id: z.ZodString;
|
|
161
|
+
label: z.ZodOptional<z.ZodString>;
|
|
162
|
+
autoProvision: z.ZodDefault<z.ZodBoolean>;
|
|
163
|
+
defaultRole: z.ZodOptional<z.ZodString>;
|
|
164
|
+
kind: z.ZodLiteral<"oidc">;
|
|
165
|
+
issuer: z.ZodString;
|
|
166
|
+
clientId: z.ZodString;
|
|
167
|
+
clientSecretEnv: z.ZodOptional<z.ZodString>;
|
|
168
|
+
scopes: z.ZodDefault<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
|
|
169
|
+
tokenAuth: z.ZodDefault<z.ZodEnum<{
|
|
170
|
+
client_secret_basic: "client_secret_basic";
|
|
171
|
+
client_secret_post: "client_secret_post";
|
|
172
|
+
none: "none";
|
|
173
|
+
}>>;
|
|
174
|
+
assumeEmailVerified: z.ZodDefault<z.ZodBoolean>;
|
|
175
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
176
|
+
id: z.ZodString;
|
|
177
|
+
label: z.ZodOptional<z.ZodString>;
|
|
178
|
+
autoProvision: z.ZodDefault<z.ZodBoolean>;
|
|
179
|
+
defaultRole: z.ZodOptional<z.ZodString>;
|
|
180
|
+
kind: z.ZodLiteral<"oauth2">;
|
|
181
|
+
authorizationEndpoint: z.ZodString;
|
|
182
|
+
tokenEndpoint: z.ZodString;
|
|
183
|
+
userinfoEndpoint: z.ZodString;
|
|
184
|
+
clientId: z.ZodString;
|
|
185
|
+
clientSecretEnv: z.ZodOptional<z.ZodString>;
|
|
186
|
+
scopes: z.ZodDefault<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
|
|
187
|
+
tokenAuth: z.ZodDefault<z.ZodEnum<{
|
|
188
|
+
client_secret_basic: "client_secret_basic";
|
|
189
|
+
client_secret_post: "client_secret_post";
|
|
190
|
+
none: "none";
|
|
191
|
+
}>>;
|
|
192
|
+
mapping: z.ZodObject<{
|
|
193
|
+
subject: z.ZodString;
|
|
194
|
+
email: z.ZodString;
|
|
195
|
+
emailVerified: z.ZodOptional<z.ZodString>;
|
|
196
|
+
}, z.core.$strict>;
|
|
197
|
+
assumeEmailVerified: z.ZodDefault<z.ZodBoolean>;
|
|
198
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
199
|
+
id: z.ZodString;
|
|
200
|
+
label: z.ZodOptional<z.ZodString>;
|
|
201
|
+
autoProvision: z.ZodDefault<z.ZodBoolean>;
|
|
202
|
+
defaultRole: z.ZodOptional<z.ZodString>;
|
|
203
|
+
kind: z.ZodLiteral<"saml">;
|
|
204
|
+
entryPoint: z.ZodString;
|
|
205
|
+
issuer: z.ZodString;
|
|
206
|
+
certificateEnv: z.ZodString;
|
|
207
|
+
emailAttribute: z.ZodDefault<z.ZodString>;
|
|
208
|
+
wantAssertionsSigned: z.ZodDefault<z.ZodBoolean>;
|
|
209
|
+
wantResponseSigned: z.ZodDefault<z.ZodBoolean>;
|
|
210
|
+
}, z.core.$strict>], "kind">>>>;
|
|
211
|
+
failureRedirect: z.ZodDefault<z.ZodString>;
|
|
212
|
+
stateTtlMs: z.ZodDefault<z.ZodNumber>;
|
|
213
|
+
}, z.core.$strict>>;
|
|
214
|
+
verification: z.ZodType<FacilisEmailVerificationConfig, unknown, z.core.$ZodTypeInternals<FacilisEmailVerificationConfig, unknown>>;
|
|
215
|
+
apiClients: z.ZodDefault<z.ZodObject<{
|
|
216
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
217
|
+
tokenTtlMs: z.ZodDefault<z.ZodNumber>;
|
|
218
|
+
rateLimit: z.ZodDefault<z.ZodObject<{
|
|
219
|
+
windowMs: z.ZodDefault<z.ZodNumber>;
|
|
220
|
+
maxRequests: z.ZodDefault<z.ZodNumber>;
|
|
221
|
+
}, z.core.$strict>>;
|
|
222
|
+
}, z.core.$strict>>;
|
|
223
|
+
}, z.core.$strict>;
|
|
224
|
+
jobs: z.ZodDefault<z.ZodObject<{
|
|
225
|
+
maxConcurrent: z.ZodDefault<z.ZodNumber>;
|
|
226
|
+
maxAttempts: z.ZodDefault<z.ZodNumber>;
|
|
227
|
+
retryDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
228
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
229
|
+
pollIntervalMs: z.ZodDefault<z.ZodNumber>;
|
|
230
|
+
persist: z.ZodDefault<z.ZodBoolean>;
|
|
231
|
+
}, z.core.$strict>>;
|
|
232
|
+
workflows: z.ZodDefault<z.ZodObject<{
|
|
233
|
+
tickIntervalMs: z.ZodDefault<z.ZodNumber>;
|
|
234
|
+
persist: z.ZodDefault<z.ZodBoolean>;
|
|
235
|
+
defaultTaskTtlMs: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
236
|
+
}, z.core.$strict>>;
|
|
237
|
+
storage: z.ZodDefault<z.ZodObject<{
|
|
238
|
+
driver: z.ZodDefault<z.ZodEnum<{
|
|
239
|
+
local: "local";
|
|
240
|
+
s3: "s3";
|
|
241
|
+
}>>;
|
|
242
|
+
bucket: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
243
|
+
region: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
244
|
+
endpoint: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
245
|
+
forcePathStyle: z.ZodDefault<z.ZodNullable<z.ZodBoolean>>;
|
|
246
|
+
maxUploadBytes: z.ZodDefault<z.ZodNumber>;
|
|
247
|
+
tokenTtlSeconds: z.ZodDefault<z.ZodNumber>;
|
|
248
|
+
encryption: z.ZodDefault<z.ZodObject<{
|
|
249
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
250
|
+
}, z.core.$strict>>;
|
|
251
|
+
}, z.core.$strict>>;
|
|
252
|
+
security: z.ZodDefault<z.ZodObject<{
|
|
253
|
+
trustedOrigins: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
254
|
+
}, z.core.$strict>>;
|
|
255
|
+
logging: z.ZodType<FacilisLoggingConfig, unknown, z.core.$ZodTypeInternals<FacilisLoggingConfig, unknown>>;
|
|
256
|
+
nitro: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
257
|
+
}, z.core.$strict>;
|
|
258
|
+
/**
|
|
259
|
+
* Validates `facilis.config.ts` and returns the resolved, full config.
|
|
260
|
+
*
|
|
261
|
+
* Throws a readable error instead of returning a partially valid contract: a project with
|
|
262
|
+
* unknown role permissions must fail at boot (SPEC §8.4.1), not on the first request.
|
|
263
|
+
*/
|
|
264
|
+
export declare function defineConfig(config: FacilisConfigInput): FacilisConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { apiClientsSchema, } from "./api-clients-config.js";
|
|
3
|
+
import { facilisJobsConfigSchema, } from "./jobs-config.js";
|
|
4
|
+
import { facilisWorkflowsConfigSchema, } from "./workflows-config.js";
|
|
5
|
+
import { facilisLoggingConfigSchema, } from "./logging-config.js";
|
|
6
|
+
import { ssoConfigSchema, } from "./sso-config.js";
|
|
7
|
+
import { facilisEmailVerificationConfigSchema, } from "./verification-config.js";
|
|
8
|
+
import { securityConfigSchema, } from "./security-config.js";
|
|
9
|
+
import { crossCheckConfig } from "./config-validation.js";
|
|
10
|
+
import { facilisStorageConfigSchema, } from "./storage-config.js";
|
|
11
|
+
export { DEFAULT_LOGGING_CONFIG, facilisLoggingConfigSchema } from "./logging-config.js";
|
|
12
|
+
export { DEFAULT_SSO_CONFIG, SSO_PROVIDER_KINDS, ssoConfigSchema, ssoProviderSchema, } from "./sso-config.js";
|
|
13
|
+
export { DEFAULT_EMAIL_VERIFICATION_CONFIG, facilisEmailVerificationConfigSchema, } from "./verification-config.js";
|
|
14
|
+
export { DEFAULT_JOBS_CONFIG, facilisJobsConfigSchema } from "./jobs-config.js";
|
|
15
|
+
export { DEFAULT_WORKFLOWS_CONFIG, facilisWorkflowsConfigSchema } from "./workflows-config.js";
|
|
16
|
+
export { DEFAULT_STORAGE_CONFIG, STORAGE_DRIVERS } from "./storage-config.js";
|
|
17
|
+
/**
|
|
18
|
+
* The configuration contract of a Facilis project (SPEC §7.1).
|
|
19
|
+
*
|
|
20
|
+
* Everything here is declared in the project's `facilis.config.ts` and is therefore code:
|
|
21
|
+
* reviewable, diffable and the only source of truth for roles and permissions (SPEC §8.4.1).
|
|
22
|
+
*/
|
|
23
|
+
/** Database driver modes supported by `@facilis.dev/db` (SPEC §7.1). */
|
|
24
|
+
export const DB_MODES = ["pglite", "postgres", "json"];
|
|
25
|
+
const DEFAULT_RELEASE_REF = "main";
|
|
26
|
+
const DEFAULT_MIGRATIONS_DIR = "./drizzle";
|
|
27
|
+
const DEFAULT_TENANT_COLUMN = "tenantId";
|
|
28
|
+
const DEFAULT_OWNER_COLUMN = "ownerId";
|
|
29
|
+
const DEFAULT_LOCALE = "en";
|
|
30
|
+
const DEFAULT_SESSION_DURATION_MS = 1000 * 60 * 60 * 24 * 7;
|
|
31
|
+
const DEFAULT_DB_CONFIG = {
|
|
32
|
+
mode: "pglite",
|
|
33
|
+
migrationsDir: DEFAULT_MIGRATIONS_DIR,
|
|
34
|
+
tenantColumns: [DEFAULT_TENANT_COLUMN],
|
|
35
|
+
ownerColumns: [DEFAULT_OWNER_COLUMN],
|
|
36
|
+
};
|
|
37
|
+
const DEFAULT_I18N_CONFIG = { defaultLocale: DEFAULT_LOCALE, locales: [DEFAULT_LOCALE] };
|
|
38
|
+
const releaseSchema = z
|
|
39
|
+
.strictObject({
|
|
40
|
+
ref: z.string().min(1, "release.ref must be a non-empty git ref"),
|
|
41
|
+
})
|
|
42
|
+
// A generated project boots against its own branch; `main` is the least surprising default.
|
|
43
|
+
.default({ ref: DEFAULT_RELEASE_REF });
|
|
44
|
+
const dbSchema = z
|
|
45
|
+
.strictObject({
|
|
46
|
+
mode: z.enum(DB_MODES).default("pglite"),
|
|
47
|
+
migrationsDir: z.string().min(1).default(DEFAULT_MIGRATIONS_DIR),
|
|
48
|
+
tenantColumns: z.array(z.string().min(1)).default([DEFAULT_TENANT_COLUMN]),
|
|
49
|
+
ownerColumns: z.array(z.string().min(1)).default([DEFAULT_OWNER_COLUMN]),
|
|
50
|
+
})
|
|
51
|
+
.default(() => DEFAULT_DB_CONFIG);
|
|
52
|
+
const i18nSchema = z
|
|
53
|
+
.strictObject({
|
|
54
|
+
defaultLocale: z.string().min(1).default(DEFAULT_LOCALE),
|
|
55
|
+
locales: z.array(z.string().min(1)).default([DEFAULT_LOCALE]),
|
|
56
|
+
})
|
|
57
|
+
.default(() => DEFAULT_I18N_CONFIG);
|
|
58
|
+
// Nitro's own options are not Facilis's contract, so the section is a pass-through: keep
|
|
59
|
+
// the object as authored and let @facilis.dev/nitro apply the framework defaults underneath.
|
|
60
|
+
const nitroSchema = z.record(z.string(), z.unknown()).default({});
|
|
61
|
+
// Session duration is a safe default (one week, matching SPEC §8.4.1's example); everything
|
|
62
|
+
// security-relevant (roles, permissions, default role) stays required so it is never implicit.
|
|
63
|
+
const authSchema = z.strictObject({
|
|
64
|
+
permissions: z.array(z.string().min(1)).default([]),
|
|
65
|
+
roles: z.record(z.string().min(1), z.array(z.string().min(1))).default({}),
|
|
66
|
+
defaultRole: z.string().min(1),
|
|
67
|
+
sessionDurationMs: z.number().int().positive().default(DEFAULT_SESSION_DURATION_MS),
|
|
68
|
+
sso: ssoConfigSchema,
|
|
69
|
+
verification: facilisEmailVerificationConfigSchema,
|
|
70
|
+
apiClients: apiClientsSchema,
|
|
71
|
+
});
|
|
72
|
+
const baseConfigSchema = z.strictObject({
|
|
73
|
+
release: releaseSchema,
|
|
74
|
+
db: dbSchema,
|
|
75
|
+
i18n: i18nSchema,
|
|
76
|
+
auth: authSchema,
|
|
77
|
+
jobs: facilisJobsConfigSchema,
|
|
78
|
+
workflows: facilisWorkflowsConfigSchema,
|
|
79
|
+
storage: facilisStorageConfigSchema,
|
|
80
|
+
security: securityConfigSchema,
|
|
81
|
+
logging: facilisLoggingConfigSchema,
|
|
82
|
+
nitro: nitroSchema,
|
|
83
|
+
});
|
|
84
|
+
/**
|
|
85
|
+
* Zod schema for the complete `FacilisConfig` shape.
|
|
86
|
+
*
|
|
87
|
+
* Sections other than `auth` have documented defaults so a generated project can grow into
|
|
88
|
+
* them; the auth block is always explicit and cross-checked (SPEC §8.4.1).
|
|
89
|
+
*/
|
|
90
|
+
export const facilisConfigSchema = baseConfigSchema.superRefine(crossCheckConfig);
|
|
91
|
+
/**
|
|
92
|
+
* Validates `facilis.config.ts` and returns the resolved, full config.
|
|
93
|
+
*
|
|
94
|
+
* Throws a readable error instead of returning a partially valid contract: a project with
|
|
95
|
+
* unknown role permissions must fail at boot (SPEC §8.4.1), not on the first request.
|
|
96
|
+
*/
|
|
97
|
+
export function defineConfig(config) {
|
|
98
|
+
const result = facilisConfigSchema.safeParse(config);
|
|
99
|
+
if (!result.success) {
|
|
100
|
+
throw new Error(`Invalid facilis config:\n${z.prettifyError(result.error)}`);
|
|
101
|
+
}
|
|
102
|
+
return result.data;
|
|
103
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical API error contract (contracts §4.4, decision O-002).
|
|
4
|
+
*
|
|
5
|
+
* Responses carry codes, never prose: user-facing strings are translated client-side from
|
|
6
|
+
* `t("error.<code>")` so the API stays locale-neutral.
|
|
7
|
+
*/
|
|
8
|
+
export declare const FACILIS_ERROR_CODES: readonly ["unauthorized", "forbidden", "email_unverified", "invalid_input", "too_many_requests", "unsupported_media_type", "conflict", "db_unavailable", "not_found", "storage_unavailable", "payload_too_large", "file_too_large", "table_not_declared", "raw_sql_not_allowed", "internal_error"];
|
|
9
|
+
export type FacilisErrorCode = (typeof FACILIS_ERROR_CODES)[number];
|
|
10
|
+
/** HTTP status per code — the only place a status is chosen (contracts §4.4). */
|
|
11
|
+
export declare const FACILIS_ERROR_STATUS: Readonly<Record<FacilisErrorCode, number>>;
|
|
12
|
+
export interface FacilisErrorBody {
|
|
13
|
+
code: FacilisErrorCode;
|
|
14
|
+
details?: unknown;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Builds the web `Response` for a Facilis error. Handlers return these directly; `h3`
|
|
18
|
+
* treats a returned Response as the final response (contracts §4.4).
|
|
19
|
+
*/
|
|
20
|
+
export declare function fail(code: FacilisErrorCode, details?: unknown): Response;
|
|
21
|
+
/** Flattens Zod issues into the `details` shape used by `invalid_input` responses. */
|
|
22
|
+
export declare function flattenZodIssues(error: z.ZodError): {
|
|
23
|
+
formErrors: string[];
|
|
24
|
+
fieldErrors: {};
|
|
25
|
+
};
|
|
26
|
+
/** `400 invalid_input` with the flattened Zod issues as `details` (contracts §4.4). */
|
|
27
|
+
export declare function invalidInput(error: z.ZodError): Response;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical API error contract (contracts §4.4, decision O-002).
|
|
4
|
+
*
|
|
5
|
+
* Responses carry codes, never prose: user-facing strings are translated client-side from
|
|
6
|
+
* `t("error.<code>")` so the API stays locale-neutral.
|
|
7
|
+
*/
|
|
8
|
+
export const FACILIS_ERROR_CODES = [
|
|
9
|
+
"unauthorized",
|
|
10
|
+
"forbidden",
|
|
11
|
+
// 403: the password was correct but the account has not verified its email yet (D-156).
|
|
12
|
+
"email_unverified",
|
|
13
|
+
"invalid_input",
|
|
14
|
+
// 429: a caller exceeded a framework rate limit (D-143); the response carries `retry-after`.
|
|
15
|
+
"too_many_requests",
|
|
16
|
+
// 415: a body-carrying route was called with the wrong media type; the body is not read.
|
|
17
|
+
"unsupported_media_type",
|
|
18
|
+
"conflict",
|
|
19
|
+
"db_unavailable",
|
|
20
|
+
"not_found",
|
|
21
|
+
"storage_unavailable",
|
|
22
|
+
// 413: the request body exceeds the wrapper cap before parsing (SEC-09).
|
|
23
|
+
"payload_too_large",
|
|
24
|
+
"file_too_large",
|
|
25
|
+
// 500: the runtime guard refused a relation outside the route's dbTables (D-157).
|
|
26
|
+
"table_not_declared",
|
|
27
|
+
// 500: db.execute was used without rawSql: true on the route (D-157).
|
|
28
|
+
"raw_sql_not_allowed",
|
|
29
|
+
"internal_error",
|
|
30
|
+
];
|
|
31
|
+
/** HTTP status per code — the only place a status is chosen (contracts §4.4). */
|
|
32
|
+
export const FACILIS_ERROR_STATUS = {
|
|
33
|
+
unauthorized: 401,
|
|
34
|
+
forbidden: 403,
|
|
35
|
+
email_unverified: 403,
|
|
36
|
+
invalid_input: 400,
|
|
37
|
+
too_many_requests: 429,
|
|
38
|
+
unsupported_media_type: 415,
|
|
39
|
+
conflict: 409,
|
|
40
|
+
db_unavailable: 500,
|
|
41
|
+
not_found: 404,
|
|
42
|
+
storage_unavailable: 503,
|
|
43
|
+
// 413: the request body exceeds the wrapper cap before parsing (SEC-09).
|
|
44
|
+
payload_too_large: 413,
|
|
45
|
+
// 413: the upload is a valid form, but the file exceeds storage.maxUploadBytes.
|
|
46
|
+
file_too_large: 413,
|
|
47
|
+
// 500: a route reached an undeclared relation or raw SQL without the flag (D-157).
|
|
48
|
+
table_not_declared: 500,
|
|
49
|
+
raw_sql_not_allowed: 500,
|
|
50
|
+
internal_error: 500,
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Builds the web `Response` for a Facilis error. Handlers return these directly; `h3`
|
|
54
|
+
* treats a returned Response as the final response (contracts §4.4).
|
|
55
|
+
*/
|
|
56
|
+
export function fail(code, details) {
|
|
57
|
+
const body = details === undefined ? { code } : { code, details };
|
|
58
|
+
const headers = new Headers({ "content-type": "application/json; charset=utf-8" });
|
|
59
|
+
return new Response(JSON.stringify(body), { status: FACILIS_ERROR_STATUS[code], headers });
|
|
60
|
+
}
|
|
61
|
+
/** Flattens Zod issues into the `details` shape used by `invalid_input` responses. */
|
|
62
|
+
export function flattenZodIssues(error) {
|
|
63
|
+
return z.flattenError(error);
|
|
64
|
+
}
|
|
65
|
+
/** `400 invalid_input` with the flattened Zod issues as `details` (contracts §4.4). */
|
|
66
|
+
export function invalidInput(error) {
|
|
67
|
+
return fail("invalid_input", flattenZodIssues(error));
|
|
68
|
+
}
|