@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 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.5/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.5` 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.5",
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.5/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.5';
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.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';
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, and literal-capped pure for loops; no effect iteration',
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.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']],
@@ -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',
@@ -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,
@@ -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,
@@ -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,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  module.exports = {
4
+ admission: { ...require('./admission.js'), ...require('./admission-text.js') },
4
5
  v1: require('./v1.js'),
5
6
  v2: require('./v2.js'),
6
7
  compile: require('./compile.js'),
@@ -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.v2';
6
- const SCHEMA_CODEC_INPUTS_VERSION = 'pulse.schema-codec-inputs.v2';
7
- const SCHEMA_AUTHORING_VERSION = 'pulse.schema-authoring.v1';
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
- return Object.freeze({ kind, fields: Object.freeze(fields) });
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-only',
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 = schemaHasOptionalProperties(schema.root);
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
  });