@pulse-compute/wasm-contracts 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/README.md +35 -2
- package/package.json +124 -6
- package/src/assets/contracts.js +296 -0
- package/src/config/provider-resolution.js +54 -0
- package/src/config/runtime-provider-kv.js +73 -0
- package/src/config/symbolic-refs.js +192 -0
- package/src/crypto/contracts.js +647 -0
- package/src/definitions/config-schema.js +30 -0
- package/src/definitions/handler-roles.js +66 -0
- package/src/definitions/path-grammar.js +11 -0
- package/src/definitions/router-api.js +149 -0
- package/src/diagnostics/codes.js +42 -0
- package/src/diagnostics/reporter.js +20 -0
- package/src/diagnostics.js +139 -0
- package/src/dispatch-table.js +23 -0
- package/src/entities/catalog.d.ts +7 -0
- package/src/entities/catalog.js +87 -0
- package/src/entities/contracts.d.ts +95 -0
- package/src/entities/contracts.js +122 -0
- package/src/entities/json-rpc.d.ts +27 -0
- package/src/entities/json-rpc.js +69 -0
- package/src/entities/runtime.d.ts +67 -0
- package/src/entities/runtime.js +288 -0
- package/src/events/contracts.js +456 -0
- package/src/execution-plan.js +42 -0
- package/src/grip/contracts.js +106 -0
- package/src/handler/backend-json-request-body.js +58 -0
- package/src/handler/beta-lifecycle-golden.js +97 -0
- package/src/handler/canonical-native-plan.js +143 -0
- package/src/handler/canonical-native-runtime.js +168 -0
- package/src/handler/canonical-runtime.js +13 -0
- package/src/handler/context.js +131 -0
- package/src/handler/ctx-contract.js +71 -0
- package/src/handler/doctor-readiness-aggregator.js +67 -0
- package/src/handler/effects.js +446 -0
- package/src/handler/io-lifecycle.js +134 -0
- package/src/handler/lifecycle-authoring-alignment.js +56 -0
- package/src/handler/lifecycle-readiness-aggregate.js +93 -0
- package/src/handler/reference-node-lifecycle-golden.js +73 -0
- package/src/handler/request-json-body.js +147 -0
- package/src/handler/runtime-api.js +471 -0
- package/src/handler/schema-decode-result.js +135 -0
- package/src/handler/schema-response-codec.js +141 -0
- package/src/handler/surface-contract.js +601 -0
- package/src/handler/types-contract.js +51 -0
- package/src/handler-table.js +28 -0
- package/src/host/backend-capabilities.js +24 -0
- package/src/host/capabilities.js +273 -0
- package/src/host/channel-broadcaster.js +60 -0
- package/src/host/json-body.js +80 -0
- package/src/host/request-result-headers.js +38 -0
- package/src/host/streaming-passthrough.js +90 -0
- package/src/host/wasm-host-abi.js +154 -0
- package/src/index.js +75 -0
- package/src/jwt/contracts.js +427 -0
- package/src/library/api-surface.js +99 -0
- package/src/library/contracts.js +273 -0
- package/src/library/manifest.js +317 -0
- package/src/library/second-package-owned-lowering.js +39 -0
- package/src/library/sidecars.js +147 -0
- package/src/logging.js +89 -0
- package/src/package/package-contract.js +975 -0
- package/src/package/package-contract.schema.json +303 -0
- package/src/package/runtime-pulse-live.js +100 -0
- package/src/path-table.js +12 -0
- package/src/path.js +275 -0
- package/src/project/config-factory.js +48 -0
- package/src/project/config-plan.js +409 -0
- package/src/project/config-runtime.js +46 -0
- package/src/project/javascript-application.js +212 -0
- package/src/project/javascript-core-capabilities.js +44 -0
- package/src/project/native-optimization.js +9 -0
- package/src/project/package-ownership.js +242 -0
- package/src/project/package-product-projection.js +171 -0
- package/src/project/reachable-graph-identity.js +219 -0
- package/src/project/reachable-graph-projections.js +279 -0
- package/src/project/reachable-graph-resolver.js +303 -0
- package/src/project/reachable-graph-v2.js +430 -0
- package/src/project/reachable-graph-v2.schema.json +285 -0
- package/src/project/reachable-graph.js +199 -0
- package/src/project/reachable-graph.schema.json +222 -0
- package/src/project/target-support-evidence.js +559 -0
- package/src/provider/canonical-provider.js +239 -0
- package/src/provider/final-wasm-policy.js +87 -0
- package/src/provider/toolchain.js +771 -0
- package/src/route-plan.js +43 -0
- package/src/s3/contracts.js +12 -0
- package/src/schema-json/compile.js +63 -0
- package/src/schema-json/contracts.js +10 -0
- package/src/schema-json/generic-parser.js +62 -0
- package/src/schema-json/registry.js +359 -0
- package/src/schema-json/semantic-trace.js +241 -0
- package/src/schema-json/v1.js +65 -0
- package/src/schema-json/v2.js +110 -0
- package/src/sha256.js +104 -0
- package/src/spine.js +381 -0
- package/src/stable-id.js +142 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { stableStringify, sha256Hex } = require('../stable-id.js');
|
|
4
|
+
|
|
5
|
+
const CRYPTO_CONFIGURATION_VERSION = 'pulse.crypto-configuration.v1';
|
|
6
|
+
const CRYPTO_TARGET_CAPABILITIES_VERSION = 'pulse.crypto-target-capabilities.v1';
|
|
7
|
+
const CRYPTO_REQUIREMENTS_VERSION = 'pulse.crypto-requirements.v1';
|
|
8
|
+
const CRYPTO_REALIZATION_PLAN_VERSION = 'pulse.crypto-realization-plan.v1';
|
|
9
|
+
const CRYPTO_NATIVE_GUEST_SOURCE_PLAN_VERSION = 'pulse.crypto-native-guest-source-plan.v1';
|
|
10
|
+
const CRYPTO_SEMANTIC_OWNER = '@pulse-compute/crypto';
|
|
11
|
+
const CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION = 'webcrypto.subtle.hmac-sha-256.v1';
|
|
12
|
+
const CRYPTO_GUEST_SOURCE_IMPLEMENTATION = 'pulse-hmac-as.v1';
|
|
13
|
+
const CRYPTO_ES256_CONTRACT_VERSION = 'pulse.crypto.es256-verification.v1';
|
|
14
|
+
const CRYPTO_ES256_RUNTIME_BUILTIN_IMPLEMENTATION = 'webcrypto.subtle.ecdsa-p256-sha-256.v1';
|
|
15
|
+
const CRYPTO_ES256_GUEST_LINKED_REALIZATION = 'guest-linked:pulse-es256-rustcrypto-p256';
|
|
16
|
+
const CRYPTO_ES256_GUEST_LINKED_IMPLEMENTATION =
|
|
17
|
+
'rustcrypto.p256-0.13.2.ecdsa-0.16.9.sha2-0.10.9.v1';
|
|
18
|
+
const CRYPTO_VERIFICATION_STATUSES = Object.freeze([
|
|
19
|
+
'valid',
|
|
20
|
+
'invalid-authenticator',
|
|
21
|
+
'invalid-key',
|
|
22
|
+
'invalid-input',
|
|
23
|
+
'realization-failure'
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const CRYPTO_ALGORITHMS = Object.freeze(['HS256', 'ES256', 'SHA-256', 'HMAC-SHA256']);
|
|
27
|
+
const CRYPTO_REALIZATIONS = Object.freeze([
|
|
28
|
+
...['SHA-256', 'HMAC-SHA256'].map((algorithm) => Object.freeze({
|
|
29
|
+
id: 'runtime-builtin', kind: 'runtime-builtin', algorithm, targets: Object.freeze(['javascript']),
|
|
30
|
+
implementation: algorithm === 'SHA-256' ? 'webcrypto.subtle.sha-256-bytes.v1' : 'webcrypto.subtle.hmac-sha-256-bytes.v1'
|
|
31
|
+
})),
|
|
32
|
+
...['SHA-256', 'HMAC-SHA256'].map((algorithm) => Object.freeze({
|
|
33
|
+
id: 'guest-source:pulse-hmac-as', kind: 'guest-source', backend: 'pulse-hmac-as',
|
|
34
|
+
implementation: CRYPTO_GUEST_SOURCE_IMPLEMENTATION, algorithm, targets: Object.freeze(['native']),
|
|
35
|
+
source: Object.freeze({ package: '@pulse-compute/crypto', export: './pulsewasm-native',
|
|
36
|
+
contractVersion: 'pulse.crypto-guest-source.v1', contribution: 'pulse-hmac-as' })
|
|
37
|
+
})),
|
|
38
|
+
Object.freeze({
|
|
39
|
+
id: 'runtime-builtin',
|
|
40
|
+
kind: 'runtime-builtin',
|
|
41
|
+
implementation: CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION,
|
|
42
|
+
algorithm: 'HS256',
|
|
43
|
+
targets: Object.freeze(['javascript'])
|
|
44
|
+
}),
|
|
45
|
+
Object.freeze({
|
|
46
|
+
id: 'guest-source:pulse-hmac-as',
|
|
47
|
+
kind: 'guest-source',
|
|
48
|
+
backend: 'pulse-hmac-as',
|
|
49
|
+
implementation: CRYPTO_GUEST_SOURCE_IMPLEMENTATION,
|
|
50
|
+
algorithm: 'HS256',
|
|
51
|
+
targets: Object.freeze(['native']),
|
|
52
|
+
source: Object.freeze({
|
|
53
|
+
package: '@pulse-compute/crypto',
|
|
54
|
+
export: './pulsewasm-native',
|
|
55
|
+
contractVersion: 'pulse.crypto-guest-source.v1',
|
|
56
|
+
contribution: 'pulse-hmac-as'
|
|
57
|
+
})
|
|
58
|
+
}),
|
|
59
|
+
Object.freeze({
|
|
60
|
+
id: 'runtime-builtin',
|
|
61
|
+
kind: 'runtime-builtin',
|
|
62
|
+
implementation: CRYPTO_ES256_RUNTIME_BUILTIN_IMPLEMENTATION,
|
|
63
|
+
algorithm: 'ES256',
|
|
64
|
+
targets: Object.freeze(['javascript'])
|
|
65
|
+
}),
|
|
66
|
+
Object.freeze({
|
|
67
|
+
id: CRYPTO_ES256_GUEST_LINKED_REALIZATION,
|
|
68
|
+
kind: 'guest-linked',
|
|
69
|
+
backend: 'pulse-es256-rustcrypto-p256',
|
|
70
|
+
implementation: CRYPTO_ES256_GUEST_LINKED_IMPLEMENTATION,
|
|
71
|
+
algorithm: 'ES256',
|
|
72
|
+
targets: Object.freeze(['native']),
|
|
73
|
+
source: Object.freeze({
|
|
74
|
+
package: '@pulse-compute/crypto',
|
|
75
|
+
export: './pulsewasm-native',
|
|
76
|
+
contractVersion: 'pulse.guest-unit-contribution.v1',
|
|
77
|
+
contribution: 'pulseEs256GuestUnit'
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
const algorithmSet = new Set(CRYPTO_ALGORITHMS);
|
|
83
|
+
const realizationsById = new Map();
|
|
84
|
+
for (const entry of CRYPTO_REALIZATIONS) {
|
|
85
|
+
const records = realizationsById.get(entry.id) || [];
|
|
86
|
+
records.push(entry);
|
|
87
|
+
realizationsById.set(entry.id, records);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function cryptoError(code, message, details = {}) {
|
|
91
|
+
const error = new TypeError(message);
|
|
92
|
+
error.code = code;
|
|
93
|
+
error.details = Object.freeze({ ...details });
|
|
94
|
+
return error;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isPlainObject(value) {
|
|
98
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
99
|
+
const prototype = Object.getPrototypeOf(value);
|
|
100
|
+
return prototype === Object.prototype || prototype === null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function deepFreeze(value) {
|
|
104
|
+
if (Array.isArray(value)) {
|
|
105
|
+
for (const child of value) deepFreeze(child);
|
|
106
|
+
return Object.freeze(value);
|
|
107
|
+
}
|
|
108
|
+
if (isPlainObject(value)) {
|
|
109
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
110
|
+
return Object.freeze(value);
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const CRYPTO_ES256_CONTRACT = deepFreeze({
|
|
116
|
+
version: CRYPTO_ES256_CONTRACT_VERSION,
|
|
117
|
+
status: 'executable-g3',
|
|
118
|
+
semanticOwner: CRYPTO_SEMANTIC_OWNER,
|
|
119
|
+
algorithm: 'ES256',
|
|
120
|
+
primitive: {
|
|
121
|
+
kind: 'ecdsa',
|
|
122
|
+
curve: 'P-256',
|
|
123
|
+
hash: 'SHA-256'
|
|
124
|
+
},
|
|
125
|
+
operation: 'verify',
|
|
126
|
+
request: {
|
|
127
|
+
function: 'crypto.signature.verify',
|
|
128
|
+
keyType: 'p256-public-key-bytes',
|
|
129
|
+
publicKeyEncoding: 'raw-x-y-big-endian',
|
|
130
|
+
publicKeyBytes: 64,
|
|
131
|
+
dataBytesMaximum: 16340,
|
|
132
|
+
signatureEncoding: 'jose-r-s-big-endian',
|
|
133
|
+
signatureBytes: 64,
|
|
134
|
+
derAccepted: false
|
|
135
|
+
},
|
|
136
|
+
runtimeBuiltin: {
|
|
137
|
+
realization: 'runtime-builtin',
|
|
138
|
+
implementation: CRYPTO_ES256_RUNTIME_BUILTIN_IMPLEMENTATION,
|
|
139
|
+
eligibleTargets: ['node-javascript', 'fastly-javascript']
|
|
140
|
+
},
|
|
141
|
+
native: {
|
|
142
|
+
realization: CRYPTO_ES256_GUEST_LINKED_REALIZATION,
|
|
143
|
+
implementation: CRYPTO_ES256_GUEST_LINKED_IMPLEMENTATION,
|
|
144
|
+
eligibleTargets: ['node-native', 'fastly-native']
|
|
145
|
+
},
|
|
146
|
+
guest: {
|
|
147
|
+
unitVersion: 'pulse.guest-unit.v2',
|
|
148
|
+
unitId: 'pulse.crypto.es256.rustcrypto-p256.v1',
|
|
149
|
+
module: 'pulse_crypto_es256',
|
|
150
|
+
owner: '@pulse-compute/crypto',
|
|
151
|
+
abi: 'pulse.crypto.es256.verify.v1',
|
|
152
|
+
export: {
|
|
153
|
+
name: 'pulse_crypto_es256_verify',
|
|
154
|
+
parameters: ['i32', 'i32'],
|
|
155
|
+
results: ['i32']
|
|
156
|
+
},
|
|
157
|
+
origin: 'package-prebuilt',
|
|
158
|
+
sourceBuildDuringApplicationBuild: false,
|
|
159
|
+
memory: {
|
|
160
|
+
identity: 'pulse.guest-memory.invocation-frame.v2',
|
|
161
|
+
import: 'env.memory',
|
|
162
|
+
minimumPages: 32,
|
|
163
|
+
maximumPages: 32,
|
|
164
|
+
growable: false
|
|
165
|
+
},
|
|
166
|
+
start: 'forbidden',
|
|
167
|
+
hostCallbacks: false,
|
|
168
|
+
allocator: false
|
|
169
|
+
},
|
|
170
|
+
frame: {
|
|
171
|
+
identity: 'pulse.crypto.es256.invocation-frame.v2',
|
|
172
|
+
byteOrder: 'little-endian',
|
|
173
|
+
pointerWidthBits: 32,
|
|
174
|
+
magic: 0x32534550,
|
|
175
|
+
version: 2,
|
|
176
|
+
headerBytes: 64,
|
|
177
|
+
capacityBytes: 16640,
|
|
178
|
+
alignmentBytes: 16,
|
|
179
|
+
signingInputBytesMaximum: 16340,
|
|
180
|
+
publicKeyBytes: 64,
|
|
181
|
+
signatureBytes: 64,
|
|
182
|
+
rawPayloadBytesMaximum: 16468,
|
|
183
|
+
reservedHeaderAndAlignmentBytes: 172,
|
|
184
|
+
canonicalOffsets: {
|
|
185
|
+
publicKey: 64,
|
|
186
|
+
signature: 128,
|
|
187
|
+
signingInput: 192
|
|
188
|
+
},
|
|
189
|
+
header: {
|
|
190
|
+
magic: 0,
|
|
191
|
+
version: 4,
|
|
192
|
+
headerLength: 8,
|
|
193
|
+
totalLength: 12,
|
|
194
|
+
algorithm: 16,
|
|
195
|
+
flags: 20,
|
|
196
|
+
signingInputOffset: 24,
|
|
197
|
+
signingInputLength: 28,
|
|
198
|
+
publicKeyOffset: 32,
|
|
199
|
+
publicKeyLength: 36,
|
|
200
|
+
signatureOffset: 40,
|
|
201
|
+
signatureLength: 44,
|
|
202
|
+
reserved0: 48,
|
|
203
|
+
reserved1: 52,
|
|
204
|
+
reserved2: 56,
|
|
205
|
+
reserved3: 60
|
|
206
|
+
},
|
|
207
|
+
algorithmCode: 1,
|
|
208
|
+
flags: 0,
|
|
209
|
+
reservedWordCount: 4,
|
|
210
|
+
reservedWordValue: 0,
|
|
211
|
+
callerOwned: true,
|
|
212
|
+
synchronous: true,
|
|
213
|
+
retainable: false,
|
|
214
|
+
clearCapacityAfterReturnOrTrap: true
|
|
215
|
+
},
|
|
216
|
+
resultStatuses: CRYPTO_VERIFICATION_STATUSES,
|
|
217
|
+
resultCodes: {
|
|
218
|
+
valid: 1,
|
|
219
|
+
invalidAuthenticator: 0,
|
|
220
|
+
invalidKey: -1,
|
|
221
|
+
invalidInput: -2,
|
|
222
|
+
realizationFailure: -3
|
|
223
|
+
},
|
|
224
|
+
automaticFallback: false
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
function requireAlgorithm(value, location) {
|
|
228
|
+
if (typeof value !== 'string' || !algorithmSet.has(value)) {
|
|
229
|
+
throw cryptoError(
|
|
230
|
+
'PULSE_CRYPTO_ALGORITHM_UNKNOWN',
|
|
231
|
+
`${location} must be a known canonical crypto algorithm.`,
|
|
232
|
+
{ location, algorithm: value, supportedAlgorithms: CRYPTO_ALGORITHMS }
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function requireKnownRealization(value, location) {
|
|
239
|
+
if (typeof value !== 'string' || !realizationsById.has(value)) {
|
|
240
|
+
throw cryptoError(
|
|
241
|
+
'PULSE_CRYPTO_REALIZATION_UNKNOWN',
|
|
242
|
+
`${location} must name an exact known crypto realization.`,
|
|
243
|
+
{
|
|
244
|
+
location,
|
|
245
|
+
realization: value,
|
|
246
|
+
knownRealizations: CRYPTO_REALIZATIONS.map((entry) => entry.id)
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return value;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function knownRealization(realization, algorithm, target) {
|
|
254
|
+
const records = realizationsById.get(realization) || [];
|
|
255
|
+
return records.find((entry) => (
|
|
256
|
+
entry.algorithm === algorithm
|
|
257
|
+
&& (target === undefined || entry.targets.includes(target))
|
|
258
|
+
));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function canonicalConfiguration(form, algorithms) {
|
|
262
|
+
return deepFreeze({
|
|
263
|
+
version: CRYPTO_CONFIGURATION_VERSION,
|
|
264
|
+
form,
|
|
265
|
+
algorithms: algorithms
|
|
266
|
+
.map((entry) => ({ algorithm: entry.algorithm, realization: entry.realization || null }))
|
|
267
|
+
.sort((left, right) => left.algorithm.localeCompare(right.algorithm))
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function normalizeCryptoConfiguration(value, options = {}) {
|
|
272
|
+
const location = options.location || 'crypto';
|
|
273
|
+
if (Array.isArray(value)) {
|
|
274
|
+
const seen = new Set();
|
|
275
|
+
const algorithms = value.map((entry, index) => {
|
|
276
|
+
const algorithm = requireAlgorithm(entry, `${location}[${index}]`);
|
|
277
|
+
if (seen.has(algorithm)) {
|
|
278
|
+
throw cryptoError(
|
|
279
|
+
'PULSE_CRYPTO_ALGORITHM_DUPLICATE',
|
|
280
|
+
`${location} contains duplicate algorithm ${algorithm}.`,
|
|
281
|
+
{ location, algorithm }
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
seen.add(algorithm);
|
|
285
|
+
return { algorithm, realization: null };
|
|
286
|
+
});
|
|
287
|
+
return canonicalConfiguration('array', algorithms);
|
|
288
|
+
}
|
|
289
|
+
if (isPlainObject(value)) {
|
|
290
|
+
const algorithms = Object.keys(value).map((algorithm) => {
|
|
291
|
+
requireAlgorithm(algorithm, `${location}.${algorithm}`);
|
|
292
|
+
const selection = value[algorithm];
|
|
293
|
+
if (!isPlainObject(selection)) {
|
|
294
|
+
throw cryptoError(
|
|
295
|
+
'PULSE_CRYPTO_REALIZATION_SELECTION_OBJECT_REQUIRED',
|
|
296
|
+
`${location}.${algorithm} must be an object.`,
|
|
297
|
+
{ location: `${location}.${algorithm}`, valueType: Array.isArray(selection) ? 'array' : typeof selection }
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const keys = Object.keys(selection);
|
|
301
|
+
const unsupported = keys.filter((key) => key !== 'realization').sort();
|
|
302
|
+
if (unsupported.length > 0) {
|
|
303
|
+
throw cryptoError(
|
|
304
|
+
'PULSE_CRYPTO_REALIZATION_SELECTION_KEY_UNSUPPORTED',
|
|
305
|
+
`${location}.${algorithm} contains unsupported selection keys.`,
|
|
306
|
+
{ location: `${location}.${algorithm}`, keys: unsupported, allowed: Object.freeze(['realization']) }
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (Object.prototype.hasOwnProperty.call(selection, 'realization') && selection.realization === undefined) {
|
|
310
|
+
throw cryptoError(
|
|
311
|
+
'PULSE_CRYPTO_REALIZATION_UNKNOWN',
|
|
312
|
+
`${location}.${algorithm}.realization must name an exact known crypto realization.`,
|
|
313
|
+
{ location: `${location}.${algorithm}.realization`, realization: selection.realization }
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
algorithm,
|
|
318
|
+
realization: Object.prototype.hasOwnProperty.call(selection, 'realization')
|
|
319
|
+
? requireKnownRealization(selection.realization, `${location}.${algorithm}.realization`)
|
|
320
|
+
: null
|
|
321
|
+
};
|
|
322
|
+
});
|
|
323
|
+
return canonicalConfiguration('object', algorithms);
|
|
324
|
+
}
|
|
325
|
+
throw cryptoError(
|
|
326
|
+
'PULSE_CRYPTO_CONFIG_SHAPE_INVALID',
|
|
327
|
+
`${location} must use the array or object form.`,
|
|
328
|
+
{ location, valueType: value === null ? 'null' : typeof value }
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function emptyCryptoConfiguration() {
|
|
333
|
+
return canonicalConfiguration('implicit', []);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function normalizeCryptoConfigurationDocument(value) {
|
|
337
|
+
if (!isPlainObject(value) || value.version !== CRYPTO_CONFIGURATION_VERSION) {
|
|
338
|
+
throw cryptoError('PULSE_CRYPTO_CONFIG_SHAPE_INVALID', 'Crypto configuration document is invalid.');
|
|
339
|
+
}
|
|
340
|
+
if (!['array', 'object', 'implicit'].includes(value.form) || !Array.isArray(value.algorithms)) {
|
|
341
|
+
throw cryptoError('PULSE_CRYPTO_CONFIG_SHAPE_INVALID', 'Crypto configuration document has an invalid canonical shape.');
|
|
342
|
+
}
|
|
343
|
+
const seen = new Set();
|
|
344
|
+
const algorithms = value.algorithms.map((entry, index) => {
|
|
345
|
+
if (!isPlainObject(entry)) {
|
|
346
|
+
throw cryptoError('PULSE_CRYPTO_CONFIG_SHAPE_INVALID', 'Crypto configuration algorithm entries must be objects.', { index });
|
|
347
|
+
}
|
|
348
|
+
const algorithm = requireAlgorithm(entry.algorithm, `crypto configuration.algorithms[${index}].algorithm`);
|
|
349
|
+
if (seen.has(algorithm)) {
|
|
350
|
+
throw cryptoError('PULSE_CRYPTO_ALGORITHM_DUPLICATE', `Crypto configuration contains duplicate algorithm ${algorithm}.`, { algorithm });
|
|
351
|
+
}
|
|
352
|
+
seen.add(algorithm);
|
|
353
|
+
return {
|
|
354
|
+
algorithm,
|
|
355
|
+
realization: entry.realization === null
|
|
356
|
+
? null
|
|
357
|
+
: requireKnownRealization(entry.realization, `crypto configuration.algorithms[${index}].realization`)
|
|
358
|
+
};
|
|
359
|
+
});
|
|
360
|
+
return canonicalConfiguration(value.form, algorithms);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function normalizeCryptoRequirements(inputs = []) {
|
|
364
|
+
if (!Array.isArray(inputs)) {
|
|
365
|
+
throw cryptoError('PULSE_CRYPTO_REQUIREMENTS_ARRAY_REQUIRED', 'Crypto requirements must be an array.');
|
|
366
|
+
}
|
|
367
|
+
const requestedByAlgorithm = new Map();
|
|
368
|
+
for (const [index, input] of inputs.entries()) {
|
|
369
|
+
if (!isPlainObject(input)) {
|
|
370
|
+
throw cryptoError(
|
|
371
|
+
'PULSE_CRYPTO_REQUIREMENT_OBJECT_REQUIRED',
|
|
372
|
+
`Crypto requirement ${index + 1} must be an object.`,
|
|
373
|
+
{ index }
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
const algorithm = requireAlgorithm(input.algorithm, `crypto requirements[${index}].algorithm`);
|
|
377
|
+
if (
|
|
378
|
+
input.semanticOwner !== undefined
|
|
379
|
+
&& (
|
|
380
|
+
typeof input.semanticOwner !== 'string'
|
|
381
|
+
|| input.semanticOwner.trim() !== CRYPTO_SEMANTIC_OWNER
|
|
382
|
+
)
|
|
383
|
+
) {
|
|
384
|
+
throw cryptoError(
|
|
385
|
+
'PULSE_CRYPTO_REQUIREMENT_SEMANTIC_OWNER_INVALID',
|
|
386
|
+
`Crypto requirement ${index + 1} must name ${CRYPTO_SEMANTIC_OWNER} as semantic owner.`,
|
|
387
|
+
{ index, algorithm, semanticOwner: input.semanticOwner }
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
if (typeof input.requestedBy !== 'string' || input.requestedBy.trim().length === 0) {
|
|
391
|
+
throw cryptoError(
|
|
392
|
+
'PULSE_CRYPTO_REQUIREMENT_OWNER_REQUIRED',
|
|
393
|
+
`Crypto requirement ${index + 1} must identify its requesting package.`,
|
|
394
|
+
{ index, algorithm }
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const owners = requestedByAlgorithm.get(algorithm) || new Set();
|
|
398
|
+
owners.add(input.requestedBy.trim());
|
|
399
|
+
requestedByAlgorithm.set(algorithm, owners);
|
|
400
|
+
}
|
|
401
|
+
const algorithms = Array.from(requestedByAlgorithm, ([algorithm, owners]) => deepFreeze({
|
|
402
|
+
algorithm,
|
|
403
|
+
requestedBy: Array.from(owners).sort()
|
|
404
|
+
})).sort((left, right) => left.algorithm.localeCompare(right.algorithm));
|
|
405
|
+
const document = {
|
|
406
|
+
version: CRYPTO_REQUIREMENTS_VERSION,
|
|
407
|
+
semanticOwner: CRYPTO_SEMANTIC_OWNER,
|
|
408
|
+
algorithms
|
|
409
|
+
};
|
|
410
|
+
return deepFreeze({
|
|
411
|
+
...document,
|
|
412
|
+
requirementsHash: sha256Hex(stableStringify(document))
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function normalizeCryptoRequirementsDocument(value) {
|
|
417
|
+
if (!isPlainObject(value) || value.version !== CRYPTO_REQUIREMENTS_VERSION || !Array.isArray(value.algorithms)) {
|
|
418
|
+
throw cryptoError('PULSE_CRYPTO_REQUIREMENTS_ARRAY_REQUIRED', 'Crypto requirements document is invalid.');
|
|
419
|
+
}
|
|
420
|
+
const flattened = [];
|
|
421
|
+
if (
|
|
422
|
+
value.semanticOwner !== undefined
|
|
423
|
+
&& value.semanticOwner !== CRYPTO_SEMANTIC_OWNER
|
|
424
|
+
) {
|
|
425
|
+
throw cryptoError(
|
|
426
|
+
'PULSE_CRYPTO_REQUIREMENT_SEMANTIC_OWNER_INVALID',
|
|
427
|
+
`Crypto requirements must name ${CRYPTO_SEMANTIC_OWNER} as semantic owner.`,
|
|
428
|
+
{ semanticOwner: value.semanticOwner }
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
for (const [index, entry] of value.algorithms.entries()) {
|
|
432
|
+
if (!isPlainObject(entry) || !Array.isArray(entry.requestedBy)) {
|
|
433
|
+
throw cryptoError('PULSE_CRYPTO_REQUIREMENT_OBJECT_REQUIRED', 'Crypto requirement document entry is invalid.', { index });
|
|
434
|
+
}
|
|
435
|
+
for (const requestedBy of entry.requestedBy) flattened.push({
|
|
436
|
+
algorithm: entry.algorithm,
|
|
437
|
+
requestedBy,
|
|
438
|
+
semanticOwner: CRYPTO_SEMANTIC_OWNER
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
return normalizeCryptoRequirements(flattened);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function defineCryptoTargetCapabilities(input) {
|
|
445
|
+
if (!isPlainObject(input)) {
|
|
446
|
+
throw cryptoError('PULSE_CRYPTO_TARGET_CAPABILITIES_OBJECT_REQUIRED', 'Crypto target capabilities must be an object.');
|
|
447
|
+
}
|
|
448
|
+
const target = typeof input.target === 'string' && input.target.trim() ? input.target.trim() : null;
|
|
449
|
+
if (!target) throw cryptoError('PULSE_CRYPTO_TARGET_REQUIRED', 'Crypto target capabilities must identify a target.');
|
|
450
|
+
if (!Array.isArray(input.algorithms)) {
|
|
451
|
+
throw cryptoError('PULSE_CRYPTO_TARGET_ALGORITHMS_ARRAY_REQUIRED', 'Crypto target algorithms must be an array.', { target });
|
|
452
|
+
}
|
|
453
|
+
const seen = new Set();
|
|
454
|
+
const algorithms = input.algorithms.map((entry, index) => {
|
|
455
|
+
if (!isPlainObject(entry)) {
|
|
456
|
+
throw cryptoError('PULSE_CRYPTO_TARGET_ALGORITHM_OBJECT_REQUIRED', 'Crypto target algorithm entries must be objects.', { target, index });
|
|
457
|
+
}
|
|
458
|
+
const algorithm = requireAlgorithm(entry.algorithm, `crypto target ${target}.algorithms[${index}]`);
|
|
459
|
+
if (seen.has(algorithm)) {
|
|
460
|
+
throw cryptoError('PULSE_CRYPTO_TARGET_ALGORITHM_DUPLICATE', `Crypto target ${target} declares ${algorithm} more than once.`, { target, algorithm });
|
|
461
|
+
}
|
|
462
|
+
seen.add(algorithm);
|
|
463
|
+
const realization = requireKnownRealization(entry.realization, `crypto target ${target}.${algorithm}.realization`);
|
|
464
|
+
const known = knownRealization(realization, algorithm, target);
|
|
465
|
+
if (!known) {
|
|
466
|
+
throw cryptoError(
|
|
467
|
+
'PULSE_CRYPTO_REALIZATION_PIN_INVALID',
|
|
468
|
+
`Crypto realization ${realization} is incompatible with ${algorithm} on target ${target}.`,
|
|
469
|
+
{ target, algorithm, realization }
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
algorithm,
|
|
474
|
+
realization,
|
|
475
|
+
kind: known.kind,
|
|
476
|
+
...(known.backend ? { backend: known.backend } : {}),
|
|
477
|
+
implementation: known.implementation,
|
|
478
|
+
implemented: entry.implemented === true,
|
|
479
|
+
status: typeof entry.status === 'string' ? entry.status : (entry.implemented === true ? 'implemented' : 'planned')
|
|
480
|
+
};
|
|
481
|
+
}).sort((left, right) => left.algorithm.localeCompare(right.algorithm));
|
|
482
|
+
return deepFreeze({
|
|
483
|
+
version: CRYPTO_TARGET_CAPABILITIES_VERSION,
|
|
484
|
+
target,
|
|
485
|
+
automaticFallback: false,
|
|
486
|
+
algorithms
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function targetCryptoCapabilities(targetDescriptor, target) {
|
|
491
|
+
const capabilities = targetDescriptor && targetDescriptor.crypto;
|
|
492
|
+
if (!capabilities || capabilities.version !== CRYPTO_TARGET_CAPABILITIES_VERSION) {
|
|
493
|
+
throw cryptoError(
|
|
494
|
+
'PULSE_CRYPTO_REALIZATION_UNAVAILABLE',
|
|
495
|
+
`Selected target ${target} does not declare crypto realization capabilities.`,
|
|
496
|
+
{ target }
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
if (capabilities.target !== target) {
|
|
500
|
+
throw cryptoError(
|
|
501
|
+
'PULSE_CRYPTO_REALIZATION_UNAVAILABLE',
|
|
502
|
+
`Selected target ${target} has a mismatched crypto capability declaration.`,
|
|
503
|
+
{ target, declaredTarget: capabilities.target }
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
return defineCryptoTargetCapabilities(capabilities);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function planCryptoRealizations(input = {}) {
|
|
510
|
+
const declaration = input.declaration && input.declaration.version === CRYPTO_CONFIGURATION_VERSION
|
|
511
|
+
? normalizeCryptoConfigurationDocument(input.declaration)
|
|
512
|
+
: emptyCryptoConfiguration();
|
|
513
|
+
const requirements = input.requirements && input.requirements.version === CRYPTO_REQUIREMENTS_VERSION
|
|
514
|
+
? normalizeCryptoRequirementsDocument(input.requirements)
|
|
515
|
+
: normalizeCryptoRequirements(input.requirements || []);
|
|
516
|
+
const target = typeof input.target === 'string' && input.target.trim() ? input.target.trim() : null;
|
|
517
|
+
const profile = typeof input.profile === 'string' && input.profile.trim() ? input.profile.trim() : null;
|
|
518
|
+
const profileProvenance = deepFreeze({
|
|
519
|
+
selectedProfile: profile,
|
|
520
|
+
selectionSource: typeof input.profileSelectionSource === 'string' && input.profileSelectionSource.trim()
|
|
521
|
+
? input.profileSelectionSource.trim()
|
|
522
|
+
: null,
|
|
523
|
+
cryptoSource: typeof input.configurationSource === 'string' && input.configurationSource.trim()
|
|
524
|
+
? input.configurationSource.trim()
|
|
525
|
+
: 'implicit'
|
|
526
|
+
});
|
|
527
|
+
const declaredByAlgorithm = new Map(declaration.algorithms.map((entry) => [entry.algorithm, entry]));
|
|
528
|
+
|
|
529
|
+
for (const requirement of requirements.algorithms) {
|
|
530
|
+
if (!declaredByAlgorithm.has(requirement.algorithm)) {
|
|
531
|
+
throw cryptoError(
|
|
532
|
+
declaration.algorithms.length === 0 ? 'PULSE_CRYPTO_CONFIG_REQUIRED' : 'PULSE_CRYPTO_ALGORITHM_UNDECLARED',
|
|
533
|
+
`Crypto algorithm ${requirement.algorithm}, requested by ${requirement.requestedBy.join(', ')}, is not declared by profile ${profile || '<unknown>'}.`,
|
|
534
|
+
{
|
|
535
|
+
profile,
|
|
536
|
+
target,
|
|
537
|
+
algorithm: requirement.algorithm,
|
|
538
|
+
requestedBy: requirement.requestedBy,
|
|
539
|
+
semanticOwner: requirements.semanticOwner,
|
|
540
|
+
profileProvenance,
|
|
541
|
+
declaredAlgorithms: declaration.algorithms.map((entry) => entry.algorithm)
|
|
542
|
+
}
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (declaration.algorithms.length === 0) {
|
|
548
|
+
const document = {
|
|
549
|
+
version: CRYPTO_REALIZATION_PLAN_VERSION,
|
|
550
|
+
semanticOwner: requirements.semanticOwner,
|
|
551
|
+
requirementsHash: requirements.requirementsHash,
|
|
552
|
+
profile,
|
|
553
|
+
profileProvenance,
|
|
554
|
+
target,
|
|
555
|
+
algorithms: Object.freeze([]),
|
|
556
|
+
automaticFallback: false
|
|
557
|
+
};
|
|
558
|
+
return deepFreeze({ ...document, planHash: sha256Hex(stableStringify(document)) });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const targetCapabilities = targetCryptoCapabilities(input.targetDescriptor, target);
|
|
562
|
+
const targetByAlgorithm = new Map(targetCapabilities.algorithms.map((entry) => [entry.algorithm, entry]));
|
|
563
|
+
const planned = declaration.algorithms.map((configured) => {
|
|
564
|
+
const targetCapability = targetByAlgorithm.get(configured.algorithm);
|
|
565
|
+
if (!targetCapability) {
|
|
566
|
+
throw cryptoError(
|
|
567
|
+
'PULSE_CRYPTO_REALIZATION_UNAVAILABLE',
|
|
568
|
+
`Selected target ${target} has no ${configured.algorithm} realization.`,
|
|
569
|
+
{ profile, target, algorithm: configured.algorithm }
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
const realization = configured.realization || targetCapability.realization;
|
|
573
|
+
const known = knownRealization(
|
|
574
|
+
realization,
|
|
575
|
+
configured.algorithm,
|
|
576
|
+
target
|
|
577
|
+
);
|
|
578
|
+
if (
|
|
579
|
+
!known
|
|
580
|
+
|| targetCapability.realization !== realization
|
|
581
|
+
) {
|
|
582
|
+
throw cryptoError(
|
|
583
|
+
'PULSE_CRYPTO_REALIZATION_PIN_INVALID',
|
|
584
|
+
`Pinned realization ${realization} is unavailable for ${configured.algorithm} on target ${target}.`,
|
|
585
|
+
{
|
|
586
|
+
profile,
|
|
587
|
+
target,
|
|
588
|
+
algorithm: configured.algorithm,
|
|
589
|
+
realization,
|
|
590
|
+
targetRealization: targetCapability.realization
|
|
591
|
+
}
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
const requirement = requirements.algorithms.find((entry) => entry.algorithm === configured.algorithm);
|
|
595
|
+
return {
|
|
596
|
+
algorithm: configured.algorithm,
|
|
597
|
+
primitive: configured.algorithm === 'ES256'
|
|
598
|
+
? Object.freeze({ kind: 'ecdsa', curve: 'P-256', hash: 'SHA-256' })
|
|
599
|
+
: Object.freeze({ kind: configured.algorithm === 'SHA-256' ? 'digest' : 'hmac', hash: 'SHA-256' }),
|
|
600
|
+
requestedBy: requirement ? requirement.requestedBy : Object.freeze([]),
|
|
601
|
+
semanticOwner: requirements.semanticOwner,
|
|
602
|
+
realization,
|
|
603
|
+
kind: known.kind,
|
|
604
|
+
...(known.backend ? { backend: known.backend } : {}),
|
|
605
|
+
implementation: known.implementation,
|
|
606
|
+
targetStatus: targetCapability.status,
|
|
607
|
+
targetImplemented: targetCapability.implemented,
|
|
608
|
+
pinned: configured.realization !== null,
|
|
609
|
+
automaticFallback: false
|
|
610
|
+
};
|
|
611
|
+
});
|
|
612
|
+
const document = {
|
|
613
|
+
version: CRYPTO_REALIZATION_PLAN_VERSION,
|
|
614
|
+
semanticOwner: requirements.semanticOwner,
|
|
615
|
+
requirementsHash: requirements.requirementsHash,
|
|
616
|
+
profile,
|
|
617
|
+
profileProvenance,
|
|
618
|
+
target,
|
|
619
|
+
algorithms: planned,
|
|
620
|
+
automaticFallback: false
|
|
621
|
+
};
|
|
622
|
+
return deepFreeze({ ...document, planHash: sha256Hex(stableStringify(document)) });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
module.exports = Object.freeze({
|
|
626
|
+
CRYPTO_CONFIGURATION_VERSION,
|
|
627
|
+
CRYPTO_TARGET_CAPABILITIES_VERSION,
|
|
628
|
+
CRYPTO_REQUIREMENTS_VERSION,
|
|
629
|
+
CRYPTO_REALIZATION_PLAN_VERSION,
|
|
630
|
+
CRYPTO_NATIVE_GUEST_SOURCE_PLAN_VERSION,
|
|
631
|
+
CRYPTO_SEMANTIC_OWNER,
|
|
632
|
+
CRYPTO_RUNTIME_BUILTIN_IMPLEMENTATION,
|
|
633
|
+
CRYPTO_GUEST_SOURCE_IMPLEMENTATION,
|
|
634
|
+
CRYPTO_ES256_CONTRACT_VERSION,
|
|
635
|
+
CRYPTO_ES256_RUNTIME_BUILTIN_IMPLEMENTATION,
|
|
636
|
+
CRYPTO_ES256_GUEST_LINKED_REALIZATION,
|
|
637
|
+
CRYPTO_ES256_GUEST_LINKED_IMPLEMENTATION,
|
|
638
|
+
CRYPTO_VERIFICATION_STATUSES,
|
|
639
|
+
CRYPTO_ES256_CONTRACT,
|
|
640
|
+
CRYPTO_ALGORITHMS,
|
|
641
|
+
CRYPTO_REALIZATIONS,
|
|
642
|
+
normalizeCryptoConfiguration,
|
|
643
|
+
emptyCryptoConfiguration,
|
|
644
|
+
normalizeCryptoRequirements,
|
|
645
|
+
defineCryptoTargetCapabilities,
|
|
646
|
+
planCryptoRealizations
|
|
647
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const CONFIG_SCHEMA_VERSION = 'pulsewasm.config-schema.v1';
|
|
4
|
+
const ENV_PROFILE_NAME = 'PULSE_PROFILE';
|
|
5
|
+
const SEMANTIC_ENV_KEYS = new Set([
|
|
6
|
+
'entry',
|
|
7
|
+
'rootRouter',
|
|
8
|
+
'router',
|
|
9
|
+
'routers',
|
|
10
|
+
'route',
|
|
11
|
+
'routes',
|
|
12
|
+
'handler',
|
|
13
|
+
'handlers',
|
|
14
|
+
'middleware',
|
|
15
|
+
'mount',
|
|
16
|
+
'mounts'
|
|
17
|
+
]);
|
|
18
|
+
const ALLOWED_ENV_METHODS = new Set(['number', 'boolean', 'define']);
|
|
19
|
+
|
|
20
|
+
function isSemanticConfigKey(key) {
|
|
21
|
+
return SEMANTIC_ENV_KEYS.has(key);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
CONFIG_SCHEMA_VERSION,
|
|
26
|
+
ENV_PROFILE_NAME,
|
|
27
|
+
SEMANTIC_ENV_KEYS,
|
|
28
|
+
ALLOWED_ENV_METHODS,
|
|
29
|
+
isSemanticConfigKey
|
|
30
|
+
};
|