sendscript 1.1.0 → 2.0.1

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.
@@ -3,7 +3,7 @@ name: Publish Package
3
3
  on:
4
4
  push:
5
5
  tags:
6
- - 'v*'
6
+ - '*'
7
7
 
8
8
  permissions:
9
9
  id-token: write # Required for OIDC
@@ -22,4 +22,4 @@ jobs:
22
22
  - run: npm ci
23
23
  - run: npm run build --if-present
24
24
  - run: npm test
25
- - run: npm publish
25
+ - run: npm publish
@@ -0,0 +1,45 @@
1
+ name: Release on tag
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - '*'
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ matrix:
16
+ node: ['lts/*', 'latest']
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - uses: actions/setup-node@v4
22
+ with:
23
+ node-version: ${{ matrix.node }}
24
+ cache: npm
25
+
26
+ - run: npm ci
27
+ - run: npm test
28
+
29
+ release:
30
+ needs: test
31
+ runs-on: ubuntu-latest
32
+
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+
36
+ - uses: actions/setup-node@v4
37
+ with:
38
+ node-version: 'latest'
39
+ cache: npm
40
+
41
+ - run: npm ci
42
+ - name: Create GitHub Release
43
+ uses: softprops/action-gh-release@v2
44
+ with:
45
+ generate_release_notes: true
package/CHANGELOG.md CHANGED
@@ -4,8 +4,22 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ #### [v2.0.1](https://github.com/bas080/sendscript/compare/v2.0.0...v2.0.1)
8
+
9
+ - Publish to npm on tag push [`357ac0e`](https://github.com/bas080/sendscript/commit/357ac0ef3384f294e4f9bb179d5a675afda5fd19)
10
+
11
+ ### [v2.0.0](https://github.com/bas080/sendscript/compare/v1.1.0...v2.0.0)
12
+
13
+ > 8 April 2026
14
+
15
+ - Rewrite to have parse take schema to prevent accessing refs [`df16f85`](https://github.com/bas080/sendscript/commit/df16f85c79299aa8bcd3e44b6cc127bd7198eace)
16
+ - Run tests before release and publish on release [`10442d6`](https://github.com/bas080/sendscript/commit/10442d6766a098926ee98daee08dba23eea0fb14)
17
+ - Create release on tag with github workflows [`4ec222b`](https://github.com/bas080/sendscript/commit/4ec222bf51fc1d3cce9fc54fdd1e9639b5d259cd)
18
+
7
19
  #### [v1.1.0](https://github.com/bas080/sendscript/compare/v1.0.6...v1.1.0)
8
20
 
21
+ > 7 April 2026
22
+
9
23
  - Support custom serialization of leaf nodes [`d56f1b7`](https://github.com/bas080/sendscript/commit/d56f1b750e2d7faf72d69f9ba8e9bb62ef5ebce7)
10
24
  - Support nested modules [`88e69e5`](https://github.com/bas080/sendscript/commit/88e69e5c05ddb85aac1797a020e92519c986e89f)
11
25
  - Document new nesting and leaf serialization feature [`b3ee9f6`](https://github.com/bas080/sendscript/commit/b3ee9f693dc6137b01c9f0072aa2dd1e940f07c9)
package/README.md CHANGED
@@ -53,10 +53,11 @@ using more advanced (de)serialization libraries.
53
53
  SendScript produces an intermediate JSON representation of the program. Let's see what that looks like.
54
54
 
55
55
  ```js
56
- import stringify from 'sendscript/stringify.mjs'
57
- import module from 'sendscript/module.mjs'
56
+ import Stringify from 'sendscript/stringify.mjs'
57
+ import references from 'sendscript/references.mjs'
58
58
 
59
- const { add } = module(['add'])
59
+ const { add } = references(['add'])
60
+ const stringify = Stringify()
60
61
 
61
62
  console.log(stringify(add(1,2)))
62
63
  ```
@@ -75,7 +76,7 @@ const module = {
75
76
  }
76
77
  }
77
78
 
78
- const parse = Parse(module)
79
+ const parse = Parse(['add'], module)
79
80
 
80
81
  const program = '["call",["ref","add"],[1,2]]'
81
82
 
@@ -131,7 +132,8 @@ import { Server } from 'socket.io'
131
132
  import Parse from 'sendscript/parse.mjs'
132
133
  import * as math from './math.mjs'
133
134
 
134
- const parse = Parse(math)
135
+ const schema = Object.keys(math)
136
+ const parse = Parse(schema, math)
135
137
  const server = new Server()
136
138
  const port = process.env.PORT || 3000
137
139
 
@@ -158,11 +160,13 @@ Now for a client that sends a program to the server.
158
160
  // ./example/client.socket.io.mjs
159
161
 
160
162
  import socketClient from 'socket.io-client'
161
- import stringify from 'sendscript/stringify.mjs'
162
- import module from 'sendscript/module.mjs'
163
- import * as math from './math.mjs'
163
+ import Stringify from 'sendscript/stringify.mjs'
164
+ import references from 'sendscript/references.mjs'
164
165
  import assert from 'node:assert'
165
166
 
167
+ const { add, square } = references(['add', 'square'])
168
+ const stringify = Stringify()
169
+
166
170
  const port = process.env.PORT || 3000
167
171
  const client = socketClient(`http://localhost:${port}`)
168
172
 
@@ -176,8 +180,6 @@ const send = program => {
176
180
  })
177
181
  }
178
182
 
179
- const { add, square } = module(math)
180
-
181
183
  // The program to be sent over the wire
182
184
  const program = square(add(1, add(add(2, 3), 4)))
183
185
 
@@ -253,52 +255,31 @@ export const add = (a: number, b: number) => a + b
253
255
  export const square = (a: number) => a * a
254
256
  ```
255
257
 
256
- We want to use this module on the client. We create a client version of that module and coerce the types to match those of the server.
257
-
258
- ```bash
259
- cat ./example/typescript/math.client.ts
260
- ```
261
- ```ts
262
- import module from 'sendscript/module.mjs'
263
- import type * as mathTypes from './math.ts'
264
-
265
- const math = module([
266
- 'add',
267
- 'square'
268
- ]) as typeof mathTypes
269
-
270
- export default math
271
- ```
272
-
273
- We now use the client version of this module.
258
+ We can then coerce the types of the instrumented stubs.
274
259
 
275
260
  ```bash
276
261
  cat ./example/typescript/client.ts
277
262
  ```
278
263
  ```ts
279
- import stringify from 'sendscript/stringify.mjs'
264
+ import math from './math.client.ts'
265
+ import Stringify from 'sendscript/stringify.mjs'
266
+
267
+ const stringify = Stringify()
280
268
 
281
- async function send<T>(program: T): Promise<T>{
269
+ // The return type of this function matches the type passed as the return of the program.
270
+ async function send<T>(program: T): Promise<T> {
282
271
  return (await fetch('/api', {
283
272
  method: 'POST',
284
273
  body: stringify(program)
285
274
  })).json()
286
275
  }
287
276
 
288
- import math from './math.client.ts'
289
-
290
- const { add, square } = math
291
-
292
277
  send(square(add(1, 2)))
293
278
  ```
294
279
 
295
280
  We'll also generate the docs for this module.
296
281
 
297
282
  ```bash
298
- npm install --no-save \
299
- typedoc \
300
- typedoc-plugin-markdown
301
-
302
283
  npx typedoc --plugin typedoc-plugin-markdown --out ./example/typescript/docs ./example/typescript/math.ts
303
284
  ```
304
285
 
@@ -316,32 +297,33 @@ Sendscript allows you to define your API as a **nested object of functions**, ma
316
297
 
317
298
  ### Defining a Nested Module
318
299
 
319
- You can define a schema as either:
320
-
321
- 1. **An object with nested objects** – submodules.
322
- 2. **An array of function names** – automatically instrumented.
300
+ You can define a schema as **an array of function names**
323
301
 
324
302
  ```js
325
- import module from 'sendscript/module.mjs'
326
-
327
- const myModule = module({
328
- math: ['add', 'sub'],
329
-
330
- // Use an object with keys and true value
331
- vector: {
332
- add: true,
333
- multiply: true
334
- },
335
303
 
336
- // or use an array.
337
- utils: ['identity', 'always'],
304
+ const schema = [
305
+ 'help',
306
+ 'version',
307
+
308
+ ['math', [
309
+ 'add',
310
+ 'sub'
311
+ ]],
312
+ ['vector', [
313
+ 'add',
314
+ 'multiply'
315
+ ]],
316
+ ['utils', [
317
+ 'identity',
318
+ 'always'
319
+ ]],
338
320
  })
339
321
  ```
340
322
 
341
323
  Functions are referenced via their **path in the module tree**:
342
324
 
343
325
  ```js
344
- const { math, vector } = myModule
326
+ const { math, vector } = references(schema)
345
327
 
346
328
  math.add(
347
329
  1,
@@ -389,9 +371,9 @@ Here's how to use [superjson](https://github.com/blitz-js/superjson) to support
389
371
 
390
372
  ```js
391
373
  import SuperJSON from 'superjson'
392
- import stringify from 'sendscript/stringify.mjs'
374
+ import Stringify from 'sendscript/stringify.mjs'
375
+ import references from 'sendscript/references.mjs'
393
376
  import Parse from 'sendscript/parse.mjs'
394
- import module from 'sendscript/module.mjs'
395
377
 
396
378
  const leafSerializer = (value) => {
397
379
  if (value === undefined) return JSON.stringify({ __undefined__: true })
@@ -404,7 +386,10 @@ const leafDeserializer = (text) => {
404
386
  return SuperJSON.deserialize(parsed)
405
387
  }
406
388
 
407
- const { processData } = module(['processData'])
389
+ const schema = ['processData']
390
+
391
+ const { processData } = references(schema)
392
+ const stringify = Stringify(leafSerializer)
408
393
 
409
394
  // Program with Date, RegExp, and other types
410
395
  const program = {
@@ -416,17 +401,20 @@ const program = {
416
401
  }
417
402
 
418
403
  // Serialize with custom leaf serializer
419
- const json = stringify(processData(program), leafSerializer)
404
+ const json = stringify(processData(program))
420
405
 
421
- // Parse with custom leaf deserializer
422
- const parse = Parse({
406
+ // The environment
407
+ const env = {
423
408
  processData: (data) => ({
424
409
  success: true,
425
410
  received: data
426
411
  })
427
- })
412
+ }
413
+
414
+ // Parse with custom leaf deserializer
415
+ const parse = Parse(schema, env, leadDeserializer)
428
416
 
429
- const result = parse(json, leafDeserializer)
417
+ const result = parse(json)
430
418
  ```
431
419
 
432
420
  The leaf wrapper format is `['leaf', serializedPayload]`, making it unambiguous and safe from colliding with SendScript operators.
@@ -441,19 +429,19 @@ npm t -- report text-summary
441
429
  ```
442
430
  ```
443
431
 
444
- > sendscript@1.1.0 test
432
+ > sendscript@2.0.1 test
445
433
  > tap -R silent
446
434
 
447
435
 
448
- > sendscript@1.1.0 test
436
+ > sendscript@2.0.1 test
449
437
  > tap report text-summary
450
438
 
451
439
 
452
440
  =============================== Coverage summary ===============================
453
- Statements : 100% ( 328/328 )
454
- Branches : 100% ( 138/138 )
455
- Functions : 100% ( 23/23 )
456
- Lines : 100% ( 328/328 )
441
+ Statements : 100% ( 348/348 )
442
+ Branches : 100% ( 145/145 )
443
+ Functions : 100% ( 24/24 )
444
+ Lines : 100% ( 348/348 )
457
445
  ================================================================================
458
446
  ```
459
447
 
package/README.mz CHANGED
@@ -30,10 +30,11 @@ using more advanced (de)serialization libraries.
30
30
  SendScript produces an intermediate JSON representation of the program. Let's see what that looks like.
31
31
 
32
32
  ```js|json node --input-type=module | tee /tmp/sendscript.json
33
- import stringify from 'sendscript/stringify.mjs'
34
- import module from 'sendscript/module.mjs'
33
+ import Stringify from 'sendscript/stringify.mjs'
34
+ import references from 'sendscript/references.mjs'
35
35
 
36
- const { add } = module(['add'])
36
+ const { add } = references(['add'])
37
+ const stringify = Stringify()
37
38
 
38
39
  console.log(stringify(add(1,2)))
39
40
  ```
@@ -49,7 +50,7 @@ const module = {
49
50
  }
50
51
  }
51
52
 
52
- const parse = Parse(module)
53
+ const parse = Parse(['add'], module)
53
54
 
54
55
  const program = '["call",["ref","add"],[1,2]]'
55
56
 
@@ -102,7 +103,8 @@ import { Server } from 'socket.io'
102
103
  import Parse from 'sendscript/parse.mjs'
103
104
  import * as math from './math.mjs'
104
105
 
105
- const parse = Parse(math)
106
+ const schema = Object.keys(math)
107
+ const parse = Parse(schema, math)
106
108
  const server = new Server()
107
109
  const port = process.env.PORT || 3000
108
110
 
@@ -129,11 +131,13 @@ Now for a client that sends a program to the server.
129
131
  // ./example/client.socket.io.mjs
130
132
 
131
133
  import socketClient from 'socket.io-client'
132
- import stringify from 'sendscript/stringify.mjs'
133
- import module from 'sendscript/module.mjs'
134
- import * as math from './math.mjs'
134
+ import Stringify from 'sendscript/stringify.mjs'
135
+ import references from 'sendscript/references.mjs'
135
136
  import assert from 'node:assert'
136
137
 
138
+ const { add, square } = references(['add', 'square'])
139
+ const stringify = Stringify()
140
+
137
141
  const port = process.env.PORT || 3000
138
142
  const client = socketClient(`http://localhost:${port}`)
139
143
 
@@ -147,8 +151,6 @@ const send = program => {
147
151
  })
148
152
  }
149
153
 
150
- const { add, square } = module(math)
151
-
152
154
  // The program to be sent over the wire
153
155
  const program = square(add(1, add(add(2, 3), 4)))
154
156
 
@@ -217,13 +219,7 @@ Let's say we have this module which we use on the server.
217
219
  cat ./example/typescript/math.ts
218
220
  ```
219
221
 
220
- We want to use this module on the client. We create a client version of that module and coerce the types to match those of the server.
221
-
222
- ```bash|ts bash
223
- cat ./example/typescript/math.client.ts
224
- ```
225
-
226
- We now use the client version of this module.
222
+ We can then coerce the types of the instrumented stubs.
227
223
 
228
224
  ```bash|ts bash
229
225
  cat ./example/typescript/client.ts
@@ -232,10 +228,6 @@ cat ./example/typescript/client.ts
232
228
  We'll also generate the docs for this module.
233
229
 
234
230
  ```bash bash 1>&2
235
- npm install --no-save \
236
- typedoc \
237
- typedoc-plugin-markdown
238
-
239
231
  npx typedoc --plugin typedoc-plugin-markdown --out ./example/typescript/docs ./example/typescript/math.ts
240
232
  ```
241
233
 
@@ -253,32 +245,33 @@ Sendscript allows you to define your API as a **nested object of functions**, ma
253
245
 
254
246
  ### Defining a Nested Module
255
247
 
256
- You can define a schema as either:
257
-
258
- 1. **An object with nested objects** – submodules.
259
- 2. **An array of function names** – automatically instrumented.
248
+ You can define a schema as **an array of function names**
260
249
 
261
250
  ```js
262
- import module from 'sendscript/module.mjs'
263
251
 
264
- const myModule = module({
265
- math: ['add', 'sub'],
266
-
267
- // Use an object with keys and true value
268
- vector: {
269
- add: true,
270
- multiply: true
271
- },
272
-
273
- // or use an array.
274
- utils: ['identity', 'always'],
252
+ const schema = [
253
+ 'help',
254
+ 'version',
255
+
256
+ ['math', [
257
+ 'add',
258
+ 'sub'
259
+ ]],
260
+ ['vector', [
261
+ 'add',
262
+ 'multiply'
263
+ ]],
264
+ ['utils', [
265
+ 'identity',
266
+ 'always'
267
+ ]],
275
268
  })
276
269
  ```
277
270
 
278
271
  Functions are referenced via their **path in the module tree**:
279
272
 
280
273
  ```js
281
- const { math, vector } = myModule
274
+ const { math, vector } = references(schema)
282
275
 
283
276
  math.add(
284
277
  1,
@@ -326,9 +319,9 @@ Here's how to use [superjson](https://github.com/blitz-js/superjson) to support
326
319
 
327
320
  ```js
328
321
  import SuperJSON from 'superjson'
329
- import stringify from 'sendscript/stringify.mjs'
322
+ import Stringify from 'sendscript/stringify.mjs'
323
+ import references from 'sendscript/references.mjs'
330
324
  import Parse from 'sendscript/parse.mjs'
331
- import module from 'sendscript/module.mjs'
332
325
 
333
326
  const leafSerializer = (value) => {
334
327
  if (value === undefined) return JSON.stringify({ __undefined__: true })
@@ -341,7 +334,10 @@ const leafDeserializer = (text) => {
341
334
  return SuperJSON.deserialize(parsed)
342
335
  }
343
336
 
344
- const { processData } = module(['processData'])
337
+ const schema = ['processData']
338
+
339
+ const { processData } = references(schema)
340
+ const stringify = Stringify(leafSerializer)
345
341
 
346
342
  // Program with Date, RegExp, and other types
347
343
  const program = {
@@ -353,17 +349,20 @@ const program = {
353
349
  }
354
350
 
355
351
  // Serialize with custom leaf serializer
356
- const json = stringify(processData(program), leafSerializer)
352
+ const json = stringify(processData(program))
357
353
 
358
- // Parse with custom leaf deserializer
359
- const parse = Parse({
354
+ // The environment
355
+ const env = {
360
356
  processData: (data) => ({
361
357
  success: true,
362
358
  received: data
363
359
  })
364
- })
360
+ }
361
+
362
+ // Parse with custom leaf deserializer
363
+ const parse = Parse(schema, env, leadDeserializer)
365
364
 
366
- const result = parse(json, leafDeserializer)
365
+ const result = parse(json)
367
366
  ```
368
367
 
369
368
  The leaf wrapper format is `['leaf', serializedPayload]`, making it unambiguous and safe from colliding with SendScript operators.
@@ -1,11 +1,13 @@
1
1
  // ./example/client.socket.io.mjs
2
2
 
3
3
  import socketClient from 'socket.io-client'
4
- import stringify from 'sendscript/stringify.mjs'
5
- import module from 'sendscript/module.mjs'
6
- import * as math from './math.mjs'
4
+ import Stringify from 'sendscript/stringify.mjs'
5
+ import references from 'sendscript/references.mjs'
7
6
  import assert from 'node:assert'
8
7
 
8
+ const { add, square } = references(['add', 'square'])
9
+ const stringify = Stringify()
10
+
9
11
  const port = process.env.PORT || 3000
10
12
  const client = socketClient(`http://localhost:${port}`)
11
13
 
@@ -19,8 +21,6 @@ const send = program => {
19
21
  })
20
22
  }
21
23
 
22
- const { add, square } = module(math)
23
-
24
24
  // The program to be sent over the wire
25
25
  const program = square(add(1, add(add(2, 3), 4)))
26
26
 
@@ -4,7 +4,8 @@ import { Server } from 'socket.io'
4
4
  import Parse from 'sendscript/parse.mjs'
5
5
  import * as math from './math.mjs'
6
6
 
7
- const parse = Parse(math)
7
+ const schema = Object.keys(math)
8
+ const parse = Parse(schema, math)
8
9
  const server = new Server()
9
10
  const port = process.env.PORT || 3000
10
11
 
@@ -1,14 +1,14 @@
1
- import stringify from 'sendscript/stringify.mjs'
1
+ import math from './math.client.ts'
2
+ import Stringify from 'sendscript/stringify.mjs'
2
3
 
3
- async function send<T>(program: T): Promise<T>{
4
+ const stringify = Stringify()
5
+
6
+ // The return type of this function matches the type passed as the return of the program.
7
+ async function send<T>(program: T): Promise<T> {
4
8
  return (await fetch('/api', {
5
9
  method: 'POST',
6
10
  body: stringify(program)
7
11
  })).json()
8
12
  }
9
13
 
10
- import math from './math.client.ts'
11
-
12
- const { add, square } = math
13
-
14
14
  send(square(add(1, 2)))
@@ -8,7 +8,7 @@
8
8
 
9
9
  > **add**(`a`, `b`): `number`
10
10
 
11
- Defined in: [math.ts:1](https://github.com/bas080/sendscript/blob/68bfab27a561a994381cdcf5a7cf367e29a2d07d/example/typescript/math.ts#L1)
11
+ Defined in: [math.ts:1](https://github.com/bas080/sendscript/blob/357ac0ef3384f294e4f9bb179d5a675afda5fd19/example/typescript/math.ts#L1)
12
12
 
13
13
  ## Parameters
14
14
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  > **square**(`a`): `number`
10
10
 
11
- Defined in: [math.ts:2](https://github.com/bas080/sendscript/blob/68bfab27a561a994381cdcf5a7cf367e29a2d07d/example/typescript/math.ts#L2)
11
+ Defined in: [math.ts:2](https://github.com/bas080/sendscript/blob/357ac0ef3384f294e4f9bb179d5a675afda5fd19/example/typescript/math.ts#L2)
12
12
 
13
13
  ## Parameters
14
14
 
@@ -1,9 +1,5 @@
1
- import module from 'sendscript/module.mjs'
2
1
  import type * as mathTypes from './math.ts'
2
+ import Stringify from 'sendscript/stringify.mjs'
3
+ import references from 'sendscript/references.mjs'
3
4
 
4
- const math = module([
5
- 'add',
6
- 'square'
7
- ]) as typeof mathTypes
8
-
9
- export default math
5
+ export default references(['add', 'square']) as typeof mathTypes
package/index.test.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { test } from 'tap'
2
- import stringify from './stringify.mjs'
3
- import ssparse from './parse.mjs'
4
- import module from './module.mjs'
2
+ import Stringify from './stringify.mjs'
3
+ import references from './references.mjs'
4
+ import Parse from './parse.mjs'
5
5
 
6
6
  const myModule = {
7
7
  nested: {
@@ -30,27 +30,19 @@ const myModule = {
30
30
  obj.b = 'c'
31
31
  return obj
32
32
  },
33
- Function,
34
33
  Promise
35
34
  }
36
35
 
37
- const schema = Object.keys(myModule).reduce((acc, key) => {
38
- acc[key] = true
36
+ const schema = Object.keys(myModule)
37
+ schema.push(['nested', [
38
+ ['again', ['T']]
39
+ ]])
39
40
 
40
- return acc
41
- }, {})
41
+ const api = references(schema)
42
+ const stringify = Stringify()
43
+ const parse = Parse(schema, myModule)
42
44
 
43
- schema.nested = { again: ['T'] }
44
-
45
- const sendscript = {
46
- stringify,
47
- parse: ssparse(myModule),
48
- module: module(schema)
49
- }
50
-
51
- const { parse } = sendscript
52
-
53
- const run = (program) => sendscript.parse(sendscript.stringify(program))
45
+ const run = (program) => parse(stringify(program))
54
46
 
55
47
  const RealPromise = Promise
56
48
 
@@ -61,7 +53,6 @@ test('should evaluate basic expressions correctly', async (t) => {
61
53
  resolve,
62
54
  delayedIdentity,
63
55
  noop,
64
- Function,
65
56
  Promise,
66
57
  instanceOf,
67
58
  asyncFn,
@@ -74,7 +65,16 @@ test('should evaluate basic expressions correctly', async (t) => {
74
65
  always,
75
66
  multiply3,
76
67
  nested
77
- } = sendscript.module
68
+ } = api
69
+
70
+ t.test('mix await without await', async t => {
71
+ const [one, two] = await run([await resolve(1), resolve(2)])
72
+
73
+ t.equal(one, 1)
74
+ t.ok(two instanceof RealPromise)
75
+
76
+ t.end()
77
+ })
78
78
 
79
79
  t.test('calling nested function works', t => {
80
80
  t.equal(run(nested.again.T()), true)
@@ -143,9 +143,6 @@ test('should evaluate basic expressions correctly', async (t) => {
143
143
  t.strictSame(await run(asyncFn()), 'my-async-function')
144
144
  t.strictSame(await run(await resolve('my-promise')), 'my-promise')
145
145
  t.strictSame(run(instanceOf(resolve(asyncFn), Promise)), true)
146
- t.strictSame(
147
- await run(instanceOf(await resolve(asyncFn), Function)), true
148
- )
149
146
  t.strictSame(
150
147
  await run({ a: await resolve('b') }),
151
148
  { a: 'b' }
@@ -218,7 +215,7 @@ test('should evaluate basic expressions correctly', async (t) => {
218
215
  })
219
216
 
220
217
  t.test('null-prototype object traversal', (t) => {
221
- const { nullProto } = sendscript.module
218
+ const { nullProto } = api
222
219
  t.strictSame(run({ a: nullProto() }), { a: { b: 'c' } })
223
220
  t.end()
224
221
  })
@@ -238,3 +235,78 @@ test('should evaluate basic expressions correctly', async (t) => {
238
235
  t.end()
239
236
  })
240
237
  })
238
+
239
+ test('stringify: invalid children throws', t => {
240
+ const invalidSchema = [
241
+ ['math', 'add']
242
+ ]
243
+
244
+ t.throws(() => Parse(invalidSchema, {}))
245
+ t.throws(() => Parse([Symbol('no-allowed')], {}))
246
+ t.end()
247
+ })
248
+
249
+ test('forbidden/reflection access should be blocked', async (t) => {
250
+ function run (program) {
251
+ return parse(JSON.stringify(program))
252
+ }
253
+
254
+ t.test('cannot access constructor via ref', (t) => {
255
+ // try to reach the constructor of nested.again.T
256
+ t.throws(() => run(['ref', 'nested', 'again', 'T', 'constructor']))
257
+ t.end()
258
+ })
259
+
260
+ t.test('cannot access prototype property', (t) => {
261
+ // direct prototype traversal attempt
262
+ t.throws(() => run(['ref', 'nested', '__proto__']))
263
+ t.throws(() => run(['ref', 'nested', 'again', '__proto__']))
264
+ t.end()
265
+ })
266
+
267
+ t.test('cannot reach Function constructor', (t) => {
268
+ // many modules may accidentally expose Function; this should not be available via ref
269
+ t.throws(() => run(['ref', 'Function']))
270
+ t.end()
271
+ })
272
+
273
+ t.test('cannot call Function to execute code', (t) => {
274
+ // If Function were reachable, this would execute arbitrary code
275
+ const program = ['call', ['ref', 'Function'], [['leaf', '"return 1 + 2"']]]
276
+ t.throws(() => run(program))
277
+ t.end()
278
+ })
279
+
280
+ t.test('cannot access global Promise constructor via ref', (t) => {
281
+ t.throws(() => run(['ref', 'Promise', 'prototype', 'then']))
282
+ t.end()
283
+ })
284
+
285
+ t.test('cannot access Object.prototype methods', (t) => {
286
+ // toString is a common vector for prototype access
287
+ t.throws(() => run(['ref', 'toString']))
288
+ t.throws(() => run(['ref', 'nested', 'again', 'T', 'toString']))
289
+ t.end()
290
+ })
291
+
292
+ t.test('cannot reach process or global (if accidentally exposed)', (t) => {
293
+ // attempt common global names; parser should not resolve them
294
+ t.throws(() => run(['ref', 'process']))
295
+ t.throws(() => run(['ref', 'global']))
296
+ t.end()
297
+ })
298
+
299
+ t.test('cannot use constructor.constructor to reach Function', (t) => {
300
+ // attempt: nested.again.T.constructor.constructor
301
+ t.throws(() => run(['ref', 'nested', 'again', 'T', 'constructor', 'constructor']))
302
+ t.end()
303
+ })
304
+
305
+ t.test('throws when trying to get something that does not exist', (t) => {
306
+ // Ensure array that contains a ref-like array stays as data if not a SendScript ref wrapper:
307
+ t.throws(() => run(['ref', 'doesNotExist']))
308
+ t.end()
309
+ })
310
+
311
+ t.end()
312
+ })
@@ -1,6 +1,7 @@
1
1
  import { test } from 'tap'
2
2
  import SuperJSON from 'superjson'
3
- import Sendscript from './index.mjs'
3
+ import Parse from './parse.mjs'
4
+ import Stringify from './stringify.mjs'
4
5
 
5
6
  const leafSerializer = (value) => {
6
7
  if (value === undefined) return JSON.stringify({ __sendscript_undefined__: true })
@@ -17,10 +18,14 @@ const module = {
17
18
  identity: (x) => x
18
19
  }
19
20
 
20
- const sendscript = Sendscript(Object.keys(module))
21
- const { parse, stringify } = sendscript
22
- const run = (program, serializer, deserializer) =>
23
- parse(stringify(program, serializer), deserializer)
21
+ const schema = Object.keys(module)
22
+
23
+ const run = (program, serializer, deserializer) => {
24
+ const parse = Parse(schema, module, deserializer)
25
+ const stringify = Stringify(serializer)
26
+
27
+ return parse(stringify(program))
28
+ }
24
29
 
25
30
  test('custom leaf serializer/deserializer using superjson', async (t) => {
26
31
  const value = {
@@ -64,13 +69,3 @@ test('default leaf deserializer when not provided', async (t) => {
64
69
  t.strictSame(result, value)
65
70
  t.end()
66
71
  })
67
-
68
- test('default leaf deserializer handles undefined parameter', (t) => {
69
- const parse = Sendscript({}).parse
70
- // Create a simple JSON with a leaf then parse using default deserializer
71
- // The reviver will never pass undefined to deserializer, but we test it defensively
72
- const json = '["leaf","{\\"test\\":1}"]'
73
- const result = parse(json)
74
- t.strictSame(result, { test: 1 })
75
- t.end()
76
- })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sendscript",
3
- "version": "1.1.0",
3
+ "version": "2.0.1",
4
4
  "description": "Blur the line between server and client code.",
5
5
  "module": true,
6
6
  "main": "index.mjs",
@@ -29,6 +29,8 @@
29
29
  "tape-check": "^1.0.0-rc.0"
30
30
  },
31
31
  "dependencies": {
32
- "debug": "^4.4.3"
32
+ "debug": "^4.4.3",
33
+ "typedoc": "^0.28.18",
34
+ "typedoc-plugin-markdown": "^4.11.0"
33
35
  }
34
36
  }
package/parse.mjs CHANGED
@@ -1,6 +1,29 @@
1
1
  import Debug from './debug.mjs'
2
2
  import { SendScriptReferenceError } from './error.mjs'
3
3
 
4
+ function flattenSchema (schema) {
5
+ const obj = {}
6
+
7
+ for (const item of schema) {
8
+ if (typeof item === 'string') {
9
+ // leaf function
10
+ obj[item] = true
11
+ } else if (Array.isArray(item)) {
12
+ const [name, children] = item
13
+ // TODO: Test this
14
+ if (!Array.isArray(children)) {
15
+ throw new Error(`Expected children array for namespace "${name}"`)
16
+ }
17
+ obj[name] = flattenSchema(children)
18
+ // TODO: Test this also
19
+ } else {
20
+ throw new Error('Schema items must be strings or [name, children] arrays')
21
+ }
22
+ }
23
+
24
+ return obj
25
+ }
26
+
4
27
  const debug = Debug.extend('parse')
5
28
 
6
29
  const isThenable = (value) => (
@@ -89,8 +112,10 @@ const spy = (fn) => (...args) => {
89
112
 
90
113
  const defaultLeafDeserializer = (text) => JSON.parse(text)
91
114
 
92
- export default (env) =>
93
- function parse (program, deserialize = defaultLeafDeserializer) {
115
+ export default (schemaArg, env, deserialize = defaultLeafDeserializer) => {
116
+ const schema = flattenSchema(schemaArg)
117
+
118
+ return function parse (program) {
94
119
  debug('program', program)
95
120
 
96
121
  const reviver = spy((key, value) => {
@@ -140,10 +165,12 @@ export default (env) =>
140
165
  if (operator === 'ref') {
141
166
  const path = rest // e.g., ["math","add"]
142
167
  let current = env
168
+ let schemaCurrent = schema
143
169
 
144
170
  for (const segment of path) {
145
- if (current && Object.hasOwn(current, segment)) {
171
+ if (schemaCurrent && Object.hasOwn(schemaCurrent, segment)) {
146
172
  current = current[segment]
173
+ schemaCurrent = schemaCurrent[segment]
147
174
  } else {
148
175
  throw new SendScriptReferenceError({ key, value })
149
176
  }
@@ -160,3 +187,4 @@ export default (env) =>
160
187
 
161
188
  return result
162
189
  }
190
+ }
@@ -37,22 +37,21 @@ function instrument (path) {
37
37
  }
38
38
 
39
39
  export default function module (schema, parentPath = []) {
40
- if (Array.isArray(schema)) {
41
- return schema.reduce((acc, name) => {
42
- acc[name] = instrument([...parentPath, name])
43
- return acc
44
- }, {})
45
- }
46
-
47
- return Object.keys(schema).reduce((acc, key) => {
48
- const value = schema[key]
49
-
50
- if (Array.isArray(value)) {
51
- acc[key] = module(value, [...parentPath, key])
52
- } else if (typeof value === 'object' && value !== null) {
53
- acc[key] = module(value, [...parentPath, key])
40
+ return schema.reduce((acc, item) => {
41
+ if (typeof item === 'string') {
42
+ // leaf function
43
+ acc[item] = instrument([...parentPath, item])
44
+ } else if (Array.isArray(item)) {
45
+ const [name, children] = item
46
+
47
+ if (Array.isArray(children)) {
48
+ // recurse: children can be strings or [name, children] arrays
49
+ acc[name] = module(children, [...parentPath, name])
50
+ } else {
51
+ throw new Error(`Expected children array for namespace "${name}"`)
52
+ }
54
53
  } else {
55
- acc[key] = instrument([...parentPath, key])
54
+ throw new Error('Schema items must be strings or [name, children] arrays')
56
55
  }
57
56
 
58
57
  return acc
@@ -0,0 +1,8 @@
1
+ import { test } from 'tap'
2
+ import references from './references.mjs'
3
+
4
+ test('invalid uses of references', t => {
5
+ t.throws(() => references([['a']]))
6
+ t.throws(() => references([{}]))
7
+ t.end()
8
+ })
package/stringify.mjs CHANGED
@@ -69,6 +69,10 @@ function transformValue (value, leafSerializer) {
69
69
  return ['leaf', leafSerializer(value)]
70
70
  }
71
71
 
72
- export default function stringify (program, leafSerializer = JSON.stringify) {
73
- return JSON.stringify(transformValue(program, leafSerializer))
72
+ export default function stringify (leafSerializer = JSON.stringify) {
73
+ function stringify (program) {
74
+ return JSON.stringify(transformValue(program, leafSerializer))
75
+ }
76
+
77
+ return stringify
74
78
  }
package/index.mjs DELETED
@@ -1,11 +0,0 @@
1
- import stringify from './stringify.mjs'
2
- import makeModule from './module.mjs'
3
- import parse from './parse.mjs'
4
-
5
- export default function sendscript (module) {
6
- return {
7
- stringify,
8
- parse: parse(module),
9
- module: makeModule(module)
10
- }
11
- }