@pulse-compute/wasm-contracts 1.0.0-beta.4 → 1.0.0-beta.6
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 +2 -2
- package/package.json +2 -2
- package/src/assets/contracts.js +7 -1
- package/src/assets/embedded.as.ts +48 -0
- package/src/assets/embedded.js +63 -0
- package/src/crypto/contracts.js +11 -3
- package/src/definitions/router-api.js +3 -2
- package/src/diagnostics/codes.js +1 -0
- package/src/diagnostics.js +1 -1
- package/src/entities/contracts.d.ts +1 -0
- package/src/entities/contracts.js +21 -0
- package/src/handler/canonical-native-plan.js +61 -2
- package/src/handler/canonical-native-runtime.js +59 -3
- package/src/handler/canonical-runtime.js +10 -1
- package/src/handler/ctx-contract.js +3 -3
- package/src/handler/runtime-api.js +13 -3
- package/src/handler/surface-contract.js +46 -0
- package/src/handler/types-contract.js +1 -1
- package/src/jwt/contracts.js +42 -1
- package/src/library/manifest.js +11 -0
- package/src/project/native-optimization.js +3 -1
- package/src/provider/canonical-provider.js +7 -1
- package/src/provider/toolchain.js +8 -0
- package/src/s3/contracts.js +4 -4
- package/src/schema-json/admission-text.js +212 -0
- package/src/schema-json/admission.js +99 -0
- package/src/schema-json/contracts.js +1 -0
- package/src/schema-json/registry.js +119 -18
- package/src/schema-json/semantic-trace.js +4 -2
|
@@ -57,6 +57,18 @@ function surface(definition) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const HANDLER_SURFACE_DEFINITIONS = Object.freeze([
|
|
60
|
+
surface({ id: 'ctx.req.readTextChunk', class: 'effect', canonicalOperation: 'output.readTextChunk',
|
|
61
|
+
publicForms: ['ctx.req.readTextChunk()'], awaitPolicy: 'required-when-consumed', validPositions: ['statement'],
|
|
62
|
+
targetSupport: { javascript: true, native: true }, nativeBehavior: 'request-owned-input-suspension', status: 'experimental' }),
|
|
63
|
+
...['start', 'write'].map(operation => surface({
|
|
64
|
+
id: `ctx.output.${operation}`, class: 'effect', canonicalOperation: `output.${operation}`,
|
|
65
|
+
publicForms: [`ctx.output.${operation}(...)`], awaitPolicy: 'required-when-consumed',
|
|
66
|
+
validPositions: ['statement'], targetSupport: { javascript: true, native: true },
|
|
67
|
+
nativeBehavior: 'request-owned-output-suspension', status: 'experimental'
|
|
68
|
+
})),
|
|
69
|
+
surface({ id: 'ctx.output.close', class: 'sync', canonicalOperation: 'response.output.close',
|
|
70
|
+
publicForms: ['ctx.output.close()'], awaitPolicy: 'forbidden', validPositions: ['return'],
|
|
71
|
+
targetSupport: { javascript: true, native: true }, nativeBehavior: 'terminal-output-marker', status: 'experimental' }),
|
|
60
72
|
surface({
|
|
61
73
|
id: 'handler.managed-async-wrapper',
|
|
62
74
|
class: 'handler-wrapper',
|
|
@@ -212,6 +224,18 @@ const HANDLER_SURFACE_DEFINITIONS = Object.freeze([
|
|
|
212
224
|
nativeBehavior: 'construct-canonical-result',
|
|
213
225
|
status: 'existing-alias-normalization'
|
|
214
226
|
}),
|
|
227
|
+
surface({
|
|
228
|
+
id: 'ctx.decodeJson',
|
|
229
|
+
class: 'sync',
|
|
230
|
+
canonicalOperation: 'schema.decode.text',
|
|
231
|
+
publicForms: ["ctx.decodeJson<T>(text, 'schema.id')"],
|
|
232
|
+
awaitPolicy: 'optional-redundant-warning',
|
|
233
|
+
validPositions: ['expression', 'return', 'condition'],
|
|
234
|
+
targetSupport: { javascript: true, native: true },
|
|
235
|
+
nativeBehavior: 'decode-bounded-schema-text',
|
|
236
|
+
schemaPolicy: 'literal-schema-required',
|
|
237
|
+
status: 'supported'
|
|
238
|
+
}),
|
|
215
239
|
surface({
|
|
216
240
|
id: 'ctx.encodeJson',
|
|
217
241
|
class: 'sync',
|
|
@@ -257,6 +281,17 @@ const HANDLER_SURFACE_DEFINITIONS = Object.freeze([
|
|
|
257
281
|
nativeBehavior: 'emit-effect-and-continuation',
|
|
258
282
|
status: 'supported'
|
|
259
283
|
}),
|
|
284
|
+
surface({
|
|
285
|
+
id: 'ctx.req.body',
|
|
286
|
+
class: 'sync',
|
|
287
|
+
canonicalOperation: 'request.body.forward-marker',
|
|
288
|
+
publicForms: ["ctx.fetch(url, { method: 'POST', body: ctx.req.body() })"],
|
|
289
|
+
awaitPolicy: 'forbidden',
|
|
290
|
+
validPositions: ['fetch-body'],
|
|
291
|
+
targetSupport: { javascript: true, native: true },
|
|
292
|
+
nativeBehavior: 'host-owned-marker',
|
|
293
|
+
status: 'node-configured-only'
|
|
294
|
+
}),
|
|
260
295
|
surface({
|
|
261
296
|
id: 'ctx.req.json.schema',
|
|
262
297
|
class: 'effect',
|
|
@@ -315,6 +350,17 @@ const HANDLER_SURFACE_DEFINITIONS = Object.freeze([
|
|
|
315
350
|
nativeBehavior: 'emit-terminal-fetch-effect',
|
|
316
351
|
status: 'supported'
|
|
317
352
|
}),
|
|
353
|
+
surface({
|
|
354
|
+
id: 'ctx.time.now',
|
|
355
|
+
class: 'effect',
|
|
356
|
+
canonicalOperation: 'effect.time.now',
|
|
357
|
+
publicForms: ['ctx.time.now()'],
|
|
358
|
+
awaitPolicy: 'required-when-consumed',
|
|
359
|
+
validPositions: ['await-expression', 'parallel-member'],
|
|
360
|
+
targetSupport: { javascript: true, native: true },
|
|
361
|
+
nativeBehavior: 'emit-provider-wall-clock-effect-and-continuation',
|
|
362
|
+
status: 'supported'
|
|
363
|
+
}),
|
|
318
364
|
surface({
|
|
319
365
|
id: 'ctx.config.get',
|
|
320
366
|
class: 'effect',
|
|
@@ -20,7 +20,7 @@ function defaultHandlerTypesContract() {
|
|
|
20
20
|
set: 'void'
|
|
21
21
|
}),
|
|
22
22
|
executionContexts: Object.freeze({
|
|
23
|
-
shared: Object.freeze(['state', 'log', 'fetch', 'parallel', 'config', 'secret', 'kv', 'emit', 'encodeJson']),
|
|
23
|
+
shared: Object.freeze(['state', 'log', 'time', 'fetch', 'parallel', 'config', 'secret', 'kv', 'emit', 'encodeJson', 'decodeJson']),
|
|
24
24
|
http: Object.freeze(['req', 'json', 'text', 'response']),
|
|
25
25
|
route: Object.freeze(['param']),
|
|
26
26
|
event: Object.freeze(['event.type', 'event.payload']),
|
package/src/jwt/contracts.js
CHANGED
|
@@ -17,7 +17,7 @@ const JWT_CONTRACT_ID = 'pulse.jwt';
|
|
|
17
17
|
const JWT_PACKAGE_NAME = '@pulse-compute/jwt';
|
|
18
18
|
|
|
19
19
|
const JWT_ALGORITHMS = Object.freeze(['HS256', 'RS256', 'ES256', 'EdDSA']);
|
|
20
|
-
const JWT_IMPLEMENTED_ALGORITHMS = Object.freeze(['HS256', 'ES256']);
|
|
20
|
+
const JWT_IMPLEMENTED_ALGORITHMS = Object.freeze(['HS256', 'ES256', 'RS256']);
|
|
21
21
|
const JWT_KEY_TYPES = Object.freeze(['secret', 'jwk', 'jwks']);
|
|
22
22
|
const JWT_IMPLEMENTED_KEY_TYPES = Object.freeze(['secret', 'jwk', 'jwks']);
|
|
23
23
|
const JWT_NATIVE_REALIZATION_KINDS = Object.freeze(['crypto-composed']);
|
|
@@ -139,6 +139,38 @@ const JWT_VERIFY_OPERATION = Object.freeze({
|
|
|
139
139
|
clock: 'provider-wall-clock'
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
const JWT_SIGN_OPERATION = Object.freeze({
|
|
143
|
+
version: JWT_CANONICAL_EFFECT_VERSION, contractId: JWT_CONTRACT_ID,
|
|
144
|
+
package: JWT_PACKAGE_NAME, import: JWT_PACKAGE_NAME,
|
|
145
|
+
kind: 'jwt.sign', providerKind: 'jwt', operation: 'sign', capability: 'jwt.sign',
|
|
146
|
+
result: 'string', placement: 'variable', clock: 'provider-wall-clock'
|
|
147
|
+
});
|
|
148
|
+
const JWT_SIGN_CONTRACT = Object.freeze({
|
|
149
|
+
version: 'pulse.jwt-sign.v2', algorithms: Object.freeze(['HS256', 'ES256', 'RS256']), keyType: 'secret',
|
|
150
|
+
kidBytesMaximum: 256, es256PrivateKey: 'secret-json-p256-private-jwk',
|
|
151
|
+
header: Object.freeze({ alg: 'selected-algorithm', typ: 'JWT', kid: 'optional-explicit-key-id' }),
|
|
152
|
+
claimsBytes: 8192, claimsEntries: 1024, claimsDepth: 32,
|
|
153
|
+
keyBytesMinimum: 32, keyBytesMaximum: 4096,
|
|
154
|
+
expiresInSecondsMinimum: 1, expiresInSecondsMaximum: Number.MAX_SAFE_INTEGER,
|
|
155
|
+
expirationSecondsMaximum: 8_640_000_000_000, lifetimePolicy: 'application-owned',
|
|
156
|
+
reservedClaims: Object.freeze(['iat', 'exp', 'nbf']),
|
|
157
|
+
providerRequirements: Object.freeze(['jwt.sign', 'secret.get', 'time.wall-clock']),
|
|
158
|
+
cryptoAlgorithms: Object.freeze({ HS256: 'HMAC-SHA256', ES256: 'ES256', RS256: 'RS256' }), automaticFallback: false
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
function normalizeJwtSignEffect(effect) {
|
|
162
|
+
if (!ordinaryObject(effect) || ['contractId', 'package', 'kind', 'providerKind', 'operation', 'capability', 'result']
|
|
163
|
+
.some(name => dataProperty(effect, name) !== JWT_SIGN_OPERATION[name])) {
|
|
164
|
+
throw new PulseJwtContractError('Pulse JWT provider received an invalid signing operation.', { field: 'identity' });
|
|
165
|
+
}
|
|
166
|
+
const payload = dataProperty(effect, 'payload');
|
|
167
|
+
if (!ordinaryObject(payload) || Reflect.ownKeys(payload).length !== 2
|
|
168
|
+
|| !ordinaryObject(dataProperty(payload, 'claims')) || !ordinaryObject(dataProperty(payload, 'options'))) {
|
|
169
|
+
throw new PulseJwtContractError('Pulse JWT signing requires claims and options.', { field: 'payload' });
|
|
170
|
+
}
|
|
171
|
+
return Object.freeze({ claims: dataProperty(payload, 'claims'), options: dataProperty(payload, 'options') });
|
|
172
|
+
}
|
|
173
|
+
|
|
142
174
|
const JWT_EXECUTION_REQUIREMENTS = Object.freeze({
|
|
143
175
|
containment: 'request-owned-effect',
|
|
144
176
|
cancellation: 'request-lifecycle-signal',
|
|
@@ -175,6 +207,7 @@ const JWT_NATIVE_LOWERING_CONTRACT = Object.freeze({
|
|
|
175
207
|
'jwt.verify',
|
|
176
208
|
'jwt.verify.hs256',
|
|
177
209
|
'jwt.verify.es256',
|
|
210
|
+
'jwt.verify.rs256',
|
|
178
211
|
'secret.get',
|
|
179
212
|
'time.wall-clock'
|
|
180
213
|
]),
|
|
@@ -192,6 +225,11 @@ const JWT_NATIVE_LOWERING_CONTRACT = Object.freeze({
|
|
|
192
225
|
realization: 'guest-linked:pulse-es256-rustcrypto-p256',
|
|
193
226
|
implementation: 'rustcrypto.p256-0.13.2.ecdsa-0.16.9.sha2-0.10.9.v1',
|
|
194
227
|
guestUnitRequired: true
|
|
228
|
+
}),
|
|
229
|
+
RS256: Object.freeze({
|
|
230
|
+
realization: 'guest-linked:pulse-rs256-bearssl-i31',
|
|
231
|
+
implementation: 'bearssl.0.6.rsa-i31.sha256.v1',
|
|
232
|
+
guestUnitRequired: true
|
|
195
233
|
})
|
|
196
234
|
}),
|
|
197
235
|
cryptoSemanticOwner: CRYPTO_SEMANTIC_OWNER,
|
|
@@ -414,6 +452,9 @@ module.exports = Object.freeze({
|
|
|
414
452
|
JWT_RESOURCE_LIMITS,
|
|
415
453
|
JWT_ES256_KEY_NORMALIZATION_CONTRACT,
|
|
416
454
|
JWT_VERIFY_OPERATION,
|
|
455
|
+
JWT_SIGN_OPERATION,
|
|
456
|
+
JWT_SIGN_CONTRACT,
|
|
457
|
+
normalizeJwtSignEffect,
|
|
417
458
|
JWT_EXECUTION_REQUIREMENTS,
|
|
418
459
|
JWT_NATIVE_LOWERING_CONTRACT,
|
|
419
460
|
JWT_TARGET_REALIZATIONS,
|
package/src/library/manifest.js
CHANGED
|
@@ -87,6 +87,7 @@ function normalizeManifest(manifest) {
|
|
|
87
87
|
version: compiler.version,
|
|
88
88
|
entry: compiler.entry,
|
|
89
89
|
export: compiler.export,
|
|
90
|
+
...(compiler.nativeApplicationExport !== undefined ? { nativeApplicationExport: compiler.nativeApplicationExport } : {}),
|
|
90
91
|
artifact: compiler.artifact,
|
|
91
92
|
builderOwner: compiler.builderOwner,
|
|
92
93
|
trust: compiler.trust,
|
|
@@ -267,6 +268,16 @@ function validateLowerableLibraryManifest(manifest) {
|
|
|
267
268
|
));
|
|
268
269
|
}
|
|
269
270
|
|
|
271
|
+
if (compiler.nativeApplicationExport !== undefined && (!isNonEmptyString(compiler.nativeApplicationExport)
|
|
272
|
+
|| !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(compiler.nativeApplicationExport))) {
|
|
273
|
+
diagnostics.push(makeDiagnostic(
|
|
274
|
+
'PULSEWASM_LOWERABLE_LIBRARY_COMPILER_EXPORT_REQUIRED',
|
|
275
|
+
'Native application builders must name a static compiler export.',
|
|
276
|
+
'Set compiler.nativeApplicationExport to an exported first-party builder function name.',
|
|
277
|
+
{ nativeApplicationExport: compiler.nativeApplicationExport }
|
|
278
|
+
));
|
|
279
|
+
}
|
|
280
|
+
|
|
270
281
|
if (compiler.trust !== 'first-party') {
|
|
271
282
|
diagnostics.push(makeDiagnostic(
|
|
272
283
|
'PULSEWASM_LOWERABLE_LIBRARY_COMPILER_TRUST_REQUIRED',
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const NATIVE_OPTIMIZATION_VERSION = 'pulse.native-optimization.v1';
|
|
4
4
|
const EXPERIMENTAL_NATIVE_SIZE_MODE = 'experimental-native-size';
|
|
5
|
+
const EXPERIMENTAL_NATIVE_BOUNDED_SIZE_MODE = 'experimental-native-bounded-size';
|
|
5
6
|
|
|
6
7
|
module.exports = Object.freeze({
|
|
7
8
|
NATIVE_OPTIMIZATION_VERSION,
|
|
8
|
-
EXPERIMENTAL_NATIVE_SIZE_MODE
|
|
9
|
+
EXPERIMENTAL_NATIVE_SIZE_MODE,
|
|
10
|
+
EXPERIMENTAL_NATIVE_BOUNDED_SIZE_MODE
|
|
9
11
|
});
|
|
@@ -5,10 +5,15 @@ const CANONICAL_PROVIDER_PLAN_VERSION = 'pulse.canonical-provider-plan.v1';
|
|
|
5
5
|
|
|
6
6
|
const CANONICAL_PROVIDER_CAPABILITIES = Object.freeze([
|
|
7
7
|
'request',
|
|
8
|
+
'request.body.forward',
|
|
8
9
|
'response.json',
|
|
9
10
|
'response.text',
|
|
10
11
|
'response.custom',
|
|
12
|
+
'response.output',
|
|
13
|
+
'request.body.transform',
|
|
11
14
|
'fetch',
|
|
15
|
+
'time.wall-clock',
|
|
16
|
+
'crypto.digestText',
|
|
12
17
|
'config.get',
|
|
13
18
|
'secret.get',
|
|
14
19
|
'kv.get',
|
|
@@ -21,10 +26,11 @@ const CANONICAL_PROVIDER_CAPABILITIES = Object.freeze([
|
|
|
21
26
|
'grip.hold',
|
|
22
27
|
'grip.publish',
|
|
23
28
|
'grip.broadcast',
|
|
24
|
-
'jwt.verify',
|
|
29
|
+
'jwt.verify', 'jwt.sign',
|
|
25
30
|
's3.head',
|
|
26
31
|
's3.getText',
|
|
27
32
|
's3.putText',
|
|
33
|
+
's3.getBody',
|
|
28
34
|
'opaque.pass-through'
|
|
29
35
|
]);
|
|
30
36
|
|
|
@@ -52,6 +52,8 @@ const DRIVER_FIELDS = Object.freeze([
|
|
|
52
52
|
'targets',
|
|
53
53
|
'execute',
|
|
54
54
|
'prepareNativeExecution',
|
|
55
|
+
'prepareNativeRequest',
|
|
56
|
+
'createGeneratedOutput',
|
|
55
57
|
'createLoweringPlan',
|
|
56
58
|
'inspectRealization',
|
|
57
59
|
'writeTarget',
|
|
@@ -370,6 +372,10 @@ function defineProviderDriver(input, expected = {}) {
|
|
|
370
372
|
if (input.prepareNativeExecution !== undefined && typeof input.prepareNativeExecution !== 'function') {
|
|
371
373
|
throw new TypeError(`Pulse provider driver ${id} prepareNativeExecution must be a function.`);
|
|
372
374
|
}
|
|
375
|
+
if (input.prepareNativeRequest !== undefined && typeof input.prepareNativeRequest !== 'function') {
|
|
376
|
+
throw new TypeError(`Pulse provider driver ${id} prepareNativeRequest must be a function.`);
|
|
377
|
+
}
|
|
378
|
+
if (input.createGeneratedOutput !== undefined && typeof input.createGeneratedOutput !== 'function') throw new TypeError('Pulse provider createGeneratedOutput must be a function.');
|
|
373
379
|
const descriptor = input.descriptor === undefined ? undefined : normalizeDescriptor(input.descriptor);
|
|
374
380
|
if (descriptor && descriptor.id !== id) throw new TypeError(`Pulse provider driver ${id} descriptor identity does not match.`);
|
|
375
381
|
const javascript = normalizeJavascriptDriver(input.javascript, targets.javascript);
|
|
@@ -397,6 +403,8 @@ function defineProviderDriver(input, expected = {}) {
|
|
|
397
403
|
targets,
|
|
398
404
|
execute: input.execute,
|
|
399
405
|
prepareNativeExecution: input.prepareNativeExecution,
|
|
406
|
+
prepareNativeRequest: input.prepareNativeRequest,
|
|
407
|
+
createGeneratedOutput: input.createGeneratedOutput,
|
|
400
408
|
createLoweringPlan: input.createLoweringPlan,
|
|
401
409
|
inspectRealization: input.inspectRealization,
|
|
402
410
|
writeTarget: input.writeTarget,
|
package/src/s3/contracts.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
const S3_CONTRACT_ID = 'pulse.s3';
|
|
4
4
|
const S3_PACKAGE = '@pulse-compute/s3';
|
|
5
5
|
const S3_LOWERING_PLAN_VERSION = 'pulse.s3-lowering-plan.v1';
|
|
6
|
-
const S3_LIMITS = Object.freeze({ keyBytes: 1024, textBytes: 32768, metadataBytes: 1024, headerBytes: 16384, envelopeBytes:
|
|
7
|
-
const S3_OPERATIONS = Object.freeze(Object.fromEntries(['head', 'getText', 'putText'].map((name) => [name, Object.freeze({
|
|
8
|
-
kind: `s3.${name}`, capability: `s3.${name}`, result: name === 'head' ? 's3-head-result' : name === 'getText' ? 's3-get-text-result' : 's3-put-text-result'
|
|
6
|
+
const S3_LIMITS = Object.freeze({ keyBytes: 1024, textBytes: 2097152, defaultTextBytes: 32768, metadataBytes: 1024, headerBytes: 16384, envelopeBytes: 12648448 });
|
|
7
|
+
const S3_OPERATIONS = Object.freeze(Object.fromEntries(['head', 'getText', 'putText', 'getBody'].map((name) => [name, Object.freeze({
|
|
8
|
+
kind: `s3.${name}`, capability: `s3.${name}`, result: name === 'getBody' ? 'opaque-response' : name === 'head' ? 's3-head-result' : name === 'getText' ? 's3-get-text-result' : 's3-put-text-result'
|
|
9
9
|
})])));
|
|
10
|
-
const S3_PROVIDER_REQUIREMENTS = Object.freeze(['s3.head', 's3.getText', 's3.putText', 'secret.get', 'time.wall-clock']);
|
|
10
|
+
const S3_PROVIDER_REQUIREMENTS = Object.freeze(['s3.head', 's3.getText', 's3.putText', 's3.getBody', 'secret.get', 'time.wall-clock']);
|
|
11
11
|
const S3_CRYPTO_ALGORITHMS = Object.freeze(['SHA-256', 'HMAC-SHA256']);
|
|
12
12
|
module.exports = Object.freeze({ S3_CONTRACT_ID, S3_PACKAGE, S3_LOWERING_PLAN_VERSION, S3_LIMITS, S3_OPERATIONS, S3_PROVIDER_REQUIREMENTS, S3_CRYPTO_ALGORITHMS });
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { JsonAdmissionBudget } = require('./admission.js');
|
|
4
|
+
|
|
5
|
+
class JsonAdmissionFrame {
|
|
6
|
+
kind/*: i32 */ = 0; // 0 array; 1 object
|
|
7
|
+
state/*: i32 */ = 0;
|
|
8
|
+
count/*: i32 */ = 0;
|
|
9
|
+
start/*: i32 */ = 0;
|
|
10
|
+
names/*: Map<string, i32> */ = new Map();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Iterative syntax and resource admission; never constructs JSON containers.
|
|
14
|
+
// Root containers have depth 1, root scalars depth 0. Nodes include every
|
|
15
|
+
// syntactic value, even overwritten/unknown values. Keys are not value nodes.
|
|
16
|
+
// duplicateMode: 0 allow, 1 reject, 2 report offsets for schema-owned policy.
|
|
17
|
+
class JsonTextAdmission extends JsonAdmissionBudget {
|
|
18
|
+
text/*: string */;
|
|
19
|
+
duplicateMode/*: i32 */;
|
|
20
|
+
offset/*: i32 */ = 0;
|
|
21
|
+
textBytes/*: i32 */ = 0;
|
|
22
|
+
frames/*: Array<JsonAdmissionFrame> */ = [];
|
|
23
|
+
duplicateObjects/*: Array<i32> */ = [];
|
|
24
|
+
duplicateKeys/*: Array<i32> */ = [];
|
|
25
|
+
duplicateFirstKeys/*: Array<i32> */ = [];
|
|
26
|
+
constructor(text/*: string */, limits/*: JsonAdmissionLimits */, duplicateMode/*: i32 */) {
|
|
27
|
+
super(limits);
|
|
28
|
+
this.text = text;
|
|
29
|
+
this.duplicateMode = duplicateMode;
|
|
30
|
+
}
|
|
31
|
+
space()/*: void */ {
|
|
32
|
+
while (this.offset < this.text.length) {
|
|
33
|
+
const c = this.text.charCodeAt(this.offset);
|
|
34
|
+
if (c !== 32 && c !== 9 && c !== 10 && c !== 13) break;
|
|
35
|
+
this.offset++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
rawBytes()/*: bool */ {
|
|
39
|
+
// UTF-8 byte count matches replacement encoding for raw lone surrogates.
|
|
40
|
+
// This prepass stops before any syntax-frame or decoded-key allocation.
|
|
41
|
+
if (this.text.length > this.limits.maxTextBytes) return this.fail(2);
|
|
42
|
+
for (let i = 0; i < this.text.length; i++) {
|
|
43
|
+
const c = this.text.charCodeAt(i);
|
|
44
|
+
let n = c < 128 ? 1 : c < 2048 ? 2 : 3;
|
|
45
|
+
if (c >= 0xd800 && c <= 0xdbff && i + 1 < this.text.length) {
|
|
46
|
+
const next = this.text.charCodeAt(i + 1);
|
|
47
|
+
if (next >= 0xdc00 && next <= 0xdfff) { n = 4; i++; }
|
|
48
|
+
}
|
|
49
|
+
if (n > this.limits.maxTextBytes - this.textBytes) return this.fail(2);
|
|
50
|
+
this.textBytes += n;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
string(key/*: bool */)/*: string */ {
|
|
55
|
+
const capture = key && this.duplicateMode !== 0;
|
|
56
|
+
const parts/*: Array<string> */ = [];
|
|
57
|
+
let length = 0;
|
|
58
|
+
let high = false;
|
|
59
|
+
this.offset++; // opening quote
|
|
60
|
+
if (!this.addBytes(2)) return '';
|
|
61
|
+
while (this.offset < this.text.length && this.failure === 0) {
|
|
62
|
+
let c = this.text.charCodeAt(this.offset++);
|
|
63
|
+
if (c === 34) {
|
|
64
|
+
if (high) this.addBytes(6);
|
|
65
|
+
return capture ? parts.join('') : '';
|
|
66
|
+
}
|
|
67
|
+
if (c < 32) { this.fail(1); return ''; }
|
|
68
|
+
if (c === 92) {
|
|
69
|
+
if (this.offset >= this.text.length) { this.fail(1); return ''; }
|
|
70
|
+
c = this.text.charCodeAt(this.offset++);
|
|
71
|
+
if (c === 117) {
|
|
72
|
+
let unit = 0;
|
|
73
|
+
for (let n = 0; n < 4; n++) {
|
|
74
|
+
if (this.offset >= this.text.length) { this.fail(1); return ''; }
|
|
75
|
+
const h = this.text.charCodeAt(this.offset++);
|
|
76
|
+
const digit = h >= 48 && h <= 57 ? h - 48 : h >= 65 && h <= 70 ? h - 55 : h >= 97 && h <= 102 ? h - 87 : -1;
|
|
77
|
+
if (digit < 0) { this.fail(1); return ''; }
|
|
78
|
+
unit = unit * 16 + digit;
|
|
79
|
+
}
|
|
80
|
+
c = unit;
|
|
81
|
+
} else if (c === 98) c = 8;
|
|
82
|
+
else if (c === 102) c = 12;
|
|
83
|
+
else if (c === 110) c = 10;
|
|
84
|
+
else if (c === 114) c = 13;
|
|
85
|
+
else if (c === 116) c = 9;
|
|
86
|
+
else if (c !== 34 && c !== 92 && c !== 47) { this.fail(1); return ''; }
|
|
87
|
+
}
|
|
88
|
+
const limit = key ? this.limits.maxKeyLength : this.limits.maxStringLength;
|
|
89
|
+
if (length >= limit) { this.fail(key ? 7 : 8); return ''; }
|
|
90
|
+
length++;
|
|
91
|
+
if (capture) parts.push(String.fromCharCode(c));
|
|
92
|
+
if (high) {
|
|
93
|
+
high = false;
|
|
94
|
+
if (c >= 0xdc00 && c <= 0xdfff) { this.addBytes(4); continue; }
|
|
95
|
+
if (!this.addBytes(6)) return '';
|
|
96
|
+
}
|
|
97
|
+
if (c >= 0xd800 && c <= 0xdbff) high = true;
|
|
98
|
+
else this.addBytes(c < 32 ? 6 : c === 34 || c === 92 ? 2 : c < 128 ? 1 : c < 2048 ? 2 : c >= 0xdc00 && c <= 0xdfff ? 6 : 3);
|
|
99
|
+
}
|
|
100
|
+
this.fail(1);
|
|
101
|
+
return '';
|
|
102
|
+
}
|
|
103
|
+
digit()/*: bool */ {
|
|
104
|
+
if (this.offset >= this.text.length) return false;
|
|
105
|
+
const c = this.text.charCodeAt(this.offset);
|
|
106
|
+
return c >= 48 && c <= 57;
|
|
107
|
+
}
|
|
108
|
+
number()/*: bool */ {
|
|
109
|
+
const start = this.offset;
|
|
110
|
+
if (this.text.charCodeAt(this.offset) === 45) this.offset++;
|
|
111
|
+
if (!this.digit()) return this.fail(1);
|
|
112
|
+
if (this.text.charCodeAt(this.offset) === 48) this.offset++;
|
|
113
|
+
else while (this.digit()) this.offset++;
|
|
114
|
+
if (this.offset < this.text.length && this.text.charCodeAt(this.offset) === 46) {
|
|
115
|
+
this.offset++;
|
|
116
|
+
if (!this.digit()) return this.fail(1);
|
|
117
|
+
while (this.digit()) this.offset++;
|
|
118
|
+
}
|
|
119
|
+
if (this.offset < this.text.length) {
|
|
120
|
+
const c = this.text.charCodeAt(this.offset);
|
|
121
|
+
if (c === 69 || c === 101) {
|
|
122
|
+
this.offset++;
|
|
123
|
+
if (this.offset < this.text.length && (this.text.charCodeAt(this.offset) === 43 || this.text.charCodeAt(this.offset) === 45)) this.offset++;
|
|
124
|
+
if (!this.digit()) return this.fail(1);
|
|
125
|
+
while (this.digit()) this.offset++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!isFinite(parseFloat(this.text.substring(start, this.offset)))) return this.fail(11);
|
|
129
|
+
return this.addBytes(24); // finite-f64 conservative spelling budget
|
|
130
|
+
}
|
|
131
|
+
value()/*: bool */ {
|
|
132
|
+
this.space();
|
|
133
|
+
if (this.offset >= this.text.length) return this.fail(1);
|
|
134
|
+
const c = this.text.charCodeAt(this.offset);
|
|
135
|
+
const container = c === 91 || c === 123;
|
|
136
|
+
if (!this.node(container ? this.frames.length + 1 : this.frames.length)) return false;
|
|
137
|
+
if (container) {
|
|
138
|
+
if (!this.addBytes(1)) return false;
|
|
139
|
+
const frame = new JsonAdmissionFrame();
|
|
140
|
+
frame.kind = c === 123 ? 1 : 0;
|
|
141
|
+
frame.start = this.offset++;
|
|
142
|
+
this.frames.push(frame);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
if (c === 34) { this.string(false); return this.failure === 0; }
|
|
146
|
+
let literal = '';
|
|
147
|
+
if (c === 116) literal = 'true';
|
|
148
|
+
else if (c === 102) literal = 'false';
|
|
149
|
+
else if (c === 110) literal = 'null';
|
|
150
|
+
if (literal.length > 0) {
|
|
151
|
+
if (this.text.substring(this.offset, this.offset + literal.length) !== literal) return this.fail(1);
|
|
152
|
+
this.offset += literal.length;
|
|
153
|
+
return this.addBytes(literal.length);
|
|
154
|
+
}
|
|
155
|
+
return this.number();
|
|
156
|
+
}
|
|
157
|
+
scan()/*: bool */ {
|
|
158
|
+
if (this.duplicateMode < 0 || this.duplicateMode > 2) return this.fail(12);
|
|
159
|
+
if (!this.rawBytes() || !this.value()) return false;
|
|
160
|
+
while (this.frames.length > 0 && this.failure === 0) {
|
|
161
|
+
this.space();
|
|
162
|
+
if (this.offset >= this.text.length) return this.fail(1);
|
|
163
|
+
const frame = this.frames[this.frames.length - 1];
|
|
164
|
+
const c = this.text.charCodeAt(this.offset);
|
|
165
|
+
const end = frame.kind === 0 ? 93 : 125;
|
|
166
|
+
const after = frame.kind === 0 ? 1 : 3;
|
|
167
|
+
if ((frame.state === 0 || frame.state === after) && c === end) {
|
|
168
|
+
this.offset++;
|
|
169
|
+
if (!this.addBytes(1)) return false;
|
|
170
|
+
this.frames.pop();
|
|
171
|
+
} else if (frame.state === after) {
|
|
172
|
+
if (c !== 44) return this.fail(1);
|
|
173
|
+
this.offset++;
|
|
174
|
+
if (!this.addBytes(1)) return false;
|
|
175
|
+
frame.state = frame.kind === 0 ? 2 : 4;
|
|
176
|
+
} else if (frame.kind === 0) {
|
|
177
|
+
if (frame.count >= this.limits.maxArrayItems) return this.fail(6);
|
|
178
|
+
frame.count++;
|
|
179
|
+
frame.state = 1;
|
|
180
|
+
if (!this.value()) return false;
|
|
181
|
+
} else if (frame.state === 0 || frame.state === 4) {
|
|
182
|
+
if (c !== 34) return this.fail(1);
|
|
183
|
+
if (frame.count >= this.limits.maxObjectMembers) return this.fail(5);
|
|
184
|
+
frame.count++;
|
|
185
|
+
const start = this.offset;
|
|
186
|
+
const name = this.string(true);
|
|
187
|
+
if (this.failure !== 0) return false;
|
|
188
|
+
if (this.duplicateMode !== 0) {
|
|
189
|
+
if (frame.names.has(name)) {
|
|
190
|
+
if (this.duplicateMode === 1) return this.fail(10);
|
|
191
|
+
this.duplicateObjects.push(frame.start);
|
|
192
|
+
this.duplicateKeys.push(start);
|
|
193
|
+
this.duplicateFirstKeys.push(frame.names.get(name)/* nonnull */);
|
|
194
|
+
} else frame.names.set(name, start);
|
|
195
|
+
}
|
|
196
|
+
frame.state = 1;
|
|
197
|
+
} else if (frame.state === 1) {
|
|
198
|
+
if (c !== 58) return this.fail(1);
|
|
199
|
+
this.offset++;
|
|
200
|
+
if (!this.addBytes(1)) return false;
|
|
201
|
+
frame.state = 2;
|
|
202
|
+
} else {
|
|
203
|
+
frame.state = 3;
|
|
204
|
+
if (!this.value()) return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
this.space();
|
|
208
|
+
return this.failure === 0 && (this.offset === this.text.length || this.fail(1));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { JsonAdmissionFrame, JsonTextAdmission };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { sha256Hex, stableStringify } = require('../stable-id.js');
|
|
4
|
+
|
|
5
|
+
const JSON_ADMISSION_VERSION = 'pulse.json-admission.v1';
|
|
6
|
+
const JSON_LIMIT_FIELDS = Object.freeze([
|
|
7
|
+
'maxTextBytes', 'maxDepth', 'maxNodes', 'maxObjectMembers',
|
|
8
|
+
'maxArrayItems', 'maxKeyLength', 'maxStringLength', 'maxJsonBytes'
|
|
9
|
+
]);
|
|
10
|
+
const JSON_ADMISSION_FAILURES = Object.freeze([
|
|
11
|
+
'none', 'malformed', 'text-bytes', 'depth', 'nodes', 'object-members',
|
|
12
|
+
'array-items', 'key-length', 'string-length', 'json-bytes',
|
|
13
|
+
'duplicate-key', 'non-finite-number', 'unsupported-value', 'cycle'
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
// No application defaults in this internal foundation. All counters fit i32;
|
|
17
|
+
// additions check remaining capacity before adding, including in generated AS.
|
|
18
|
+
function normalizeJsonAdmissionLimits(input) {
|
|
19
|
+
const invalid = () => {
|
|
20
|
+
const error = new TypeError('JSON admission requires all eight positive i32 limits as own data properties.');
|
|
21
|
+
error.code = 'PULSEWASM_JSON_LIMITS_INVALID';
|
|
22
|
+
throw error;
|
|
23
|
+
};
|
|
24
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) invalid();
|
|
25
|
+
const proto = Object.getPrototypeOf(input);
|
|
26
|
+
if (proto !== Object.prototype && proto !== null) invalid();
|
|
27
|
+
const keys = Reflect.ownKeys(input);
|
|
28
|
+
if (keys.length !== JSON_LIMIT_FIELDS.length || keys.some(key => !JSON_LIMIT_FIELDS.includes(key))) invalid();
|
|
29
|
+
const limits = {};
|
|
30
|
+
for (const key of JSON_LIMIT_FIELDS) {
|
|
31
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
32
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) invalid();
|
|
33
|
+
const value = descriptor.value;
|
|
34
|
+
if (!Number.isInteger(value) || value < 1 || value > 0x7fffffff) invalid();
|
|
35
|
+
limits[key] = value;
|
|
36
|
+
}
|
|
37
|
+
return Object.freeze(limits);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function jsonAdmissionIdentity(input) {
|
|
41
|
+
const limits = normalizeJsonAdmissionLimits(input);
|
|
42
|
+
return Object.freeze({ version: JSON_ADMISSION_VERSION, limits,
|
|
43
|
+
hash: sha256Hex(stableStringify({ version: JSON_ADMISSION_VERSION, limits })) });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// These classes are executable reference semantics shared with Native codegen.
|
|
47
|
+
// Type comments are erased by JavaScript and realized by the AS generator.
|
|
48
|
+
class JsonAdmissionLimits {
|
|
49
|
+
maxTextBytes/*: i32 */ = 0;
|
|
50
|
+
maxDepth/*: i32 */ = 0;
|
|
51
|
+
maxNodes/*: i32 */ = 0;
|
|
52
|
+
maxObjectMembers/*: i32 */ = 0;
|
|
53
|
+
maxArrayItems/*: i32 */ = 0;
|
|
54
|
+
maxKeyLength/*: i32 */ = 0;
|
|
55
|
+
maxStringLength/*: i32 */ = 0;
|
|
56
|
+
maxJsonBytes/*: i32 */ = 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class JsonAdmissionBudget {
|
|
60
|
+
limits/*: JsonAdmissionLimits */;
|
|
61
|
+
failure/*: i32 */ = 0;
|
|
62
|
+
nodes/*: i32 */ = 0;
|
|
63
|
+
depth/*: i32 */ = 0;
|
|
64
|
+
jsonBytes/*: i32 */ = 0;
|
|
65
|
+
constructor(limits/*: JsonAdmissionLimits */) { this.limits = limits; }
|
|
66
|
+
fail(reason/*: i32 */)/*: bool */ {
|
|
67
|
+
if (this.failure === 0) this.failure = reason;
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
addBytes(amount/*: i32 */)/*: bool */ {
|
|
71
|
+
if (this.failure !== 0) return false;
|
|
72
|
+
if (amount < 0 || amount > this.limits.maxJsonBytes - this.jsonBytes) return this.fail(9);
|
|
73
|
+
this.jsonBytes += amount;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
node(containerDepth/*: i32 */)/*: bool */ {
|
|
77
|
+
if (this.failure !== 0) return false;
|
|
78
|
+
if (this.nodes >= this.limits.maxNodes) return this.fail(4);
|
|
79
|
+
this.nodes++;
|
|
80
|
+
if (containerDepth > this.limits.maxDepth) return this.fail(3);
|
|
81
|
+
if (containerDepth > this.depth) this.depth = containerDepth;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
stringBytes(text/*: string */, key/*: bool */)/*: bool */ {
|
|
85
|
+
if (text.length > (key ? this.limits.maxKeyLength : this.limits.maxStringLength)) return this.fail(key ? 7 : 8);
|
|
86
|
+
if (!this.addBytes(2)) return false;
|
|
87
|
+
for (let i = 0; i < text.length; i++) {
|
|
88
|
+
const c = text.charCodeAt(i);
|
|
89
|
+
let size = c < 32 ? 6 : c === 34 || c === 92 ? 2 : c < 128 ? 1 : c < 2048 ? 2 : 3;
|
|
90
|
+
if (c >= 0xd800 && c <= 0xdbff && i + 1 < text.length && text.charCodeAt(i + 1) >= 0xdc00 && text.charCodeAt(i + 1) <= 0xdfff) { size = 4; i++; }
|
|
91
|
+
else if (c >= 0xd800 && c <= 0xdfff) size = 6;
|
|
92
|
+
if (!this.addBytes(size)) return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { JSON_ADMISSION_VERSION, JSON_LIMIT_FIELDS, JSON_ADMISSION_FAILURES,
|
|
99
|
+
normalizeJsonAdmissionLimits, jsonAdmissionIdentity, JsonAdmissionLimits, JsonAdmissionBudget };
|