@better-forge/config-as-code 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 +10 -3
- package/dist/document.d.ts +68 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +189 -0
- package/dist/document.js.map +1 -0
- package/dist/engine.d.ts +124 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +163 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +24 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +56 -0
- package/dist/keys.js.map +1 -0
- package/dist/routes.d.ts +34 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +114 -0
- package/dist/routes.js.map +1 -0
- package/dist/yaml.d.ts +21 -0
- package/dist/yaml.d.ts.map +1 -0
- package/dist/yaml.js +67 -0
- package/dist/yaml.js.map +1 -0
- package/package.json +51 -6
- package/src/document.ts +199 -0
- package/src/engine.ts +258 -0
- package/src/index.ts +16 -0
- package/src/keys.ts +64 -0
- package/src/routes.ts +143 -0
- package/src/yaml.ts +64 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { backfillKeys, ENTITY_KEY_ERROR, ENTITY_KEY_PATTERN, isValidEntityKey, keyForNew, randomEntityKey, slugifyKey, type KeyOwner, type KeyResult, } from './keys.js';
|
|
2
|
+
export { MAX_DOCUMENTS, readDocuments, toYaml, toYamlStream } from './yaml.js';
|
|
3
|
+
export { byKey, canonical, clip, ConfigExportError, ConfigInvalidError, createVaultExporter, createVaultResolver, documentKey, findCycle, forbiddenNameAt, isObject, keyList, nestedDeeperThan, Problems, unknownFields, type ConfigProblem, type ConfigVaultRef, type Json, type VaultRows, } from './document.js';
|
|
4
|
+
export { createConfigEngine, type ApplyContext, type AuditEntry, type ChangeAction, type ConfigChange, type ConfigEngine, type ConfigEngineOptions, type ConfigKind, type ExportContext, type ImportOptions, type ImportResult, type PlanContext, } from './engine.js';
|
|
5
|
+
export { createConfigRoutes, type ConfigCaller, type ConfigRoutesOptions, type KindAccess } from './routes.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,SAAS,EAAE,eAAe,EAAE,UAAU,EAC5G,KAAK,QAAQ,EAAE,KAAK,SAAS,GAC9B,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAC9E,OAAO,EACL,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,WAAW,EAAE,SAAS,EAAE,eAAe,EAChJ,QAAQ,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,aAAa,EAC5D,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,KAAK,IAAI,EAAE,KAAK,SAAS,GACnE,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,kBAAkB,EAClB,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,KAAK,UAAU,EACtI,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,GAC5E,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,kBAAkB,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { backfillKeys, ENTITY_KEY_ERROR, ENTITY_KEY_PATTERN, isValidEntityKey, keyForNew, randomEntityKey, slugifyKey, } from './keys.js';
|
|
2
|
+
export { MAX_DOCUMENTS, readDocuments, toYaml, toYamlStream } from './yaml.js';
|
|
3
|
+
export { byKey, canonical, clip, ConfigExportError, ConfigInvalidError, createVaultExporter, createVaultResolver, documentKey, findCycle, forbiddenNameAt, isObject, keyList, nestedDeeperThan, Problems, unknownFields, } from './document.js';
|
|
4
|
+
export { createConfigEngine, } from './engine.js';
|
|
5
|
+
export { createConfigRoutes } from './routes.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,SAAS,EAAE,eAAe,EAAE,UAAU,GAE7G,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAC9E,OAAO,EACL,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,WAAW,EAAE,SAAS,EAAE,eAAe,EAChJ,QAAQ,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,aAAa,GAE7D,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,kBAAkB,GAGnB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,kBAAkB,EAAgE,MAAM,aAAa,CAAA"}
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
/** Keys appear in config files and URLs: lowercase, digits, `-` and `_`, starting with a letter or digit. */
|
|
3
|
+
export declare const ENTITY_KEY_PATTERN: RegExp;
|
|
4
|
+
export declare const ENTITY_KEY_ERROR = "Key must be 1-64 characters: lowercase letters, digits, \"-\" or \"_\", starting with a letter or digit";
|
|
5
|
+
/** Fallback for rows inserted without a key (the routes derive one from the name instead). */
|
|
6
|
+
export declare function randomEntityKey(): string;
|
|
7
|
+
export declare function isValidEntityKey(value: unknown): value is string;
|
|
8
|
+
export declare function slugifyKey(name: string): string;
|
|
9
|
+
/** Resolves the id of the row holding `key`, if any. */
|
|
10
|
+
export type KeyOwner = (key: string) => Promise<number | undefined>;
|
|
11
|
+
export type KeyResult = {
|
|
12
|
+
key: string;
|
|
13
|
+
} | {
|
|
14
|
+
status: 400 | 409;
|
|
15
|
+
error: string;
|
|
16
|
+
};
|
|
17
|
+
/** Key for a new row: the requested one when given (validated, must be free), else the first free slug of `name`. */
|
|
18
|
+
export declare function keyForNew(name: string, requested: unknown, ownerOf: KeyOwner): Promise<KeyResult>;
|
|
19
|
+
/**
|
|
20
|
+
* Gives every row of `tables` that has no key yet one made from its name, in id order, the same way `keyForNew` does
|
|
21
|
+
* for a new row (`name`, then `name-2`, `name-3`, ...). The tables need `id`, `name` and `key` columns.
|
|
22
|
+
*/
|
|
23
|
+
export declare function backfillKeys(sqlite: DatabaseSync, tables: readonly string[]): void;
|
|
24
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAG/C,6GAA6G;AAC7G,eAAO,MAAM,kBAAkB,QAA+B,CAAA;AAC9D,eAAO,MAAM,gBAAgB,4GAAwG,CAAA;AAErI,8FAA8F;AAC9F,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEhE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW/C;AAED,wDAAwD;AACxD,MAAM,MAAM,QAAQ,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;AAEnE,MAAM,MAAM,SAAS,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9E,qHAAqH;AACrH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC,CASvG;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAalF"}
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
/** Keys appear in config files and URLs: lowercase, digits, `-` and `_`, starting with a letter or digit. */
|
|
3
|
+
export const ENTITY_KEY_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
4
|
+
export const ENTITY_KEY_ERROR = 'Key must be 1-64 characters: lowercase letters, digits, "-" or "_", starting with a letter or digit';
|
|
5
|
+
/** Fallback for rows inserted without a key (the routes derive one from the name instead). */
|
|
6
|
+
export function randomEntityKey() {
|
|
7
|
+
return `k-${randomBytes(5).toString('hex')}`;
|
|
8
|
+
}
|
|
9
|
+
export function isValidEntityKey(value) {
|
|
10
|
+
return typeof value === 'string' && ENTITY_KEY_PATTERN.test(value);
|
|
11
|
+
}
|
|
12
|
+
export function slugifyKey(name) {
|
|
13
|
+
const slug = name
|
|
14
|
+
.normalize('NFKD')
|
|
15
|
+
.replace(/[̀-ͯ]/g, '')
|
|
16
|
+
.replace(/[łŁ]/g, 'l') // does not decompose into l + diacritic
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
19
|
+
.replace(/^-+|-+$/g, '')
|
|
20
|
+
.slice(0, 48)
|
|
21
|
+
.replace(/-+$/, '');
|
|
22
|
+
return slug || 'item';
|
|
23
|
+
}
|
|
24
|
+
/** Key for a new row: the requested one when given (validated, must be free), else the first free slug of `name`. */
|
|
25
|
+
export async function keyForNew(name, requested, ownerOf) {
|
|
26
|
+
if (requested !== undefined) {
|
|
27
|
+
if (!isValidEntityKey(requested))
|
|
28
|
+
return { status: 400, error: ENTITY_KEY_ERROR };
|
|
29
|
+
return (await ownerOf(requested)) === undefined ? { key: requested } : { status: 409, error: `Key "${requested}" is already in use` };
|
|
30
|
+
}
|
|
31
|
+
const base = slugifyKey(name);
|
|
32
|
+
let candidate = base;
|
|
33
|
+
for (let n = 2; (await ownerOf(candidate)) !== undefined; n++)
|
|
34
|
+
candidate = `${base}-${n}`;
|
|
35
|
+
return { key: candidate };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Gives every row of `tables` that has no key yet one made from its name, in id order, the same way `keyForNew` does
|
|
39
|
+
* for a new row (`name`, then `name-2`, `name-3`, ...). The tables need `id`, `name` and `key` columns.
|
|
40
|
+
*/
|
|
41
|
+
export function backfillKeys(sqlite, tables) {
|
|
42
|
+
for (const table of tables) {
|
|
43
|
+
const rows = sqlite.prepare(`SELECT id, name FROM ${table} WHERE key = '' ORDER BY id`).all();
|
|
44
|
+
const taken = new Set(sqlite.prepare(`SELECT key FROM ${table} WHERE key <> ''`).all().map((row) => row.key));
|
|
45
|
+
const assign = sqlite.prepare(`UPDATE ${table} SET key = ? WHERE id = ?`);
|
|
46
|
+
for (const { id, name } of rows) {
|
|
47
|
+
const base = slugifyKey(name);
|
|
48
|
+
let key = base;
|
|
49
|
+
for (let n = 2; taken.has(key); n++)
|
|
50
|
+
key = `${base}-${n}`;
|
|
51
|
+
taken.add(key);
|
|
52
|
+
assign.run(key, id);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=keys.js.map
|
package/dist/keys.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEzC,6GAA6G;AAC7G,MAAM,CAAC,MAAM,kBAAkB,GAAG,4BAA4B,CAAA;AAC9D,MAAM,CAAC,MAAM,gBAAgB,GAAG,qGAAqG,CAAA;AAErI,8FAA8F;AAC9F,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAA;AAC9C,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AACpE,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,IAAI,GAAG,IAAI;SACd,SAAS,CAAC,MAAM,CAAC;SACjB,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,wCAAwC;SAC9D,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;SACZ,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;IACrB,OAAO,IAAI,IAAI,MAAM,CAAA;AACvB,CAAC;AAOD,qHAAqH;AACrH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY,EAAE,SAAkB,EAAE,OAAiB;IACjF,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC;YAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAA;QACjF,OAAO,CAAC,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,SAAS,qBAAqB,EAAE,CAAA;IACvI,CAAC;IACD,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;IAC7B,IAAI,SAAS,GAAG,IAAI,CAAA;IACpB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC,EAAE;QAAE,SAAS,GAAG,GAAG,IAAI,IAAI,CAAC,EAAE,CAAA;IACzF,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,CAAA;AAC3B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAoB,EAAE,MAAyB;IAC1E,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,wBAAwB,KAAK,6BAA6B,CAAC,CAAC,GAAG,EAAyC,CAAA;QACpI,MAAM,KAAK,GAAG,IAAI,GAAG,CAAE,MAAM,CAAC,OAAO,CAAC,mBAAmB,KAAK,kBAAkB,CAAC,CAAC,GAAG,EAA6B,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAA;QACzI,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,KAAK,2BAA2B,CAAC,CAAA;QACzE,KAAK,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC;YAChC,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;YAC7B,IAAI,GAAG,GAAG,IAAI,CAAA;YACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;gBAAE,GAAG,GAAG,GAAG,IAAI,IAAI,CAAC,EAAE,CAAA;YACzD,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YACd,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;QACrB,CAAC;IACH,CAAC;AACH,CAAC"}
|
package/dist/routes.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest, onRequestAsyncHookHandler } from 'fastify';
|
|
2
|
+
import type { ConfigEngine } from './engine.js';
|
|
3
|
+
/** Who may export and import one kind of document. */
|
|
4
|
+
export interface KindAccess {
|
|
5
|
+
/** The API token permission prefix: `<resource>:read` to export, `<resource>:write` to import. */
|
|
6
|
+
tokenResource: string;
|
|
7
|
+
/** The roles of a signed-in user who may export and import it. */
|
|
8
|
+
roles: readonly string[];
|
|
9
|
+
/** The role named in the message when a user may not. */
|
|
10
|
+
roleLabel: string;
|
|
11
|
+
/** Left out of a full export for a reader who may not see it, instead of refusing the whole export. */
|
|
12
|
+
optional?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** The caller of a request, as far as the routes need to know. */
|
|
15
|
+
export interface ConfigCaller {
|
|
16
|
+
role: string;
|
|
17
|
+
apiToken?: {
|
|
18
|
+
scopes: readonly string[];
|
|
19
|
+
} | undefined;
|
|
20
|
+
}
|
|
21
|
+
export interface ConfigRoutesOptions<Actor> {
|
|
22
|
+
engine: ConfigEngine<any, any, Actor>;
|
|
23
|
+
access: Readonly<Record<string, KindAccess>>;
|
|
24
|
+
identity: (req: FastifyRequest) => ConfigCaller;
|
|
25
|
+
/** What the caller is recorded as in the audit log. */
|
|
26
|
+
actor: (req: FastifyRequest) => Actor;
|
|
27
|
+
/** Hooks that run before a file is read (the application's own role check). */
|
|
28
|
+
guards: readonly onRequestAsyncHookHandler[];
|
|
29
|
+
/** Kinds a file needs permission for beyond the kinds of its documents (for example the kind a document links to). */
|
|
30
|
+
linkedKinds?: (documents: unknown[]) => readonly string[];
|
|
31
|
+
}
|
|
32
|
+
/** The routes to export the configuration as YAML and to validate or apply a YAML file. A plugin to register with `app.register`. */
|
|
33
|
+
export declare function createConfigRoutes<Actor>(options: ConfigRoutesOptions<Actor>): (app: FastifyInstance) => Promise<void>;
|
|
34
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,cAAc,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAA;AAGvG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAO/C,sDAAsD;AACtD,MAAM,WAAW,UAAU;IACzB,kGAAkG;IAClG,aAAa,EAAE,MAAM,CAAA;IACrB,kEAAkE;IAClE,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACxB,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAA;IACjB,uGAAuG;IACvG,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE;QAAE,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,GAAG,SAAS,CAAA;CACrD;AAED,MAAM,WAAW,mBAAmB,CAAC,KAAK;IAExC,MAAM,EAAE,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;IACrC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAA;IAC5C,QAAQ,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,YAAY,CAAA;IAC/C,uDAAuD;IACvD,KAAK,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,KAAK,CAAA;IACrC,+EAA+E;IAC/E,MAAM,EAAE,SAAS,yBAAyB,EAAE,CAAA;IAC5C,sHAAsH;IACtH,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,SAAS,MAAM,EAAE,CAAA;CAC1D;AAED,qIAAqI;AACrI,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,mBAAmB,CAAC,KAAK,CAAC,IA6BxC,KAAK,eAAe,mBAuExD"}
|
package/dist/routes.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { tokenAllows } from '@better-forge/contracts';
|
|
2
|
+
import { clip, ConfigExportError, ConfigInvalidError } from './document.js';
|
|
3
|
+
import { readDocuments, toYaml, toYamlStream } from './yaml.js';
|
|
4
|
+
/** A configuration is a few hundred kilobytes at most; this leaves room without letting a request tie up the parser. */
|
|
5
|
+
const BODY_LIMIT = 2 * 1024 * 1024;
|
|
6
|
+
const YAML_TYPES = ['application/yaml', 'application/x-yaml', 'text/yaml'];
|
|
7
|
+
/** The routes to export the configuration as YAML and to validate or apply a YAML file. A plugin to register with `app.register`. */
|
|
8
|
+
export function createConfigRoutes(options) {
|
|
9
|
+
const { engine, access, identity } = options;
|
|
10
|
+
const kindNames = engine.kinds;
|
|
11
|
+
const tokenResources = kindNames.map((kind) => access[kind].tokenResource);
|
|
12
|
+
/** The kinds `caller` may not read (or write), with the reason, or null when it may do all of them. */
|
|
13
|
+
function refusal(caller, kinds, mode) {
|
|
14
|
+
const refused = [];
|
|
15
|
+
for (const kind of kinds) {
|
|
16
|
+
const rule = access[kind];
|
|
17
|
+
if (caller.apiToken) {
|
|
18
|
+
const needed = `${rule.tokenResource}:${mode}`;
|
|
19
|
+
if (!tokenAllows(caller.apiToken.scopes, needed))
|
|
20
|
+
refused.push(`${kind} (the token needs "${needed}")`);
|
|
21
|
+
}
|
|
22
|
+
else if (!rule.roles.includes(caller.role)) {
|
|
23
|
+
refused.push(`${kind} (needs the ${rule.roleLabel} role)`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return refused.length ? `You may not ${mode === 'read' ? 'export' : 'import'}: ${refused.join(', ')}` : null;
|
|
27
|
+
}
|
|
28
|
+
/** The kinds a file needs permission for: its own, and those its documents link to. */
|
|
29
|
+
function kindsNeeded(documents) {
|
|
30
|
+
const kinds = engine.kindsIn(documents);
|
|
31
|
+
for (const linked of options.linkedKinds?.(documents) ?? [])
|
|
32
|
+
kinds.add(linked);
|
|
33
|
+
return kinds;
|
|
34
|
+
}
|
|
35
|
+
const optionalKinds = kindNames.filter((kind) => access[kind].optional);
|
|
36
|
+
return async function configRoutes(app) {
|
|
37
|
+
// A YAML body is handed to the handler as text and parsed there, after the request has been authenticated.
|
|
38
|
+
app.addContentTypeParser(YAML_TYPES, { parseAs: 'string', bodyLimit: BODY_LIMIT }, (_req, body, done) => done(null, body));
|
|
39
|
+
// One document (`?kind=Note&key=first`), or all of them, as YAML.
|
|
40
|
+
app.get('/export', async (req, reply) => {
|
|
41
|
+
const { kind, key } = req.query;
|
|
42
|
+
if (kind !== undefined && !kindNames.includes(kind)) {
|
|
43
|
+
return reply.code(400).send({ error: `kind must be one of: ${kindNames.join(', ')}` });
|
|
44
|
+
}
|
|
45
|
+
if (kind !== undefined && !key)
|
|
46
|
+
return reply.code(400).send({ error: `key is required to export a ${kind}` });
|
|
47
|
+
const caller = identity(req);
|
|
48
|
+
const denied = refusal(caller, kind ? [kind] : kindNames.filter((each) => !optionalKinds.includes(each)), 'read');
|
|
49
|
+
if (denied)
|
|
50
|
+
return reply.code(403).send({ error: denied });
|
|
51
|
+
// Without a key a full export holds the optional kinds only for who may read them.
|
|
52
|
+
const included = new Set(kindNames.filter((each) => !optionalKinds.includes(each) || refusal(caller, [each], 'read') === null));
|
|
53
|
+
let documents;
|
|
54
|
+
try {
|
|
55
|
+
documents = await engine.exportDocuments(kind ? { kind, key: key } : undefined, { included });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error instanceof ConfigExportError)
|
|
59
|
+
return reply.code(409).send({ error: error.message });
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
if (kind) {
|
|
63
|
+
const found = engine.pickDocument(documents, kind, key);
|
|
64
|
+
if (!found)
|
|
65
|
+
return reply.code(404).send({ error: `There is no ${kind} with the key "${clip(String(key))}"` });
|
|
66
|
+
return reply.type('application/yaml; charset=utf-8').send(toYaml(found));
|
|
67
|
+
}
|
|
68
|
+
return reply.type('application/yaml; charset=utf-8').send(toYamlStream(documents));
|
|
69
|
+
});
|
|
70
|
+
async function run(req, reply, dryRun) {
|
|
71
|
+
const { prune } = req.query;
|
|
72
|
+
if (prune !== undefined && prune !== 'true' && prune !== 'false')
|
|
73
|
+
return reply.code(400).send({ error: 'prune must be true or false' });
|
|
74
|
+
const read = readDocuments(req.body);
|
|
75
|
+
if ('error' in read)
|
|
76
|
+
return reply.code(400).send({ error: read.error });
|
|
77
|
+
if (read.documents.length === 0)
|
|
78
|
+
return reply.code(400).send({ error: 'The file contains no documents' });
|
|
79
|
+
const caller = identity(req);
|
|
80
|
+
const denied = refusal(caller, kindsNeeded(read.documents), 'write');
|
|
81
|
+
if (denied)
|
|
82
|
+
return reply.code(403).send({ error: denied });
|
|
83
|
+
try {
|
|
84
|
+
return await engine.importDocuments(read.documents, { dryRun, actor: options.actor(req), token: caller.apiToken, prune: prune === 'true' });
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof ConfigInvalidError)
|
|
88
|
+
return reply.code(400).send({ error: error.message, problems: error.problems });
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Both check the caller before the body is read: parsing a 2 MiB file is work only someone who may import it may ask for.
|
|
93
|
+
// What the caller may import is decided per kind of document once the file is read.
|
|
94
|
+
const mayWriteSomething = async (req, reply) => {
|
|
95
|
+
const token = identity(req).apiToken;
|
|
96
|
+
if (token && !tokenResources.some((resource) => tokenAllows(token.scopes, `${resource}:write`))) {
|
|
97
|
+
const needs = tokenResources.map((resource) => `"${resource}:write"`);
|
|
98
|
+
return reply.code(403).send({ error: `This token has no permission to import: it needs ${needs.slice(0, -1).join(', ')} or ${needs.at(-1)}` });
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
// Only YAML is read, and this is checked before the body is, so a JSON body is never parsed.
|
|
102
|
+
const onlyYaml = async (req, reply) => {
|
|
103
|
+
const type = (req.headers['content-type'] ?? '').split(';')[0].trim().toLowerCase();
|
|
104
|
+
if (!YAML_TYPES.includes(type))
|
|
105
|
+
return reply.code(415).send({ error: 'Send the documents as YAML, with Content-Type: application/yaml' });
|
|
106
|
+
};
|
|
107
|
+
const routeOptions = { onRequest: [...options.guards, mayWriteSomething, onlyYaml], bodyLimit: BODY_LIMIT };
|
|
108
|
+
// What importing the documents would change, without changing anything.
|
|
109
|
+
app.post('/validate', routeOptions, (req, reply) => run(req, reply, true));
|
|
110
|
+
// Makes the installation match the documents, completely or not at all.
|
|
111
|
+
app.post('/apply', routeOptions, (req, reply) => run(req, reply, false));
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
|
package/dist/yaml.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Documents one file may hold. */
|
|
2
|
+
export declare const MAX_DOCUMENTS = 2000;
|
|
3
|
+
/**
|
|
4
|
+
* No line folding: URLs and templates must stay on one line to be readable and diff well. Version 1.1 makes the
|
|
5
|
+
* writer quote what an older parser would misread ("22:00" is a number there, "yes" and "on" are booleans), so the
|
|
6
|
+
* file means the same to every YAML tool, Terraform's included.
|
|
7
|
+
*/
|
|
8
|
+
export declare function toYaml(value: unknown): string;
|
|
9
|
+
/** Several documents in one YAML file, separated the usual way. */
|
|
10
|
+
export declare const toYamlStream: (documents: unknown[]) => string;
|
|
11
|
+
/**
|
|
12
|
+
* The documents of a YAML text, in the order they are written (empty ones left out), or the reason it was refused.
|
|
13
|
+
* The text is parsed with the YAML 1.2 core schema; a `%YAML` line would switch to rules under which `yes` is a
|
|
14
|
+
* boolean and `22:00` a number, so it is refused.
|
|
15
|
+
*/
|
|
16
|
+
export declare function readDocuments(body: unknown): {
|
|
17
|
+
documents: unknown[];
|
|
18
|
+
} | {
|
|
19
|
+
error: string;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=yaml.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"yaml.d.ts","sourceRoot":"","sources":["../src/yaml.ts"],"names":[],"mappings":"AAIA,mCAAmC;AACnC,eAAO,MAAM,aAAa,OAAQ,CAAA;AAUlC;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAY7C;AAED,mEAAmE;AACnE,eAAO,MAAM,YAAY,GAAI,WAAW,OAAO,EAAE,KAAG,MAA6C,CAAA;AAIjG;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG;IAAE,SAAS,EAAE,OAAO,EAAE,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAmBzF"}
|
package/dist/yaml.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Document, parseAllDocuments, visit } from 'yaml';
|
|
2
|
+
/** Anchors are legitimate in a hand-written file, but a few are plenty and unbounded aliases are a decompression bomb. */
|
|
3
|
+
const MAX_YAML_ALIASES = 20;
|
|
4
|
+
/** Documents one file may hold. */
|
|
5
|
+
export const MAX_DOCUMENTS = 2_000;
|
|
6
|
+
/** Longest part of a parser error that is repeated to the caller. */
|
|
7
|
+
const MAX_ERROR_LENGTH = 300;
|
|
8
|
+
/**
|
|
9
|
+
* Strings the library leaves bare although a parser reads them as something else: `<<` is the YAML 1.1 merge key (as a
|
|
10
|
+
* key it merges, or fails), and `0o17` is a number in YAML 1.2 but a string in 1.1.
|
|
11
|
+
*/
|
|
12
|
+
const NEEDS_QUOTES = /^(<<|=|0o[0-7]+)$/;
|
|
13
|
+
/**
|
|
14
|
+
* No line folding: URLs and templates must stay on one line to be readable and diff well. Version 1.1 makes the
|
|
15
|
+
* writer quote what an older parser would misread ("22:00" is a number there, "yes" and "on" are booleans), so the
|
|
16
|
+
* file means the same to every YAML tool, Terraform's included.
|
|
17
|
+
*/
|
|
18
|
+
export function toYaml(value) {
|
|
19
|
+
const document = new Document(value, { version: '1.1' });
|
|
20
|
+
visit(document, {
|
|
21
|
+
Scalar(position, node) {
|
|
22
|
+
if (typeof node.value !== 'string' || !NEEDS_QUOTES.test(node.value))
|
|
23
|
+
return;
|
|
24
|
+
node.type = 'QUOTE_DOUBLE';
|
|
25
|
+
// The 1.1 schema recognises the string `<<` as the merge key and would write it bare whatever the style; as a
|
|
26
|
+
// key it needs an explicit string tag. As a value it is plain text and quoting is enough.
|
|
27
|
+
if (position === 'key' && node.value === '<<')
|
|
28
|
+
node.tag = 'tag:yaml.org,2002:str';
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
return document.toString({ lineWidth: 0 });
|
|
32
|
+
}
|
|
33
|
+
/** Several documents in one YAML file, separated the usual way. */
|
|
34
|
+
export const toYamlStream = (documents) => documents.map(toYaml).join('---\n');
|
|
35
|
+
const clip = (value, max) => value.length > max ? `${value.slice(0, max)}…` : value;
|
|
36
|
+
/**
|
|
37
|
+
* The documents of a YAML text, in the order they are written (empty ones left out), or the reason it was refused.
|
|
38
|
+
* The text is parsed with the YAML 1.2 core schema; a `%YAML` line would switch to rules under which `yes` is a
|
|
39
|
+
* boolean and `22:00` a number, so it is refused.
|
|
40
|
+
*/
|
|
41
|
+
export function readDocuments(body) {
|
|
42
|
+
if (typeof body !== 'string')
|
|
43
|
+
return { error: 'Send the documents as YAML' };
|
|
44
|
+
if (/^%YAML/m.test(body))
|
|
45
|
+
return { error: 'Invalid YAML: %YAML directives are not supported' };
|
|
46
|
+
try {
|
|
47
|
+
const documents = [];
|
|
48
|
+
for (const document of parseAllDocuments(body)) {
|
|
49
|
+
if (document.errors.length)
|
|
50
|
+
throw document.errors[0];
|
|
51
|
+
const value = document.toJS({ maxAliasCount: MAX_YAML_ALIASES });
|
|
52
|
+
// An alias inside the thing it is an alias of (`a: &x { b: *x }`) parses into a circular object, which JSON
|
|
53
|
+
// cannot hold and nothing downstream could compare or store.
|
|
54
|
+
JSON.stringify(value);
|
|
55
|
+
if (value !== null && value !== undefined)
|
|
56
|
+
documents.push(value);
|
|
57
|
+
if (documents.length > MAX_DOCUMENTS)
|
|
58
|
+
return { error: `Too many documents: at most ${MAX_DOCUMENTS}` };
|
|
59
|
+
}
|
|
60
|
+
return { documents };
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
const circular = error instanceof TypeError && /circular/i.test(error.message);
|
|
64
|
+
return { error: circular ? 'Invalid YAML: an alias may not refer to a value that contains it' : clip(`Invalid YAML: ${error instanceof Error ? error.message : String(error)}`, MAX_ERROR_LENGTH) };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=yaml.js.map
|
package/dist/yaml.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"yaml.js","sourceRoot":"","sources":["../src/yaml.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,MAAM,CAAA;AAEzD,0HAA0H;AAC1H,MAAM,gBAAgB,GAAG,EAAE,CAAA;AAC3B,mCAAmC;AACnC,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,CAAA;AAClC,qEAAqE;AACrE,MAAM,gBAAgB,GAAG,GAAG,CAAA;AAE5B;;;GAGG;AACH,MAAM,YAAY,GAAG,mBAAmB,CAAA;AAExC;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAC,KAAc;IACnC,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAA;IACxD,KAAK,CAAC,QAAQ,EAAE;QACd,MAAM,CAAC,QAAQ,EAAE,IAAI;YACnB,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;gBAAE,OAAM;YAC5E,IAAI,CAAC,IAAI,GAAG,cAAc,CAAA;YAC1B,8GAA8G;YAC9G,0FAA0F;YAC1F,IAAI,QAAQ,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI;gBAAE,IAAI,CAAC,GAAG,GAAG,uBAAuB,CAAA;QACnF,CAAC;KACF,CAAC,CAAA;IACF,OAAO,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAA;AAC5C,CAAC;AAED,mEAAmE;AACnE,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,SAAoB,EAAU,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;AAEjG,MAAM,IAAI,GAAG,CAAC,KAAa,EAAE,GAAW,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAA;AAEnG;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAAa;IACzC,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,KAAK,EAAE,4BAA4B,EAAE,CAAA;IAC5E,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,kDAAkD,EAAE,CAAA;IAC9F,IAAI,CAAC;QACH,MAAM,SAAS,GAAc,EAAE,CAAA;QAC/B,KAAK,MAAM,QAAQ,IAAI,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC;YAC/C,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM;gBAAE,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;YACpD,MAAM,KAAK,GAAY,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,EAAE,gBAAgB,EAAE,CAAC,CAAA;YACzE,4GAA4G;YAC5G,6DAA6D;YAC7D,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;YACrB,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;gBAAE,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YAChE,IAAI,SAAS,CAAC,MAAM,GAAG,aAAa;gBAAE,OAAO,EAAE,KAAK,EAAE,+BAA+B,aAAa,EAAE,EAAE,CAAA;QACxG,CAAC;QACD,OAAO,EAAE,SAAS,EAAE,CAAA;IACtB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,QAAQ,GAAG,KAAK,YAAY,SAAS,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QAC9E,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,kEAAkE,CAAC,CAAC,CAAC,IAAI,CAAC,iBAAiB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,gBAAgB,CAAC,EAAE,CAAA;IACrM,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@better-forge/config-as-code",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@better-forge/config-as-code",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "YAML configuration engine with a registry of object kinds.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/BElluu/Better-Forge.git",
|
|
9
|
+
"directory": "packages/config-as-code"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/BElluu/Better-Forge/tree/main/packages/config-as-code#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/BElluu/Better-Forge/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"src",
|
|
31
|
+
"!src/**/*.test.*"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.json",
|
|
35
|
+
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"fastify": "^5.3.2"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"typescript": "^5.7.3",
|
|
42
|
+
"fastify": "^5.3.2"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"yaml": "^2.9.1",
|
|
46
|
+
"@better-forge/contracts": "0.1.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=26.10.0"
|
|
50
|
+
}
|
|
51
|
+
}
|