@remit/config-format 0.0.1
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/package.json +48 -0
- package/schema/reader-config.v1.schema.json +840 -0
- package/src/bin/check-schema.ts +21 -0
- package/src/bin/emit-schema.ts +12 -0
- package/src/document.test.ts +216 -0
- package/src/document.ts +262 -0
- package/src/envelope.ts +15 -0
- package/src/errors.ts +119 -0
- package/src/fixtures/reader-config.v1.json +169 -0
- package/src/fixtures.ts +13 -0
- package/src/golden.test.ts +48 -0
- package/src/index.test.ts +25 -0
- package/src/index.ts +9 -0
- package/src/json-schema.test.ts +66 -0
- package/src/json-schema.ts +24 -0
- package/src/migrations.test.ts +79 -0
- package/src/migrations.ts +45 -0
- package/src/read.test.ts +198 -0
- package/src/read.ts +109 -0
- package/src/schema-artifact.ts +19 -0
- package/src/version.ts +18 -0
- package/tsconfig.json +8 -0
package/src/read.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { $ZodIssue, $ZodIssueUnrecognizedKeys } from "zod/v4/core";
|
|
2
|
+
import {
|
|
3
|
+
type ReaderConfigDocument,
|
|
4
|
+
ReaderConfigDocumentSchema,
|
|
5
|
+
} from "./document.js";
|
|
6
|
+
import { ConfigEnvelopeSchema } from "./envelope.js";
|
|
7
|
+
import {
|
|
8
|
+
ConfigCredentialError,
|
|
9
|
+
ConfigKindError,
|
|
10
|
+
ConfigMalformedError,
|
|
11
|
+
ConfigNotAnObjectError,
|
|
12
|
+
ConfigUnknownKeysError,
|
|
13
|
+
ConfigVersionError,
|
|
14
|
+
formatPath,
|
|
15
|
+
} from "./errors.js";
|
|
16
|
+
import {
|
|
17
|
+
type ConfigDocumentShape,
|
|
18
|
+
type ConfigMigration,
|
|
19
|
+
liftToCurrentVersion,
|
|
20
|
+
} from "./migrations.js";
|
|
21
|
+
import { CONFIG_KIND, CURRENT_SCHEMA_VERSION } from "./version.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Substrings that make a key a credential. Matched on the lowercased key so a
|
|
25
|
+
* field arrives refused whatever the exporter chose to call it — `password`,
|
|
26
|
+
* `smtpPasswordHash`, `oauthRefreshToken` are all the same mistake.
|
|
27
|
+
*/
|
|
28
|
+
const CREDENTIAL_MARKERS = [
|
|
29
|
+
"password",
|
|
30
|
+
"passphrase",
|
|
31
|
+
"secret",
|
|
32
|
+
"token",
|
|
33
|
+
"apikey",
|
|
34
|
+
"privatekey",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const isCredentialKey = (key: string): boolean => {
|
|
38
|
+
const lowered = key.toLowerCase();
|
|
39
|
+
return CREDENTIAL_MARKERS.some((marker) => lowered.includes(marker));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const unrecognizedKeys = (
|
|
43
|
+
issues: readonly $ZodIssue[],
|
|
44
|
+
): $ZodIssueUnrecognizedKeys[] =>
|
|
45
|
+
issues.filter(
|
|
46
|
+
(issue): issue is $ZodIssueUnrecognizedKeys =>
|
|
47
|
+
issue.code === "unrecognized_keys",
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read a configuration document in two passes: the envelope decides what the
|
|
52
|
+
* file is and which version it claims, the migration chain lifts it to the
|
|
53
|
+
* version this reader knows, and only then does the strict schema see it.
|
|
54
|
+
* Doing it the other way round makes every older document look like a
|
|
55
|
+
* malformed current one.
|
|
56
|
+
*/
|
|
57
|
+
export function readConfigDocument(
|
|
58
|
+
input: unknown,
|
|
59
|
+
migrations?: readonly ConfigMigration[],
|
|
60
|
+
): ReaderConfigDocument {
|
|
61
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
62
|
+
throw new ConfigNotAnObjectError(
|
|
63
|
+
"Not a reader configuration document: expected a JSON object.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const envelope = ConfigEnvelopeSchema.safeParse(input);
|
|
68
|
+
if (!envelope.success) {
|
|
69
|
+
const kindIssue = envelope.error.issues.find(
|
|
70
|
+
(issue) => issue.path[0] === "kind",
|
|
71
|
+
);
|
|
72
|
+
if (kindIssue) {
|
|
73
|
+
throw new ConfigKindError(
|
|
74
|
+
CONFIG_KIND,
|
|
75
|
+
(input as ConfigDocumentShape).kind,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
throw new ConfigMalformedError(envelope.error.issues);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (envelope.data.schemaVersion > CURRENT_SCHEMA_VERSION) {
|
|
82
|
+
throw new ConfigVersionError(
|
|
83
|
+
envelope.data.schemaVersion,
|
|
84
|
+
CURRENT_SCHEMA_VERSION,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const lifted = liftToCurrentVersion(
|
|
89
|
+
input as ConfigDocumentShape,
|
|
90
|
+
envelope.data.schemaVersion,
|
|
91
|
+
migrations,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const parsed = ReaderConfigDocumentSchema.safeParse(lifted);
|
|
95
|
+
if (parsed.success) return parsed.data;
|
|
96
|
+
|
|
97
|
+
const unknown = unrecognizedKeys(parsed.error.issues);
|
|
98
|
+
for (const issue of unknown) {
|
|
99
|
+
const credentials = issue.keys.filter(isCredentialKey);
|
|
100
|
+
if (credentials.length > 0) {
|
|
101
|
+
throw new ConfigCredentialError(formatPath(issue.path), credentials);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const first = unknown[0];
|
|
105
|
+
if (first)
|
|
106
|
+
throw new ConfigUnknownKeysError(formatPath(first.path), first.keys);
|
|
107
|
+
|
|
108
|
+
throw new ConfigMalformedError(parsed.error.issues);
|
|
109
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { CONFIG_SCHEMA_ARTIFACT_PATH } from "./version.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The reviewable copy of the JSON Schema. Committed inside the package so a
|
|
6
|
+
* change to the document shape shows up as a schema diff in the same review,
|
|
7
|
+
* which is the only thing the check can compare against — the build artifact
|
|
8
|
+
* below is regenerated from empty on every CI run and would agree with itself.
|
|
9
|
+
*/
|
|
10
|
+
export const committedSchemaPath = (): string =>
|
|
11
|
+
fileURLToPath(
|
|
12
|
+
new URL("../schema/reader-config.v1.schema.json", import.meta.url),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
/** The generated artifact, at its path under the repository root. */
|
|
16
|
+
export const artifactSchemaPath = (): string =>
|
|
17
|
+
fileURLToPath(
|
|
18
|
+
new URL(`../../../${CONFIG_SCHEMA_ARTIFACT_PATH}`, import.meta.url),
|
|
19
|
+
);
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A reader configuration document declares its own kind and version in two
|
|
3
|
+
* fields that never move between versions. Everything else in the document is
|
|
4
|
+
* free to change shape; these two are the contract that lets a reader decide
|
|
5
|
+
* what it is looking at before it knows how to read it.
|
|
6
|
+
*/
|
|
7
|
+
export const CONFIG_KIND = "reader.config";
|
|
8
|
+
|
|
9
|
+
/** The document version this package writes and can read up to. */
|
|
10
|
+
export const CURRENT_SCHEMA_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
/** `$id` of the emitted JSON Schema artifact. */
|
|
13
|
+
export const CONFIG_SCHEMA_ID =
|
|
14
|
+
"https://schemas.remit.email/reader-config.v1.schema.json";
|
|
15
|
+
|
|
16
|
+
/** Path of the emitted JSON Schema artifact, relative to the repository root. */
|
|
17
|
+
export const CONFIG_SCHEMA_ARTIFACT_PATH =
|
|
18
|
+
"build/config-schema/reader-config.v1.schema.json";
|