@zhin.js/gateway 0.0.1-bootstrap.0 → 1.1.2
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 +61 -0
- package/lib/config/ai-config-loader.d.ts +22 -0
- package/lib/config/ai-config-loader.js +96 -0
- package/lib/config/configuration-chat-policy.d.ts +57 -0
- package/lib/config/configuration-chat-policy.js +423 -0
- package/lib/config/configuration-patch.d.ts +16 -0
- package/lib/config/configuration-patch.js +105 -0
- package/lib/config/configuration-path.d.ts +6 -0
- package/lib/config/configuration-path.js +40 -0
- package/lib/config/configuration-projection.d.ts +6 -0
- package/lib/config/configuration-projection.js +28 -0
- package/lib/config/configuration-redaction.d.ts +18 -0
- package/lib/config/configuration-redaction.js +75 -0
- package/lib/config/configuration.d.ts +81 -0
- package/lib/config/configuration.js +686 -0
- package/lib/config/console-schema.d.ts +11 -0
- package/lib/config/console-schema.js +223 -0
- package/lib/config/environment-files.d.ts +3 -0
- package/lib/config/environment-files.js +14 -0
- package/lib/config/literal-secrets.d.ts +34 -0
- package/lib/config/literal-secrets.js +136 -0
- package/lib/config/plugin-package-map.d.ts +2 -0
- package/lib/config/plugin-package-map.js +14 -0
- package/lib/config/plugin-schema-catalog.d.ts +22 -0
- package/lib/config/plugin-schema-catalog.js +262 -0
- package/lib/config.d.ts +13 -0
- package/lib/config.js +13 -0
- package/lib/database/config.d.ts +22 -0
- package/lib/database/config.js +159 -0
- package/lib/database/index.d.ts +2 -0
- package/lib/database/index.js +2 -0
- package/lib/database/project-database.d.ts +31 -0
- package/lib/database/project-database.js +130 -0
- package/lib/database.d.ts +2 -0
- package/lib/database.js +2 -0
- package/lib/http/console-rpc.d.ts +27 -0
- package/lib/http/console-rpc.js +51 -0
- package/lib/http/gateway-console.d.ts +63 -0
- package/lib/http/gateway-console.js +309 -0
- package/lib/http/gateway-events.d.ts +40 -0
- package/lib/http/gateway-events.js +185 -0
- package/lib/http/gateway-http-server.d.ts +95 -0
- package/lib/http/gateway-http-server.js +522 -0
- package/lib/http/local-principal.d.ts +2 -0
- package/lib/http/local-principal.js +16 -0
- package/lib/http/plugin-switch-rpc.d.ts +19 -0
- package/lib/http/plugin-switch-rpc.js +214 -0
- package/lib/http/run-gateway.d.ts +43 -0
- package/lib/http/run-gateway.js +268 -0
- package/lib/http/start-gateway.d.ts +114 -0
- package/lib/http/start-gateway.js +226 -0
- package/lib/http-response.d.ts +6 -0
- package/lib/http-response.js +25 -0
- package/lib/http.d.ts +6 -0
- package/lib/http.js +6 -0
- package/lib/iam/crypto.d.ts +12 -0
- package/lib/iam/crypto.js +58 -0
- package/lib/iam/index.d.ts +8 -0
- package/lib/iam/index.js +8 -0
- package/lib/iam/install.d.ts +36 -0
- package/lib/iam/install.js +74 -0
- package/lib/iam/local-admin.d.ts +27 -0
- package/lib/iam/local-admin.js +102 -0
- package/lib/iam/rate-limit.d.ts +15 -0
- package/lib/iam/rate-limit.js +52 -0
- package/lib/iam/routes.d.ts +23 -0
- package/lib/iam/routes.js +157 -0
- package/lib/iam/rpc.d.ts +14 -0
- package/lib/iam/rpc.js +100 -0
- package/lib/iam/service.d.ts +203 -0
- package/lib/iam/service.js +784 -0
- package/lib/iam/setup-service.d.ts +83 -0
- package/lib/iam/setup-service.js +206 -0
- package/lib/iam/tables.d.ts +20 -0
- package/lib/iam/tables.js +82 -0
- package/lib/iam.d.ts +2 -0
- package/lib/iam.js +2 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.js +22 -0
- package/lib/local/client.d.ts +71 -0
- package/lib/local/client.js +296 -0
- package/lib/local/handler.d.ts +30 -0
- package/lib/local/handler.js +86 -0
- package/lib/local/local-socket.d.ts +55 -0
- package/lib/local/local-socket.js +232 -0
- package/lib/local/paths.d.ts +34 -0
- package/lib/local/paths.js +58 -0
- package/lib/local/protocol.d.ts +140 -0
- package/lib/local/protocol.js +90 -0
- package/lib/local/state-file.d.ts +8 -0
- package/lib/local/state-file.js +45 -0
- package/lib/local.d.ts +10 -0
- package/lib/local.js +10 -0
- package/lib/onboarding/allowlist.d.ts +54 -0
- package/lib/onboarding/allowlist.js +106 -0
- package/lib/onboarding/errors.d.ts +7 -0
- package/lib/onboarding/errors.js +17 -0
- package/lib/onboarding/index.d.ts +6 -0
- package/lib/onboarding/index.js +6 -0
- package/lib/onboarding/install.d.ts +99 -0
- package/lib/onboarding/install.js +132 -0
- package/lib/onboarding/package-manager.d.ts +47 -0
- package/lib/onboarding/package-manager.js +179 -0
- package/lib/onboarding/service.d.ts +53 -0
- package/lib/onboarding/service.js +144 -0
- package/lib/onboarding/state-store.d.ts +26 -0
- package/lib/onboarding/state-store.js +97 -0
- package/lib/onboarding.d.ts +2 -0
- package/lib/onboarding.js +2 -0
- package/lib/packages/authorization.d.ts +24 -0
- package/lib/packages/authorization.js +128 -0
- package/lib/packages/errors.d.ts +11 -0
- package/lib/packages/errors.js +26 -0
- package/lib/packages/index.d.ts +5 -0
- package/lib/packages/index.js +5 -0
- package/lib/packages/rpc.d.ts +30 -0
- package/lib/packages/rpc.js +160 -0
- package/lib/packages/service.d.ts +139 -0
- package/lib/packages/service.js +504 -0
- package/lib/packages/snapshot.d.ts +14 -0
- package/lib/packages/snapshot.js +73 -0
- package/lib/packages.d.ts +2 -0
- package/lib/packages.js +2 -0
- package/lib/plugin-management/activity.d.ts +6 -0
- package/lib/plugin-management/activity.js +14 -0
- package/lib/plugin-management/config-explanation.d.ts +6 -0
- package/lib/plugin-management/config-explanation.js +113 -0
- package/lib/plugin-management/use-cases.d.ts +15 -0
- package/lib/plugin-management/use-cases.js +120 -0
- package/lib/plugin-management/workflow.d.ts +136 -0
- package/lib/plugin-management/workflow.js +878 -0
- package/lib/plugin-management.d.ts +5 -0
- package/lib/plugin-management.js +5 -0
- package/lib/project/config-file.d.ts +6 -0
- package/lib/project/config-file.js +58 -0
- package/lib/project/environment.d.ts +13 -0
- package/lib/project/environment.js +52 -0
- package/lib/project/instance-declarations.d.ts +55 -0
- package/lib/project/instance-declarations.js +542 -0
- package/lib/project/legacy-config.d.ts +25 -0
- package/lib/project/legacy-config.js +70 -0
- package/lib/project/node-requirements.d.ts +7 -0
- package/lib/project/node-requirements.js +20 -0
- package/lib/project/tsx-module-loader.d.ts +7 -0
- package/lib/project/tsx-module-loader.js +29 -0
- package/lib/project.d.ts +7 -0
- package/lib/project.js +7 -0
- package/lib/server.d.ts +11 -0
- package/lib/server.js +11 -0
- package/lib/supervisor/instance-logs.d.ts +39 -0
- package/lib/supervisor/instance-logs.js +152 -0
- package/lib/supervisor/instance-supervisor.d.ts +140 -0
- package/lib/supervisor/instance-supervisor.js +408 -0
- package/lib/supervisor/run-instance.d.ts +35 -0
- package/lib/supervisor/run-instance.js +161 -0
- package/lib/supervisor/terminal-state.d.ts +30 -0
- package/lib/supervisor/terminal-state.js +57 -0
- package/lib/supervisor.d.ts +4 -0
- package/lib/supervisor.js +4 -0
- package/lib/terminal/backends.d.ts +38 -0
- package/lib/terminal/backends.js +193 -0
- package/lib/terminal/config.d.ts +63 -0
- package/lib/terminal/config.js +237 -0
- package/lib/terminal/host.d.ts +26 -0
- package/lib/terminal/host.js +76 -0
- package/lib/terminal/index.d.ts +60 -0
- package/lib/terminal/index.js +135 -0
- package/lib/terminal/modules.d.ts +87 -0
- package/lib/terminal/modules.js +44 -0
- package/lib/terminal/service.d.ts +101 -0
- package/lib/terminal/service.js +455 -0
- package/lib/terminal/tickets.d.ts +46 -0
- package/lib/terminal/tickets.js +76 -0
- package/lib/terminal.d.ts +3 -0
- package/lib/terminal.js +3 -0
- package/lib/usage/aggregate.d.ts +87 -0
- package/lib/usage/aggregate.js +212 -0
- package/lib/usage.d.ts +2 -0
- package/lib/usage.js +2 -0
- package/package.json +117 -4
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type Schema = Record<string, unknown>;
|
|
2
|
+
/** The one JSON Schema union that existing string Select fields can represent exactly. */
|
|
3
|
+
export declare function readStringLiteralOptions(input: Record<string, unknown>): readonly {
|
|
4
|
+
label: string;
|
|
5
|
+
value: string;
|
|
6
|
+
'x-i18n'?: Record<string, unknown>;
|
|
7
|
+
}[] | null;
|
|
8
|
+
/** Display projection only. Runtime retains the original JSON Schema validator. */
|
|
9
|
+
export declare function jsonSchemaToConsoleSchema(input: unknown, key?: string): Schema | null;
|
|
10
|
+
export declare function isConsoleSchemaJson(schema: Schema): boolean;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { isSecretConfigSchema } from '@zhin.js/runtime';
|
|
2
|
+
const record = (value) => Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
3
|
+
const own = (value, key) => Object.hasOwn(value, key);
|
|
4
|
+
const jsonTypes = new Set(['object', 'array', 'string', 'number', 'integer', 'boolean', 'null']);
|
|
5
|
+
const unsupportedKeywords = ['not', 'if', 'then', 'else', 'dependentSchemas', '$dynamicRef', 'patternProperties', 'contains'];
|
|
6
|
+
/** The one JSON Schema union that existing string Select fields can represent exactly. */
|
|
7
|
+
export function readStringLiteralOptions(input) {
|
|
8
|
+
if (!Array.isArray(input.anyOf) || input.anyOf.length === 0 || input.anyOf.length > 64)
|
|
9
|
+
return null;
|
|
10
|
+
const annotations = ['title', 'description', 'x-descriptionZh', 'x-i18n', '$comment'];
|
|
11
|
+
const parentKeys = ['anyOf', 'type', 'default', 'writeOnly', 'readOnly', 'deprecated', 'x-zhin-config', 'x-zhin-secret', ...annotations];
|
|
12
|
+
if (Object.keys(input).some(key => !parentKeys.includes(key))
|
|
13
|
+
|| (input.type !== undefined && input.type !== 'string'))
|
|
14
|
+
return null;
|
|
15
|
+
const values = new Set();
|
|
16
|
+
const options = [];
|
|
17
|
+
for (const branch of input.anyOf) {
|
|
18
|
+
if (!branch || typeof branch !== 'object' || Array.isArray(branch))
|
|
19
|
+
return null;
|
|
20
|
+
const item = branch;
|
|
21
|
+
if (Object.keys(item).some(key => !['const', 'type', 'default', ...annotations].includes(key))
|
|
22
|
+
|| !Object.prototype.hasOwnProperty.call(item, 'const')
|
|
23
|
+
|| typeof item.const !== 'string' || item.const.length === 0 || item.const.length > 256
|
|
24
|
+
|| values.has(item.const) || (item.type !== undefined && item.type !== 'string')
|
|
25
|
+
|| (Object.prototype.hasOwnProperty.call(item, 'default') && item.default !== item.const))
|
|
26
|
+
return null;
|
|
27
|
+
values.add(item.const);
|
|
28
|
+
const label = [item['x-descriptionZh'], item.title, item.description].find(value => typeof value === 'string');
|
|
29
|
+
const translations = item['x-i18n'];
|
|
30
|
+
options.push({ label: typeof label === 'string' ? label : item.const, value: item.const,
|
|
31
|
+
...(translations && typeof translations === 'object' && !Array.isArray(translations)
|
|
32
|
+
? { 'x-i18n': structuredClone(translations) } : {}) });
|
|
33
|
+
}
|
|
34
|
+
if (Object.prototype.hasOwnProperty.call(input, 'default')
|
|
35
|
+
&& (typeof input.default !== 'string' || !values.has(input.default)))
|
|
36
|
+
return null;
|
|
37
|
+
const metadata = input['x-zhin-config'];
|
|
38
|
+
if (metadata && typeof metadata === 'object' && 'options' in metadata) {
|
|
39
|
+
const custom = metadata.options;
|
|
40
|
+
if (!Array.isArray(custom) || custom.length !== options.length
|
|
41
|
+
|| new Set(custom.map(item => item?.value)).size !== options.length
|
|
42
|
+
|| custom.some(item => !item || typeof item !== 'object'
|
|
43
|
+
|| typeof item.label !== 'string' || !values.has(item.value)))
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return options;
|
|
47
|
+
}
|
|
48
|
+
/** Display projection only. Runtime retains the original JSON Schema validator. */
|
|
49
|
+
export function jsonSchemaToConsoleSchema(input, key) {
|
|
50
|
+
if (input === undefined || input === null)
|
|
51
|
+
return null;
|
|
52
|
+
return convert(input, key, { root: input, references: new Set(), depth: 0, budget: { remaining: 8192 } });
|
|
53
|
+
}
|
|
54
|
+
function convert(input, key, context) {
|
|
55
|
+
const fallback = (reason) => ({ type: 'any', ...(key === undefined ? {} : { key }), ...(reason ? { reason } : {}) });
|
|
56
|
+
if (--context.budget.remaining < 0 || context.depth > 64)
|
|
57
|
+
return fallback('projection-limit');
|
|
58
|
+
if (typeof input === 'boolean')
|
|
59
|
+
return input ? fallback() : fallback('never-schema');
|
|
60
|
+
if (!record(input))
|
|
61
|
+
return fallback('malformed-schema');
|
|
62
|
+
const schema = input;
|
|
63
|
+
const properties = new Set([...context.properties ?? [], ...Object.keys(record(schema.properties) ? schema.properties : {})]);
|
|
64
|
+
const child = (value, name, references = context.references) => convert(value, name, { ...context, depth: context.depth + 1, references, properties: undefined });
|
|
65
|
+
const sameValue = (value, references = context.references) => convert(value, undefined, { ...context, depth: context.depth + 1, references, properties });
|
|
66
|
+
const finish = (value) => annotate(value, schema, key);
|
|
67
|
+
if (typeof schema.$ref === 'string') {
|
|
68
|
+
const reference = schema.$ref;
|
|
69
|
+
if (reference !== '#' && !reference.startsWith('#/'))
|
|
70
|
+
return finish(fallback('external-reference'));
|
|
71
|
+
if (context.references.has(reference))
|
|
72
|
+
return finish(fallback('cyclic-reference'));
|
|
73
|
+
let target = context.root;
|
|
74
|
+
try {
|
|
75
|
+
for (const raw of reference === '#' ? [] : reference.slice(2).split('/')) {
|
|
76
|
+
const part = decodeURIComponent(raw).replaceAll('~1', '/').replaceAll('~0', '~');
|
|
77
|
+
if (!record(target) || !own(target, part))
|
|
78
|
+
return finish(fallback('unresolved-reference'));
|
|
79
|
+
target = target[part];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return finish(fallback('unresolved-reference'));
|
|
84
|
+
}
|
|
85
|
+
const references = new Set(context.references);
|
|
86
|
+
references.add(reference);
|
|
87
|
+
const resolved = sameValue(target, references);
|
|
88
|
+
const { $ref: _reference, ...siblings } = schema;
|
|
89
|
+
const validations = Object.keys(siblings).filter(name => !annotationKeys.has(name) && !['$defs', 'definitions', '$id', '$schema', '$comment'].includes(name));
|
|
90
|
+
return finish(validations.length ? { type: 'intersect', list: [resolved, sameValue(siblings)] } : resolved);
|
|
91
|
+
}
|
|
92
|
+
// Unsupported semantics stay editable as JSON at this field; siblings never disappear.
|
|
93
|
+
if (unsupportedKeywords.some(name => own(schema, name))) {
|
|
94
|
+
if (properties.size && validationOnly(schema, properties)) {
|
|
95
|
+
const constraint = { type: 'constraint', reason: 'host-validation' };
|
|
96
|
+
const { if: _if, then: _then, else: _else, not: _not, ...base } = schema;
|
|
97
|
+
return finish(schema.properties ? { type: 'intersect', list: [constraint, sameValue(base)] } : constraint);
|
|
98
|
+
}
|
|
99
|
+
return finish(fallback('unsupported-construct'));
|
|
100
|
+
}
|
|
101
|
+
const literalOptions = readStringLiteralOptions(schema);
|
|
102
|
+
if (literalOptions)
|
|
103
|
+
return finish({ type: 'string', options: literalOptions });
|
|
104
|
+
if (isConsoleSchemaJson(schema)) {
|
|
105
|
+
const result = { ...schema };
|
|
106
|
+
if (record(schema.object))
|
|
107
|
+
result.object = Object.fromEntries(Object.entries(schema.object).map(([name, value]) => [name, child(value)]));
|
|
108
|
+
if (Array.isArray(schema.list))
|
|
109
|
+
result.list = schema.list.map(value => child(value));
|
|
110
|
+
if (schema.inner !== undefined)
|
|
111
|
+
result.inner = schema.inner === false ? false : child(schema.inner);
|
|
112
|
+
if (schema.additional !== undefined)
|
|
113
|
+
result.additional = schema.additional === false ? false : child(schema.additional);
|
|
114
|
+
if (schema.type === 'const' && !own(schema, 'value') && own(schema, 'default'))
|
|
115
|
+
result.value = schema.default;
|
|
116
|
+
return finish(result);
|
|
117
|
+
}
|
|
118
|
+
for (const [keyword, type] of [['allOf', 'intersect'], ['anyOf', 'union'], ['oneOf', 'union']]) {
|
|
119
|
+
if (!Array.isArray(schema[keyword]))
|
|
120
|
+
continue;
|
|
121
|
+
if (schema[keyword].length === 0)
|
|
122
|
+
return finish(fallback('malformed-schema'));
|
|
123
|
+
const { [keyword]: _branches, ...siblings } = schema;
|
|
124
|
+
const list = schema[keyword].map(value => sameValue(value));
|
|
125
|
+
const structuralSibling = ['properties', 'items', 'prefixItems', 'additionalProperties'].some(name => own(siblings, name));
|
|
126
|
+
const composed = { type, list, ...(type === 'union' ? { mode: keyword } : {}) };
|
|
127
|
+
return finish(structuralSibling ? { type: 'intersect', list: [composed, sameValue(siblings)] } : composed);
|
|
128
|
+
}
|
|
129
|
+
if (Array.isArray(schema.type)) {
|
|
130
|
+
const { type: _types, ...constraints } = schema;
|
|
131
|
+
return finish({ type: 'union', list: schema.type.map(type => child({ ...constraints, type })) });
|
|
132
|
+
}
|
|
133
|
+
if (own(schema, 'const'))
|
|
134
|
+
return finish({ type: 'const', value: schema.const });
|
|
135
|
+
const type = typeof schema.type === 'string' ? schema.type : schema.properties ? 'object' : schema.items || schema.prefixItems ? 'array' : undefined;
|
|
136
|
+
if (type === 'object') {
|
|
137
|
+
const properties = record(schema.properties) ? schema.properties : {};
|
|
138
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
139
|
+
const object = Object.fromEntries(Object.entries(properties).map(([name, value]) => [name, { ...child(value, name), ...(required.has(name) ? { required: true } : {}) }]));
|
|
140
|
+
const additional = schema.additionalProperties === false ? false : child(schema.additionalProperties ?? true);
|
|
141
|
+
return finish(Object.keys(object).length === 0 && record(schema.additionalProperties)
|
|
142
|
+
? { type: 'dict', inner: additional } : { type: 'object', object, additional });
|
|
143
|
+
}
|
|
144
|
+
if (type === 'array') {
|
|
145
|
+
const prefix = Array.isArray(schema.prefixItems) ? schema.prefixItems : Array.isArray(schema.items) ? schema.items : undefined;
|
|
146
|
+
return finish(prefix ? { type: 'tuple', list: prefix.map(value => child(value)), inner: schema.items === false || schema.additionalItems === false ? false : child(Array.isArray(schema.items) ? schema.additionalItems ?? true : schema.items ?? true) }
|
|
147
|
+
: { type: 'list', inner: child(schema.items ?? true) });
|
|
148
|
+
}
|
|
149
|
+
const inferred = type ?? (Array.isArray(schema.enum) && schema.enum.length ? (schema.enum[0] === null ? 'null' : typeof schema.enum[0]) : 'any');
|
|
150
|
+
return finish({ type: inferred === 'integer' ? 'number' : jsonTypes.has(inferred) ? inferred : 'any', ...(inferred === 'integer' ? { integer: true } : {}) });
|
|
151
|
+
}
|
|
152
|
+
const annotationKeys = new Set(['title', 'description', 'x-descriptionZh', 'x-i18n', 'default', 'readOnly', 'writeOnly', 'examples', 'deprecated', 'x-zhin-config', 'x-zhin-secret', 'key', 'hidden', 'component', 'options', 'effect', 'secret']);
|
|
153
|
+
function annotate(result, schema, key) {
|
|
154
|
+
const output = { ...result };
|
|
155
|
+
if (record(schema['x-i18n']))
|
|
156
|
+
output['x-i18n'] = schema['x-i18n'];
|
|
157
|
+
for (const name of ['key', 'title', 'default', 'min', 'max', 'step', 'integer', 'pattern', 'minLength', 'maxLength', 'minItems', 'maxItems', 'minimum', 'maximum', 'multipleOf', 'uniqueItems', 'minProperties', 'maxProperties', 'exclusiveMinimum', 'exclusiveMaximum', 'readOnly', 'deprecated', 'format', 'unevaluatedProperties'])
|
|
158
|
+
if (own(schema, name))
|
|
159
|
+
output[name] = schema[name];
|
|
160
|
+
if (key !== undefined && output.key === undefined)
|
|
161
|
+
output.key = key;
|
|
162
|
+
const description = typeof schema['x-descriptionZh'] === 'string' ? schema['x-descriptionZh'] : schema.description;
|
|
163
|
+
if (typeof description === 'string')
|
|
164
|
+
output.description = description;
|
|
165
|
+
// Existing bilingual Host schemas already keep the English original beside x-descriptionZh.
|
|
166
|
+
if (typeof schema['x-descriptionZh'] === 'string' && typeof schema.description === 'string') {
|
|
167
|
+
const catalog = record(output['x-i18n']) ? output['x-i18n'] : {};
|
|
168
|
+
const english = record(catalog.en) ? catalog.en : {};
|
|
169
|
+
output['x-i18n'] = { ...catalog, en: { description: schema.description, ...english } };
|
|
170
|
+
}
|
|
171
|
+
if (typeof schema.required === 'boolean')
|
|
172
|
+
output.required = schema.required;
|
|
173
|
+
if (Array.isArray(schema.enum))
|
|
174
|
+
output.options = schema.enum.map(value => ({ label: String(value), value }));
|
|
175
|
+
for (const name of ['hidden', 'component', 'options', 'effect'])
|
|
176
|
+
if (schema[name] !== undefined)
|
|
177
|
+
output[name] = schema[name];
|
|
178
|
+
const metadata = record(schema['x-zhin-config']) ? schema['x-zhin-config'] : {};
|
|
179
|
+
for (const name of ['hidden', 'component', 'options', 'effect'])
|
|
180
|
+
if (metadata[name] !== undefined)
|
|
181
|
+
output[name] = metadata[name];
|
|
182
|
+
if (output.secret !== true && typeof schema.secret === 'boolean')
|
|
183
|
+
output.secret = schema.secret;
|
|
184
|
+
if (output.secret === true || isSecretConfigSchema(schema)) {
|
|
185
|
+
output.secret = true;
|
|
186
|
+
if (output.component !== 'textarea')
|
|
187
|
+
output.component = 'password';
|
|
188
|
+
}
|
|
189
|
+
const bounds = schema.type === 'string' ? ['minLength', 'maxLength'] : schema.type === 'array' ? ['minItems', 'maxItems'] : ['minimum', 'maximum'];
|
|
190
|
+
if (typeof schema[bounds[0]] === 'number')
|
|
191
|
+
output.min = schema[bounds[0]];
|
|
192
|
+
if (typeof schema[bounds[1]] === 'number')
|
|
193
|
+
output.max = schema[bounds[1]];
|
|
194
|
+
if (typeof schema.multipleOf === 'number')
|
|
195
|
+
output.step = schema.multipleOf;
|
|
196
|
+
return output;
|
|
197
|
+
}
|
|
198
|
+
export function isConsoleSchemaJson(schema) {
|
|
199
|
+
if (schema.object !== undefined || schema.list !== undefined || schema.inner !== undefined || schema.additional !== undefined)
|
|
200
|
+
return true;
|
|
201
|
+
return typeof schema.type === 'string' && (['dict', 'union', 'tuple', 'intersect', 'const', 'any', 'date', 'regexp', 'list'].includes(schema.type)
|
|
202
|
+
|| ['string', 'number', 'boolean', 'null'].includes(schema.type) && (schema.key !== undefined || typeof schema.required === 'boolean' || ['hidden', 'component', 'options', 'effect', 'secret'].some(name => own(schema, name))) && !['const', 'anyOf', 'oneOf', 'allOf', '$ref'].some(name => own(schema, name)));
|
|
203
|
+
}
|
|
204
|
+
/** Validation-only branches do not own a second editable copy of the same value. */
|
|
205
|
+
function validationOnly(schema, properties) {
|
|
206
|
+
if (['$ref', '$dynamicRef', 'default', 'items', 'prefixItems', 'additionalProperties', 'patternProperties', 'contains', 'dependentSchemas'].some(name => own(schema, name)))
|
|
207
|
+
return false;
|
|
208
|
+
if (Array.isArray(schema.required) && schema.required.some(name => !properties.has(String(name))))
|
|
209
|
+
return false;
|
|
210
|
+
if (record(schema.properties)) {
|
|
211
|
+
for (const [name, definition] of Object.entries(schema.properties)) {
|
|
212
|
+
if (!properties.has(name) || !record(definition) || !validationOnly(definition, new Set()))
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const name of ['if', 'then', 'else', 'not'])
|
|
217
|
+
if (own(schema, name) && (!record(schema[name]) || !validationOnly(schema[name], properties)))
|
|
218
|
+
return false;
|
|
219
|
+
for (const name of ['allOf', 'anyOf', 'oneOf'])
|
|
220
|
+
if (Array.isArray(schema[name]) && !schema[name].every(value => record(value) && validationOnly(value, properties)))
|
|
221
|
+
return false;
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function readEnvFile(projectRoot: string, filename: string): Promise<string>;
|
|
2
|
+
/** Locked, atomic replacement that keeps the file mode (`0600` when new); ADR 0057 §4. */
|
|
3
|
+
export declare function writeEnvFile(projectRoot: string, filename: string, content: string): Promise<void>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readDotenvFile, writeDotenvFile } from '@zhin.js/runtime';
|
|
3
|
+
const ENV_FILES = new Set(['.env', '.env.development', '.env.production']);
|
|
4
|
+
export async function readEnvFile(projectRoot, filename) {
|
|
5
|
+
if (!ENV_FILES.has(filename))
|
|
6
|
+
throw new Error(`Invalid env file: ${filename}`);
|
|
7
|
+
return readDotenvFile(join(projectRoot, filename));
|
|
8
|
+
}
|
|
9
|
+
/** Locked, atomic replacement that keeps the file mode (`0600` when new); ADR 0057 §4. */
|
|
10
|
+
export async function writeEnvFile(projectRoot, filename, content) {
|
|
11
|
+
if (!ENV_FILES.has(filename))
|
|
12
|
+
throw new Error(`Invalid env file: ${filename}`);
|
|
13
|
+
await writeDotenvFile(join(projectRoot, filename), content);
|
|
14
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type LiteralConfigSecret } from '@zhin.js/runtime';
|
|
2
|
+
import type { RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
|
|
3
|
+
/**
|
|
4
|
+
* Secret paths holding literals in `document`, using the Host schema, the root `schema.json`
|
|
5
|
+
* and each declared instance's `schema.json` (annotations), plus the field-name fallback.
|
|
6
|
+
* Plugins whose schema is only inline (`definePlugin({ config })`) use the name fallback here.
|
|
7
|
+
*/
|
|
8
|
+
export declare function findProjectLiteralSecrets(projectRoot: string, document: RuntimeConfigDocument): Promise<readonly LiteralConfigSecret[]>;
|
|
9
|
+
/** `ZHIN_<PATH_UPPER_SNAKE>`: `http.token` → `ZHIN_HTTP_TOKEN`, `qq.endpoints[0].appSecret` → `ZHIN_QQ_ENDPOINTS_0_APP_SECRET`. */
|
|
10
|
+
export declare function literalSecretEnvironmentName(path: readonly string[]): string;
|
|
11
|
+
export interface LiteralSecretMove {
|
|
12
|
+
readonly path: readonly string[];
|
|
13
|
+
readonly variable: string;
|
|
14
|
+
/** The variable already held this exact value; `.env` was not changed for it. */
|
|
15
|
+
readonly reused: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface LiteralSecretSkip {
|
|
18
|
+
readonly path: readonly string[];
|
|
19
|
+
readonly reason: string;
|
|
20
|
+
}
|
|
21
|
+
export interface LiteralSecretFixResult {
|
|
22
|
+
readonly moved: readonly LiteralSecretMove[];
|
|
23
|
+
readonly skipped: readonly LiteralSecretSkip[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Moves every literal secret of `configFile` into `<projectRoot>/.env`.
|
|
27
|
+
*
|
|
28
|
+
* Order: `.env` first (locked, atomic; a variable that already holds the same value is reused,
|
|
29
|
+
* a name taken by another value gets `_2`, `_3`, …), then the config file through
|
|
30
|
+
* ConfigFileDocument (revision check, lock, atomic). If the config changed in between, the
|
|
31
|
+
* write is refused with a conflict and nothing in the config changes; the next run reuses the
|
|
32
|
+
* variables already written, so the operation is idempotent.
|
|
33
|
+
*/
|
|
34
|
+
export declare function moveLiteralSecretsToEnv(projectRoot: string, configFile: string): Promise<LiteralSecretFixResult>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR 0057 §4: literal secrets in the YAML/JSON config move to `.env`.
|
|
3
|
+
* `zhin doctor` reports them; `zhin doctor --fix` writes each literal into `.env` as
|
|
4
|
+
* `ZHIN_<PATH>` and replaces the config value with `${ZHIN_<PATH>}`.
|
|
5
|
+
*/
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { parse as parseDotenv } from 'dotenv';
|
|
9
|
+
import { HOST_CONFIG_SCHEMA, createConfigDocument, findLiteralConfigSecrets, readApplicationPluginDeclaration, updateDotenvFile, } from '@zhin.js/runtime';
|
|
10
|
+
import { PluginSchemaCatalog } from './plugin-schema-catalog.js';
|
|
11
|
+
/**
|
|
12
|
+
* Secret paths holding literals in `document`, using the Host schema, the root `schema.json`
|
|
13
|
+
* and each declared instance's `schema.json` (annotations), plus the field-name fallback.
|
|
14
|
+
* Plugins whose schema is only inline (`definePlugin({ config })`) use the name fallback here.
|
|
15
|
+
*/
|
|
16
|
+
export async function findProjectLiteralSecrets(projectRoot, document) {
|
|
17
|
+
let instanceKeys = [];
|
|
18
|
+
try {
|
|
19
|
+
instanceKeys = readApplicationPluginDeclaration(document).instances.map(item => item.instanceKey);
|
|
20
|
+
}
|
|
21
|
+
catch { /* Invalid declarations are reported elsewhere; still scan with the name fallback. */ }
|
|
22
|
+
const schemas = await new PluginSchemaCatalog(projectRoot).readJsonMany(['plugin', ...instanceKeys], document);
|
|
23
|
+
const plugins = [];
|
|
24
|
+
for (const [key, schema] of schemas) {
|
|
25
|
+
if (schema && !(schema instanceof Error))
|
|
26
|
+
plugins.push([[key], schema]);
|
|
27
|
+
}
|
|
28
|
+
return findLiteralConfigSecrets(document, { host: HOST_CONFIG_SCHEMA, plugins });
|
|
29
|
+
}
|
|
30
|
+
/** `ZHIN_<PATH_UPPER_SNAKE>`: `http.token` → `ZHIN_HTTP_TOKEN`, `qq.endpoints[0].appSecret` → `ZHIN_QQ_ENDPOINTS_0_APP_SECRET`. */
|
|
31
|
+
export function literalSecretEnvironmentName(path) {
|
|
32
|
+
const body = path
|
|
33
|
+
.map(segment => segment.replace(/([a-z0-9])([A-Z])/gu, '$1_$2'))
|
|
34
|
+
.join('_')
|
|
35
|
+
.replace(/[^A-Za-z0-9]+/gu, '_')
|
|
36
|
+
.replace(/^_+|_+$/gu, '')
|
|
37
|
+
.toUpperCase();
|
|
38
|
+
return `ZHIN_${body || 'SECRET'}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Moves every literal secret of `configFile` into `<projectRoot>/.env`.
|
|
42
|
+
*
|
|
43
|
+
* Order: `.env` first (locked, atomic; a variable that already holds the same value is reused,
|
|
44
|
+
* a name taken by another value gets `_2`, `_3`, …), then the config file through
|
|
45
|
+
* ConfigFileDocument (revision check, lock, atomic). If the config changed in between, the
|
|
46
|
+
* write is refused with a conflict and nothing in the config changes; the next run reuses the
|
|
47
|
+
* variables already written, so the operation is idempotent.
|
|
48
|
+
*/
|
|
49
|
+
export async function moveLiteralSecretsToEnv(projectRoot, configFile) {
|
|
50
|
+
const config = createConfigDocument(configFile);
|
|
51
|
+
const snapshot = await config.read();
|
|
52
|
+
const secrets = await findProjectLiteralSecrets(projectRoot, snapshot.document);
|
|
53
|
+
if (secrets.length === 0)
|
|
54
|
+
return Object.freeze({ moved: Object.freeze([]), skipped: Object.freeze([]) });
|
|
55
|
+
// `.env.<environment>` overrides `.env`: a name used there with another value is taken.
|
|
56
|
+
const overlays = await Promise.all(['.env.development', '.env.production'].map(name => readOptional(join(projectRoot, name))));
|
|
57
|
+
const shadowing = overlays.map(source => parseDotenv(source));
|
|
58
|
+
const moved = [];
|
|
59
|
+
const skipped = [];
|
|
60
|
+
await updateDotenvFile(join(projectRoot, '.env'), current => {
|
|
61
|
+
moved.length = 0;
|
|
62
|
+
skipped.length = 0;
|
|
63
|
+
const existing = { ...parseDotenv(current) };
|
|
64
|
+
const lines = [];
|
|
65
|
+
for (const secret of secrets) {
|
|
66
|
+
const value = readPath(snapshot.document, secret.path);
|
|
67
|
+
if (typeof value !== 'string')
|
|
68
|
+
continue;
|
|
69
|
+
const line = (name) => dotenvLine(name, value);
|
|
70
|
+
if (!line('ZHIN_PROBE')) {
|
|
71
|
+
skipped.push(Object.freeze({ path: secret.path, reason: '值含换行或无法安全写入 .env,请手动迁移' }));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const base = literalSecretEnvironmentName(secret.path);
|
|
75
|
+
let variable = base;
|
|
76
|
+
for (let suffix = 2;; suffix += 1) {
|
|
77
|
+
const taken = existing[variable] !== undefined && existing[variable] !== value
|
|
78
|
+
|| shadowing.some(layer => layer[variable] !== undefined && layer[variable] !== value);
|
|
79
|
+
if (!taken)
|
|
80
|
+
break;
|
|
81
|
+
variable = `${base}_${suffix}`;
|
|
82
|
+
}
|
|
83
|
+
const reused = existing[variable] === value;
|
|
84
|
+
if (!reused) {
|
|
85
|
+
lines.push(line(variable));
|
|
86
|
+
existing[variable] = value;
|
|
87
|
+
}
|
|
88
|
+
moved.push(Object.freeze({ path: secret.path, variable, reused }));
|
|
89
|
+
}
|
|
90
|
+
if (lines.length === 0)
|
|
91
|
+
return undefined;
|
|
92
|
+
const separator = current === '' || current.endsWith('\n') ? '' : '\n';
|
|
93
|
+
return `${current}${separator}${current.trim() ? '\n' : ''}# zhin doctor --fix:从 ${configFileLabel(configFile)} 移入的密钥(ADR 0057)\n${lines.join('\n')}\n`;
|
|
94
|
+
});
|
|
95
|
+
if (moved.length) {
|
|
96
|
+
const patches = moved.map(item => ({ op: 'set', path: item.path, value: `\${${item.variable}}` }));
|
|
97
|
+
const prepared = await config.prepare(snapshot, patches);
|
|
98
|
+
await prepared.commit();
|
|
99
|
+
}
|
|
100
|
+
return Object.freeze({ moved: Object.freeze(moved), skipped: Object.freeze(skipped) });
|
|
101
|
+
}
|
|
102
|
+
/** A `NAME=value` dotenv line that parses back to exactly `value`, or undefined when none does. */
|
|
103
|
+
function dotenvLine(name, value) {
|
|
104
|
+
if (/[\r\n]/u.test(value))
|
|
105
|
+
return undefined;
|
|
106
|
+
for (const quote of ["'", '`', '"']) {
|
|
107
|
+
if (value.includes(quote))
|
|
108
|
+
continue;
|
|
109
|
+
const line = `${name}=${quote}${value}${quote}`;
|
|
110
|
+
if (parseDotenv(line)[name] === value)
|
|
111
|
+
return line;
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
function readPath(document, path) {
|
|
116
|
+
let current = document;
|
|
117
|
+
for (const segment of path) {
|
|
118
|
+
if (!current || typeof current !== 'object' || !Object.hasOwn(current, segment))
|
|
119
|
+
return undefined;
|
|
120
|
+
current = current[segment];
|
|
121
|
+
}
|
|
122
|
+
return current;
|
|
123
|
+
}
|
|
124
|
+
function configFileLabel(file) {
|
|
125
|
+
return file.split(/[\\/]/u).pop() ?? file;
|
|
126
|
+
}
|
|
127
|
+
async function readOptional(file) {
|
|
128
|
+
try {
|
|
129
|
+
return await readFile(file, 'utf8');
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
133
|
+
return '';
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { createConfigDocument, readApplicationPluginDeclaration } from '@zhin.js/runtime';
|
|
3
|
+
import { findConfigFile } from '../project/config-file.js';
|
|
4
|
+
/** Reads only the application's instance declaration authority. */
|
|
5
|
+
export async function readPluginPackageMap(projectRoot, currentDocument) {
|
|
6
|
+
let document = currentDocument;
|
|
7
|
+
if (!document) {
|
|
8
|
+
const file = findConfigFile(projectRoot);
|
|
9
|
+
if (!file)
|
|
10
|
+
throw new Error('Missing configVersion: 2 configuration; run zhin setup');
|
|
11
|
+
document = (await createConfigDocument(join(projectRoot, file)).read()).document;
|
|
12
|
+
}
|
|
13
|
+
return new Map(readApplicationPluginDeclaration(document).instances.map(item => [item.instanceKey, item.package]));
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
|
|
2
|
+
/** Resolves schemas using the same package roots and declarations as Runtime. */
|
|
3
|
+
export declare class PluginSchemaCatalog {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(projectRoot: string, declarations?: () => ReadonlyMap<string, Readonly<Record<string, unknown>>> | Promise<ReadonlyMap<string, Readonly<Record<string, unknown>>>>);
|
|
6
|
+
read(pluginName?: string, document?: RuntimeConfigDocument): Promise<unknown>;
|
|
7
|
+
readJson(pluginName: string, document?: RuntimeConfigDocument): Promise<Readonly<Record<string, unknown>> | null>;
|
|
8
|
+
/**
|
|
9
|
+
* Raw JSON schemas for several keys sharing one resolver and one declaration read.
|
|
10
|
+
* A key whose schema cannot be read maps to its `Error`, so callers can fail closed per key.
|
|
11
|
+
*/
|
|
12
|
+
readJsonMany(keys: Iterable<string>, document?: RuntimeConfigDocument): Promise<ReadonlyMap<string, Readonly<Record<string, unknown>> | null | Error>>;
|
|
13
|
+
/**
|
|
14
|
+
* Form schema of an installed plugin package that no instance declares yet (ADR 0053 §2 step 4):
|
|
15
|
+
* the package must be a production dependency (`dependencies` / `optionalDependencies`) of the
|
|
16
|
+
* project and a Plugin package. Returns the declared-instance form shape plus `declared: false`,
|
|
17
|
+
* `null` when such a package ships no `schema.json`, and `undefined` when `packageName` is not
|
|
18
|
+
* such a package (declared, not a dependency, not installed, not a Plugin, or not a package name).
|
|
19
|
+
*/
|
|
20
|
+
readUndeclared(packageName: string, currentDocument?: RuntimeConfigDocument): Promise<Readonly<Record<string, unknown>> | null | undefined>;
|
|
21
|
+
readAll(keys: Iterable<string>, document?: RuntimeConfigDocument): Promise<Record<string, unknown>>;
|
|
22
|
+
}
|