@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.
@@ -57,6 +57,18 @@ function surface(definition) {
57
57
  }
58
58
 
59
59
  const HANDLER_SURFACE_DEFINITIONS = Object.freeze([
60
+ surface({ id: 'ctx.req.readTextChunk', class: 'effect', canonicalOperation: 'output.readTextChunk',
61
+ publicForms: ['ctx.req.readTextChunk()'], awaitPolicy: 'required-when-consumed', validPositions: ['statement'],
62
+ targetSupport: { javascript: true, native: true }, nativeBehavior: 'request-owned-input-suspension', status: 'experimental' }),
63
+ ...['start', 'write'].map(operation => surface({
64
+ id: `ctx.output.${operation}`, class: 'effect', canonicalOperation: `output.${operation}`,
65
+ publicForms: [`ctx.output.${operation}(...)`], awaitPolicy: 'required-when-consumed',
66
+ validPositions: ['statement'], targetSupport: { javascript: true, native: true },
67
+ nativeBehavior: 'request-owned-output-suspension', status: 'experimental'
68
+ })),
69
+ surface({ id: 'ctx.output.close', class: 'sync', canonicalOperation: 'response.output.close',
70
+ publicForms: ['ctx.output.close()'], awaitPolicy: 'forbidden', validPositions: ['return'],
71
+ targetSupport: { javascript: true, native: true }, nativeBehavior: 'terminal-output-marker', status: 'experimental' }),
60
72
  surface({
61
73
  id: 'handler.managed-async-wrapper',
62
74
  class: 'handler-wrapper',
@@ -212,6 +224,18 @@ const HANDLER_SURFACE_DEFINITIONS = Object.freeze([
212
224
  nativeBehavior: 'construct-canonical-result',
213
225
  status: 'existing-alias-normalization'
214
226
  }),
227
+ surface({
228
+ id: 'ctx.decodeJson',
229
+ class: 'sync',
230
+ canonicalOperation: 'schema.decode.text',
231
+ publicForms: ["ctx.decodeJson<T>(text, 'schema.id')"],
232
+ awaitPolicy: 'optional-redundant-warning',
233
+ validPositions: ['expression', 'return', 'condition'],
234
+ targetSupport: { javascript: true, native: true },
235
+ nativeBehavior: 'decode-bounded-schema-text',
236
+ schemaPolicy: 'literal-schema-required',
237
+ status: 'supported'
238
+ }),
215
239
  surface({
216
240
  id: 'ctx.encodeJson',
217
241
  class: 'sync',
@@ -257,6 +281,17 @@ const HANDLER_SURFACE_DEFINITIONS = Object.freeze([
257
281
  nativeBehavior: 'emit-effect-and-continuation',
258
282
  status: 'supported'
259
283
  }),
284
+ surface({
285
+ id: 'ctx.req.body',
286
+ class: 'sync',
287
+ canonicalOperation: 'request.body.forward-marker',
288
+ publicForms: ["ctx.fetch(url, { method: 'POST', body: ctx.req.body() })"],
289
+ awaitPolicy: 'forbidden',
290
+ validPositions: ['fetch-body'],
291
+ targetSupport: { javascript: true, native: true },
292
+ nativeBehavior: 'host-owned-marker',
293
+ status: 'node-configured-only'
294
+ }),
260
295
  surface({
261
296
  id: 'ctx.req.json.schema',
262
297
  class: 'effect',
@@ -315,6 +350,17 @@ const HANDLER_SURFACE_DEFINITIONS = Object.freeze([
315
350
  nativeBehavior: 'emit-terminal-fetch-effect',
316
351
  status: 'supported'
317
352
  }),
353
+ surface({
354
+ id: 'ctx.time.now',
355
+ class: 'effect',
356
+ canonicalOperation: 'effect.time.now',
357
+ publicForms: ['ctx.time.now()'],
358
+ awaitPolicy: 'required-when-consumed',
359
+ validPositions: ['await-expression', 'parallel-member'],
360
+ targetSupport: { javascript: true, native: true },
361
+ nativeBehavior: 'emit-provider-wall-clock-effect-and-continuation',
362
+ status: 'supported'
363
+ }),
318
364
  surface({
319
365
  id: 'ctx.config.get',
320
366
  class: 'effect',
@@ -20,7 +20,7 @@ function defaultHandlerTypesContract() {
20
20
  set: 'void'
21
21
  }),
22
22
  executionContexts: Object.freeze({
23
- shared: Object.freeze(['state', 'log', 'fetch', 'parallel', 'config', 'secret', 'kv', 'emit', 'encodeJson']),
23
+ shared: Object.freeze(['state', 'log', 'time', 'fetch', 'parallel', 'config', 'secret', 'kv', 'emit', 'encodeJson', 'decodeJson']),
24
24
  http: Object.freeze(['req', 'json', 'text', 'response']),
25
25
  route: Object.freeze(['param']),
26
26
  event: Object.freeze(['event.type', 'event.payload']),
@@ -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,10 +5,15 @@ const CANONICAL_PROVIDER_PLAN_VERSION = 'pulse.canonical-provider-plan.v1';
5
5
 
6
6
  const CANONICAL_PROVIDER_CAPABILITIES = Object.freeze([
7
7
  'request',
8
+ 'request.body.forward',
8
9
  'response.json',
9
10
  'response.text',
10
11
  'response.custom',
12
+ 'response.output',
13
+ 'request.body.transform',
11
14
  'fetch',
15
+ 'time.wall-clock',
16
+ 'crypto.digestText',
12
17
  'config.get',
13
18
  'secret.get',
14
19
  'kv.get',
@@ -21,10 +26,11 @@ const CANONICAL_PROVIDER_CAPABILITIES = Object.freeze([
21
26
  'grip.hold',
22
27
  'grip.publish',
23
28
  'grip.broadcast',
24
- 'jwt.verify',
29
+ 'jwt.verify', 'jwt.sign',
25
30
  's3.head',
26
31
  's3.getText',
27
32
  's3.putText',
33
+ 's3.getBody',
28
34
  'opaque.pass-through'
29
35
  ]);
30
36
 
@@ -52,6 +52,8 @@ const DRIVER_FIELDS = Object.freeze([
52
52
  'targets',
53
53
  'execute',
54
54
  'prepareNativeExecution',
55
+ 'prepareNativeRequest',
56
+ 'createGeneratedOutput',
55
57
  'createLoweringPlan',
56
58
  'inspectRealization',
57
59
  'writeTarget',
@@ -370,6 +372,10 @@ function defineProviderDriver(input, expected = {}) {
370
372
  if (input.prepareNativeExecution !== undefined && typeof input.prepareNativeExecution !== 'function') {
371
373
  throw new TypeError(`Pulse provider driver ${id} prepareNativeExecution must be a function.`);
372
374
  }
375
+ if (input.prepareNativeRequest !== undefined && typeof input.prepareNativeRequest !== 'function') {
376
+ throw new TypeError(`Pulse provider driver ${id} prepareNativeRequest must be a function.`);
377
+ }
378
+ if (input.createGeneratedOutput !== undefined && typeof input.createGeneratedOutput !== 'function') throw new TypeError('Pulse provider createGeneratedOutput must be a function.');
373
379
  const descriptor = input.descriptor === undefined ? undefined : normalizeDescriptor(input.descriptor);
374
380
  if (descriptor && descriptor.id !== id) throw new TypeError(`Pulse provider driver ${id} descriptor identity does not match.`);
375
381
  const javascript = normalizeJavascriptDriver(input.javascript, targets.javascript);
@@ -397,6 +403,8 @@ function defineProviderDriver(input, expected = {}) {
397
403
  targets,
398
404
  execute: input.execute,
399
405
  prepareNativeExecution: input.prepareNativeExecution,
406
+ prepareNativeRequest: input.prepareNativeRequest,
407
+ createGeneratedOutput: input.createGeneratedOutput,
400
408
  createLoweringPlan: input.createLoweringPlan,
401
409
  inspectRealization: input.inspectRealization,
402
410
  writeTarget: input.writeTarget,
@@ -3,10 +3,10 @@
3
3
  const S3_CONTRACT_ID = 'pulse.s3';
4
4
  const S3_PACKAGE = '@pulse-compute/s3';
5
5
  const S3_LOWERING_PLAN_VERSION = 'pulse.s3-lowering-plan.v1';
6
- const S3_LIMITS = Object.freeze({ keyBytes: 1024, textBytes: 32768, metadataBytes: 1024, headerBytes: 16384, envelopeBytes: 262144 });
7
- const S3_OPERATIONS = Object.freeze(Object.fromEntries(['head', 'getText', 'putText'].map((name) => [name, Object.freeze({
8
- kind: `s3.${name}`, capability: `s3.${name}`, result: name === 'head' ? 's3-head-result' : name === 'getText' ? 's3-get-text-result' : 's3-put-text-result'
6
+ const S3_LIMITS = Object.freeze({ keyBytes: 1024, textBytes: 2097152, defaultTextBytes: 32768, metadataBytes: 1024, headerBytes: 16384, envelopeBytes: 12648448 });
7
+ const S3_OPERATIONS = Object.freeze(Object.fromEntries(['head', 'getText', 'putText', 'getBody'].map((name) => [name, Object.freeze({
8
+ kind: `s3.${name}`, capability: `s3.${name}`, result: name === 'getBody' ? 'opaque-response' : name === 'head' ? 's3-head-result' : name === 'getText' ? 's3-get-text-result' : 's3-put-text-result'
9
9
  })])));
10
- const S3_PROVIDER_REQUIREMENTS = Object.freeze(['s3.head', 's3.getText', 's3.putText', 'secret.get', 'time.wall-clock']);
10
+ const S3_PROVIDER_REQUIREMENTS = Object.freeze(['s3.head', 's3.getText', 's3.putText', 's3.getBody', 'secret.get', 'time.wall-clock']);
11
11
  const S3_CRYPTO_ALGORITHMS = Object.freeze(['SHA-256', 'HMAC-SHA256']);
12
12
  module.exports = Object.freeze({ S3_CONTRACT_ID, S3_PACKAGE, S3_LOWERING_PLAN_VERSION, S3_LIMITS, S3_OPERATIONS, S3_PROVIDER_REQUIREMENTS, S3_CRYPTO_ALGORITHMS });
@@ -0,0 +1,212 @@
1
+ 'use strict';
2
+
3
+ const { JsonAdmissionBudget } = require('./admission.js');
4
+
5
+ class JsonAdmissionFrame {
6
+ kind/*: i32 */ = 0; // 0 array; 1 object
7
+ state/*: i32 */ = 0;
8
+ count/*: i32 */ = 0;
9
+ start/*: i32 */ = 0;
10
+ names/*: Map<string, i32> */ = new Map();
11
+ }
12
+
13
+ // Iterative syntax and resource admission; never constructs JSON containers.
14
+ // Root containers have depth 1, root scalars depth 0. Nodes include every
15
+ // syntactic value, even overwritten/unknown values. Keys are not value nodes.
16
+ // duplicateMode: 0 allow, 1 reject, 2 report offsets for schema-owned policy.
17
+ class JsonTextAdmission extends JsonAdmissionBudget {
18
+ text/*: string */;
19
+ duplicateMode/*: i32 */;
20
+ offset/*: i32 */ = 0;
21
+ textBytes/*: i32 */ = 0;
22
+ frames/*: Array<JsonAdmissionFrame> */ = [];
23
+ duplicateObjects/*: Array<i32> */ = [];
24
+ duplicateKeys/*: Array<i32> */ = [];
25
+ duplicateFirstKeys/*: Array<i32> */ = [];
26
+ constructor(text/*: string */, limits/*: JsonAdmissionLimits */, duplicateMode/*: i32 */) {
27
+ super(limits);
28
+ this.text = text;
29
+ this.duplicateMode = duplicateMode;
30
+ }
31
+ space()/*: void */ {
32
+ while (this.offset < this.text.length) {
33
+ const c = this.text.charCodeAt(this.offset);
34
+ if (c !== 32 && c !== 9 && c !== 10 && c !== 13) break;
35
+ this.offset++;
36
+ }
37
+ }
38
+ rawBytes()/*: bool */ {
39
+ // UTF-8 byte count matches replacement encoding for raw lone surrogates.
40
+ // This prepass stops before any syntax-frame or decoded-key allocation.
41
+ if (this.text.length > this.limits.maxTextBytes) return this.fail(2);
42
+ for (let i = 0; i < this.text.length; i++) {
43
+ const c = this.text.charCodeAt(i);
44
+ let n = c < 128 ? 1 : c < 2048 ? 2 : 3;
45
+ if (c >= 0xd800 && c <= 0xdbff && i + 1 < this.text.length) {
46
+ const next = this.text.charCodeAt(i + 1);
47
+ if (next >= 0xdc00 && next <= 0xdfff) { n = 4; i++; }
48
+ }
49
+ if (n > this.limits.maxTextBytes - this.textBytes) return this.fail(2);
50
+ this.textBytes += n;
51
+ }
52
+ return true;
53
+ }
54
+ string(key/*: bool */)/*: string */ {
55
+ const capture = key && this.duplicateMode !== 0;
56
+ const parts/*: Array<string> */ = [];
57
+ let length = 0;
58
+ let high = false;
59
+ this.offset++; // opening quote
60
+ if (!this.addBytes(2)) return '';
61
+ while (this.offset < this.text.length && this.failure === 0) {
62
+ let c = this.text.charCodeAt(this.offset++);
63
+ if (c === 34) {
64
+ if (high) this.addBytes(6);
65
+ return capture ? parts.join('') : '';
66
+ }
67
+ if (c < 32) { this.fail(1); return ''; }
68
+ if (c === 92) {
69
+ if (this.offset >= this.text.length) { this.fail(1); return ''; }
70
+ c = this.text.charCodeAt(this.offset++);
71
+ if (c === 117) {
72
+ let unit = 0;
73
+ for (let n = 0; n < 4; n++) {
74
+ if (this.offset >= this.text.length) { this.fail(1); return ''; }
75
+ const h = this.text.charCodeAt(this.offset++);
76
+ const digit = h >= 48 && h <= 57 ? h - 48 : h >= 65 && h <= 70 ? h - 55 : h >= 97 && h <= 102 ? h - 87 : -1;
77
+ if (digit < 0) { this.fail(1); return ''; }
78
+ unit = unit * 16 + digit;
79
+ }
80
+ c = unit;
81
+ } else if (c === 98) c = 8;
82
+ else if (c === 102) c = 12;
83
+ else if (c === 110) c = 10;
84
+ else if (c === 114) c = 13;
85
+ else if (c === 116) c = 9;
86
+ else if (c !== 34 && c !== 92 && c !== 47) { this.fail(1); return ''; }
87
+ }
88
+ const limit = key ? this.limits.maxKeyLength : this.limits.maxStringLength;
89
+ if (length >= limit) { this.fail(key ? 7 : 8); return ''; }
90
+ length++;
91
+ if (capture) parts.push(String.fromCharCode(c));
92
+ if (high) {
93
+ high = false;
94
+ if (c >= 0xdc00 && c <= 0xdfff) { this.addBytes(4); continue; }
95
+ if (!this.addBytes(6)) return '';
96
+ }
97
+ if (c >= 0xd800 && c <= 0xdbff) high = true;
98
+ else this.addBytes(c < 32 ? 6 : c === 34 || c === 92 ? 2 : c < 128 ? 1 : c < 2048 ? 2 : c >= 0xdc00 && c <= 0xdfff ? 6 : 3);
99
+ }
100
+ this.fail(1);
101
+ return '';
102
+ }
103
+ digit()/*: bool */ {
104
+ if (this.offset >= this.text.length) return false;
105
+ const c = this.text.charCodeAt(this.offset);
106
+ return c >= 48 && c <= 57;
107
+ }
108
+ number()/*: bool */ {
109
+ const start = this.offset;
110
+ if (this.text.charCodeAt(this.offset) === 45) this.offset++;
111
+ if (!this.digit()) return this.fail(1);
112
+ if (this.text.charCodeAt(this.offset) === 48) this.offset++;
113
+ else while (this.digit()) this.offset++;
114
+ if (this.offset < this.text.length && this.text.charCodeAt(this.offset) === 46) {
115
+ this.offset++;
116
+ if (!this.digit()) return this.fail(1);
117
+ while (this.digit()) this.offset++;
118
+ }
119
+ if (this.offset < this.text.length) {
120
+ const c = this.text.charCodeAt(this.offset);
121
+ if (c === 69 || c === 101) {
122
+ this.offset++;
123
+ if (this.offset < this.text.length && (this.text.charCodeAt(this.offset) === 43 || this.text.charCodeAt(this.offset) === 45)) this.offset++;
124
+ if (!this.digit()) return this.fail(1);
125
+ while (this.digit()) this.offset++;
126
+ }
127
+ }
128
+ if (!isFinite(parseFloat(this.text.substring(start, this.offset)))) return this.fail(11);
129
+ return this.addBytes(24); // finite-f64 conservative spelling budget
130
+ }
131
+ value()/*: bool */ {
132
+ this.space();
133
+ if (this.offset >= this.text.length) return this.fail(1);
134
+ const c = this.text.charCodeAt(this.offset);
135
+ const container = c === 91 || c === 123;
136
+ if (!this.node(container ? this.frames.length + 1 : this.frames.length)) return false;
137
+ if (container) {
138
+ if (!this.addBytes(1)) return false;
139
+ const frame = new JsonAdmissionFrame();
140
+ frame.kind = c === 123 ? 1 : 0;
141
+ frame.start = this.offset++;
142
+ this.frames.push(frame);
143
+ return true;
144
+ }
145
+ if (c === 34) { this.string(false); return this.failure === 0; }
146
+ let literal = '';
147
+ if (c === 116) literal = 'true';
148
+ else if (c === 102) literal = 'false';
149
+ else if (c === 110) literal = 'null';
150
+ if (literal.length > 0) {
151
+ if (this.text.substring(this.offset, this.offset + literal.length) !== literal) return this.fail(1);
152
+ this.offset += literal.length;
153
+ return this.addBytes(literal.length);
154
+ }
155
+ return this.number();
156
+ }
157
+ scan()/*: bool */ {
158
+ if (this.duplicateMode < 0 || this.duplicateMode > 2) return this.fail(12);
159
+ if (!this.rawBytes() || !this.value()) return false;
160
+ while (this.frames.length > 0 && this.failure === 0) {
161
+ this.space();
162
+ if (this.offset >= this.text.length) return this.fail(1);
163
+ const frame = this.frames[this.frames.length - 1];
164
+ const c = this.text.charCodeAt(this.offset);
165
+ const end = frame.kind === 0 ? 93 : 125;
166
+ const after = frame.kind === 0 ? 1 : 3;
167
+ if ((frame.state === 0 || frame.state === after) && c === end) {
168
+ this.offset++;
169
+ if (!this.addBytes(1)) return false;
170
+ this.frames.pop();
171
+ } else if (frame.state === after) {
172
+ if (c !== 44) return this.fail(1);
173
+ this.offset++;
174
+ if (!this.addBytes(1)) return false;
175
+ frame.state = frame.kind === 0 ? 2 : 4;
176
+ } else if (frame.kind === 0) {
177
+ if (frame.count >= this.limits.maxArrayItems) return this.fail(6);
178
+ frame.count++;
179
+ frame.state = 1;
180
+ if (!this.value()) return false;
181
+ } else if (frame.state === 0 || frame.state === 4) {
182
+ if (c !== 34) return this.fail(1);
183
+ if (frame.count >= this.limits.maxObjectMembers) return this.fail(5);
184
+ frame.count++;
185
+ const start = this.offset;
186
+ const name = this.string(true);
187
+ if (this.failure !== 0) return false;
188
+ if (this.duplicateMode !== 0) {
189
+ if (frame.names.has(name)) {
190
+ if (this.duplicateMode === 1) return this.fail(10);
191
+ this.duplicateObjects.push(frame.start);
192
+ this.duplicateKeys.push(start);
193
+ this.duplicateFirstKeys.push(frame.names.get(name)/* nonnull */);
194
+ } else frame.names.set(name, start);
195
+ }
196
+ frame.state = 1;
197
+ } else if (frame.state === 1) {
198
+ if (c !== 58) return this.fail(1);
199
+ this.offset++;
200
+ if (!this.addBytes(1)) return false;
201
+ frame.state = 2;
202
+ } else {
203
+ frame.state = 3;
204
+ if (!this.value()) return false;
205
+ }
206
+ }
207
+ this.space();
208
+ return this.failure === 0 && (this.offset === this.text.length || this.fail(1));
209
+ }
210
+ }
211
+
212
+ module.exports = { JsonAdmissionFrame, JsonTextAdmission };
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ const { sha256Hex, stableStringify } = require('../stable-id.js');
4
+
5
+ const JSON_ADMISSION_VERSION = 'pulse.json-admission.v1';
6
+ const JSON_LIMIT_FIELDS = Object.freeze([
7
+ 'maxTextBytes', 'maxDepth', 'maxNodes', 'maxObjectMembers',
8
+ 'maxArrayItems', 'maxKeyLength', 'maxStringLength', 'maxJsonBytes'
9
+ ]);
10
+ const JSON_ADMISSION_FAILURES = Object.freeze([
11
+ 'none', 'malformed', 'text-bytes', 'depth', 'nodes', 'object-members',
12
+ 'array-items', 'key-length', 'string-length', 'json-bytes',
13
+ 'duplicate-key', 'non-finite-number', 'unsupported-value', 'cycle'
14
+ ]);
15
+
16
+ // No application defaults in this internal foundation. All counters fit i32;
17
+ // additions check remaining capacity before adding, including in generated AS.
18
+ function normalizeJsonAdmissionLimits(input) {
19
+ const invalid = () => {
20
+ const error = new TypeError('JSON admission requires all eight positive i32 limits as own data properties.');
21
+ error.code = 'PULSEWASM_JSON_LIMITS_INVALID';
22
+ throw error;
23
+ };
24
+ if (!input || typeof input !== 'object' || Array.isArray(input)) invalid();
25
+ const proto = Object.getPrototypeOf(input);
26
+ if (proto !== Object.prototype && proto !== null) invalid();
27
+ const keys = Reflect.ownKeys(input);
28
+ if (keys.length !== JSON_LIMIT_FIELDS.length || keys.some(key => !JSON_LIMIT_FIELDS.includes(key))) invalid();
29
+ const limits = {};
30
+ for (const key of JSON_LIMIT_FIELDS) {
31
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
32
+ if (!descriptor || !Object.hasOwn(descriptor, 'value')) invalid();
33
+ const value = descriptor.value;
34
+ if (!Number.isInteger(value) || value < 1 || value > 0x7fffffff) invalid();
35
+ limits[key] = value;
36
+ }
37
+ return Object.freeze(limits);
38
+ }
39
+
40
+ function jsonAdmissionIdentity(input) {
41
+ const limits = normalizeJsonAdmissionLimits(input);
42
+ return Object.freeze({ version: JSON_ADMISSION_VERSION, limits,
43
+ hash: sha256Hex(stableStringify({ version: JSON_ADMISSION_VERSION, limits })) });
44
+ }
45
+
46
+ // These classes are executable reference semantics shared with Native codegen.
47
+ // Type comments are erased by JavaScript and realized by the AS generator.
48
+ class JsonAdmissionLimits {
49
+ maxTextBytes/*: i32 */ = 0;
50
+ maxDepth/*: i32 */ = 0;
51
+ maxNodes/*: i32 */ = 0;
52
+ maxObjectMembers/*: i32 */ = 0;
53
+ maxArrayItems/*: i32 */ = 0;
54
+ maxKeyLength/*: i32 */ = 0;
55
+ maxStringLength/*: i32 */ = 0;
56
+ maxJsonBytes/*: i32 */ = 0;
57
+ }
58
+
59
+ class JsonAdmissionBudget {
60
+ limits/*: JsonAdmissionLimits */;
61
+ failure/*: i32 */ = 0;
62
+ nodes/*: i32 */ = 0;
63
+ depth/*: i32 */ = 0;
64
+ jsonBytes/*: i32 */ = 0;
65
+ constructor(limits/*: JsonAdmissionLimits */) { this.limits = limits; }
66
+ fail(reason/*: i32 */)/*: bool */ {
67
+ if (this.failure === 0) this.failure = reason;
68
+ return false;
69
+ }
70
+ addBytes(amount/*: i32 */)/*: bool */ {
71
+ if (this.failure !== 0) return false;
72
+ if (amount < 0 || amount > this.limits.maxJsonBytes - this.jsonBytes) return this.fail(9);
73
+ this.jsonBytes += amount;
74
+ return true;
75
+ }
76
+ node(containerDepth/*: i32 */)/*: bool */ {
77
+ if (this.failure !== 0) return false;
78
+ if (this.nodes >= this.limits.maxNodes) return this.fail(4);
79
+ this.nodes++;
80
+ if (containerDepth > this.limits.maxDepth) return this.fail(3);
81
+ if (containerDepth > this.depth) this.depth = containerDepth;
82
+ return true;
83
+ }
84
+ stringBytes(text/*: string */, key/*: bool */)/*: bool */ {
85
+ if (text.length > (key ? this.limits.maxKeyLength : this.limits.maxStringLength)) return this.fail(key ? 7 : 8);
86
+ if (!this.addBytes(2)) return false;
87
+ for (let i = 0; i < text.length; i++) {
88
+ const c = text.charCodeAt(i);
89
+ let size = c < 32 ? 6 : c === 34 || c === 92 ? 2 : c < 128 ? 1 : c < 2048 ? 2 : 3;
90
+ if (c >= 0xd800 && c <= 0xdbff && i + 1 < text.length && text.charCodeAt(i + 1) >= 0xdc00 && text.charCodeAt(i + 1) <= 0xdfff) { size = 4; i++; }
91
+ else if (c >= 0xd800 && c <= 0xdfff) size = 6;
92
+ if (!this.addBytes(size)) return false;
93
+ }
94
+ return true;
95
+ }
96
+ }
97
+
98
+ module.exports = { JSON_ADMISSION_VERSION, JSON_LIMIT_FIELDS, JSON_ADMISSION_FAILURES,
99
+ normalizeJsonAdmissionLimits, jsonAdmissionIdentity, JsonAdmissionLimits, JsonAdmissionBudget };
@@ -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'),