@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
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,19 +1,50 @@
|
|
|
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';
|
|
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
|
+
});
|
|
6
29
|
|
|
7
30
|
const CANONICAL_NATIVE_STATEMENT_KINDS = Object.freeze([
|
|
8
31
|
'local',
|
|
9
32
|
'effect',
|
|
10
33
|
'effect-group',
|
|
11
34
|
'if',
|
|
35
|
+
'pure-loop',
|
|
36
|
+
'read-loop',
|
|
37
|
+
'break',
|
|
38
|
+
'continue',
|
|
12
39
|
'return',
|
|
40
|
+
'handler-call',
|
|
41
|
+
'helper-call',
|
|
42
|
+
'stage-call',
|
|
13
43
|
'expression'
|
|
14
44
|
]);
|
|
15
45
|
|
|
16
46
|
const CANONICAL_NATIVE_EXPRESSION_KINDS = Object.freeze([
|
|
47
|
+
'pure-helper-call',
|
|
17
48
|
'literal',
|
|
18
49
|
'undefined',
|
|
19
50
|
'local',
|
|
@@ -42,11 +73,15 @@ const CANONICAL_NATIVE_CONTEXT_READS = Object.freeze([
|
|
|
42
73
|
]);
|
|
43
74
|
|
|
44
75
|
const CANONICAL_NATIVE_INTRINSICS = Object.freeze([
|
|
76
|
+
'package.application',
|
|
45
77
|
'request.header',
|
|
46
78
|
'request.text',
|
|
79
|
+
'request.body.forward-marker',
|
|
80
|
+
'response.output.close',
|
|
47
81
|
'request.json',
|
|
48
82
|
'response.json',
|
|
49
83
|
'schema.encode.text',
|
|
84
|
+
'schema.decode.text',
|
|
50
85
|
'response.text',
|
|
51
86
|
'response.custom',
|
|
52
87
|
'kv.namespace',
|
|
@@ -119,7 +154,23 @@ const CANONICAL_NATIVE_PLAN_POLICY = Object.freeze({
|
|
|
119
154
|
javascriptRuntime: false,
|
|
120
155
|
promiseSemantics: false,
|
|
121
156
|
asyncify: false,
|
|
122
|
-
controlFlow: 'structured
|
|
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
|
+
}),
|
|
123
174
|
suspension: 'explicit effect and effect-group statements with stable continuation IDs',
|
|
124
175
|
values: 'versioned JSON expression tree with stable local identities',
|
|
125
176
|
logging: 'compile-time threshold pruning plus synchronous provider-adapter emission',
|
|
@@ -129,7 +180,15 @@ const CANONICAL_NATIVE_PLAN_POLICY = Object.freeze({
|
|
|
129
180
|
});
|
|
130
181
|
|
|
131
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,
|
|
132
187
|
CANONICAL_NATIVE_PLAN_VERSION,
|
|
188
|
+
CANONICAL_NATIVE_HANDLER_BODY_VERSION,
|
|
189
|
+
CANONICAL_NATIVE_STAGE_CONTRACT,
|
|
190
|
+
CANONICAL_PURE_LOOP_LIMITS,
|
|
191
|
+
CANONICAL_READ_LOOP_CONTRACT,
|
|
133
192
|
CANONICAL_NATIVE_PLAN_HASH_ALGORITHM,
|
|
134
193
|
CANONICAL_NATIVE_PLAN_OWNERSHIP_VERSION,
|
|
135
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']],
|
|
@@ -63,6 +90,7 @@ const CANONICAL_NATIVE_IMPORTS = Object.freeze([
|
|
|
63
90
|
['value_unary', ['i32', 'i32'], ['i32']],
|
|
64
91
|
['value_truthy', ['i32'], ['i32']],
|
|
65
92
|
['value_nullish', ['i32'], ['i32']],
|
|
93
|
+
['value_string_trim', ['i32'], ['i32']],
|
|
66
94
|
['log', ['i32', 'i32'], []],
|
|
67
95
|
['request_method', [], ['i32']],
|
|
68
96
|
['request_url', [], ['i32']],
|
|
@@ -70,12 +98,20 @@ const CANONICAL_NATIVE_IMPORTS = Object.freeze([
|
|
|
70
98
|
['request_headers', [], ['i32']],
|
|
71
99
|
['request_header', ['i32'], ['i32']],
|
|
72
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']],
|
|
73
104
|
['request_json', ['i32'], ['i32']],
|
|
74
105
|
['router_match', ['i32', 'i32'], ['i32']],
|
|
75
106
|
['router_param', ['i32', 'i32', 'i32'], ['i32']],
|
|
107
|
+
// 0: no failure; positive: application error handle; negative: terminal failure.
|
|
108
|
+
['router_error_take', [], ['i32']],
|
|
76
109
|
['response_json', ['i32', 'i32'], ['i32']],
|
|
77
110
|
['schema_encode', ['i32', 'i32'], ['i32']],
|
|
111
|
+
['schema_decode', ['i32', 'i32'], ['i32']],
|
|
78
112
|
['response_text', ['i32', 'i32'], ['i32']],
|
|
113
|
+
// Optional output-v1 extension, required only by generated-output artifacts.
|
|
114
|
+
['output_close', [], ['i32']],
|
|
79
115
|
['response_custom', ['i32'], ['i32']],
|
|
80
116
|
['grip_is_websocket', [], ['i32']],
|
|
81
117
|
['grip_subscribe', ['i32', 'i32'], ['i32']],
|
|
@@ -115,6 +151,21 @@ const CANONICAL_NATIVE_IMPORT_NAMES = Object.freeze(CANONICAL_NATIVE_IMPORTS.map
|
|
|
115
151
|
const CANONICAL_NATIVE_EXPORT_NAMES = Object.freeze(CANONICAL_NATIVE_EXPORTS.map(([name]) => name));
|
|
116
152
|
const CANONICAL_NATIVE_SCHEMA_EXPORT_NAMES = Object.freeze(CANONICAL_NATIVE_SCHEMA_EXPORTS.map(([name]) => name));
|
|
117
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
|
+
|
|
118
169
|
const CANONICAL_NATIVE_DIAGNOSTIC_CODES = Object.freeze({
|
|
119
170
|
PLAN_REQUIRED: 'PULSE_CANONICAL_NATIVE_WASM_PLAN_REQUIRED',
|
|
120
171
|
PLAN_INVALID: 'PULSE_CANONICAL_NATIVE_WASM_PLAN_INVALID',
|
|
@@ -139,6 +190,7 @@ const CANONICAL_NATIVE_POLICY = Object.freeze({
|
|
|
139
190
|
suspension: 'effect_begin records descriptors before one explicit suspension boundary; results are injected before pulse_resume',
|
|
140
191
|
groupedEffects: 'all group members begin before one suspension boundary and all results must be ready before resume',
|
|
141
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',
|
|
142
194
|
authority: 'request data and consequential effects remain host-owned; guest control flow remains authoritative',
|
|
143
195
|
schemaJson: 'normalized registry IR generates json-as 1.5.0 guest codecs; host preflight enforces shared policy and structured errors',
|
|
144
196
|
schemaBodyOwnership: 'request and fetched-response bytes are snapshotted once; application JSON values are encoded before suspension'
|
|
@@ -150,6 +202,9 @@ module.exports = Object.freeze({
|
|
|
150
202
|
CANONICAL_NATIVE_AS_GENERATOR_VERSION,
|
|
151
203
|
CANONICAL_NATIVE_COMPILER_VERSION,
|
|
152
204
|
CANONICAL_NATIVE_HOST_VERSION,
|
|
205
|
+
CANONICAL_NATIVE_READ_LOOP_MEMORY,
|
|
206
|
+
CANONICAL_NATIVE_VALUE_TRANSFER_MAX_BYTES,
|
|
207
|
+
hasBoundedReadLoop,
|
|
153
208
|
CANONICAL_NATIVE_RUN_STATUS,
|
|
154
209
|
CANONICAL_NATIVE_RESULT_STATUS,
|
|
155
210
|
CANONICAL_NATIVE_ERROR_CODES,
|
|
@@ -165,5 +220,6 @@ module.exports = Object.freeze({
|
|
|
165
220
|
CANONICAL_NATIVE_SCHEMA_EXPORTS,
|
|
166
221
|
CANONICAL_NATIVE_SCHEMA_EXPORT_NAMES,
|
|
167
222
|
CANONICAL_NATIVE_DIAGNOSTIC_CODES,
|
|
168
|
-
CANONICAL_NATIVE_POLICY
|
|
223
|
+
CANONICAL_NATIVE_POLICY,
|
|
224
|
+
CANONICAL_NATIVE_FINAL_WASM_POLICY
|
|
169
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
|
});
|
|
@@ -6,8 +6,8 @@ const CTX_CONTRACT_VERSION = 'pulse.ctx-contract.v4';
|
|
|
6
6
|
const CTX_CONTRACT_ID = 'pulse.ctx';
|
|
7
7
|
const CTX_SURFACE_IDS = Object.freeze([
|
|
8
8
|
'ctx.param','ctx.req.method','ctx.req.url','ctx.req.path','ctx.req.header','ctx.req.headers',
|
|
9
|
-
'ctx.state.get','ctx.state.set','ctx.encodeJson','ctx.json','ctx.text','ctx.response','ctx.req.text','ctx.req.json.schema',
|
|
10
|
-
'ctx.req.json.generic','ctx.fetch.projected','ctx.fetch.opaque-return','ctx.config.get','ctx.secret.get',
|
|
9
|
+
'ctx.state.get','ctx.state.set','ctx.encodeJson','ctx.decodeJson','ctx.json','ctx.text','ctx.response','ctx.req.text','ctx.req.json.schema',
|
|
10
|
+
'ctx.req.json.generic','ctx.fetch.projected','ctx.fetch.opaque-return','ctx.time.now','ctx.config.get','ctx.secret.get',
|
|
11
11
|
'ctx.kv.get','ctx.kv.put','ctx.kv.getVersioned','ctx.kv.insertIfAbsent','ctx.kv.compareAndSwap','ctx.emit','package.operation'
|
|
12
12
|
]);
|
|
13
13
|
const surfaceById = new Map(HANDLER_SURFACE_DEFINITIONS.map((entry) => [entry.id, entry]));
|
|
@@ -33,7 +33,7 @@ function defaultCtxContract() {
|
|
|
33
33
|
handlerSurfaceVersion: HANDLER_SURFACE_CONTRACT_VERSION,
|
|
34
34
|
surfaces: CTX_SURFACES,
|
|
35
35
|
planes: Object.freeze({
|
|
36
|
-
sharedAuthoring: Object.freeze(['state', 'log', 'fetch', 'parallel', 'config', 'secret', 'kv', 'emit', 'encodeJson']),
|
|
36
|
+
sharedAuthoring: Object.freeze(['state', 'log', 'time', 'fetch', 'parallel', 'config', 'secret', 'kv', 'emit', 'encodeJson', 'decodeJson']),
|
|
37
37
|
httpAuthoring: Object.freeze(['req', 'json', 'text', 'response']),
|
|
38
38
|
routeAuthoring: Object.freeze(['param']),
|
|
39
39
|
eventAuthoring: Object.freeze(['event.type', 'event.payload']),
|
|
@@ -12,6 +12,7 @@ const ERROR_KINDS = Object.freeze({
|
|
|
12
12
|
fetchTimeout: 'FetchTimeoutError',
|
|
13
13
|
bodyTooLarge: 'BodyTooLargeError',
|
|
14
14
|
bodyDecode: 'BodyDecodeError',
|
|
15
|
+
requestBodyInvalidUtf8: 'RequestBodyInvalidUtf8Error',
|
|
15
16
|
bodyUnavailable: 'BodyUnavailableError',
|
|
16
17
|
opaqueBodyInspection: 'OpaqueBodyInspectionError',
|
|
17
18
|
responseEncode: 'ResponseEncodeError',
|
|
@@ -113,7 +114,7 @@ function readInputBody(input) {
|
|
|
113
114
|
: Object.prototype.hasOwnProperty.call(source, 'text')
|
|
114
115
|
? source.text
|
|
115
116
|
: source.jsonText;
|
|
116
|
-
return { available: true,
|
|
117
|
+
return { available: true, value };
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
function createStructuredBodyValue(input = {}, options = {}) {
|
|
@@ -124,7 +125,8 @@ function createStructuredBodyValue(input = {}, options = {}) {
|
|
|
124
125
|
const contentClass = classifyContentType(contentType);
|
|
125
126
|
const forceStructured = options.forceStructured === true;
|
|
126
127
|
const body = readInputBody(input);
|
|
127
|
-
const
|
|
128
|
+
const raw = body.value instanceof Uint8Array ? body.value : undefined;
|
|
129
|
+
const bytes = raw ? raw.byteLength : utf8ByteLength(body.value);
|
|
128
130
|
const counters = { bodyCopies: 0, textTransforms: 0, jsonTransforms: 0, errors: 0 };
|
|
129
131
|
let textMemo;
|
|
130
132
|
let jsonMemo;
|
|
@@ -156,12 +158,20 @@ function createStructuredBodyValue(input = {}, options = {}) {
|
|
|
156
158
|
textMemo = rejected;
|
|
157
159
|
return textMemo;
|
|
158
160
|
}
|
|
161
|
+
let value;
|
|
162
|
+
try {
|
|
163
|
+
value = raw ? new TextDecoder('utf-8', { fatal: options.strictUtf8 === true, ignoreBOM: true }).decode(raw) : String(body.value ?? '');
|
|
164
|
+
} catch (_) {
|
|
165
|
+
counters.errors += 1;
|
|
166
|
+
textMemo = runtimeValueError(ERROR_KINDS.requestBodyInvalidUtf8, 'Pulse request text must be well-formed UTF-8.');
|
|
167
|
+
return textMemo;
|
|
168
|
+
}
|
|
159
169
|
counters.bodyCopies += 1;
|
|
160
170
|
counters.textTransforms += 1;
|
|
161
171
|
textMemo = deepFreeze({
|
|
162
172
|
ok: true,
|
|
163
173
|
kind: 'structured-text',
|
|
164
|
-
value
|
|
174
|
+
value,
|
|
165
175
|
bytes,
|
|
166
176
|
contentType: String(contentType || ''),
|
|
167
177
|
contentClass,
|