@pulse-compute/wasm-contracts 1.0.0-beta.5 → 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 +55 -2
- package/src/handler/canonical-native-runtime.js +55 -3
- package/src/handler/canonical-runtime.js +10 -1
- package/src/handler/surface-contract.js +23 -0
- 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 +5 -1
- package/src/provider/toolchain.js +8 -0
- package/src/s3/contracts.js +3 -3
- 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 +98 -9
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
> **Supported entry points:** `@pulse-compute/wasm-contracts/provider/toolchain`<br>
|
|
8
8
|
> **Stability:** The provider toolchain contract is versioned and supported for the Beta; other protocol and proof interfaces remain internal.<br>
|
|
9
9
|
> **npm:** [`@pulse-compute/wasm-contracts`](https://www.npmjs.com/package/@pulse-compute/wasm-contracts)<br>
|
|
10
|
-
> **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.
|
|
10
|
+
> **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.6/packages/implementation-packages/)
|
|
11
11
|
>
|
|
12
|
-
> This release-status block is generated from the synchronized `Pulse 1.0.0-beta.
|
|
12
|
+
> This release-status block is generated from the synchronized `Pulse 1.0.0-beta.6` package policy.
|
|
13
13
|
<!-- pulse-package-status:end -->
|
|
14
14
|
|
|
15
15
|
This package owns stable data contracts shared by the compiler, runtime, and providers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulse-compute/wasm-contracts",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.6",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": "^22.14.0 || ^24.0.0"
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"url": "git+https://github.com/pulse-compute/pulse.git",
|
|
127
127
|
"directory": "wasm/packages/contracts"
|
|
128
128
|
},
|
|
129
|
-
"homepage": "https://pulsecompute.io/v1.0.0-beta.
|
|
129
|
+
"homepage": "https://pulsecompute.io/v1.0.0-beta.6/packages/implementation-packages/",
|
|
130
130
|
"bugs": {
|
|
131
131
|
"url": "https://github.com/pulse-compute/pulse/issues"
|
|
132
132
|
}
|
package/src/assets/contracts.js
CHANGED
|
@@ -37,7 +37,7 @@ const ASSETS_INITIAL_PROVIDER_TARGETS = Object.freeze(['node', 'fastly']);
|
|
|
37
37
|
|
|
38
38
|
const ASSETS_PAYLOAD_MODE_STATUSES = LOWERABLE_LIBRARY_PAYLOAD_MODE_STATUSES;
|
|
39
39
|
|
|
40
|
-
const ASSETS_SUPPORTED_BODY_KINDS = Object.freeze(['text-response-body']);
|
|
40
|
+
const ASSETS_SUPPORTED_BODY_KINDS = Object.freeze(['text-response-body', 'embedded-blob-response']);
|
|
41
41
|
const ASSETS_LOWERING_PLAN_ONLY_BODY_KINDS = Object.freeze(['stream-pass-through-response']);
|
|
42
42
|
const ASSETS_RESERVED_BODY_KINDS = Object.freeze(['binary-buffer']);
|
|
43
43
|
const ASSETS_UNSUPPORTED_BODY_KINDS = Object.freeze(['json-response-body']);
|
|
@@ -78,6 +78,11 @@ const ASSETS_DIAGNOSTIC_CODES = Object.freeze({
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
const ASSETS_PAYLOAD_MODE_CLASSIFICATION = Object.freeze({
|
|
81
|
+
embeddedBlobResponse: Object.freeze({
|
|
82
|
+
mode: 'embedded-blob-response', status: 'implemented-now', hostResultKind: 'opaque-response',
|
|
83
|
+
hostCapabilities: Object.freeze(['assets', 'result']), owner: '@pulse-compute/assets',
|
|
84
|
+
notes: 'Native-only, compile-time admitted embeddedManifest literal; bounded selected bytes remain provider-owned.'
|
|
85
|
+
}),
|
|
81
86
|
textResponseBody: Object.freeze({
|
|
82
87
|
mode: 'text-response-body',
|
|
83
88
|
status: 'implemented-now',
|
|
@@ -249,6 +254,7 @@ function assetsProtocolExtension() {
|
|
|
249
254
|
}
|
|
250
255
|
|
|
251
256
|
module.exports = {
|
|
257
|
+
...require('./embedded.js'),
|
|
252
258
|
ASSETS_CONTRACT_VERSION,
|
|
253
259
|
ASSETS_HOST_ABI_VERSION,
|
|
254
260
|
ASSETS_PROVIDER_CONFIG_VERSION,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Native counterpart of embeddedAssetResponse; inputs are admitted by the Assets lowerer.
|
|
2
|
+
class __PulseEmbeddedSelection {
|
|
3
|
+
status: i32 = 200; start: i32 = 0; end: i32 = -1; contentRange: string = '';
|
|
4
|
+
}
|
|
5
|
+
function __pulse_embedded_decimal(value: string): f64 {
|
|
6
|
+
if (!value.length || (value.length > 1 && value.charCodeAt(0) == 48)) return -1;
|
|
7
|
+
let output: f64 = 0;
|
|
8
|
+
for (let i = 0; i < value.length; i++) {
|
|
9
|
+
const digit = value.charCodeAt(i) - 48; if (digit < 0 || digit > 9) return -1;
|
|
10
|
+
output = output * 10 + digit; if (output > 9007199254740991) return -1;
|
|
11
|
+
}
|
|
12
|
+
return output;
|
|
13
|
+
}
|
|
14
|
+
function __pulse_embedded_selection(length: i32, method: string, etag: string, validator: string | null, range: string | null, ifRange: string | null): __PulseEmbeddedSelection {
|
|
15
|
+
const output = new __PulseEmbeddedSelection(); output.end = length - 1;
|
|
16
|
+
if (length < 0) { output.status = 404; return output; }
|
|
17
|
+
if (validator !== null) {
|
|
18
|
+
let tag = validator;
|
|
19
|
+
if (tag.startsWith('W/')) tag = tag.slice(2);
|
|
20
|
+
if (validator != '*') {
|
|
21
|
+
if (tag.length < 2 || tag.charAt(0) != '"' || tag.charAt(tag.length - 1) != '"') { output.status = 400; return output; }
|
|
22
|
+
for (let i = 1; i < tag.length - 1; i++) if (tag.charCodeAt(i) < 33 || tag.charCodeAt(i) > 126 || tag.charCodeAt(i) == 34) { output.status = 400; return output; }
|
|
23
|
+
}
|
|
24
|
+
if (validator == '*' || tag == etag) { output.status = 304; return output; }
|
|
25
|
+
}
|
|
26
|
+
if (method == 'GET' && range !== null && (ifRange === null || ifRange == etag)) {
|
|
27
|
+
if (!range.startsWith('bytes=')) { output.status = 400; return output; }
|
|
28
|
+
const parts = range.slice(6).split('-');
|
|
29
|
+
if (parts.length != 2) { output.status = 400; return output; }
|
|
30
|
+
const start = __pulse_embedded_decimal(parts[0]), end = __pulse_embedded_decimal(parts[1]);
|
|
31
|
+
if (start < 0 || end < start) { output.status = 400; return output; }
|
|
32
|
+
if (start >= length) { output.status = 416; output.contentRange = 'bytes */' + length.toString(); return output; }
|
|
33
|
+
output.start = i32(start); output.end = i32(Math.min(end, f64(length - 1))); output.status = 206;
|
|
34
|
+
output.contentRange = 'bytes ' + output.start.toString() + '-' + output.end.toString() + '/' + length.toString();
|
|
35
|
+
}
|
|
36
|
+
return output;
|
|
37
|
+
}
|
|
38
|
+
function __pulse_embedded_bytes(data: string, start: i32, end: i32): Uint8Array {
|
|
39
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
40
|
+
const output = new Uint8Array(max<i32>(0, end - start + 1));
|
|
41
|
+
// Decode only the selected interval, without a full-file temporary copy.
|
|
42
|
+
for (let index = start; index <= end; index++) {
|
|
43
|
+
const group = (index / 3) * 4, part = index % 3;
|
|
44
|
+
const a = alphabet.indexOf(data.charAt(group + part)), b = alphabet.indexOf(data.charAt(group + part + 1));
|
|
45
|
+
output[index - start] = u8(part == 0 ? (a << 2) | (b >> 4) : part == 1 ? (a << 4) | (b >> 2) : (a << 6) | b);
|
|
46
|
+
}
|
|
47
|
+
return output;
|
|
48
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Compile-time admission and host response policy for explicitly embedded bytes.
|
|
3
|
+
const LIMITS = Object.freeze({ files:256, fileBytes:262144, totalBytes:1048576, pathBytes:1024, serializedBytes:2097152 });
|
|
4
|
+
const VERSION = 'pulse.embedded-assets.v1';
|
|
5
|
+
function fail() { throw new TypeError('Invalid or oversized embedded asset manifest.'); }
|
|
6
|
+
function fields(value, names) {
|
|
7
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length !== names.length || names.some(name=>!Object.hasOwn(value,name))) fail();
|
|
8
|
+
}
|
|
9
|
+
function assetPath(value) {
|
|
10
|
+
if (typeof value !== 'string' || !value.isWellFormed() || !value.startsWith('/') || value === '/' || Buffer.byteLength(value)>LIMITS.pathBytes
|
|
11
|
+
|| /[\\%?#\u0000-\u001f\u007f-\u009f]/u.test(value) || value.slice(1).split('/').some(p=>!p||p==='.'||p==='..')) fail();
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function selectEmbeddedAsset(serialized, key) {
|
|
15
|
+
if (typeof serialized !== 'string' || serialized.length > LIMITS.serializedBytes || Buffer.byteLength(serialized)>LIMITS.serializedBytes) fail();
|
|
16
|
+
let manifest; try { manifest=JSON.parse(serialized); } catch { fail(); }
|
|
17
|
+
// Canonical serialization rejects duplicate fields and ambiguous encodings.
|
|
18
|
+
if (JSON.stringify(manifest)!==serialized) fail();
|
|
19
|
+
fields(manifest,['version','id','byteLength','files']);
|
|
20
|
+
if (manifest.version!==VERSION || !Array.isArray(manifest.files) || manifest.files.length>LIMITS.files) fail();
|
|
21
|
+
const hash = value=>require('node:crypto').createHash('sha256').update(value).digest('hex');
|
|
22
|
+
let total=0,previous='';
|
|
23
|
+
for (const file of manifest.files) {
|
|
24
|
+
fields(file,['path','data','byteLength','sha256','contentType']);
|
|
25
|
+
const path=assetPath(file.path);
|
|
26
|
+
if (path<=previous || !Number.isInteger(file.byteLength) || file.byteLength<0 || file.byteLength>LIMITS.fileBytes
|
|
27
|
+
|| typeof file.contentType!=='string' || !/^[\x20-\x7e]{1,128}$/.test(file.contentType) || file.contentType.trim()!==file.contentType
|
|
28
|
+
|| typeof file.data!=='string' || file.data.length!==4*Math.ceil(file.byteLength/3)) fail();
|
|
29
|
+
previous=path; total+=file.byteLength; if(total>LIMITS.totalBytes) fail();
|
|
30
|
+
const bytes=Buffer.from(file.data,'base64');
|
|
31
|
+
if(bytes.length!==file.byteLength || bytes.toString('base64')!==file.data || hash(bytes)!==file.sha256) fail();
|
|
32
|
+
}
|
|
33
|
+
if(manifest.byteLength!==total || hash(JSON.stringify([VERSION,manifest.files.map(f=>[f.path,f.contentType,f.byteLength,f.sha256])]))!==manifest.id) fail();
|
|
34
|
+
assetPath(key);
|
|
35
|
+
const file=manifest.files.find(f=>f.path===key);
|
|
36
|
+
return Object.freeze({embeddedId:manifest.id,embeddedFound:!!file,embeddedData:file?.data??'',embeddedLength:file?.byteLength??0,embeddedType:file?.contentType??'',embeddedEtag:file?`"${file.sha256}"`:''});
|
|
37
|
+
}
|
|
38
|
+
function embeddedAssetResponse(payload, requestHeaders) {
|
|
39
|
+
const headers=new Headers(requestHeaders), output=new Headers();
|
|
40
|
+
const length=payload.embeddedLength;
|
|
41
|
+
if(!payload.embeddedFound) return {status:404,headers:output,bytes:null};
|
|
42
|
+
if(!Number.isInteger(length)||length<0||length>LIMITS.fileBytes||typeof payload.embeddedData!=='string'||payload.embeddedData.length!==4*Math.ceil(length/3)) fail();
|
|
43
|
+
output.set('content-type',payload.embeddedType); output.set('etag',payload.embeddedEtag); output.set('accept-ranges','bytes');
|
|
44
|
+
const validator=headers.get('if-none-match');
|
|
45
|
+
if(validator!==null) {
|
|
46
|
+
if(!/^(?:\*|(?:W\/)?"[\x21\x23-\x7e]*")$/.test(validator)) return {status:400,headers:new Headers(),bytes:null};
|
|
47
|
+
if(validator==='*'||validator.replace(/^W\//,'')===payload.embeddedEtag) return {status:304,headers:output,bytes:null};
|
|
48
|
+
}
|
|
49
|
+
let start=0,end=length-1,status=200;
|
|
50
|
+
const range=headers.get('range'), ifRange=headers.get('if-range');
|
|
51
|
+
if(payload.method==='GET'&&range!==null&&(ifRange===null||ifRange===payload.embeddedEtag)) {
|
|
52
|
+
const match=/^bytes=(0|[1-9][0-9]*)-(0|[1-9][0-9]*)$/.exec(range);
|
|
53
|
+
if(!match||!Number.isSafeInteger(Number(match[2]))||Number(match[1])>Number(match[2])) return {status:400,headers:new Headers(),bytes:null};
|
|
54
|
+
start=Number(match[1]);end=Math.min(Number(match[2]),end);
|
|
55
|
+
if(start>=length){output.set('content-range',`bytes */${length}`);return {status:416,headers:output,bytes:null};}
|
|
56
|
+
status=206;output.set('content-range',`bytes ${start}-${end}/${length}`);
|
|
57
|
+
}
|
|
58
|
+
output.set('content-length',String(Math.max(0,end-start+1)));
|
|
59
|
+
const bytes=payload.method==='HEAD'?null:Buffer.from(payload.embeddedData,'base64').subarray(start,end+1);
|
|
60
|
+
return {status,headers:output,bytes};
|
|
61
|
+
}
|
|
62
|
+
function embeddedNativeSource() { return require('node:fs').readFileSync(require('node:path').join(__dirname,'embedded.as.ts'),'utf8'); }
|
|
63
|
+
module.exports={embeddedNativeSource,EMBEDDED_ASSET_NATIVE_LIMITS:LIMITS,selectEmbeddedAsset,embeddedAssetResponse};
|
package/src/crypto/contracts.js
CHANGED
|
@@ -23,8 +23,15 @@ const CRYPTO_VERIFICATION_STATUSES = Object.freeze([
|
|
|
23
23
|
'realization-failure'
|
|
24
24
|
]);
|
|
25
25
|
|
|
26
|
-
const
|
|
26
|
+
const CRYPTO_RS256_RUNTIME_BUILTIN_IMPLEMENTATION = 'webcrypto.subtle.rsassa-pkcs1-v1_5-sha-256.v1';
|
|
27
|
+
const CRYPTO_RS256_GUEST_LINKED_REALIZATION = 'guest-linked:pulse-rs256-bearssl-i31';
|
|
28
|
+
const CRYPTO_RS256_GUEST_LINKED_IMPLEMENTATION = 'bearssl.0.6.rsa-i31.sha256.v1';
|
|
29
|
+
const CRYPTO_ALGORITHMS = Object.freeze(['HS256', 'ES256', 'RS256', 'SHA-256', 'HMAC-SHA256']);
|
|
27
30
|
const CRYPTO_REALIZATIONS = Object.freeze([
|
|
31
|
+
{ id: 'runtime-builtin', kind: 'runtime-builtin', algorithm: 'RS256', targets: ['javascript'], implementation: CRYPTO_RS256_RUNTIME_BUILTIN_IMPLEMENTATION },
|
|
32
|
+
{ id: CRYPTO_RS256_GUEST_LINKED_REALIZATION, kind: 'guest-linked', algorithm: 'RS256', targets: ['native'],
|
|
33
|
+
backend: 'pulse-rs256-bearssl-i31', implementation: CRYPTO_RS256_GUEST_LINKED_IMPLEMENTATION,
|
|
34
|
+
source: { package: '@pulse-compute/crypto', export: './pulsewasm-native', contractVersion: 'pulse.guest-unit-contribution.v1', contribution: 'pulseEs256GuestUnit' } },
|
|
28
35
|
...['SHA-256', 'HMAC-SHA256'].map((algorithm) => Object.freeze({
|
|
29
36
|
id: 'runtime-builtin', kind: 'runtime-builtin', algorithm, targets: Object.freeze(['javascript']),
|
|
30
37
|
implementation: algorithm === 'SHA-256' ? 'webcrypto.subtle.sha-256-bytes.v1' : 'webcrypto.subtle.hmac-sha-256-bytes.v1'
|
|
@@ -148,7 +155,7 @@ const CRYPTO_ES256_CONTRACT = deepFreeze({
|
|
|
148
155
|
unitId: 'pulse.crypto.es256.rustcrypto-p256.v1',
|
|
149
156
|
module: 'pulse_crypto_es256',
|
|
150
157
|
owner: '@pulse-compute/crypto',
|
|
151
|
-
abi: 'pulse.crypto.es256.verify.
|
|
158
|
+
abi: 'pulse.crypto.es256-rs256.verify-and-sign.v3',
|
|
152
159
|
export: {
|
|
153
160
|
name: 'pulse_crypto_es256_verify',
|
|
154
161
|
parameters: ['i32', 'i32'],
|
|
@@ -594,7 +601,7 @@ function planCryptoRealizations(input = {}) {
|
|
|
594
601
|
const requirement = requirements.algorithms.find((entry) => entry.algorithm === configured.algorithm);
|
|
595
602
|
return {
|
|
596
603
|
algorithm: configured.algorithm,
|
|
597
|
-
primitive: configured.algorithm === 'ES256'
|
|
604
|
+
primitive: configured.algorithm === 'RS256' ? Object.freeze({ kind: 'rsa-pkcs1-v1_5', hash: 'SHA-256' }) : configured.algorithm === 'ES256'
|
|
598
605
|
? Object.freeze({ kind: 'ecdsa', curve: 'P-256', hash: 'SHA-256' })
|
|
599
606
|
: Object.freeze({ kind: configured.algorithm === 'SHA-256' ? 'digest' : 'hmac', hash: 'SHA-256' }),
|
|
600
607
|
requestedBy: requirement ? requirement.requestedBy : Object.freeze([]),
|
|
@@ -623,6 +630,7 @@ function planCryptoRealizations(input = {}) {
|
|
|
623
630
|
}
|
|
624
631
|
|
|
625
632
|
module.exports = Object.freeze({
|
|
633
|
+
CRYPTO_RS256_RUNTIME_BUILTIN_IMPLEMENTATION, CRYPTO_RS256_GUEST_LINKED_REALIZATION, CRYPTO_RS256_GUEST_LINKED_IMPLEMENTATION,
|
|
626
634
|
CRYPTO_CONFIGURATION_VERSION,
|
|
627
635
|
CRYPTO_TARGET_CAPABILITIES_VERSION,
|
|
628
636
|
CRYPTO_REQUIREMENTS_VERSION,
|
|
@@ -41,9 +41,10 @@ const ROUTER_API_DEFINITIONS = Object.freeze([
|
|
|
41
41
|
name: 'mount',
|
|
42
42
|
kind: 'mount',
|
|
43
43
|
opKind: 'mount',
|
|
44
|
-
allowedArities: Object.freeze([2]),
|
|
44
|
+
allowedArities: Object.freeze([2, 3]),
|
|
45
45
|
signatures: Object.freeze([
|
|
46
|
-
Object.freeze({ arity: 2, pathArg: 0, routerArg: 1 })
|
|
46
|
+
Object.freeze({ arity: 2, pathArg: 0, routerArg: 1 }),
|
|
47
|
+
Object.freeze({ arity: 3, pathArg: 0, routerArg: 1, eligibilityArg: 2 })
|
|
47
48
|
]),
|
|
48
49
|
pathPolicy: 'mount-static'
|
|
49
50
|
}),
|
package/src/diagnostics/codes.js
CHANGED
|
@@ -10,6 +10,7 @@ const DIAGNOSTIC_CODES = Object.freeze({
|
|
|
10
10
|
UNSUPPORTED_HANDLER: 'PULSEWASM_UNSUPPORTED_HANDLER',
|
|
11
11
|
UNSUPPORTED_CHANNEL: 'PULSEWASM_UNSUPPORTED_CHANNEL',
|
|
12
12
|
UNSUPPORTED_LIFECYCLE: 'PULSEWASM_UNSUPPORTED_LIFECYCLE',
|
|
13
|
+
UNSUPPORTED_MOUNT_ELIGIBILITY: 'PULSEWASM_UNSUPPORTED_MOUNT_ELIGIBILITY',
|
|
13
14
|
UNSUPPORTED_MOUNT_TARGET: 'PULSEWASM_UNSUPPORTED_MOUNT_TARGET',
|
|
14
15
|
COMPUTED_METHOD: 'PULSEWASM_COMPUTED_METHOD',
|
|
15
16
|
DYNAMIC_ROUTE_REGISTRATION: 'PULSEWASM_DYNAMIC_ROUTE_REGISTRATION',
|
package/src/diagnostics.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
|
|
5
|
-
const PACKAGE_VERSION = 'pulsewasm-route-extractor@1.0.0-beta.
|
|
5
|
+
const PACKAGE_VERSION = 'pulsewasm-route-extractor@1.0.0-beta.6';
|
|
6
6
|
const DIAGNOSTICS_VERSION = 'pulsewasm.diagnostics.v1';
|
|
7
7
|
|
|
8
8
|
function stableFileName(fileName, cwd) {
|
|
@@ -71,6 +71,7 @@ declare const contracts: {
|
|
|
71
71
|
normalizeStaticMetadata(value: unknown, limits?: EntitiesLimits): Readonly<Record<string, unknown>> | undefined;
|
|
72
72
|
normalizeEntityDeclaration(input: unknown, limits?: EntitiesLimits): NormalizedEntityDeclaration;
|
|
73
73
|
normalizeHandler<T extends Function>(handler: T): T;
|
|
74
|
+
nativeEntitySchemaIssue(schema: Readonly<{ id: string; root?: unknown; jsonLimits?: unknown }>): Readonly<{ schemaId: string; reason: string; path: string; kind?: string }> | null;
|
|
74
75
|
normalizeHandlerReference(input: unknown): Readonly<Record<string, string>>;
|
|
75
76
|
normalizeJsonRpcAdapterOptions(input?: Readonly<{ namedParamsOnly?: true; acceptEmptyObjectForNoInput?: boolean }>): Readonly<{
|
|
76
77
|
namedParamsOnly: true;
|
|
@@ -98,6 +98,26 @@ function normalizeEntityPlan(input) {
|
|
|
98
98
|
return Object.freeze({ ...normalized, planHash: sha256Hex(stableStringify(normalized)) });
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// One capability decision shared by catalog inspection and Native source
|
|
102
|
+
// generation. Closed typed nesting is implemented; dynamic JSON and explicit
|
|
103
|
+
// admission policies must never be silently projected or ignored.
|
|
104
|
+
function nativeEntitySchemaIssue(schema) {
|
|
105
|
+
if (schema.jsonLimits) return Object.freeze({ schemaId: schema.id, reason: 'json-admission-not-realized', path: 'root' });
|
|
106
|
+
function visit(node, path) {
|
|
107
|
+
if (!node) return null; // Declaration-only compiler passes have no schema IR.
|
|
108
|
+
if (node.additionalProperties) return { schemaId: schema.id, reason: 'open-object-not-realized', path };
|
|
109
|
+
if (['scalar-record', 'json-value', 'json-object'].includes(node.kind)) return { schemaId: schema.id, reason: 'dynamic-json-not-realized', path, kind: node.kind };
|
|
110
|
+
if (node.kind === 'nullable') return visit(node.value, path + '.value');
|
|
111
|
+
if (node.kind === 'array') return visit(node.element, path + '[]');
|
|
112
|
+
if (node.kind === 'object') {
|
|
113
|
+
for (const field of node.fields) { const issue = visit(field.value, path + '.' + field.name); if (issue) return issue; }
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const issue = visit(schema.root, 'root');
|
|
118
|
+
return issue ? Object.freeze(issue) : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
101
121
|
module.exports = Object.freeze({
|
|
102
122
|
...runtime,
|
|
103
123
|
ENTITIES_PACKAGE_VERSION,
|
|
@@ -116,6 +136,7 @@ module.exports = Object.freeze({
|
|
|
116
136
|
ENTITIES_PACKAGE_INTRINSIC_VERSION,
|
|
117
137
|
ENTITIES_PUBLIC_SYMBOLS,
|
|
118
138
|
ENTITIES_PACKAGE_INTRINSICS,
|
|
139
|
+
nativeEntitySchemaIssue,
|
|
119
140
|
normalizeHandlerReference,
|
|
120
141
|
normalizeEntityRegistry,
|
|
121
142
|
normalizeEntityPlan
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const CANONICAL_NATIVE_PLAN_VERSION = 'pulse.canonical-native-plan.
|
|
3
|
+
const CANONICAL_NATIVE_PLAN_VERSION = 'pulse.canonical-native-plan.v9';
|
|
4
|
+
const CANONICAL_NATIVE_PURE_LOOP_CALL_VERSION = 'pulse.bounded-pure-loop-call.v1';
|
|
5
|
+
const CANONICAL_NATIVE_TYPED_KV_BORROW_VERSION = 'pulse.typed-kv-borrow.v1';
|
|
6
|
+
const CANONICAL_NATIVE_PURE_BORROW_VERSION = 'pulse.pure-borrow.v1';
|
|
7
|
+
const CANONICAL_NATIVE_PURE_HELPER_VERSION = 'pulse.canonical-native-pure-helper.v2';
|
|
8
|
+
const CANONICAL_NATIVE_HANDLER_BODY_VERSION = 'pulse.canonical-native-handler-body.v1';
|
|
9
|
+
const CANONICAL_NATIVE_STAGE_CONTRACT = Object.freeze({
|
|
10
|
+
version: 'pulse.canonical-native-stage.v1',
|
|
11
|
+
maxEffectSites: 64,
|
|
12
|
+
outputs: Object.freeze(['response', 'next', 'error', 'suspend', 'failure']),
|
|
13
|
+
frame: Object.freeze({ lifetime: 'invocation', reset: 'stage-entry', suspension: 'retain', nesting: false })
|
|
14
|
+
});
|
|
4
15
|
const CANONICAL_NATIVE_PLAN_HASH_ALGORITHM = 'sha256';
|
|
5
16
|
const CANONICAL_NATIVE_PLAN_OWNERSHIP_VERSION = 'pulse.canonical-native-ownership.v1';
|
|
6
17
|
const CANONICAL_PURE_LOOP_LIMITS = Object.freeze({ maxIterations: 1024, maxNestedIterations: 65536 });
|
|
18
|
+
const CANONICAL_READ_LOOP_CONTRACT = Object.freeze({
|
|
19
|
+
version: 'pulse.bounded-read-loop.v1',
|
|
20
|
+
maxIterations: 64,
|
|
21
|
+
maxNestedIterations: CANONICAL_PURE_LOOP_LIMITS.maxNestedIterations,
|
|
22
|
+
effectKinds: Object.freeze(['s3.getText', 'kv.getVersioned', 'crypto.digestText', 'output.write', 'output.readTextChunk']),
|
|
23
|
+
valueIntrinsics: Object.freeze(['schema.decode.text', 'schema.encode.text', 'response.text', 'response.json', 'response.custom']),
|
|
24
|
+
nestedEffects: false,
|
|
25
|
+
helperCallVersion: 'pulse.bounded-read-loop-helper.v1',
|
|
26
|
+
continueTarget: 'increment',
|
|
27
|
+
breakTarget: 'exit'
|
|
28
|
+
});
|
|
7
29
|
|
|
8
30
|
const CANONICAL_NATIVE_STATEMENT_KINDS = Object.freeze([
|
|
9
31
|
'local',
|
|
@@ -11,13 +33,18 @@ const CANONICAL_NATIVE_STATEMENT_KINDS = Object.freeze([
|
|
|
11
33
|
'effect-group',
|
|
12
34
|
'if',
|
|
13
35
|
'pure-loop',
|
|
36
|
+
'read-loop',
|
|
14
37
|
'break',
|
|
15
38
|
'continue',
|
|
16
39
|
'return',
|
|
40
|
+
'handler-call',
|
|
41
|
+
'helper-call',
|
|
42
|
+
'stage-call',
|
|
17
43
|
'expression'
|
|
18
44
|
]);
|
|
19
45
|
|
|
20
46
|
const CANONICAL_NATIVE_EXPRESSION_KINDS = Object.freeze([
|
|
47
|
+
'pure-helper-call',
|
|
21
48
|
'literal',
|
|
22
49
|
'undefined',
|
|
23
50
|
'local',
|
|
@@ -46,8 +73,11 @@ const CANONICAL_NATIVE_CONTEXT_READS = Object.freeze([
|
|
|
46
73
|
]);
|
|
47
74
|
|
|
48
75
|
const CANONICAL_NATIVE_INTRINSICS = Object.freeze([
|
|
76
|
+
'package.application',
|
|
49
77
|
'request.header',
|
|
50
78
|
'request.text',
|
|
79
|
+
'request.body.forward-marker',
|
|
80
|
+
'response.output.close',
|
|
51
81
|
'request.json',
|
|
52
82
|
'response.json',
|
|
53
83
|
'schema.encode.text',
|
|
@@ -124,7 +154,23 @@ const CANONICAL_NATIVE_PLAN_POLICY = Object.freeze({
|
|
|
124
154
|
javascriptRuntime: false,
|
|
125
155
|
promiseSemantics: false,
|
|
126
156
|
asyncify: false,
|
|
127
|
-
controlFlow: 'structured statements, if/else,
|
|
157
|
+
controlFlow: 'structured statements, if/else, literal-capped pure for loops and non-nested bounded sequential read loops',
|
|
158
|
+
handlerBodies: Object.freeze({
|
|
159
|
+
version: CANONICAL_NATIVE_HANDLER_BODY_VERSION,
|
|
160
|
+
family: 'terminal HTTP route bodies without next transfer',
|
|
161
|
+
calls: 'one static tail call from the dispatcher; no recursion or captured locals',
|
|
162
|
+
outcomes: 'response, suspension, normalized application-error transfer, terminal failure',
|
|
163
|
+
suspension: 'resume the owning body by program counter; no live call stack',
|
|
164
|
+
budget: 'one charge per original state; the call reference adds no state'
|
|
165
|
+
}),
|
|
166
|
+
sharedStages: Object.freeze({
|
|
167
|
+
version: CANONICAL_NATIVE_STAGE_CONTRACT.version,
|
|
168
|
+
family: 'transfer-capable HTTP routes and middleware with 1..64 bound sequential text-fetch, time.now or crypto.digestText sites',
|
|
169
|
+
inputs: 'request context, stage-owned locals and registration-owned return/effect/continuation bindings',
|
|
170
|
+
outputs: 'response, next, error, suspension or terminal failure',
|
|
171
|
+
calls: 'dispatcher admission only; bounded pure loops retain canonical caps; no captures, recursion, helper calls, groups or effect loops',
|
|
172
|
+
suspension: 'one invocation-owned frame; reset locals at entry and retain across suspension'
|
|
173
|
+
}),
|
|
128
174
|
suspension: 'explicit effect and effect-group statements with stable continuation IDs',
|
|
129
175
|
values: 'versioned JSON expression tree with stable local identities',
|
|
130
176
|
logging: 'compile-time threshold pruning plus synchronous provider-adapter emission',
|
|
@@ -134,8 +180,15 @@ const CANONICAL_NATIVE_PLAN_POLICY = Object.freeze({
|
|
|
134
180
|
});
|
|
135
181
|
|
|
136
182
|
module.exports = Object.freeze({
|
|
183
|
+
CANONICAL_NATIVE_PURE_LOOP_CALL_VERSION,
|
|
184
|
+
CANONICAL_NATIVE_PURE_BORROW_VERSION,
|
|
185
|
+
CANONICAL_NATIVE_TYPED_KV_BORROW_VERSION,
|
|
186
|
+
CANONICAL_NATIVE_PURE_HELPER_VERSION,
|
|
137
187
|
CANONICAL_NATIVE_PLAN_VERSION,
|
|
188
|
+
CANONICAL_NATIVE_HANDLER_BODY_VERSION,
|
|
189
|
+
CANONICAL_NATIVE_STAGE_CONTRACT,
|
|
138
190
|
CANONICAL_PURE_LOOP_LIMITS,
|
|
191
|
+
CANONICAL_READ_LOOP_CONTRACT,
|
|
139
192
|
CANONICAL_NATIVE_PLAN_HASH_ALGORITHM,
|
|
140
193
|
CANONICAL_NATIVE_PLAN_OWNERSHIP_VERSION,
|
|
141
194
|
CANONICAL_NATIVE_STATEMENT_KINDS,
|
|
@@ -2,9 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
const CANONICAL_NATIVE_WASM_VERSION = 'pulse.canonical-native-wasm.v2';
|
|
4
4
|
const CANONICAL_NATIVE_ABI_VERSION = 2;
|
|
5
|
-
const CANONICAL_NATIVE_AS_GENERATOR_VERSION = 'pulse.canonical-native-as-generator.
|
|
5
|
+
const CANONICAL_NATIVE_AS_GENERATOR_VERSION = 'pulse.canonical-native-as-generator.v9';
|
|
6
6
|
const CANONICAL_NATIVE_COMPILER_VERSION = 'pulse.canonical-native-wasm-compiler.v3';
|
|
7
|
-
const CANONICAL_NATIVE_HOST_VERSION = 'pulse.canonical-native-host.
|
|
7
|
+
const CANONICAL_NATIVE_HOST_VERSION = 'pulse.canonical-native-host.v4';
|
|
8
|
+
|
|
9
|
+
// Execution-wide containment for plans with bounded storage-read loops. These
|
|
10
|
+
// are accounting limits, not a JavaScript process/RSS limit. Native providers
|
|
11
|
+
// retain handles until completion; no iteration reset may refund this budget.
|
|
12
|
+
const CANONICAL_NATIVE_READ_LOOP_MEMORY = Object.freeze({
|
|
13
|
+
version: 'pulse.native-read-loop-memory.v1',
|
|
14
|
+
maxBytes: 64 * 1024 * 1024,
|
|
15
|
+
maxValues: 1024 * 1024,
|
|
16
|
+
valueBytes: 32,
|
|
17
|
+
edgeBytes: 8,
|
|
18
|
+
maximumMemoryPages: 4096,
|
|
19
|
+
lifetime: 'execution',
|
|
20
|
+
reclamation: 'retain-until-terminal'
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function hasBoundedReadLoop(plan) {
|
|
24
|
+
function visit(node) {
|
|
25
|
+
if (!node || typeof node !== 'object') return false;
|
|
26
|
+
return node.kind === 'read-loop' || Object.values(node).some(visit);
|
|
27
|
+
}
|
|
28
|
+
return visit(plan && plan.entry) || visit(plan && plan.handlers) || visit(plan && plan.helpers);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Optional data-only host-to-guest JSON copy. UTF-8 accounting precedes guest allocation.
|
|
32
|
+
const CANONICAL_NATIVE_VALUE_TRANSFER_MAX_BYTES = 1024 * 1024;
|
|
8
33
|
|
|
9
34
|
const CANONICAL_NATIVE_RUN_STATUS = Object.freeze({
|
|
10
35
|
COMPLETE: 0,
|
|
@@ -44,6 +69,8 @@ const CANONICAL_NATIVE_ASSIGNMENT_OPERATORS = Object.freeze([
|
|
|
44
69
|
const CANONICAL_NATIVE_IMPORT_MODULE = 'pulse_host';
|
|
45
70
|
const CANONICAL_NATIVE_ALLOWED_ENV_IMPORTS = Object.freeze(['abort', 'seed']);
|
|
46
71
|
const CANONICAL_NATIVE_IMPORTS = Object.freeze([
|
|
72
|
+
// Optional, bounded JSON transfer into a managed guest string; no host authority.
|
|
73
|
+
['value_json', ['i32'], ['i32']],
|
|
47
74
|
['value_undefined', [], ['i32']],
|
|
48
75
|
['value_null', [], ['i32']],
|
|
49
76
|
['value_boolean', ['i32'], ['i32']],
|
|
@@ -71,6 +98,9 @@ const CANONICAL_NATIVE_IMPORTS = Object.freeze([
|
|
|
71
98
|
['request_headers', [], ['i32']],
|
|
72
99
|
['request_header', ['i32'], ['i32']],
|
|
73
100
|
['request_text', [], ['i32']],
|
|
101
|
+
// Optional incoming-request-v1 extension: execution-local opaque marker,
|
|
102
|
+
// never a guest byte buffer or an invocation ticket. Older hosts fail import validation.
|
|
103
|
+
['request_body', [], ['i32']],
|
|
74
104
|
['request_json', ['i32'], ['i32']],
|
|
75
105
|
['router_match', ['i32', 'i32'], ['i32']],
|
|
76
106
|
['router_param', ['i32', 'i32', 'i32'], ['i32']],
|
|
@@ -80,6 +110,8 @@ const CANONICAL_NATIVE_IMPORTS = Object.freeze([
|
|
|
80
110
|
['schema_encode', ['i32', 'i32'], ['i32']],
|
|
81
111
|
['schema_decode', ['i32', 'i32'], ['i32']],
|
|
82
112
|
['response_text', ['i32', 'i32'], ['i32']],
|
|
113
|
+
// Optional output-v1 extension, required only by generated-output artifacts.
|
|
114
|
+
['output_close', [], ['i32']],
|
|
83
115
|
['response_custom', ['i32'], ['i32']],
|
|
84
116
|
['grip_is_websocket', [], ['i32']],
|
|
85
117
|
['grip_subscribe', ['i32', 'i32'], ['i32']],
|
|
@@ -119,6 +151,21 @@ const CANONICAL_NATIVE_IMPORT_NAMES = Object.freeze(CANONICAL_NATIVE_IMPORTS.map
|
|
|
119
151
|
const CANONICAL_NATIVE_EXPORT_NAMES = Object.freeze(CANONICAL_NATIVE_EXPORTS.map(([name]) => name));
|
|
120
152
|
const CANONICAL_NATIVE_SCHEMA_EXPORT_NAMES = Object.freeze(CANONICAL_NATIVE_SCHEMA_EXPORTS.map(([name]) => name));
|
|
121
153
|
|
|
154
|
+
// Canonical compilation produces a Pulse host-ABI module before provider
|
|
155
|
+
// packaging. Its imports are checked independently of the final provider ABI.
|
|
156
|
+
const { defineFinalWasmPolicy, FINAL_WASM_POLICY_VERSION } = require('../provider/final-wasm-policy.js');
|
|
157
|
+
const CANONICAL_NATIVE_FINAL_WASM_POLICY = defineFinalWasmPolicy({
|
|
158
|
+
version: FINAL_WASM_POLICY_VERSION,
|
|
159
|
+
descriptorOwner: '@pulse-compute/wasm-contracts',
|
|
160
|
+
toolchainVersion: 'pulse.provider-toolchain.v1',
|
|
161
|
+
descriptorIdentity: 'pulse-canonical-native-host',
|
|
162
|
+
permittedImports: [
|
|
163
|
+
...CANONICAL_NATIVE_IMPORT_NAMES.map(name => ({ module: 'pulse_host', name, kind: 'function' })),
|
|
164
|
+
...CANONICAL_NATIVE_ALLOWED_ENV_IMPORTS.map(name => ({ module: 'env', name, kind: 'function' }))
|
|
165
|
+
],
|
|
166
|
+
requiredExports: CANONICAL_NATIVE_EXPORTS.map(([name, second]) => ({ name, kind: second === 'memory' ? 'memory' : 'function' }))
|
|
167
|
+
});
|
|
168
|
+
|
|
122
169
|
const CANONICAL_NATIVE_DIAGNOSTIC_CODES = Object.freeze({
|
|
123
170
|
PLAN_REQUIRED: 'PULSE_CANONICAL_NATIVE_WASM_PLAN_REQUIRED',
|
|
124
171
|
PLAN_INVALID: 'PULSE_CANONICAL_NATIVE_WASM_PLAN_INVALID',
|
|
@@ -143,6 +190,7 @@ const CANONICAL_NATIVE_POLICY = Object.freeze({
|
|
|
143
190
|
suspension: 'effect_begin records descriptors before one explicit suspension boundary; results are injected before pulse_resume',
|
|
144
191
|
groupedEffects: 'all group members begin before one suspension boundary and all results must be ready before resume',
|
|
145
192
|
incompleteResume: 'rejected without advancing the program counter or consuming available results',
|
|
193
|
+
resultFreshness: 'managed hosts authenticate single-use invocation tickets; raw ABI v2 index/handle exports are trusted-host operations',
|
|
146
194
|
authority: 'request data and consequential effects remain host-owned; guest control flow remains authoritative',
|
|
147
195
|
schemaJson: 'normalized registry IR generates json-as 1.5.0 guest codecs; host preflight enforces shared policy and structured errors',
|
|
148
196
|
schemaBodyOwnership: 'request and fetched-response bytes are snapshotted once; application JSON values are encoded before suspension'
|
|
@@ -154,6 +202,9 @@ module.exports = Object.freeze({
|
|
|
154
202
|
CANONICAL_NATIVE_AS_GENERATOR_VERSION,
|
|
155
203
|
CANONICAL_NATIVE_COMPILER_VERSION,
|
|
156
204
|
CANONICAL_NATIVE_HOST_VERSION,
|
|
205
|
+
CANONICAL_NATIVE_READ_LOOP_MEMORY,
|
|
206
|
+
CANONICAL_NATIVE_VALUE_TRANSFER_MAX_BYTES,
|
|
207
|
+
hasBoundedReadLoop,
|
|
157
208
|
CANONICAL_NATIVE_RUN_STATUS,
|
|
158
209
|
CANONICAL_NATIVE_RESULT_STATUS,
|
|
159
210
|
CANONICAL_NATIVE_ERROR_CODES,
|
|
@@ -169,5 +220,6 @@ module.exports = Object.freeze({
|
|
|
169
220
|
CANONICAL_NATIVE_SCHEMA_EXPORTS,
|
|
170
221
|
CANONICAL_NATIVE_SCHEMA_EXPORT_NAMES,
|
|
171
222
|
CANONICAL_NATIVE_DIAGNOSTIC_CODES,
|
|
172
|
-
CANONICAL_NATIVE_POLICY
|
|
223
|
+
CANONICAL_NATIVE_POLICY,
|
|
224
|
+
CANONICAL_NATIVE_FINAL_WASM_POLICY
|
|
173
225
|
});
|
|
@@ -4,10 +4,19 @@ const CANONICAL_PROGRAM_VERSION = 'pulse.canonical-program.v2';
|
|
|
4
4
|
const CANONICAL_RUNTIME_PROTOCOL_VERSION = 'pulse.canonical-runtime-protocol.v2';
|
|
5
5
|
const CANONICAL_EFFECT_MARKER_KINDS = Object.freeze(['effect', 'group']);
|
|
6
6
|
const CANONICAL_PACKAGE_EFFECT_VERSION = 'pulse.canonical-package-effect.v1';
|
|
7
|
+
const CANONICAL_EFFECT_INVOCATION_POLICY = Object.freeze({
|
|
8
|
+
version: 'pulse.effect-invocation.v1',
|
|
9
|
+
defaultMaxEffects: 1024,
|
|
10
|
+
identity: 'execution-owned single-use ticket; static effect and continuation sites remain reusable',
|
|
11
|
+
terminal: 'completion, failure and cancellation invalidate outstanding tickets',
|
|
12
|
+
budget: 'cumulative effect invocations and one inherited monotonic deadline',
|
|
13
|
+
nativeResultBoundary: 'managed host; raw guest ABI v2 setters remain trusted-host operations'
|
|
14
|
+
});
|
|
7
15
|
|
|
8
16
|
module.exports = Object.freeze({
|
|
9
17
|
CANONICAL_PROGRAM_VERSION,
|
|
10
18
|
CANONICAL_RUNTIME_PROTOCOL_VERSION,
|
|
11
19
|
CANONICAL_EFFECT_MARKER_KINDS,
|
|
12
|
-
CANONICAL_PACKAGE_EFFECT_VERSION
|
|
20
|
+
CANONICAL_PACKAGE_EFFECT_VERSION,
|
|
21
|
+
CANONICAL_EFFECT_INVOCATION_POLICY
|
|
13
22
|
});
|
|
@@ -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',
|
|
@@ -269,6 +281,17 @@ const HANDLER_SURFACE_DEFINITIONS = Object.freeze([
|
|
|
269
281
|
nativeBehavior: 'emit-effect-and-continuation',
|
|
270
282
|
status: 'supported'
|
|
271
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
|
+
}),
|
|
272
295
|
surface({
|
|
273
296
|
id: 'ctx.req.json.schema',
|
|
274
297
|
class: 'effect',
|
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,9 +5,12 @@ 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',
|
|
12
15
|
'time.wall-clock',
|
|
13
16
|
'crypto.digestText',
|
|
@@ -23,10 +26,11 @@ const CANONICAL_PROVIDER_CAPABILITIES = Object.freeze([
|
|
|
23
26
|
'grip.hold',
|
|
24
27
|
'grip.publish',
|
|
25
28
|
'grip.broadcast',
|
|
26
|
-
'jwt.verify',
|
|
29
|
+
'jwt.verify', 'jwt.sign',
|
|
27
30
|
's3.head',
|
|
28
31
|
's3.getText',
|
|
29
32
|
's3.putText',
|
|
33
|
+
's3.getBody',
|
|
30
34
|
'opaque.pass-through'
|
|
31
35
|
]);
|
|
32
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
|
@@ -4,9 +4,9 @@ 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
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'].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'
|
|
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 };
|
|
@@ -1,14 +1,49 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { sha256Hex, stableStringify } = require('../stable-id.js');
|
|
4
|
+
const { JSON_LIMIT_FIELDS, normalizeJsonAdmissionLimits } = require('./admission.js');
|
|
4
5
|
|
|
5
|
-
const SCHEMA_REGISTRY_IR_VERSION = 'pulse.schema-registry-ir.
|
|
6
|
-
const SCHEMA_CODEC_INPUTS_VERSION = 'pulse.schema-codec-inputs.
|
|
7
|
-
const SCHEMA_AUTHORING_VERSION = 'pulse.schema-authoring.
|
|
6
|
+
const SCHEMA_REGISTRY_IR_VERSION = 'pulse.schema-registry-ir.v5';
|
|
7
|
+
const SCHEMA_CODEC_INPUTS_VERSION = 'pulse.schema-codec-inputs.v5';
|
|
8
|
+
const SCHEMA_AUTHORING_VERSION = 'pulse.schema-authoring.v3';
|
|
8
9
|
const SCHEMA_ID_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)+$/;
|
|
9
10
|
const RESPONSE_CASE_ID_PATTERN = SCHEMA_ID_PATTERN;
|
|
10
11
|
const SCALAR_KINDS = Object.freeze(['string', 'boolean', 'i32', 'u32', 'f64']);
|
|
11
|
-
const NODE_KINDS = Object.freeze([...SCALAR_KINDS, 'object', 'array', 'string-enum', 'nullable']);
|
|
12
|
+
const NODE_KINDS = Object.freeze([...SCALAR_KINDS, 'object', 'array', 'string-enum', 'nullable', 'scalar-record', 'json-value', 'json-object']);
|
|
13
|
+
const SCALAR_RECORD_LIMITS = Object.freeze({ maxKeys: 32, maxKeyLength: 64, maxStringLength: 1024, maxBytes: 8192, numberBytes: 24 });
|
|
14
|
+
const JSON_SCHEMA_DEFAULT_LIMITS = Object.freeze({ maxTextBytes: 65536, maxDepth: 32, maxNodes: 4096,
|
|
15
|
+
maxObjectMembers: 256, maxArrayItems: 1024, maxKeyLength: 256, maxStringLength: 16384, maxJsonBytes: 65536 });
|
|
16
|
+
|
|
17
|
+
function normalizeJsonSchemaLimits(input = {}) {
|
|
18
|
+
if (!plainObject(input)) throw schemaContractError('PULSE_SCHEMA_JSON_LIMITS_INVALID', 'Schema JSON limits must be a static object.');
|
|
19
|
+
const values = { ...JSON_SCHEMA_DEFAULT_LIMITS };
|
|
20
|
+
for (const key of Reflect.ownKeys(input)) {
|
|
21
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
22
|
+
if (!JSON_LIMIT_FIELDS.includes(key) || !descriptor || !Object.hasOwn(descriptor, 'value')) {
|
|
23
|
+
throw schemaContractError('PULSE_SCHEMA_JSON_LIMITS_INVALID', 'Unknown JSON limit or non-data property.');
|
|
24
|
+
}
|
|
25
|
+
values[key] = descriptor.value;
|
|
26
|
+
}
|
|
27
|
+
try { return normalizeJsonAdmissionLimits(values); }
|
|
28
|
+
catch { throw schemaContractError('PULSE_SCHEMA_JSON_LIMITS_INVALID', 'JSON limits must be positive i32 integers.'); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Shared conservative UTF-8 JSON string sizing. Native code generation reuses
|
|
32
|
+
// this pure function body; keep it independent of host APIs and mutable state.
|
|
33
|
+
function scalarRecordStringBytes(text) {
|
|
34
|
+
let bytes = 2;
|
|
35
|
+
for (let i = 0; i < text.length; i++) {
|
|
36
|
+
const code = text.charCodeAt(i);
|
|
37
|
+
if (code < 32) bytes += 6;
|
|
38
|
+
else if (code === 34 || code === 92) bytes += 2;
|
|
39
|
+
else if (code < 128) bytes += 1;
|
|
40
|
+
else if (code < 2048) bytes += 2;
|
|
41
|
+
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length && text.charCodeAt(i + 1) >= 0xdc00 && text.charCodeAt(i + 1) <= 0xdfff) { bytes += 4; i++; }
|
|
42
|
+
else if (code >= 0xd800 && code <= 0xdfff) bytes += 6;
|
|
43
|
+
else bytes += 3;
|
|
44
|
+
}
|
|
45
|
+
return bytes;
|
|
46
|
+
}
|
|
12
47
|
|
|
13
48
|
function schemaContractError(code, message, details = {}) {
|
|
14
49
|
const error = new TypeError(message);
|
|
@@ -73,6 +108,14 @@ function normalizeSchemaNode(input, field = 'schema.root', stack = []) {
|
|
|
73
108
|
throw schemaContractError('PULSE_SCHEMA_IR_NODE_KIND_UNSUPPORTED', `${field}.kind is unsupported: ${kind}.`, { field, kind });
|
|
74
109
|
}
|
|
75
110
|
if (SCALAR_KINDS.includes(kind)) return Object.freeze({ kind });
|
|
111
|
+
if (kind === 'json-value' || kind === 'json-object') return Object.freeze({ kind });
|
|
112
|
+
if (kind === 'scalar-record') {
|
|
113
|
+
if (input.limits !== undefined && (!plainObject(input.limits)
|
|
114
|
+
|| stableStringify(input.limits) !== stableStringify(SCALAR_RECORD_LIMITS))) {
|
|
115
|
+
throw schemaContractError('PULSE_SCHEMA_IR_NODE_KIND_UNSUPPORTED', `${field} must use the fixed ScalarRecord limits.`, { field, kind });
|
|
116
|
+
}
|
|
117
|
+
return Object.freeze({ kind, limits: SCALAR_RECORD_LIMITS });
|
|
118
|
+
}
|
|
76
119
|
if (kind === 'array') {
|
|
77
120
|
return Object.freeze({ kind, element: normalizeSchemaNode(input.element, `${field}.element`, stack) });
|
|
78
121
|
}
|
|
@@ -119,7 +162,13 @@ function normalizeSchemaNode(input, field = 'schema.root', stack = []) {
|
|
|
119
162
|
source: entry.source ? normalizeSource(entry.source, `${field}.fields[${index}].source`) : undefined
|
|
120
163
|
});
|
|
121
164
|
});
|
|
122
|
-
|
|
165
|
+
if (input.additionalProperties !== undefined
|
|
166
|
+
&& (!plainObject(input.additionalProperties) || input.additionalProperties.kind !== 'json-value'
|
|
167
|
+
|| Object.keys(input.additionalProperties).length !== 1)) {
|
|
168
|
+
throw schemaContractError('PULSE_SCHEMA_IR_NODE_KIND_UNSUPPORTED', `${field}.additionalProperties must be a JSON value policy.`, { field, kind });
|
|
169
|
+
}
|
|
170
|
+
return Object.freeze({ kind, fields: Object.freeze(fields),
|
|
171
|
+
...(input.additionalProperties === undefined ? {} : { additionalProperties: Object.freeze({ kind: 'json-value' }) }) });
|
|
123
172
|
}
|
|
124
173
|
|
|
125
174
|
function normalizeSchema(input, index) {
|
|
@@ -135,6 +184,8 @@ function normalizeSchema(input, index) {
|
|
|
135
184
|
id,
|
|
136
185
|
typeName: requiredString(input.typeName, `schemas[${index}].typeName`),
|
|
137
186
|
root,
|
|
187
|
+
...(input.jsonLimits !== undefined || schemaHasNestedJson(root)
|
|
188
|
+
? { jsonLimits: normalizeJsonSchemaLimits(input.jsonLimits) } : {}),
|
|
138
189
|
source: normalizeSource(input.source, `schemas[${index}].source`)
|
|
139
190
|
});
|
|
140
191
|
}
|
|
@@ -196,9 +247,16 @@ function normalizeSchemaRegistry(input) {
|
|
|
196
247
|
requiredFieldsOnly: false,
|
|
197
248
|
optionalFields: 'omit-absent-own-properties',
|
|
198
249
|
presentUndefined: 'reject',
|
|
199
|
-
unknownInputFields: 'drop',
|
|
200
|
-
outputFields: 'declared-
|
|
250
|
+
unknownInputFields: 'drop-unless-open',
|
|
251
|
+
outputFields: 'declared-and-admitted-open-properties',
|
|
201
252
|
outputFieldOrder: 'declaration',
|
|
253
|
+
scalarRecords: 'bounded-own-scalar-properties',
|
|
254
|
+
scalarRecordDuplicateKeys: 'reject-after-unescaping',
|
|
255
|
+
nestedJson: 'bounded-recursive-json-values',
|
|
256
|
+
nestedJsonDuplicateKeys: 'reject-after-unescaping',
|
|
257
|
+
openObjects: 'known-fields-first-then-bounded-json-extras',
|
|
258
|
+
openObjectDuplicateKeys: 'reject-all-names-after-unescaping',
|
|
259
|
+
jsonAdmission: 'whole-document-before-materialization',
|
|
202
260
|
numericPolicy: 'finite-rfc-json',
|
|
203
261
|
parity: 'semantic',
|
|
204
262
|
dynamicIds: false,
|
|
@@ -264,12 +322,30 @@ function schemaHasOptionalProperties(node) {
|
|
|
264
322
|
return node.kind === 'object' && node.fields.some(field => !field.required || schemaHasOptionalProperties(field.value));
|
|
265
323
|
}
|
|
266
324
|
|
|
325
|
+
function schemaHasScalarRecord(node) {
|
|
326
|
+
if (node.kind === 'scalar-record') return true;
|
|
327
|
+
if (node.kind === 'nullable') return schemaHasScalarRecord(node.value);
|
|
328
|
+
if (node.kind === 'array') return schemaHasScalarRecord(node.element);
|
|
329
|
+
return node.kind === 'object' && node.fields.some(field => schemaHasScalarRecord(field.value));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function schemaNeedsValueProjection(node) {
|
|
333
|
+
return schemaHasOptionalProperties(node) || schemaHasScalarRecord(node) || schemaHasNestedJson(node);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function schemaHasNestedJson(node) {
|
|
337
|
+
if (node.kind === 'json-value' || node.kind === 'json-object') return true;
|
|
338
|
+
if (node.kind === 'nullable') return schemaHasNestedJson(node.value);
|
|
339
|
+
if (node.kind === 'array') return schemaHasNestedJson(node.element);
|
|
340
|
+
return node.kind === 'object' && (Boolean(node.additionalProperties) || node.fields.some(field => schemaHasNestedJson(field.value)));
|
|
341
|
+
}
|
|
342
|
+
|
|
267
343
|
function codecInputsForRegistry(registryInput) {
|
|
268
344
|
const registry = normalizeSchemaRegistry(registryInput);
|
|
269
345
|
const nativeSchemas = registry.schemas.map((schema) => {
|
|
270
346
|
const schemaClasses = [];
|
|
271
347
|
const symbols = new Map();
|
|
272
|
-
const presence =
|
|
348
|
+
const presence = schemaNeedsValueProjection(schema.root) || Boolean(schema.jsonLimits);
|
|
273
349
|
if (!presence) collectNativeClasses(schema, schema.root, symbols, schemaClasses);
|
|
274
350
|
const rootClass = presence ? 'JSON.Value' : symbols.get('');
|
|
275
351
|
return Object.freeze({
|
|
@@ -336,7 +412,9 @@ function defaultSchemaRegistryContract() {
|
|
|
336
412
|
scalarKinds: SCALAR_KINDS,
|
|
337
413
|
nodeKinds: NODE_KINDS,
|
|
338
414
|
helpers: ['defineSchemaRegistry', 'schema', 'response'],
|
|
339
|
-
markerTypes: ['Int32', 'Uint32'],
|
|
415
|
+
markerTypes: ['Int32', 'Uint32', 'ScalarRecord', 'JsonValue', 'JsonObject', 'OpenObject'],
|
|
416
|
+
scalarRecordLimits: SCALAR_RECORD_LIMITS,
|
|
417
|
+
jsonDefaultLimits: JSON_SCHEMA_DEFAULT_LIMITS,
|
|
340
418
|
policies: {
|
|
341
419
|
canonicalEntrypoint: 'pulse.schema',
|
|
342
420
|
defaultExportRequired: true,
|
|
@@ -345,6 +423,10 @@ function defaultSchemaRegistryContract() {
|
|
|
345
423
|
exactLiteralIdsAtBoundaries: true,
|
|
346
424
|
oldSchemasJsonSupported: false,
|
|
347
425
|
optionalPropertiesSupported: true,
|
|
426
|
+
scalarRecordsSupported: true,
|
|
427
|
+
nestedJsonSupported: true,
|
|
428
|
+
typedOpenObjectsSupported: true,
|
|
429
|
+
staticJsonLimitsSupported: true,
|
|
348
430
|
recursiveSchemasSupported: false,
|
|
349
431
|
publicJsonAsImportsSupported: false,
|
|
350
432
|
automaticFallback: false
|
|
@@ -361,11 +443,18 @@ module.exports = Object.freeze({
|
|
|
361
443
|
RESPONSE_CASE_ID_PATTERN,
|
|
362
444
|
SCALAR_KINDS,
|
|
363
445
|
NODE_KINDS,
|
|
446
|
+
SCALAR_RECORD_LIMITS,
|
|
447
|
+
JSON_SCHEMA_DEFAULT_LIMITS,
|
|
448
|
+
normalizeJsonSchemaLimits,
|
|
449
|
+
scalarRecordStringBytes,
|
|
364
450
|
schemaContractError,
|
|
365
451
|
normalizeSchemaNode,
|
|
366
452
|
normalizeSchemaRegistry,
|
|
367
453
|
codecInputsForRegistry,
|
|
368
454
|
schemaHasOptionalProperties,
|
|
455
|
+
schemaHasScalarRecord,
|
|
456
|
+
schemaHasNestedJson,
|
|
457
|
+
schemaNeedsValueProjection,
|
|
369
458
|
defaultSchemaBoundaryPolicy,
|
|
370
459
|
defaultSchemaRegistryContract
|
|
371
460
|
});
|