@toa.io/extensions.exposition 1.0.0-alpha.60 → 1.0.0-alpha.61

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.
Files changed (50) hide show
  1. package/components/octets.storage/manifest.toa.yaml +1 -0
  2. package/components/octets.storage/operations/get.js +2 -2
  3. package/components/octets.storage/operations/store.js +31 -23
  4. package/documentation/octets.md +21 -4
  5. package/features/cors.feature +1 -1
  6. package/features/methods.feature +47 -0
  7. package/features/octets.download.feature +73 -1
  8. package/features/octets.feature +34 -0
  9. package/features/steps/Parameters.ts +1 -1
  10. package/package.json +4 -4
  11. package/schemas/method.cos.yaml +1 -1
  12. package/schemas/octets/store.cos.yaml +25 -4
  13. package/source/HTTP/Server.ts +1 -1
  14. package/source/HTTP/exceptions.ts +6 -0
  15. package/source/RTD/syntax/types.ts +1 -1
  16. package/source/directives/cors/CORS.ts +1 -1
  17. package/source/directives/io/Input.ts +2 -2
  18. package/source/directives/io/Output.ts +1 -1
  19. package/source/directives/octets/Context.ts +3 -2
  20. package/source/directives/octets/Fetch.ts +1 -1
  21. package/source/directives/octets/Store.ts +13 -4
  22. package/source/directives/octets/bytes.test.ts +30 -0
  23. package/source/directives/octets/bytes.ts +18 -0
  24. package/source/directives/octets/schemas.ts +0 -2
  25. package/transpiled/HTTP/Server.js +1 -1
  26. package/transpiled/HTTP/Server.js.map +1 -1
  27. package/transpiled/HTTP/exceptions.d.ts +3 -0
  28. package/transpiled/HTTP/exceptions.js +7 -1
  29. package/transpiled/HTTP/exceptions.js.map +1 -1
  30. package/transpiled/RTD/syntax/types.js +1 -1
  31. package/transpiled/RTD/syntax/types.js.map +1 -1
  32. package/transpiled/directives/cors/CORS.js +1 -1
  33. package/transpiled/directives/cors/CORS.js.map +1 -1
  34. package/transpiled/directives/io/Input.js.map +1 -1
  35. package/transpiled/directives/io/Output.js.map +1 -1
  36. package/transpiled/directives/octets/Context.js +4 -24
  37. package/transpiled/directives/octets/Context.js.map +1 -1
  38. package/transpiled/directives/octets/Fetch.js +1 -1
  39. package/transpiled/directives/octets/Fetch.js.map +1 -1
  40. package/transpiled/directives/octets/Store.d.ts +3 -0
  41. package/transpiled/directives/octets/Store.js +9 -3
  42. package/transpiled/directives/octets/Store.js.map +1 -1
  43. package/transpiled/directives/octets/bytes.d.ts +1 -0
  44. package/transpiled/directives/octets/bytes.js +21 -0
  45. package/transpiled/directives/octets/bytes.js.map +1 -0
  46. package/transpiled/directives/octets/schemas.d.ts +0 -2
  47. package/transpiled/directives/octets/schemas.js +1 -3
  48. package/transpiled/directives/octets/schemas.js.map +1 -1
  49. package/transpiled/tsconfig.tsbuildinfo +1 -1
  50. package/schemas/octets/context.cos.yaml +0 -1
@@ -10,6 +10,7 @@ operations:
10
10
  storage*: string
11
11
  request*: ~
12
12
  accept: string
13
+ limit: number
13
14
  meta: <string>
14
15
  trust: ~ # array of strings or regular expressions
