@pulse-compute/cli 0.0.0 → 1.0.0-beta.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/API.md +572 -0
- package/CHANGELOG.md +71 -0
- package/README.md +71 -1
- package/bin/pulse.js +15 -0
- package/bin/pulsewasm-extract.js +22 -0
- package/cli-spec.json +575 -0
- package/completions/_pulse +106 -0
- package/completions/pulse.bash +31 -0
- package/completions/pulse.fish +76 -0
- package/docs/README.md +77 -0
- package/docs/architecture/current-contracts.md +470 -0
- package/docs/architecture/overview.md +93 -0
- package/docs/architecture/vision.md +515 -0
- package/docs/concepts/bodies.md +186 -0
- package/docs/concepts/compilation-and-lowering.md +496 -0
- package/docs/concepts/contracts-and-providers.md +158 -0
- package/docs/concepts/effects-and-continuations.md +300 -0
- package/docs/concepts/entities-and-adapters.md +71 -0
- package/docs/concepts/package-owned-lowering.md +175 -0
- package/docs/concepts/targets-and-hosts.md +70 -0
- package/docs/contributing/README.md +38 -0
- package/docs/contributing/adding-core-provider.md +315 -0
- package/docs/contributing/adding-first-party-lowerer.md +327 -0
- package/docs/contributing/entities-lowering.md +91 -0
- package/docs/contributing/package-lowerer-contract.md +383 -0
- package/docs/contributing/pulse-aware-packages.md +149 -0
- package/docs/examples.md +112 -0
- package/docs/fixtures/inspect-fetch-composition.selected.json +181 -0
- package/docs/getting-started.md +128 -0
- package/docs/guides/compatibility-imports.md +60 -0
- package/docs/guides/deploying-fastly.md +188 -0
- package/docs/guides/deploying-node.md +143 -0
- package/docs/guides/events.md +233 -0
- package/docs/guides/fastly-capabilities.md +153 -0
- package/docs/guides/fetching-and-composition.md +101 -0
- package/docs/guides/grip.md +94 -0
- package/docs/guides/json-schemas.md +233 -0
- package/docs/guides/migrating-from-express.md +192 -0
- package/docs/guides/project-lifecycle.md +143 -0
- package/docs/guides/routing.md +202 -0
- package/docs/guides/troubleshooting.md +47 -0
- package/docs/maintainers/README.md +60 -0
- package/docs/maintainers/codex-maintainer.md +71 -0
- package/docs/maintainers/documentation-deployment.md +147 -0
- package/docs/maintainers/documentation-system.md +58 -0
- package/docs/maintainers/documentation-versioning.md +65 -0
- package/docs/maintainers/maintainer-charter.md +100 -0
- package/docs/maintainers/maintenance-policy.json +809 -0
- package/docs/maintainers/maintenance-policy.md +124 -0
- package/docs/maintainers/npm-publishing.md +154 -0
- package/docs/maintainers/plugin-readiness.json +19 -0
- package/docs/maintainers/plugin-readiness.md +31 -0
- package/docs/maintainers/public-site.md +158 -0
- package/docs/maintainers/release-acceptance.md +293 -0
- package/docs/maintainers/release-manifest.md +89 -0
- package/docs/maintainers/repository-setup.md +145 -0
- package/docs/maintainers/scope-policy.md +105 -0
- package/docs/maintainers/support-and-triage.md +74 -0
- package/docs/maintainers/testing.md +334 -0
- package/docs/packages/README.md +60 -0
- package/docs/packages/assets.md +85 -0
- package/docs/packages/cli.md +190 -0
- package/docs/packages/crypto.md +78 -0
- package/docs/packages/entities.md +129 -0
- package/docs/packages/grip.md +82 -0
- package/docs/packages/implementation-packages.md +122 -0
- package/docs/packages/jwt.md +55 -0
- package/docs/packages/provider-fastly.md +243 -0
- package/docs/packages/pulse.md +159 -0
- package/docs/packages/runtime.md +265 -0
- package/docs/packages/s3.md +101 -0
- package/docs/preview-scope.md +142 -0
- package/docs/reference/README.md +33 -0
- package/docs/reference/cli-spec.json +575 -0
- package/docs/reference/cli.md +457 -0
- package/docs/reference/compatibility-matrix.md +169 -0
- package/docs/reference/diagnostics.md +1872 -0
- package/docs/reference/documentation-versions.json +22 -0
- package/docs/reference/environment.md +347 -0
- package/docs/reference/handler-authoring.md +264 -0
- package/docs/reference/project-config.md +582 -0
- package/docs/reference/project-config.schema.json +981 -0
- package/docs/reference/release-manifest.json +441 -0
- package/docs/reference/shell-completion.md +23 -0
- package/documentation-site.json +668 -0
- package/documentation-versions.json +22 -0
- package/examples/01-hello-json/.pulse/.gitignore +3 -0
- package/examples/01-hello-json/.pulse/config.ts +16 -0
- package/examples/01-hello-json/README.md +119 -0
- package/examples/01-hello-json/package.json +20 -0
- package/examples/01-hello-json/src/index.ts +9 -0
- package/examples/01-hello-json/tests/pulse.harness.ts +22 -0
- package/examples/01-hello-json/tsconfig.json +14 -0
- package/examples/02-request-schema/.pulse/.gitignore +3 -0
- package/examples/02-request-schema/.pulse/config.ts +17 -0
- package/examples/02-request-schema/README.md +145 -0
- package/examples/02-request-schema/package.json +20 -0
- package/examples/02-request-schema/src/index.ts +18 -0
- package/examples/02-request-schema/src/schemas.ts +20 -0
- package/examples/02-request-schema/tests/pulse.harness.ts +37 -0
- package/examples/02-request-schema/tsconfig.json +14 -0
- package/examples/03-fetch-composition/.pulse/.gitignore +3 -0
- package/examples/03-fetch-composition/.pulse/config.ts +31 -0
- package/examples/03-fetch-composition/README.md +210 -0
- package/examples/03-fetch-composition/package.json +20 -0
- package/examples/03-fetch-composition/src/index.ts +60 -0
- package/examples/03-fetch-composition/tests/pulse.harness.ts +47 -0
- package/examples/03-fetch-composition/tsconfig.json +14 -0
- package/examples/05-fastly-capabilities/.pulse/.gitignore +3 -0
- package/examples/05-fastly-capabilities/.pulse/config.ts +57 -0
- package/examples/05-fastly-capabilities/README.md +257 -0
- package/examples/05-fastly-capabilities/package.json +22 -0
- package/examples/05-fastly-capabilities/src/index.ts +55 -0
- package/examples/05-fastly-capabilities/tests/pulse.harness.ts +66 -0
- package/examples/05-fastly-capabilities/tsconfig.json +14 -0
- package/examples/07-opaque-proxy/.pulse/.gitignore +3 -0
- package/examples/07-opaque-proxy/.pulse/config.ts +38 -0
- package/examples/07-opaque-proxy/README.md +150 -0
- package/examples/07-opaque-proxy/package.json +21 -0
- package/examples/07-opaque-proxy/src/index.ts +9 -0
- package/examples/07-opaque-proxy/tests/pulse.harness.ts +25 -0
- package/examples/07-opaque-proxy/tsconfig.json +14 -0
- package/examples/09-router-lowering/.pulse/.gitignore +3 -0
- package/examples/09-router-lowering/.pulse/config.ts +22 -0
- package/examples/09-router-lowering/README.md +287 -0
- package/examples/09-router-lowering/package.json +22 -0
- package/examples/09-router-lowering/src/index.ts +65 -0
- package/examples/09-router-lowering/tests/pulse.harness.ts +91 -0
- package/examples/09-router-lowering/tsconfig.json +14 -0
- package/examples/10-entities-tools/.pulse/.gitignore +4 -0
- package/examples/10-entities-tools/.pulse/config.ts +23 -0
- package/examples/10-entities-tools/README.md +165 -0
- package/examples/10-entities-tools/package.json +21 -0
- package/examples/10-entities-tools/src/handlers.ts +15 -0
- package/examples/10-entities-tools/src/index.ts +31 -0
- package/examples/10-entities-tools/src/schemas.ts +18 -0
- package/examples/10-entities-tools/tests/pulse.harness.ts +40 -0
- package/examples/10-entities-tools/tools-facade.cjs +158 -0
- package/examples/10-entities-tools/tsconfig.json +14 -0
- package/examples/11-events/.pulse/.gitignore +4 -0
- package/examples/11-events/.pulse/config.ts +24 -0
- package/examples/11-events/README.md +194 -0
- package/examples/11-events/package.json +20 -0
- package/examples/11-events/src/index.ts +27 -0
- package/examples/11-events/src/schemas.ts +19 -0
- package/examples/11-events/tests/pulse.harness.ts +34 -0
- package/examples/11-events/tsconfig.json +15 -0
- package/examples/12-mcp-proxy/.pulse/.gitignore +3 -0
- package/examples/12-mcp-proxy/.pulse/config.ts +25 -0
- package/examples/12-mcp-proxy/README.md +149 -0
- package/examples/12-mcp-proxy/package.json +20 -0
- package/examples/12-mcp-proxy/src/index.ts +17 -0
- package/examples/12-mcp-proxy/tests/pulse.harness.ts +29 -0
- package/examples/12-mcp-proxy/tsconfig.json +14 -0
- package/examples/13-jwt-es256/.pulse/config.ts +19 -0
- package/examples/13-jwt-es256/README.md +195 -0
- package/examples/13-jwt-es256/package.json +21 -0
- package/examples/13-jwt-es256/src/index.ts +21 -0
- package/examples/13-jwt-es256/tests/pulse.harness.ts +54 -0
- package/examples/13-jwt-es256/tsconfig.json +14 -0
- package/examples/README.md +36 -0
- package/package.json +66 -6
- package/project-config.schema.json +981 -0
- package/release-manifest.json +441 -0
- package/src/command-spec.js +279 -0
- package/src/completion.js +113 -0
- package/src/diagnostics.js +350 -0
- package/src/documentation.js +45 -0
- package/src/index.d.ts +133 -0
- package/src/index.js +15 -0
- package/src/internal/command-executor.d.ts +29 -0
- package/src/internal/command-executor.js +143 -0
- package/src/internal/command-plan.d.ts +9 -0
- package/src/internal/command-plan.js +73 -0
- package/src/internal/command-reporter.d.ts +15 -0
- package/src/internal/command-reporter.js +133 -0
- package/src/internal/command-request.d.ts +35 -0
- package/src/internal/command-request.js +154 -0
- package/src/internal/node-http.js +54 -0
- package/src/internal/project-context.d.ts +66 -0
- package/src/internal/project-context.js +175 -0
- package/src/project-config-reference.js +4 -0
- package/src/project-config-schema.d.ts +23 -0
- package/src/project-config-schema.js +271 -0
- package/src/project-config.js +724 -0
- package/src/project-execution.js +2946 -0
- package/src/provider-drivers.js +3 -0
- package/src/target-support.js +3 -0
- package/src/typescript-module-loader.js +269 -0
- package/src/workflow.js +78 -0
- package/src/workspace.js +82 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { NormalizedCommandRequest } from './command-request';
|
|
2
|
+
|
|
3
|
+
export interface ProjectContextWorkspace {
|
|
4
|
+
readonly version: string;
|
|
5
|
+
readonly kind: 'conventional';
|
|
6
|
+
readonly root: string;
|
|
7
|
+
readonly configFile: string;
|
|
8
|
+
readonly source: string;
|
|
9
|
+
readonly boundary: string;
|
|
10
|
+
readonly configInsideWorkspace: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ProjectContextProfile {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly source: string;
|
|
16
|
+
readonly host: string;
|
|
17
|
+
readonly target: 'native' | 'javascript';
|
|
18
|
+
readonly reporting: 'off' | 'error' | 'warn' | 'info' | 'debug';
|
|
19
|
+
readonly reportingLevel: 0 | 1 | 2 | 3 | 4;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProjectContextPlan {
|
|
23
|
+
readonly version: string;
|
|
24
|
+
readonly projectVersion: string;
|
|
25
|
+
readonly pulse: Readonly<{
|
|
26
|
+
entry: string;
|
|
27
|
+
schema: string | null;
|
|
28
|
+
tests: string | null;
|
|
29
|
+
defaultProfile: string;
|
|
30
|
+
strict: boolean;
|
|
31
|
+
reporting: 'off' | 'error' | 'warn' | 'info' | 'debug';
|
|
32
|
+
}>;
|
|
33
|
+
readonly profile: ProjectContextProfile;
|
|
34
|
+
readonly bindings: Readonly<{ config: readonly string[]; secret: readonly string[] }>;
|
|
35
|
+
readonly fragmentKeys: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ProjectContext {
|
|
39
|
+
readonly cwd: string;
|
|
40
|
+
readonly workspace: ProjectContextWorkspace;
|
|
41
|
+
readonly configFile: string;
|
|
42
|
+
readonly selectedProfile: ProjectContextProfile;
|
|
43
|
+
readonly selectionSource: string;
|
|
44
|
+
readonly projectPlan: ProjectContextPlan;
|
|
45
|
+
readonly projectHash: string;
|
|
46
|
+
readonly planHash: string;
|
|
47
|
+
readonly entryFile: string;
|
|
48
|
+
readonly schemaFile: string | null;
|
|
49
|
+
readonly testsFile: string | null;
|
|
50
|
+
readonly target: 'native' | 'javascript';
|
|
51
|
+
readonly reporting: 'off' | 'error' | 'warn' | 'info' | 'debug';
|
|
52
|
+
readonly reportingLevel: 0 | 1 | 2 | 3 | 4;
|
|
53
|
+
readonly provider: string;
|
|
54
|
+
readonly outDir: string;
|
|
55
|
+
readonly invocationOverrides: Readonly<Partial<Record<'directory' | 'profile' | 'outDir' | 'host' | 'port' | 'watch', unknown>>>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export declare const INVOCATION_OVERRIDE_KEYS: readonly ['directory', 'profile', 'outDir', 'host', 'port', 'watch'];
|
|
59
|
+
export declare function commandRequiresProjectContext(request: NormalizedCommandRequest | readonly string[] | Readonly<Record<string, unknown>>): boolean;
|
|
60
|
+
export declare function createProjectContext(request: NormalizedCommandRequest | Readonly<Record<string, unknown>>, project: Readonly<Record<string, unknown>>, options?: Readonly<{ cwd?: string }>): ProjectContext;
|
|
61
|
+
export declare function resolveProjectContext(request: NormalizedCommandRequest | readonly string[] | Readonly<Record<string, unknown>>, options?: Readonly<{ cwd?: string; environment?: NodeJS.ProcessEnv; env?: NodeJS.ProcessEnv }>): ProjectContext;
|
|
62
|
+
export declare function isProjectContext(value: unknown): value is ProjectContext;
|
|
63
|
+
export declare function resolvedProjectForContext(context: ProjectContext): Readonly<Record<string, unknown>>;
|
|
64
|
+
export declare function requestForContext(context: ProjectContext): NormalizedCommandRequest;
|
|
65
|
+
export declare function projectDocumentForContext(context: ProjectContext): Readonly<Record<string, unknown>>;
|
|
66
|
+
export declare function projectContextDocument(context: ProjectContext): ProjectContext;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const {
|
|
5
|
+
projectJson,
|
|
6
|
+
resolveProject
|
|
7
|
+
} = require('../project-config.js');
|
|
8
|
+
const { normalizeCommandRequest } = require('./command-request.js');
|
|
9
|
+
|
|
10
|
+
const INVOCATION_OVERRIDE_KEYS = Object.freeze([
|
|
11
|
+
'directory', 'profile', 'outDir', 'host', 'port', 'watch'
|
|
12
|
+
]);
|
|
13
|
+
const PROJECT_CONTEXT_DATA = new WeakMap();
|
|
14
|
+
|
|
15
|
+
function deepFreeze(value) {
|
|
16
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
17
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
18
|
+
return Object.freeze(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function commandRequiresProjectContext(value) {
|
|
22
|
+
const request = normalizeCommandRequest(value);
|
|
23
|
+
if (request.kind !== 'command' || request.help || request.command === 'init') return false;
|
|
24
|
+
if (request.command === 'inspect' && request.artifact) return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function selectedProfileDocument(project) {
|
|
29
|
+
const profile = project.selectedProfile || project.profile;
|
|
30
|
+
if (!profile || typeof profile !== 'object') return null;
|
|
31
|
+
return deepFreeze({
|
|
32
|
+
name: String(profile.name),
|
|
33
|
+
source: String(profile.source),
|
|
34
|
+
host: String(profile.host),
|
|
35
|
+
target: String(profile.target),
|
|
36
|
+
reporting: String(profile.reporting),
|
|
37
|
+
reportingLevel: Number(project.reportingLevel)
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function workspaceDocument(project) {
|
|
42
|
+
if (!project.workspace) return null;
|
|
43
|
+
return deepFreeze({
|
|
44
|
+
version: project.workspace.version,
|
|
45
|
+
kind: project.workspace.kind,
|
|
46
|
+
root: project.workspace.root,
|
|
47
|
+
configFile: project.workspace.configFile,
|
|
48
|
+
source: project.workspace.source,
|
|
49
|
+
boundary: project.workspace.boundary,
|
|
50
|
+
configInsideWorkspace: project.workspace.configInsideWorkspace !== false
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function projectPlanDocument(project, profile) {
|
|
55
|
+
const plan = project.profilePlan;
|
|
56
|
+
if (!plan) return null;
|
|
57
|
+
// Fragment values can contain development fixtures and resolved local secret
|
|
58
|
+
// values. The command boundary records only ownership keys and symbolic names.
|
|
59
|
+
return deepFreeze({
|
|
60
|
+
version: plan.version,
|
|
61
|
+
projectVersion: plan.projectVersion,
|
|
62
|
+
pulse: {
|
|
63
|
+
entry: plan.pulse.entry,
|
|
64
|
+
schema: plan.pulse.schema,
|
|
65
|
+
tests: plan.pulse.tests,
|
|
66
|
+
defaultProfile: plan.pulse.defaultProfile,
|
|
67
|
+
strict: plan.pulse.strict !== false,
|
|
68
|
+
reporting: plan.pulse.reporting
|
|
69
|
+
},
|
|
70
|
+
profile,
|
|
71
|
+
bindings: {
|
|
72
|
+
config: [...(project.bindings && project.bindings.config || [])],
|
|
73
|
+
secret: [...(project.bindings && project.bindings.secret || [])]
|
|
74
|
+
},
|
|
75
|
+
fragmentKeys: Object.keys(project.fragments || {}).sort()
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function invocationOverrideDocument(request) {
|
|
80
|
+
const overrides = {};
|
|
81
|
+
for (const key of INVOCATION_OVERRIDE_KEYS) {
|
|
82
|
+
if (request[key] !== undefined) overrides[key] = request[key];
|
|
83
|
+
}
|
|
84
|
+
return deepFreeze(overrides);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createProjectContext(requestValue, project, options = {}) {
|
|
88
|
+
const request = normalizeCommandRequest(requestValue);
|
|
89
|
+
if (request.kind !== 'command') throw new TypeError('ProjectContext requires a command request');
|
|
90
|
+
if (!project || typeof project !== 'object') throw new TypeError('ProjectContext requires a resolved Pulse project');
|
|
91
|
+
const profile = selectedProfileDocument(project);
|
|
92
|
+
const projectDocument = projectJson(project);
|
|
93
|
+
const context = deepFreeze({
|
|
94
|
+
cwd: path.resolve(options.cwd || process.cwd()),
|
|
95
|
+
workspace: projectDocument.workspace || workspaceDocument(project),
|
|
96
|
+
configFile: projectDocument.configFile || null,
|
|
97
|
+
selectedProfile: profile,
|
|
98
|
+
selectionSource: profile ? profile.source : null,
|
|
99
|
+
projectPlan: projectPlanDocument(project, profile),
|
|
100
|
+
projectHash: projectDocument.projectHash || null,
|
|
101
|
+
planHash: projectDocument.planHash || null,
|
|
102
|
+
entryFile: projectDocument.entryFile,
|
|
103
|
+
schemaFile: project.schemaFile || null,
|
|
104
|
+
testsFile: project.testsFile || null,
|
|
105
|
+
target: projectDocument.target || 'native',
|
|
106
|
+
reporting: projectDocument.reporting,
|
|
107
|
+
reportingLevel: projectDocument.reportingLevel,
|
|
108
|
+
provider: projectDocument.provider,
|
|
109
|
+
outDir: projectDocument.outDir,
|
|
110
|
+
invocationOverrides: invocationOverrideDocument(request)
|
|
111
|
+
});
|
|
112
|
+
PROJECT_CONTEXT_DATA.set(context, Object.freeze({ request, project, projectDocument }));
|
|
113
|
+
return context;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function resolveProjectContext(requestValue, options = {}) {
|
|
117
|
+
const request = normalizeCommandRequest(requestValue);
|
|
118
|
+
if (!commandRequiresProjectContext(request)) {
|
|
119
|
+
const name = request.kind === 'command' ? request.command : request.invocation;
|
|
120
|
+
throw new TypeError(`Pulse ${name} does not use a project context`);
|
|
121
|
+
}
|
|
122
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
123
|
+
const project = resolveProject({
|
|
124
|
+
cwd,
|
|
125
|
+
directory: request.directory,
|
|
126
|
+
profile: request.profile,
|
|
127
|
+
env: options.environment || options.env || process.env,
|
|
128
|
+
outDir: request.outDir,
|
|
129
|
+
host: request.host,
|
|
130
|
+
port: request.port,
|
|
131
|
+
watch: request.watch
|
|
132
|
+
});
|
|
133
|
+
return createProjectContext(request, project, { cwd });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function contextData(context) {
|
|
137
|
+
const data = PROJECT_CONTEXT_DATA.get(context);
|
|
138
|
+
if (!data || !context) {
|
|
139
|
+
throw new TypeError('ProjectContext was not created by the authoritative resolver');
|
|
140
|
+
}
|
|
141
|
+
return data;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isProjectContext(value) {
|
|
145
|
+
return Boolean(value && PROJECT_CONTEXT_DATA.has(value));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resolvedProjectForContext(context) {
|
|
149
|
+
return contextData(context).project;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function requestForContext(context) {
|
|
153
|
+
return contextData(context).request;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function projectDocumentForContext(context) {
|
|
157
|
+
return contextData(context).projectDocument;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function projectContextDocument(context) {
|
|
161
|
+
contextData(context);
|
|
162
|
+
return context;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = deepFreeze({
|
|
166
|
+
INVOCATION_OVERRIDE_KEYS,
|
|
167
|
+
commandRequiresProjectContext,
|
|
168
|
+
createProjectContext,
|
|
169
|
+
resolveProjectContext,
|
|
170
|
+
isProjectContext,
|
|
171
|
+
resolvedProjectForContext,
|
|
172
|
+
requestForContext,
|
|
173
|
+
projectDocumentForContext,
|
|
174
|
+
projectContextDocument
|
|
175
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface PulseProjectConfigValidationIssue {
|
|
2
|
+
readonly path: string;
|
|
3
|
+
readonly message: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface PulseProjectConfigSchemaDocument {
|
|
7
|
+
readonly schemaVersion: 'pulse.project-config-schema.v1';
|
|
8
|
+
readonly discovery: readonly string[];
|
|
9
|
+
readonly precedence: readonly string[];
|
|
10
|
+
readonly sections: readonly Readonly<Record<string, unknown>>[];
|
|
11
|
+
readonly fields: readonly Readonly<Record<string, unknown>>[];
|
|
12
|
+
readonly runtimeRules: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
13
|
+
readonly jsonSchema: Readonly<Record<string, unknown>>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export declare const PROJECT_CONFIG_SCHEMA_VERSION: 'pulse.project-config-schema.v1';
|
|
17
|
+
export declare const PROJECT_CONFIG_REFERENCE_VERSION: 'pulse.project-config-schema.v1';
|
|
18
|
+
export declare const PROJECT_CONFIG_JSON_SCHEMA: Readonly<Record<string, unknown>>;
|
|
19
|
+
export declare const CONFIG_DISCOVERY: readonly string[];
|
|
20
|
+
export declare const CONFIG_PRECEDENCE: readonly string[];
|
|
21
|
+
export declare const CONFIG_FIELDS: readonly Readonly<Record<string, unknown>>[];
|
|
22
|
+
export declare function validateProjectConfigStructure(input: unknown): readonly PulseProjectConfigValidationIssue[];
|
|
23
|
+
export declare function projectConfigSchemaDocument(): PulseProjectConfigSchemaDocument;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
DEFAULT_REPORTING_LEVEL,
|
|
5
|
+
REPORTING_LEVEL_NAMES
|
|
6
|
+
} = require('@pulse-compute/wasm-contracts/logging');
|
|
7
|
+
const {
|
|
8
|
+
CRYPTO_ALGORITHMS,
|
|
9
|
+
CRYPTO_REALIZATIONS,
|
|
10
|
+
normalizeCryptoConfiguration
|
|
11
|
+
} = require('@pulse-compute/wasm-contracts/crypto/contracts');
|
|
12
|
+
const { providerIds, providerConfigReferences } = require('./provider-drivers.js');
|
|
13
|
+
|
|
14
|
+
const PROJECT_CONFIG_SCHEMA_VERSION = 'pulse.project-config-schema.v1';
|
|
15
|
+
const PROJECT_CONFIG_REFERENCE_VERSION = PROJECT_CONFIG_SCHEMA_VERSION;
|
|
16
|
+
const builtinProviderIds = providerIds();
|
|
17
|
+
const providerReferenceContributions = providerConfigReferences();
|
|
18
|
+
const providerConfigSections = Object.freeze(providerReferenceContributions.flatMap((entry) => entry.sections || []));
|
|
19
|
+
const providerConfigFields = Object.freeze(providerReferenceContributions.flatMap((entry) => entry.fields || []));
|
|
20
|
+
const providerIdType = `${builtinProviderIds.map((id) => `'${id}'`).join(' | ')} | scoped package name`;
|
|
21
|
+
const providerIdList = builtinProviderIds.map((id) => `\`${id}\``).join(', ');
|
|
22
|
+
const cryptoRealizationIds = [...new Set(CRYPTO_REALIZATIONS.map((entry) => entry.id))];
|
|
23
|
+
|
|
24
|
+
function field(value) {
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
required: false,
|
|
27
|
+
default: '—',
|
|
28
|
+
allowed: '—',
|
|
29
|
+
precedence: 'Configuration value.',
|
|
30
|
+
security: 'No special handling.',
|
|
31
|
+
diagnostics: Object.freeze([]),
|
|
32
|
+
...value,
|
|
33
|
+
diagnostics: Object.freeze([...(value.diagnostics || [])])
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const CONFIG_SECTIONS = Object.freeze([
|
|
38
|
+
Object.freeze({ id: 'project', title: 'Project and provider selection', description: 'Select the canonical entry, provider, and build output.' }),
|
|
39
|
+
Object.freeze({ id: 'crypto', title: 'Cryptographic requirements', description: 'Declare exact algorithms for the global or selected profile and resolve one target realization without fallback. This surface is part of the synchronized 1.0.0-beta.2 package set.' }),
|
|
40
|
+
Object.freeze({ id: 'schemas', title: 'JSON schema policy', description: 'Configure bounded schema-body policy. Conventional projects declare registry identity only through pulse.schema.' }),
|
|
41
|
+
Object.freeze({ id: 'dev', title: 'Local development', description: 'Configure the foreground local server and local-only provider inputs.' }),
|
|
42
|
+
...providerConfigSections
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const CONFIG_FIELDS = Object.freeze([
|
|
46
|
+
field({ section: 'project', path: 'entry', type: 'string', default: '`./src/index.ts`', scope: 'all project commands', description: 'Project-relative TypeScript handler entry.', precedence: '`pulse.entry` in `.pulse/config.ts`.', security: 'Must resolve to an existing file.', diagnostics: ['PULSE_ENTRY_NOT_FOUND'] }),
|
|
47
|
+
field({ section: 'project', path: 'provider', type: `${providerIdType} | provider configuration`, default: '`node`', allowed: `${providerIdList}, another bare host id, or an exact scoped package name`, scope: 'compile/build/dev/test', description: 'Selects provider validation, local conformance, and build realization through the provider package toolchain contract.', precedence: 'Bare host ids resolve to `@pulse-compute/provider-<id>`; scoped package names are loaded exactly from the project.', security: 'Package loading is explicit and limited to the selected package `./toolchain` export; there is no scanning or fallback discovery.', diagnostics: ['PULSE_PROVIDER_PACKAGE_NOT_FOUND', 'PULSE_PROVIDER_TOOLCHAIN_LOAD_FAILED', 'PULSE_PROVIDER_TOOLCHAIN_INVALID', 'PULSE_PROVIDER_UNSUPPORTED', 'PULSE_PROVIDER_COMPILE_ONLY', 'PULSE_TEST_PROVIDER_REQUIRED', 'PULSE_DEV_PROVIDER_UNSUPPORTED'] }),
|
|
48
|
+
field({ section: 'project', path: 'outDir', type: 'string', default: '`./dist`', scope: 'compile/build', description: 'Project-relative artifact output directory.', precedence: '`--out` overrides this field for the current command.', security: 'Must remain a real child of the project root; absolute, parent-traversal, and symbolic-link traversal outside the project root is rejected.', diagnostics: ['PULSE_BUILD_OUT_UNSAFE'] }),
|
|
49
|
+
field({ section: 'project', path: 'reporting', type: "'off' | 'error' | 'warn' | 'info' | 'debug'", default: '`info`', allowed: '`off`, `error`, `warn`, `info`, `debug`', scope: 'compile/build/dev/test', description: 'Resolved synchronous logging threshold after the flat profile override is applied.', precedence: 'Selected-profile `reporting`, then `pulse.reporting`, then `info`.', security: 'Known request-owned secret values are redacted before provider emission.', diagnostics: ['PULSE_REPORTING_LEVEL_UNSUPPORTED'] }),
|
|
50
|
+
|
|
51
|
+
field({ section: 'crypto', path: 'pulse.crypto', type: "readonly PulseCryptoAlgorithm[] | Readonly<Partial<Record<PulseCryptoAlgorithm, { readonly realization?: PulseCryptoRealization }>>>", default: 'implicit no-crypto declaration', allowed: '`HS256`, `ES256`, `SHA-256`, `HMAC-SHA256`; exact pins `runtime-builtin`, `guest-source:pulse-hmac-as`, and `guest-linked:pulse-es256-rustcrypto-p256`', scope: 'global crypto requirement default', description: 'Declares the complete global algorithm set. Array entries are canonical algorithm names; object entries may pin one exact known realization.', precedence: 'Used only when the selected profile omits `crypto`.', security: 'Keys and verification bytes are not configuration values. Target selection is deterministic and never probes or falls back.', diagnostics: ['PULSE_CRYPTO_CONFIG_SHAPE_INVALID', 'PULSE_CRYPTO_ALGORITHM_DUPLICATE', 'PULSE_CRYPTO_ALGORITHM_UNKNOWN', 'PULSE_CRYPTO_REALIZATION_UNKNOWN'] }),
|
|
52
|
+
field({ section: 'crypto', path: '<profile>.crypto', type: "readonly PulseCryptoAlgorithm[] | Readonly<Partial<Record<PulseCryptoAlgorithm, { readonly realization?: PulseCryptoRealization }>>>", default: 'inherit `pulse.crypto` when absent', allowed: '`[]` and `{}` explicitly select no crypto; otherwise `HS256`, `ES256`, `SHA-256`, `HMAC-SHA256`', scope: 'selected profile', description: 'Replaces the complete global crypto declaration for the selected profile. Arrays and objects never merge with `pulse.crypto`.', precedence: 'Selected-profile declaration replaces `pulse.crypto`; absence inherits it.', security: 'Every declared algorithm must resolve for the selected target before lowering. A failed exact realization cannot select another backend.', diagnostics: ['PULSE_CRYPTO_CONFIG_REQUIRED', 'PULSE_CRYPTO_REALIZATION_PIN_INVALID', 'PULSE_CRYPTO_REALIZATION_UNAVAILABLE'] }),
|
|
53
|
+
|
|
54
|
+
field({ section: 'schemas', path: 'schemas', type: 'PulseSchemaConfig', default: '`{}`', scope: 'compile/dev/test/build', description: 'Sets schema body limits and content-type policy. Schema identities come from the module selected by pulse.schema.', security: 'Structured bodies are bounded before decoding.', diagnostics: ['PULSE_SCHEMA_COMPILE_FAILED'] }),
|
|
55
|
+
field({ section: 'schemas', path: 'schemas.contentTypePolicy', type: "'accept-json-or-missing' | 'require-json'", default: '`accept-json-or-missing`', allowed: '`accept-json-or-missing`, `require-json`', scope: 'request and fetched-body schema decode', description: 'Controls whether a JSON content type is mandatory.', diagnostics: ['PULSE_SCHEMA_CONTENT_TYPE_POLICY_INVALID', 'PULSE_SCHEMA_DECODE'] }),
|
|
56
|
+
field({ section: 'schemas', path: 'schemas.maxBytes', type: 'positive safe integer', default: '`65_536`', scope: 'structured schema decode', description: 'Maximum buffered bytes accepted by schema decoding.', security: 'Raise deliberately; this is a memory and request-amplification boundary.', diagnostics: ['PULSE_SCHEMA_BODY_LIMIT_INVALID', 'PULSE_BODY_TOO_LARGE'] }),
|
|
57
|
+
|
|
58
|
+
field({ section: 'dev', path: 'dev', type: 'object', default: '`{}`', scope: 'pulse dev and local conformance', description: 'Development listener and local fixture inputs.' }),
|
|
59
|
+
field({ section: 'dev', path: 'dev.host', type: 'string', default: '`127.0.0.1`', scope: 'pulse dev', description: 'Listener host.', precedence: '`--host` overrides this field.', security: 'Binding a non-loopback address exposes the local development server.', diagnostics: ['PULSE_DEV_HOST_INVALID'] }),
|
|
60
|
+
field({ section: 'dev', path: 'dev.port', type: 'integer', default: '`8787`', allowed: '`0`–`65535`; `0` requests an ephemeral port', scope: 'pulse dev', description: 'Listener port.', precedence: '`--port` overrides this field.', diagnostics: ['PULSE_DEV_PORT_INVALID'] }),
|
|
61
|
+
field({ section: 'dev', path: 'dev.watch', type: 'boolean', default: '`true`', scope: 'pulse dev', description: 'Watches the entry and schema dependency graph.', precedence: '`--watch` or `--no-watch` overrides this field.', security: 'Configuration-file changes still require a restart.' }),
|
|
62
|
+
field({ section: 'dev', path: 'dev.maxBodyBytes', type: 'positive safe integer', default: '`65_536`', scope: 'incoming local requests', description: 'Maximum request body accepted by the local development server.', security: 'Raise deliberately; this is a memory and request-amplification boundary.', diagnostics: ['PULSE_DEV_BODY_LIMIT_INVALID', 'PULSE_REQUEST_BODY_TOO_LARGE'] }),
|
|
63
|
+
field({ section: 'dev', path: 'dev.networkFetch', type: 'boolean', default: 'selected provider default', scope: 'local fetch realization', description: 'Allows unmatched local fetches to use the network.', precedence: 'This explicit field overrides the selected provider local default.', security: 'Keep disabled for deterministic or untrusted test inputs.', diagnostics: ['PULSE_FETCH_IMPLEMENTATION_UNAVAILABLE', 'PULSE_FETCH_NETWORK'] }),
|
|
64
|
+
field({ section: 'dev', path: 'dev.config', type: 'Readonly<Record<string, string>>', default: '`{}`', scope: 'local config capability', description: 'Local configuration-store values.', security: 'Development input only; it does not configure deployed provider stores.' }),
|
|
65
|
+
field({ section: 'dev', path: 'dev.secrets', type: 'Readonly<Record<string, string>>', default: '`{}`', scope: 'local secret capability', description: 'Local secret-store values.', security: 'Raw values are omitted from project JSON, diagnostics, and runtime error details; do not commit real production secrets.' }),
|
|
66
|
+
field({ section: 'dev', path: 'dev.kv', type: 'Readonly<Record<string, Readonly<Record<string, unknown>>>>', default: '`{}`', scope: 'local KV capability', description: 'Local KV stores keyed by configured logical store name.', security: 'Development input only; it is not deployed.' }),
|
|
67
|
+
field({ section: 'dev', path: 'dev.fetches', type: 'Readonly<Record<string, unknown>>', default: '`{}`', scope: 'local fetch capability', description: 'Deterministic fetch fixtures resolved before optional live network fetch.', security: 'Prefer fixtures for deterministic tests and offline development.' }),
|
|
68
|
+
...providerConfigFields.map(field)
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const CONFIG_DISCOVERY = Object.freeze(['.pulse/config.ts']);
|
|
72
|
+
|
|
73
|
+
const CONFIG_PRECEDENCE = Object.freeze([
|
|
74
|
+
'Discovery starts at the positional directory or the current working directory and searches upward for .pulse/config.ts.',
|
|
75
|
+
'Profile precedence is --profile, PULSE_PROFILE, then pulse.defaultProfile.',
|
|
76
|
+
'A selected-profile crypto declaration replaces pulse.crypto completely; array and object forms never merge.',
|
|
77
|
+
'Command-line output, host, port, and watch values override the corresponding active-profile field for that command. Entry, provider, and target remain project-configured.',
|
|
78
|
+
'Dedicated harness case inputs replace development inputs for that case; they do not configure deployment resources.',
|
|
79
|
+
'Provider deployment bindings come from the configured provider object; handler source never receives provider SDK objects.'
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
function deepFreeze(value) {
|
|
83
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
84
|
+
for (const entry of Object.values(value)) deepFreeze(entry);
|
|
85
|
+
return Object.freeze(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const CONFIG_RUNTIME_RULES = Object.freeze({
|
|
89
|
+
entry: Object.freeze({ kind: 'string', default: './src/index.ts', nonEmpty: true }),
|
|
90
|
+
provider: Object.freeze({ kind: 'provider-package', default: 'node', builtinValues: builtinProviderIds }),
|
|
91
|
+
outDir: Object.freeze({ kind: 'string', default: './dist', nonEmpty: true }),
|
|
92
|
+
reporting: Object.freeze({ kind: 'enum', default: DEFAULT_REPORTING_LEVEL, values: REPORTING_LEVEL_NAMES }),
|
|
93
|
+
crypto: Object.freeze({ kind: 'crypto-configuration', optional: true }),
|
|
94
|
+
'schemas.contentTypePolicy': Object.freeze({ kind: 'enum', default: 'accept-json-or-missing', values: Object.freeze(['accept-json-or-missing', 'require-json']) }),
|
|
95
|
+
'schemas.maxBytes': Object.freeze({ kind: 'positive-safe-integer', default: 65536 }),
|
|
96
|
+
'dev.host': Object.freeze({ kind: 'string', default: '127.0.0.1', nonEmpty: true }),
|
|
97
|
+
'dev.port': Object.freeze({ kind: 'integer', default: 8787, minimum: 0, maximum: 65535 }),
|
|
98
|
+
'dev.watch': Object.freeze({ kind: 'boolean', default: true }),
|
|
99
|
+
'dev.maxBodyBytes': Object.freeze({ kind: 'positive-safe-integer', default: 65536 }),
|
|
100
|
+
'harness.maxBodyBytes': Object.freeze({ kind: 'positive-safe-integer', optional: true }),
|
|
101
|
+
'harness.continuationTtlMs': Object.freeze({ kind: 'non-negative-safe-integer', optional: true })
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const PROJECT_CONFIG_JSON_SCHEMA = deepFreeze({
|
|
105
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
106
|
+
$id: 'urn:pulse:project-config:v1',
|
|
107
|
+
title: 'Pulse project configuration',
|
|
108
|
+
type: 'object',
|
|
109
|
+
additionalProperties: false,
|
|
110
|
+
properties: {
|
|
111
|
+
entry: { type: 'string', minLength: 1, default: './src/index.ts' },
|
|
112
|
+
provider: {
|
|
113
|
+
oneOf: [
|
|
114
|
+
{ type: 'string', minLength: 1 },
|
|
115
|
+
{
|
|
116
|
+
type: 'object',
|
|
117
|
+
required: ['kind'],
|
|
118
|
+
properties: { kind: { type: 'string', minLength: 1 } },
|
|
119
|
+
additionalProperties: true
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
default: 'node'
|
|
123
|
+
},
|
|
124
|
+
outDir: { type: 'string', minLength: 1, default: './dist' },
|
|
125
|
+
reporting: { enum: [...REPORTING_LEVEL_NAMES], default: DEFAULT_REPORTING_LEVEL },
|
|
126
|
+
crypto: {
|
|
127
|
+
oneOf: [
|
|
128
|
+
{
|
|
129
|
+
type: 'array',
|
|
130
|
+
uniqueItems: true,
|
|
131
|
+
items: { enum: [...CRYPTO_ALGORITHMS] }
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: 'object',
|
|
135
|
+
additionalProperties: false,
|
|
136
|
+
properties: Object.fromEntries(CRYPTO_ALGORITHMS.map((algorithm) => [
|
|
137
|
+
algorithm,
|
|
138
|
+
{
|
|
139
|
+
type: 'object',
|
|
140
|
+
additionalProperties: false,
|
|
141
|
+
properties: {
|
|
142
|
+
realization: { enum: [...cryptoRealizationIds] }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
]))
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
schemas: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
additionalProperties: false,
|
|
152
|
+
properties: {
|
|
153
|
+
contentTypePolicy: { enum: ['accept-json-or-missing', 'require-json'], default: 'accept-json-or-missing' },
|
|
154
|
+
maxBytes: { type: 'integer', minimum: 1, maximum: 9007199254740991, default: 65536 }
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
dev: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
additionalProperties: false,
|
|
160
|
+
properties: {
|
|
161
|
+
host: { type: 'string', minLength: 1, default: '127.0.0.1' },
|
|
162
|
+
port: { type: 'integer', minimum: 0, maximum: 65535, default: 8787 },
|
|
163
|
+
watch: { type: 'boolean', default: true },
|
|
164
|
+
maxBodyBytes: { type: 'integer', minimum: 1, maximum: 9007199254740991, default: 65536 },
|
|
165
|
+
networkFetch: { type: 'boolean' },
|
|
166
|
+
config: { type: 'object', additionalProperties: { type: 'string' } },
|
|
167
|
+
secrets: { type: 'object', additionalProperties: { type: 'string' } },
|
|
168
|
+
kv: { type: 'object', additionalProperties: { type: 'object' } },
|
|
169
|
+
fetches: { type: 'object' }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const CONFIG_CONTAINER_KEYS = deepFreeze({
|
|
176
|
+
'$': ['entry', 'provider', 'outDir', 'reporting', 'crypto', 'schemas', 'dev'],
|
|
177
|
+
'schemas': ['contentTypePolicy', 'maxBytes'],
|
|
178
|
+
'dev': ['host', 'port', 'watch', 'maxBodyBytes', 'networkFetch', 'config', 'secrets', 'kv', 'fetches']
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
function isRecord(value) {
|
|
182
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function validateProjectConfigStructure(input) {
|
|
186
|
+
const issues = [];
|
|
187
|
+
const issue = (path, message) => issues.push(Object.freeze({ path, message }));
|
|
188
|
+
const checkObject = (value, owner, path) => {
|
|
189
|
+
if (!isRecord(value)) { issue(path, 'must be an object'); return false; }
|
|
190
|
+
const allowed = new Set(CONFIG_CONTAINER_KEYS[owner]);
|
|
191
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) issue(path === '$' ? key : `${path}.${key}`, 'is not a supported configuration field');
|
|
192
|
+
return true;
|
|
193
|
+
};
|
|
194
|
+
if (!checkObject(input, '$', '$')) return Object.freeze(issues);
|
|
195
|
+
if (input.provider !== undefined && !(typeof input.provider === 'string' || (isRecord(input.provider) && typeof input.provider.kind === 'string'))) {
|
|
196
|
+
issue('provider', 'must be a bare provider id, exact scoped provider package name, or provider configuration object');
|
|
197
|
+
}
|
|
198
|
+
if (input.crypto !== undefined) {
|
|
199
|
+
try { normalizeCryptoConfiguration(input.crypto, { location: 'crypto' }); }
|
|
200
|
+
catch (error) { issue('crypto', error && error.message ? error.message : 'must be a valid crypto declaration'); }
|
|
201
|
+
}
|
|
202
|
+
if (input.schemas !== undefined) checkObject(input.schemas, 'schemas', 'schemas');
|
|
203
|
+
if (input.dev !== undefined) checkObject(input.dev, 'dev', 'dev');
|
|
204
|
+
return Object.freeze(issues);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function cloneDefault(value) {
|
|
208
|
+
if (Array.isArray(value)) return value.map(cloneDefault);
|
|
209
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, cloneDefault(entry)]));
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function configRuntimeRule(path) {
|
|
214
|
+
const rule = CONFIG_RUNTIME_RULES[path];
|
|
215
|
+
if (!rule) throw new Error(`Unknown project-config runtime rule ${path}`);
|
|
216
|
+
return rule;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function configDefault(path) {
|
|
220
|
+
const rule = configRuntimeRule(path);
|
|
221
|
+
if (!Object.prototype.hasOwnProperty.call(rule, 'default')) throw new Error(`Project-config rule ${path} has no default`);
|
|
222
|
+
return cloneDefault(rule.default);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validateConfigValue(path, value) {
|
|
226
|
+
const rule = configRuntimeRule(path);
|
|
227
|
+
if (value === undefined && rule.optional) return true;
|
|
228
|
+
if (rule.kind === 'string') return typeof value === 'string' && (!rule.nonEmpty || value.trim().length > 0);
|
|
229
|
+
if (rule.kind === 'boolean') return typeof value === 'boolean';
|
|
230
|
+
if (rule.kind === 'number') return typeof value === 'number' && Number.isFinite(value);
|
|
231
|
+
if (rule.kind === 'array') return Array.isArray(value);
|
|
232
|
+
if (rule.kind === 'enum') return rule.values.includes(value);
|
|
233
|
+
if (rule.kind === 'provider-package') return typeof value === 'string' && value.trim().length > 0;
|
|
234
|
+
if (rule.kind === 'crypto-configuration') {
|
|
235
|
+
try { normalizeCryptoConfiguration(value, { location: path }); return true; }
|
|
236
|
+
catch (_) { return false; }
|
|
237
|
+
}
|
|
238
|
+
if (rule.kind === 'integer') return Number.isInteger(value) && value >= rule.minimum && value <= rule.maximum;
|
|
239
|
+
if (rule.kind === 'positive-safe-integer') return Number.isSafeInteger(value) && value > 0;
|
|
240
|
+
if (rule.kind === 'non-negative-safe-integer') return Number.isSafeInteger(value) && value >= 0;
|
|
241
|
+
throw new Error(`Unsupported project-config runtime rule kind ${rule.kind}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function projectConfigSchemaDocument() {
|
|
245
|
+
return Object.freeze({
|
|
246
|
+
schemaVersion: PROJECT_CONFIG_SCHEMA_VERSION,
|
|
247
|
+
discovery: CONFIG_DISCOVERY,
|
|
248
|
+
precedence: CONFIG_PRECEDENCE,
|
|
249
|
+
sections: CONFIG_SECTIONS,
|
|
250
|
+
fields: CONFIG_FIELDS,
|
|
251
|
+
runtimeRules: CONFIG_RUNTIME_RULES,
|
|
252
|
+
jsonSchema: PROJECT_CONFIG_JSON_SCHEMA
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = Object.freeze({
|
|
257
|
+
PROJECT_CONFIG_SCHEMA_VERSION,
|
|
258
|
+
PROJECT_CONFIG_REFERENCE_VERSION,
|
|
259
|
+
CONFIG_SECTIONS,
|
|
260
|
+
CONFIG_FIELDS,
|
|
261
|
+
CONFIG_DISCOVERY,
|
|
262
|
+
CONFIG_PRECEDENCE,
|
|
263
|
+
CONFIG_RUNTIME_RULES,
|
|
264
|
+
PROJECT_CONFIG_JSON_SCHEMA,
|
|
265
|
+
CONFIG_CONTAINER_KEYS,
|
|
266
|
+
validateProjectConfigStructure,
|
|
267
|
+
configRuntimeRule,
|
|
268
|
+
configDefault,
|
|
269
|
+
validateConfigValue,
|
|
270
|
+
projectConfigSchemaDocument
|
|
271
|
+
});
|