@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 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.4/packages/implementation-packages/)
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.4` package policy.
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.4",
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.4/packages/implementation-packages/",
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
  }
@@ -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};
@@ -23,8 +23,15 @@ const CRYPTO_VERIFICATION_STATUSES = Object.freeze([
23
23
  'realization-failure'
24
24
  ]);
25
25
 
26
- const CRYPTO_ALGORITHMS = Object.freeze(['HS256', 'ES256', 'SHA-256', 'HMAC-SHA256']);
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.v1',
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
  }),
@@ -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',
@@ -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.4';
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.v2';
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 straight-line statements plus if/else branches',
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.v3';
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.v3';
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, text: Buffer.isBuffer(value) ? value.toString('utf8') : String(value ?? '') };
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 bytes = utf8ByteLength(body.text);
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: String(body.text),
174
+ value,
165
175
  bytes,
166
176
  contentType: String(contentType || ''),
167
177
  contentClass,