15
16
  errors:
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
- function get (input, context) {
4
- return context.storages[input.storage].get(input.path)
3
+ async function get (input, context) {
4
+ return await context.storages[input.storage].get(input.path)
5
5
  }
6
6
 
7
7
  exports.computation = get
@@ -5,16 +5,38 @@ const { Err } = require('error-value')
5
5
  const { match } = require('matchacho')
6
6
 
7
7
  async function store (input, context) {
8
- const { storage, request, accept, trust } = input
8
+ const { storage, request, accept, limit, trust } = input
9
9
  const path = request.url
10
10
  const claim = request.headers['content-type']
11
11
  const meta = parseMeta(request.headers['content-meta'])
12
- const body = await download(request, trust)
12
+ const location = request.headers['content-location']
13
+
14
+ /** @type {Readable} */
15
+ let body = request
16
+
17
+ const options = { claim, accept, meta }
18
+
19
+ if (location !== undefined) {
20
+ const length = Number.parseInt(request.headers['content-length'])
21
+
22
+ if (length !== 0)
23
+ return ERR_LENGTH
24
+
25
+ if (!trusted(location, trust))
26
+ return ERR_UNTRUSTED
27
+
28
+ body = await download(location)
29
+
30
+ if (body instanceof Error)
31
+ return body
32
+
33
+ options.origin = location
34
+ }
13
35
 
14
- if (body instanceof Error)
15
- return body
36
+ if (limit !== undefined)
37
+ options.limit = limit
16
38
 
17
- return context.storages[storage].put(path, body, { claim, accept, meta })
39
+ return context.storages[storage].put(path, body, options)
18
40
  }
19
41
 
20
42
  /**
@@ -41,25 +63,10 @@ function parseMeta (values) {
41
63
  }
42
64
 
43
65
  /**
44
- * @param {Request} request
45
- * @param {Trust | undefined} trust
46
- * @return {import('node:stream').Readable | Error}
66
+ * @param {string} location
67
+ * @return {Readable | Error}
47
68
  */
48
- async function download (request, trust) {
49
- /** @type {string | undefined} */
50
- const location = request.headers['content-location']
51
-
52
- if (location === undefined)
53
- return request
54
-
55
- const length = Number.parseInt(request.headers['content-length'])
56
-
57
- if (length !== 0)
58
- return ERR_LENGTH
59
-
60
- if (!trusted(location, trust))
61
- return ERR_UNTRUSTED
62
-
69
+ async function download (location) {
63
70
  const response = await fetch(location)
64
71
 
65
72
  if (!response.ok)
@@ -111,3 +118,4 @@ const ERR_UNAVAILABLE = Err('LOCATION_UNAVAILABLE', 'Location is not available')
111
118
  exports.effect = store
112
119
 
113
120
  /** @typedef {Array<string | RegExp>} Trust */
121
+ /** @typedef {import('node:stream').Readable} Readable */
@@ -25,10 +25,7 @@ the request is rejected with a `415 Unsupported Media Type` response.
25
25
 
26
26
  The value of the directive is `null` or an object with the following properties:
27
27
 
28
- - `limit`: a number of bytes (or
29
- a [string with units](https://www.npmjs.com/package/bytes#bytesparsestringnumber-value-numbernull))
30
- to limit the size of the uploaded content
31
- (default is 64MB, which should be enough for everyone ©).
28
+ - `limit`: [maximum size](#stream-size-limit) of the incoming stream.
32
29
  - `accept`: a media type or an array of media types that are acceptable.
33
30
  If the `accept` property is not specified, any media type is acceptable (which is the default).
34
31
  - `workflow`: [workflow](#workflows) to be executed once the content is successfully stored.
@@ -70,6 +67,23 @@ meta:
70
67
 
71
68
  If the Entry already exists, the `content-meta` header is ignored.
72
69
 
70
+ ### Stream size limit
71
+
72
+ The `limit` property can be used to set the maximum size of the incoming stream in bytes.
73
+
74
+ The property value can be specified as a number
75
+ (representing bytes) or a string that combines a number with a unit (e.g., `1MB`).
76
+ Both [binary and decimal prefixes](https://en.wikipedia.org/wiki/Binary_prefix) are supported.
77
+ If the prefix or unit is specified _incorrectly_ (e.g., `1mb`),
78
+ it will default to a binary prefix interpretation.
79
+
80
+ - `1b`, `1B`: 1 byte
81
+ - `1KB`: 1000 bytes
82
+ - `1KiB`: 1024 bytes
83
+ - `1kb`: 1024 bytes
84
+
85
+ The default value is `64MiB`.
86
+
73
87
  ### Downloading external content
74
88
 
75
89
  The `octets:store` directive can be used to download external content:
@@ -85,6 +99,9 @@ Requests with `content-location` header must have an empty body (`content-length
85
99
  Target origin must be allowed by the `trust` property,
86
100
  which can contain a list of trusted origins or regular expressions to match the full URL.
87
101
 
102
+ URL of the downloaded content is stored in the `origin` property of
103
+ the [Entry](/extensions/storages/readme.md#entry).
104
+
88
105
  ```yaml
89
106
  /images:
90
107
  octets:context: images
@@ -20,7 +20,7 @@ Feature: CORS Support
20
20
  """
21
21
  204 No Content
22
22
  access-control-allow-origin: https://hello.world
23
- access-control-allow-methods: GET, POST, PUT, PATCH, DELETE
23
+ access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, LOCK, UNLOCK
24
24
  access-control-allow-headers: accept, authorization, content-type, etag, if-match, if-none-match
25
25
  access-control-allow-credentials: true
26
26
  access-control-max-age: 3600
@@ -0,0 +1,47 @@
1
+ Feature: Supported methods
2
+
3
+ Scenario Outline: <method> is supported
4
+ Given the annotation:
5
+ """yaml
6
+ /:
7
+ <method>:
8
+ dev:stub:
9
+ hello: world
10
+ """
11
+ And the `greeter` is running with the following manifest:
12
+ """yaml
13
+ exposition:
14
+ /:
15
+ <method>:
16
+ dev:stub:
17
+ hello: world
18
+ """
19
+ Examples:
20
+ | method |
21
+ | GET |
22
+ | POST |
23
+ | PUT |
24
+ | DELETE |
25
+ | PATCH |
26
+ | LOCK |
27
+ | UNLOCK |
28
+
29
+ Scenario: CORS allowed methods
30
+ Given the annotation:
31
+ """yaml
32
+ /:
33
+ GET:
34
+ dev:stub:
35
+ hello: world
36
+ """
37
+ When the following request is received:
38
+ """
39
+ OPTIONS / HTTP/1.1
40
+ host: nex.toa.io
41
+ origin: https://hello.world
42
+ """
43
+ Then the following reply is sent:
44
+ """
45
+ 204 No Content
46
+ access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, LOCK, UNLOCK
47
+ """
@@ -14,7 +14,8 @@ Feature: Download and store
14
14
  - https://github.com
15
15
  /*:
16
16
  GET:
17
- octets:fetch: ~
17
+ octets:fetch:
18
+ meta: true
18
19
  """
19
20
 
20
21
  When the following request is received:
@@ -43,6 +44,24 @@ Feature: Download and store
43
44
  200 OK
44
45
  content-type: image/png
45
46
  content-length: 1288
47
+ etag: "${{ id }}"
48
+ """
49
+
50
+ # origin is stored in the entry
51
+ When the following request is received:
52
+ """
53
+ GET /${{ id }} HTTP/1.1
54
+ host: nex.toa.io
55
+ accept: application/vnd.toa.octets.entry+yaml
56
+ """
57
+ Then the following reply is sent:
58
+ """
59
+ 200 OK
60
+ content-type: application/yaml
61
+
62
+ id: ${{ id }}
63
+ type: image/png
64
+ origin: https://avatars.githubusercontent.com/u/92763022?s=48&v=4
46
65
  """
47
66
 
48
67
  # untrusted location
@@ -115,3 +134,56 @@ Feature: Download and store
115
134
  | type | location |
116
135
  | origin | https://avatars.githubusercontent.com |
117
136
  | expression | /^https://avatars\.githubusercontent\.com/ |
137
+
138
+ Scenario: Download size limit
139
+ Given the annotation:
140
+ """yaml
141
+ /:
142
+ io:output: true
143
+ auth:anonymous: true
144
+ octets:context: octets
145
+ POST:
146
+ octets:store:
147
+ limit: 1kb
148
+ trust:
149
+ - https://avatars.githubusercontent.com
150
+ """
151
+ When the following request is received:
152
+ """
153
+ POST / HTTP/1.1
154
+ host: nex.toa.io
155
+ content-location: https://avatars.githubusercontent.com/u/92763022?s=48&v=4
156
+ content-length: 0
157
+ accept: text/plain
158
+ """
159
+ Then the following reply is sent:
160
+ """
161
+ 413 Request Entity Too Large
162
+
163
+ Size limit is 1kb
164
+ """
165
+
166
+ Scenario: Allow `content-location` request header
167
+ Given the annotation:
168
+ """yaml
169
+ /:
170
+ io:output: true
171
+ auth:anonymous: true
172
+ octets:context: octets
173
+ POST:
174
+ octets:store:
175
+ limit: 1kb
176
+ trust:
177
+ - https://avatars.githubusercontent.com
178
+ """
179
+ When the following request is received:
180
+ """
181
+ OPTIONS / HTTP/1.1
182
+ host: nex.toa.io
183
+ origin: https://hello.world
184
+ """
185
+ Then the following reply is sent:
186
+ """
187
+ 204 No Content
188
+ access-control-allow-headers: accept, authorization, content-type, etag, if-match, if-none-match, content-meta, content-location
189
+ """
@@ -34,6 +34,14 @@ Feature: Octets directive family
34
34
  /*:
35
35
  GET:
36
36
  octets:fetch: ~
37
+ /limit-1kb:
38
+ POST:
39
+ octets:store:
40
+ limit: 1kb
41
+ /limit-100kb:
42
+ POST:
43
+ octets:store:
44
+ limit: 100kb
37
45
  """
38
46
 
39
47
  Scenario: Basic storage operations
@@ -146,6 +154,32 @@ Feature: Octets directive family
146
154
  201 Created
147
155
  """
148
156
 
157
+ Scenario: Size limit
158
+ When the stream of `albert.jpg` is received with the following headers:
159
+ """
160
+ POST /limit-1kb/ HTTP/1.1
161
+ host: nex.toa.io
162
+ content-type: image/jpeg
163
+ accept: text/plain
164
+ """
165
+ Then the following reply is sent:
166
+ """
167
+ 413 Request Entity Too Large
168
+
169
+ Size limit is 1kb
170
+ """
171
+
172
+ When the stream of `albert.jpg` is received with the following headers:
173
+ """
174
+ POST /limit-100kb/ HTTP/1.1
175
+ host: nex.toa.io
176
+ content-type: image/jpeg
177
+ """
178
+ Then the following reply is sent:
179
+ """
180
+ 201 Created
181
+ """
182
+
149
183
  Scenario Outline: Detecting `<type>`
150
184
  When the stream of `sample.<ext>` is received with the following headers:
151
185
  """
@@ -16,6 +16,6 @@ process.env.TOA_DEV = '1'
16
16
  process.env.TOA_STORAGES = encode({
17
17
  octets: {
18
18
  provider: 'tmp',
19
- directory: 'exposition'
19
+ directory: Math.random().toString(36).substring(2)
20
20
  }
21
21
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.exposition",
3
- "version": "1.0.0-alpha.60",
3
+ "version": "1.0.0-alpha.61",
4
4
  "description": "Toa Exposition",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -19,7 +19,7 @@
19
19
  "dependencies": {
20
20
  "@toa.io/core": "1.0.0-alpha.59",
21
21
  "@toa.io/generic": "1.0.0-alpha.59",
22
- "@toa.io/schemas": "1.0.0-alpha.59",
22
+ "@toa.io/schemas": "1.0.0-alpha.61",
23
23
  "bcryptjs": "2.4.3",
24
24
  "error-value": "0.3.0",
25
25
  "js-yaml": "4.1.0",
@@ -51,11 +51,11 @@
51
51
  "@swc/core": "1.6.6",
52
52
  "@swc/helpers": "0.5.11",
53
53
  "@toa.io/agent": "1.0.0-alpha.59",
54
- "@toa.io/extensions.storages": "1.0.0-alpha.59",
54
+ "@toa.io/extensions.storages": "1.0.0-alpha.61",
55
55
  "@types/bcryptjs": "2.4.3",
56
56
  "@types/cors": "2.8.13",
57
57
  "@types/negotiator": "0.6.1",
58
58
  "jest-esbuild": "0.3.0"
59
59
  },
60
- "gitHead": "81bf1dbf5b3f8dcb19097aca19947ddb60cb96be"
60
+ "gitHead": "25e32ccba495a1f7197e378a5208cfc1a6b3b8a1"
61
61
  }
@@ -1,4 +1,4 @@
1
- verb: /^(GET|POST|PUT|PATCH|DELETE)$/
1
+ verb: /^(GET|POST|PUT|PATCH|DELETE|LOCK|UNLOCK)$/
2
2
  mapping:
3
3
  namespace: string
4
4
  component: string
@@ -1,4 +1,25 @@
1
- accept+: string
2
- workflow+: <string>
3
- trust: [string]
4
- _: true
1
+ type: object
2
+ nullable: true
3
+ properties:
4
+ accept:
5
+ anyOf:
6
+ - type: string
7
+ - type: array
8
+ items:
9
+ type: string
10
+ limit:
11
+ type: string
12
+ pattern: ^(\d+(\.\d+)?)([kmgtKMBGT]i?)?[bB]?$
13
+ trust:
14
+ type: array
15
+ items:
16
+ type: string
17
+ workflow:
18
+ anyOf:
19
+ - &unit
20
+ type: object
21
+ patternProperties:
22
+ ^.+$:
23
+ type: string
24
+ - type: array
25
+ items: *unit
@@ -142,7 +142,7 @@ export const PORT = 8000
142
142
  export const DELAY = 3 // seconds
143
143
 
144
144
  const DEFAULTS: Omit<Properties, 'authorities'> = {
145
- methods: new Set<string>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']),
145
+ methods: new Set<string>(['OPTIONS', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'LOCK', 'UNLOCK']),
146
146
  debug: false,
147
147
  trace: false,
148
148
  port: PORT,
@@ -71,3 +71,9 @@ export class PreconditionFailed extends ClientError {
71
71
  super(412)
72
72
  }
73
73
  }
74
+
75
+ export class RequestEntityTooLarge extends ClientError {
76
+ public constructor (body?: any) {
77
+ super(413, body)
78
+ }
79
+ }
@@ -47,4 +47,4 @@ export interface Range {
47
47
  range: [number, number]
48
48
  }
49
49
 
50
- export const verbs = new Set<string>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
50
+ export const verbs = new Set<string>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'LOCK', 'UNLOCK'])
@@ -14,7 +14,7 @@ export class CORS implements Interceptor {
14
14
  ])
15
15
 
16
16
  private readonly headers = new Headers({
17
- 'access-control-allow-methods': 'GET, POST, PUT, PATCH, DELETE',
17
+ 'access-control-allow-methods': 'GET, POST, PUT, PATCH, DELETE, LOCK, UNLOCK',
18
18
  'access-control-allow-credentials': 'true',
19
19
  'access-control-allow-headers': Array.from(this.requestHeaders).join(', '),
20
20
  'access-control-max-age': '3600',
@@ -12,7 +12,7 @@ export class Input implements Directive {
12
12
  }
13
13
 
14
14
  public static validate (permissions: unknown): asserts permissions is Permissions {
15
- schemas.input.validate(permissions, 'Incorrect \'io:input\' format')
15
+ schemas.input.validate<Permissions>(permissions, 'Incorrect \'io:input\' format')
16
16
  }
17
17
 
18
18
  public attach (context: Context): void {
@@ -21,7 +21,7 @@ export class Input implements Directive {
21
21
 
22
22
  private check (body: unknown): unknown {
23
23
  try {
24
- schemas.message.validate(body)
24
+ schemas.message.validate<Message | Message[]>(body)
25
25
  } catch {
26
26
  throw new BadRequest('Invalid request body')
27
27
  }
@@ -48,7 +48,7 @@ export class Output implements Directive {
48
48
  return
49
49
  }
50
50
 
51
- schemas.message.validate(message.body,
51
+ schemas.message.validate<Message>(message.body,
52
52
  '\'io:output\' expects response to be an object or array of objects')
53
53
 
54
54
  if (Array.isArray(message.body))
@@ -1,4 +1,4 @@
1
- import * as schemas from './schemas'
1
+ import assert from 'node:assert'
2
2
  import { Directive } from './Directive'
3
3
  import type { Output } from '../../io'
4
4
 
@@ -8,7 +8,8 @@ export class Context extends Directive {
8
8
 
9
9
  public constructor (value: unknown) {
10
10
  super()
11
- schemas.context.validate(value)
11
+
12
+ assert.ok(typeof value === 'string', 'Directive \'octets:context\' must must be a string')
12
13
 
13
14
  this.storage = value
14
15
  }
@@ -64,7 +64,7 @@ export class Fetch extends Directive {
64
64
  const headers = new Headers({
65
65
  'content-type': result.type,
66
66
  'content-length': result.size.toString(),
67
- etag: result.checksum
67
+ etag: `"${result.checksum}"`
68
68
  })
69
69
 
70
70
  return {
@@ -5,6 +5,7 @@ import { cors } from '../cors'
5
5
  import * as schemas from './schemas'
6
6
  import { Workflow } from './workflows'
7
7
  import { Directive } from './Directive'
8
+ import { toBytes } from './bytes'
8
9
  import type { Readable } from 'stream'
9
10
  import type { Parameter } from '../../RTD'
10
11
  import type { Unit } from './workflows'
@@ -19,6 +20,8 @@ export class Store extends Directive {
19
20
  public readonly targeted = false
20
21
 
21
22
  private readonly accept?: string
23
+ private readonly limit: number
24
+ private readonly limitString: string
22
25
  private readonly trust?: Array<string | RegExp>
23
26
  private readonly workflow?: Workflow
24
27
  private readonly discovery: Record<string, Promise<Component>> = {}
@@ -27,7 +30,8 @@ export class Store extends Directive {
27
30
  public constructor
28
31
  (options: Options | null, discovery: Promise<Component>, remotes: Remotes) {
29
32
  super()
30
- schemas.store.validate(options)
33
+
34
+ schemas.store.validate<Options>(options)
31
35
 
32
36
  this.accept = match(options?.accept,
33
37
  String, (value: string) => value,
@@ -41,9 +45,12 @@ export class Store extends Directive {
41
45
  this.trust = options.trust.map((value: string) =>
42
46
  value.startsWith('/') ? new RegExp(value.slice(1, -1)) : value)
43
47
 
48
+ this.limitString = options?.limit ?? '64MiB'
49
+ this.limit = toBytes(this.limitString)
44
50
  this.discovery.storage = discovery
45
51
 
46
52
  cors.allow('content-meta')
53
+ cors.allow('content-location')
47
54
  }
48
55
 
49
56
  public async apply (storage: string, input: Input, parameters: Parameter[]): Promise<Output> {
@@ -53,15 +60,14 @@ export class Store extends Directive {
53
60
  input: {
54
61
  storage,
55
62
  request: input.request,
63
+ accept: this.accept,
64
+ limit: this.limit,
56
65
  trust: this.trust
57
66
  }
58
67
  }
59
68
 
60
69
  const meta = input.request.headers['content-meta']
61
70
 
62
- if (this.accept !== undefined)
63
- request.input.accept = this.accept
64
-
65
71
  if (meta !== undefined)
66
72
  request.input.meta = this.meta(meta)
67
73
 
@@ -97,6 +103,7 @@ export class Store extends Directive {
97
103
  throw match(error.code,
98
104
  'NOT_ACCEPTABLE', () => new http.UnsupportedMediaType(),
99
105
  'TYPE_MISMATCH', () => new http.BadRequest(),
106
+ 'LIMIT_EXCEEDED', () => new http.RequestEntityTooLarge(`Size limit is ${this.limitString}`),
100
107
  'LOCATION_UNTRUSTED', () => new http.Forbidden(error.message),
101
108
  'LOCATION_LENGTH', () => new http.BadRequest(error.message),
102
109
  'LOCATION_UNAVAILABLE', () => new http.NotFound(error.message),
@@ -122,6 +129,7 @@ export class Store extends Directive {
122
129
 
123
130
  export interface Options {
124
131
  accept?: string | string[]
132
+ limit?: string
125
133
  workflow?: Unit[] | Unit
126
134
  trust?: string[]
127
135
  }
@@ -131,6 +139,7 @@ interface StoreRequest {
131
139
  storage: string
132
140
  request: Input['request']
133
141
  accept?: string
142
+ limit?: number
134
143
  trust?: Array<string | RegExp>
135
144
  meta?: Record<string, string>
136
145
  }
@@ -0,0 +1,30 @@
1
+ import { toBytes } from './bytes'
2
+
3
+ it('should parse bytes', async () => {
4
+ expect(toBytes('10')).toBe(10)
5
+ expect(toBytes('10B')).toBe(10)
6
+ })
7
+
8
+ it('should parse binary prefix', async () => {
9
+ expect(toBytes('10KiB')).toBe(10240)
10
+ expect(toBytes('10MiB')).toBe(10485760)
11
+ expect(toBytes('10GiB')).toBe(10737418240)
12
+ expect(toBytes('10TiB')).toBe(10995116277760)
13
+ })
14
+
15
+ it('should parse decimal prefix', async () => {
16
+ expect(toBytes('10kB')).toBe(10000)
17
+ expect(toBytes('10MB')).toBe(10000000)
18
+ expect(toBytes('10GB')).toBe(10000000000)
19
+ expect(toBytes('10TB')).toBe(10000000000000)
20
+ })
21
+
22
+ it('should parse incorrect value as binary', async () => {
23
+ expect(toBytes('10b')).toBe(10)
24
+ expect(toBytes('10kb')).toBe(10240)
25
+ expect(toBytes('10kib')).toBe(10240)
26
+ expect(toBytes('10mb')).toBe(10485760)
27
+ expect(toBytes('10gb')).toBe(10737418240)
28
+ expect(toBytes('10tib')).toBe(10995116277760)
29
+ expect(toBytes('10Mb')).toBe(10485760)
30
+ })
@@ -0,0 +1,18 @@
1
+ import assert from 'node:assert'
2
+
3
+ export function toBytes (input: string): number {
4
+ const match = RX.exec(input)
5
+
6
+ assert.ok(match !== null, `Invalid bytes format: ${input}`)
7
+
8
+ const value = parseFloat(match.groups!.value)
9
+ const prefix = match.groups!.prefix?.[0].toLowerCase() ?? ''
10
+ const binary = match.groups!.binary !== undefined || match.groups!.unit === 'b'
11
+ const base = binary ? 1024 : 1000
12
+ const power = POWERS.indexOf(prefix)
13
+
14
+ return value * Math.pow(base, power)
15
+ }
16
+
17
+ const POWERS = ['', 'k', 'm', 'g', 't']
18
+ const RX = /^(?<value>(\d+)(\.\d+)?)(?<prefix>[kmgt](?<binary>i)?)?(?<unit>b)?$/i
@@ -10,10 +10,8 @@ import type { Unit } from './workflows'
10
10
  const path = resolve(__dirname, '../../../schemas/octets')
11
11
  const namespace = schemas.namespace(path)
12
12
 
13
- export const context: Schema<string> = namespace.schema('context')
14
13
  export const store: Schema<StoreOptions | null> = namespace.schema('store')
15
14
  export const fetch: Schema<FetchOptions | null> = namespace.schema('fetch')
16
15
  export const remove: Schema<DeleteOptions | null> = namespace.schema('delete')
17
16
  export const list: Schema<ListOptions | null> = namespace.schema('list')
18
- export const permute: Schema<null> = namespace.schema('permute')
19
17
  export const workflow: Schema<Unit[] | Unit> = namespace.schema('workflow')
@@ -134,7 +134,7 @@ async function adam(request) {
134
134
  exports.PORT = 8000;
135
135
  exports.DELAY = 3; // seconds
136
136
  const DEFAULTS = {
137
- methods: new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']),
137
+ methods: new Set(['OPTIONS', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'LOCK', 'UNLOCK']),
138
138
  debug: false,
139
139
  trace: false,
140
140
  port: exports.PORT,