@toa.io/extensions.exposition 0.22.0 → 0.23.0-dev.0
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/components/identity.basic/source/authenticate.ts +9 -6
- package/components/identity.basic/source/create.ts +1 -2
- package/components/identity.basic/source/transit.ts +10 -6
- package/components/identity.basic/source/types.ts +2 -2
- package/components/identity.tokens/source/authenticate.ts +6 -5
- package/components/identity.tokens/source/decrypt.ts +12 -4
- package/components/identity.tokens/source/encrypt.test.ts +1 -1
- package/components/identity.tokens/source/types.ts +2 -2
- package/components/octets.storage/manifest.toa.yaml +26 -0
- package/components/octets.storage/operations/delete.js +7 -0
- package/components/octets.storage/operations/fetch.js +46 -0
- package/components/octets.storage/operations/get.js +7 -0
- package/components/octets.storage/operations/list.js +7 -0
- package/components/octets.storage/operations/permute.js +7 -0
- package/components/octets.storage/operations/store.js +11 -0
- package/cucumber.js +0 -1
- package/documentation/octets.md +196 -0
- package/documentation/protocol.md +49 -5
- package/features/access.feature +1 -0
- package/features/errors.feature +18 -0
- package/features/identity.basic.feature +2 -0
- package/features/octets.feature +295 -0
- package/features/octets.workflows.feature +114 -0
- package/features/routes.feature +40 -0
- package/features/steps/HTTP.ts +47 -5
- package/features/steps/Parameters.ts +5 -1
- package/features/steps/Workspace.ts +3 -2
- package/features/steps/components/octets.tester/manifest.toa.yaml +15 -0
- package/features/steps/components/octets.tester/operations/bar.js +12 -0
- package/features/steps/components/octets.tester/operations/baz.js +11 -0
- package/features/steps/components/octets.tester/operations/diversify.js +14 -0
- package/features/steps/components/octets.tester/operations/err.js +16 -0
- package/features/steps/components/octets.tester/operations/foo.js +7 -0
- package/features/steps/components/octets.tester/operations/lenna.png +0 -0
- package/features/steps/components/pots/manifest.toa.yaml +1 -1
- package/features/streams.feature +5 -1
- package/package.json +11 -10
- package/readme.md +7 -5
- package/schemas/octets/context.cos.yaml +1 -0
- package/schemas/octets/delete.cos.yaml +1 -0
- package/schemas/octets/fetch.cos.yaml +3 -0
- package/schemas/octets/list.cos.yaml +1 -0
- package/schemas/octets/permute.cos.yaml +1 -0
- package/schemas/octets/store.cos.yaml +3 -0
- package/source/Gateway.ts +12 -7
- package/source/HTTP/Server.fixtures.ts +2 -6
- package/source/HTTP/Server.test.ts +7 -31
- package/source/HTTP/Server.ts +31 -16
- package/source/HTTP/exceptions.ts +2 -12
- package/source/HTTP/formats/index.ts +7 -4
- package/source/HTTP/formats/json.ts +3 -0
- package/source/HTTP/formats/msgpack.ts +3 -0
- package/source/HTTP/formats/text.ts +3 -0
- package/source/HTTP/formats/yaml.ts +3 -0
- package/source/HTTP/messages.test.ts +3 -49
- package/source/HTTP/messages.ts +58 -33
- package/source/RTD/Route.ts +1 -1
- package/source/RTD/segment.ts +2 -1
- package/source/Remotes.ts +8 -0
- package/source/directives/auth/Family.ts +25 -22
- package/source/directives/auth/Incept.ts +3 -3
- package/source/directives/auth/Rule.ts +1 -1
- package/source/directives/auth/types.ts +2 -2
- package/source/directives/index.ts +2 -1
- package/source/directives/octets/Context.ts +18 -0
- package/source/directives/octets/Delete.ts +32 -0
- package/source/directives/octets/Family.ts +68 -0
- package/source/directives/octets/Fetch.ts +85 -0
- package/source/directives/octets/List.ts +32 -0
- package/source/directives/octets/Permute.ts +37 -0
- package/source/directives/octets/Store.ts +158 -0
- package/source/directives/octets/index.ts +3 -0
- package/source/directives/octets/schemas.ts +12 -0
- package/source/directives/octets/types.ts +13 -0
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
import { atob } from 'buffer'
|
|
2
2
|
import { compare } from 'bcryptjs'
|
|
3
|
-
import { type Query } from '@toa.io/types'
|
|
4
|
-
import {
|
|
3
|
+
import { type Query, type Maybe } from '@toa.io/types'
|
|
4
|
+
import { Err } from 'error-value'
|
|
5
5
|
import { type Context } from './types'
|
|
6
6
|
|
|
7
|
-
export async function computation (input: string, context: Context): Promise<
|
|
7
|
+
export async function computation (input: string, context: Context): Promise<Maybe<Output>> {
|
|
8
8
|
const [username, password] = atob(input).split(':')
|
|
9
9
|
const query: Query = { criteria: `username==${username}` }
|
|
10
10
|
const credentials = await context.local.observe({ query })
|
|
11
11
|
|
|
12
|
-
if (credentials instanceof
|
|
12
|
+
if (credentials instanceof Error)
|
|
13
13
|
return credentials
|
|
14
14
|
|
|
15
15
|
if (credentials === null)
|
|
16
|
-
return
|
|
16
|
+
return ERR_NOT_FOUND
|
|
17
17
|
|
|
18
18
|
const spicy = password + context.configuration.pepper
|
|
19
19
|
const match = await compare(spicy, credentials.password)
|
|
20
20
|
|
|
21
21
|
if (match) return { identity: { id: credentials.id } }
|
|
22
|
-
else return
|
|
22
|
+
else return ERR_PASSWORD_MISMATCH
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
const ERR_NOT_FOUND = Err('NOT_FOUND')
|
|
26
|
+
const ERR_PASSWORD_MISMATCH = Err('PASSWORD_MISMATCH')
|
|
27
|
+
|
|
25
28
|
interface Output {
|
|
26
29
|
identity: {
|
|
27
30
|
id: string
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { type Nopeable } from 'nopeable'
|
|
2
1
|
import { type Context } from './types'
|
|
3
2
|
|
|
4
3
|
export async function effect
|
|
5
|
-
(input: CreateInput, context: Context): Promise<
|
|
4
|
+
(input: CreateInput, context: Context): Promise<CreateOutput> {
|
|
6
5
|
const [username, password] = atob(input.credentials).split(':')
|
|
7
6
|
const request = { input: { username, password }, query: { id: input.id } }
|
|
8
7
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { genSalt, hash } from 'bcryptjs'
|
|
2
|
-
import { type Operation } from '@toa.io/types'
|
|
3
|
-
import {
|
|
2
|
+
import { type Maybe, type Operation } from '@toa.io/types'
|
|
3
|
+
import { Err } from 'error-value'
|
|
4
4
|
import { type Context, type Entity, type TransitInput, type TransitOutput } from './types'
|
|
5
5
|
|
|
6
6
|
export class Transition implements Operation {
|
|
@@ -21,7 +21,7 @@ export class Transition implements Operation {
|
|
|
21
21
|
this.passwrodRx = toRx(context.configuration.password)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
public async execute (input: TransitInput, object: Entity): Promise<
|
|
24
|
+
public async execute (input: TransitInput, object: Entity): Promise<Maybe<TransitOutput>> {
|
|
25
25
|
const existent = object._version !== 0
|
|
26
26
|
|
|
27
27
|
if (existent)
|
|
@@ -29,17 +29,17 @@ export class Transition implements Operation {
|
|
|
29
29
|
|
|
30
30
|
if (input.username !== undefined) {
|
|
31
31
|
if (existent && object.username === this.principal)
|
|
32
|
-
return
|
|
32
|
+
return ERR_PRINCIPAL_LOCKED
|
|
33
33
|
|
|
34
34
|
if (invalid(input.username, this.usernameRx))
|
|
35
|
-
return
|
|
35
|
+
return ERR_INVALID_USERNAME
|
|
36
36
|
|
|
37
37
|
object.username = input.username
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
if (input.password !== undefined) {
|
|
41
41
|
if (invalid(input.password, this.passwrodRx))
|
|
42
|
-
return
|
|
42
|
+
return ERR_INVALID_PASSWORD
|
|
43
43
|
|
|
44
44
|
const salt = await genSalt(this.rounds)
|
|
45
45
|
const spicy = input.password + this.pepper
|
|
@@ -61,4 +61,8 @@ function invalid (value: string, expressions: RegExp[]): boolean {
|
|
|
61
61
|
return expressions.some((expression) => !expression.test(value))
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
const ERR_PRINCIPAL_LOCKED = Err('PRINCIPAL_LOCKED', 'Principal username cannot be changed.')
|
|
65
|
+
const ERR_INVALID_USERNAME = Err('INVALID_USERNAME', 'Username is not meeting the requirements.')
|
|
66
|
+
const ERR_INVALID_PASSWORD = Err('INVALID_PASSWORD', 'Password is not meeting the requirements.')
|
|
67
|
+
|
|
64
68
|
type Tokens = Context['remote']['identity']['tokens']
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type Call, type Observation, type Query } from '@toa.io/types'
|
|
1
|
+
import { type Call, type Maybe, type Observation, type Query } from '@toa.io/types'
|
|
2
2
|
|
|
3
3
|
export interface Context {
|
|
4
4
|
local: {
|
|
5
|
-
observe: Observation<Entity
|
|
5
|
+
observe: Observation<Maybe<Entity>>
|
|
6
6
|
transit: Call<TransitOutput, TransitInput>
|
|
7
7
|
}
|
|
8
8
|
remote: {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type Operation } from '@toa.io/types'
|
|
1
|
+
import { type Maybe, type Operation } from '@toa.io/types'
|
|
3
2
|
import { type AuthenticateOutput, type Context } from './types'
|
|
4
3
|
|
|
5
4
|
export class Computation implements Operation {
|
|
@@ -13,10 +12,10 @@ export class Computation implements Operation {
|
|
|
13
12
|
this.observe = context.local.observe
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
public async execute (token: string): Promise<
|
|
15
|
+
public async execute (token: string): Promise<Maybe<AuthenticateOutput>> {
|
|
17
16
|
const claim = await this.decrypt({ input: token })
|
|
18
17
|
|
|
19
|
-
if (claim instanceof
|
|
18
|
+
if (claim instanceof Error)
|
|
20
19
|
return claim
|
|
21
20
|
|
|
22
21
|
const identity = claim.identity
|
|
@@ -28,7 +27,7 @@ export class Computation implements Operation {
|
|
|
28
27
|
const revocation = await this.observe({ query: { id: identity.id } })
|
|
29
28
|
|
|
30
29
|
if (revocation !== null && iat < revocation.revokedAt)
|
|
31
|
-
return
|
|
30
|
+
return ERR_TOKEN_REVOKED
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
const refresh = stale || claim.refresh
|
|
@@ -36,3 +35,5 @@ export class Computation implements Operation {
|
|
|
36
35
|
return { identity, refresh }
|
|
37
36
|
}
|
|
38
37
|
}
|
|
38
|
+
|
|
39
|
+
const ERR_TOKEN_REVOKED = new Error('TOKEN_REVOKED')
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { V3 } from 'paseto'
|
|
2
|
-
import {
|
|
2
|
+
import { type Maybe } from '@toa.io/types'
|
|
3
3
|
import { type Context, type Claim, type DecryptOutput } from './types'
|
|
4
4
|
|
|
5
5
|
export async function computation (token: string, context: Context):
|
|
6
|
-
Promise<
|
|
6
|
+
Promise<Maybe<DecryptOutput>> {
|
|
7
7
|
let refresh = false
|
|
8
8
|
let claim = await decrypt(token, context.configuration.key0)
|
|
9
9
|
|
|
@@ -12,8 +12,14 @@ Promise<Nopeable<DecryptOutput>> {
|
|
|
12
12
|
claim = await decrypt(token, context.configuration.key1)
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
if (claim === null)
|
|
16
|
-
|
|
15
|
+
if (claim === null)
|
|
16
|
+
return ERR_INVALID_TOKEN
|
|
17
|
+
else return {
|
|
18
|
+
identity: claim.identity,
|
|
19
|
+
iat: claim.iat,
|
|
20
|
+
exp: claim.exp,
|
|
21
|
+
refresh
|
|
22
|
+
}
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
async function decrypt (token: string, key: string): Promise<Claim | null> {
|
|
@@ -23,3 +29,5 @@ async function decrypt (token: string, key: string): Promise<Claim | null> {
|
|
|
23
29
|
return null
|
|
24
30
|
}
|
|
25
31
|
}
|
|
32
|
+
|
|
33
|
+
const ERR_INVALID_TOKEN = new Error('INVALID_TOKEN')
|
|
@@ -31,5 +31,5 @@ it('should encrypt with given lifetime', async () => {
|
|
|
31
31
|
|
|
32
32
|
await timeout(lifetime * 1000)
|
|
33
33
|
|
|
34
|
-
await expect(decrypt(encrypted, context)).resolves.toMatchObject({
|
|
34
|
+
await expect(decrypt(encrypted, context)).resolves.toMatchObject({ message: 'INVALID_TOKEN' })
|
|
35
35
|
})
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { type Call, type Observation } from '@toa.io/types'
|
|
1
|
+
import { type Call, type Maybe, type Observation } from '@toa.io/types'
|
|
2
2
|
|
|
3
3
|
export interface Context {
|
|
4
4
|
local: {
|
|
5
5
|
observe: Observation<Entity>
|
|
6
|
-
decrypt: Call<DecryptOutput
|
|
6
|
+
decrypt: Call<Maybe<DecryptOutput>, string>
|
|
7
7
|
}
|
|
8
8
|
configuration: Configuration
|
|
9
9
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
namespace: octets
|
|
2
|
+
name: storage
|
|
3
|
+
|
|
4
|
+
storages: ~
|
|
5
|
+
|
|
6
|
+
operations:
|
|
7
|
+
store:
|
|
8
|
+
bindings: ~
|
|
9
|
+
input:
|
|
10
|
+
storage*: string
|
|
11
|
+
request*: ~
|
|
12
|
+
accept: string
|
|
13
|
+
fetch: &simple
|
|
14
|
+
bindings: ~
|
|
15
|
+
input:
|
|
16
|
+
storage*: string
|
|
17
|
+
path*: string
|
|
18
|
+
get: *simple
|
|
19
|
+
list: *simple
|
|
20
|
+
delete: *simple
|
|
21
|
+
permute:
|
|
22
|
+
bindings: ~
|
|
23
|
+
input:
|
|
24
|
+
storage*: string
|
|
25
|
+
path*: string
|
|
26
|
+
list*: [string]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { posix } = require('node:path')
|
|
4
|
+
const { Err } = require('error-value')
|
|
5
|
+
|
|
6
|
+
async function fetch (input, context) {
|
|
7
|
+
const storage = context.storages[input.storage]
|
|
8
|
+
const basename = posix.basename(input.path)
|
|
9
|
+
const path = posix.dirname(input.path)
|
|
10
|
+
const [id, suffix] = split(basename)
|
|
11
|
+
const entry = await storage.get(posix.join(path, id))
|
|
12
|
+
|
|
13
|
+
if (entry instanceof Error)
|
|
14
|
+
return entry
|
|
15
|
+
|
|
16
|
+
let variant
|
|
17
|
+
|
|
18
|
+
if (suffix !== undefined) {
|
|
19
|
+
variant = entry.variants.find((variant) => variant.name === suffix)
|
|
20
|
+
|
|
21
|
+
if (variant === undefined)
|
|
22
|
+
return NOT_FOUND
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const stream = await storage.fetch(input.path)
|
|
26
|
+
|
|
27
|
+
if (stream instanceof Error)
|
|
28
|
+
return stream
|
|
29
|
+
|
|
30
|
+
const { type, size } = variant ?? entry
|
|
31
|
+
|
|
32
|
+
return { stream, checksum: entry.id, type, size }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function split (basename) {
|
|
36
|
+
const dot = basename.indexOf('.')
|
|
37
|
+
|
|
38
|
+
if (dot === -1)
|
|
39
|
+
return [basename, undefined]
|
|
40
|
+
else
|
|
41
|
+
return [basename.slice(0, dot), basename.slice(dot + 1)]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const NOT_FOUND = Err('NOT_FOUND')
|
|
45
|
+
|
|
46
|
+
exports.effect = fetch
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
function store (input, context) {
|
|
4
|
+
const { storage, request } = input
|
|
5
|
+
const path = request.path
|
|
6
|
+
const claim = request.headers['content-type']
|
|
7
|
+
|
|
8
|
+
return context.storages[storage].put(path, request, { claim, accept: input.accept })
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
exports.effect = store
|
package/cucumber.js
CHANGED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# BLOBs
|
|
2
|
+
|
|
3
|
+
The `octets` directive family implements operations with BLOBs, using
|
|
4
|
+
the [Storages extention](/extensions/storages).
|
|
5
|
+
The most common use case is to handle file uploads, downloads, and processing.
|
|
6
|
+
|
|
7
|
+
## `octets:context`
|
|
8
|
+
|
|
9
|
+
Sets the [storage name](/extensions/storages/readme.md#annotation) to be used for the `octets`
|
|
10
|
+
directives under the current RTD Node.
|
|
11
|
+
|
|
12
|
+
```yaml
|
|
13
|
+
/images:
|
|
14
|
+
octets:context: images
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## `octets:store`
|
|
18
|
+
|
|
19
|
+
Stores the content of the request body into a storage, under the request path with
|
|
20
|
+
specified `content-type`.
|
|
21
|
+
|
|
22
|
+
If request's `content-type` is not acceptable, or if the request body does not pass
|
|
23
|
+
the [validation](/extensions/storages/readme.md#async-putpath-string-stream-readable-type-typecontrol-maybeentry),
|
|
24
|
+
the request is rejected with a `415 Unsupported Media Type` response.
|
|
25
|
+
|
|
26
|
+
The value of the directive is an object with the following properties:
|
|
27
|
+
|
|
28
|
+
- `accept`: a media type or an array of media types that are acceptable.
|
|
29
|
+
If the `accept` property is not specified, any media type is acceptable (which is the default).
|
|
30
|
+
- `workflow`: [workflow](#workflows) to be executed once the content is successfully stored.
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
/images:
|
|
34
|
+
octets:context: images
|
|
35
|
+
POST:
|
|
36
|
+
octets:store:
|
|
37
|
+
accept:
|
|
38
|
+
- image/jpeg
|
|
39
|
+
- image/png
|
|
40
|
+
- video/*
|
|
41
|
+
workflow:
|
|
42
|
+
resize: images.resize
|
|
43
|
+
analyze: images.analyze
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Workflows
|
|
47
|
+
|
|
48
|
+
A workflow is a list of endpoints to be called.
|
|
49
|
+
The following input will be passed to each endpoint:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
storage: string
|
|
53
|
+
path: string
|
|
54
|
+
entry: Entry
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
See [Entry](/extensions/storages/readme.md#entry) and an
|
|
58
|
+
example [workflow step processor](../features/steps/components/octets.tester).
|
|
59
|
+
|
|
60
|
+
A _workflow unit_ is an object with keys referencing the workflow step identifier, and an endpoint
|
|
61
|
+
as value.
|
|
62
|
+
Steps within a workflow unit are executed in parallel.
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
octets:store:
|
|
66
|
+
workflow:
|
|
67
|
+
resize: images.resize
|
|
68
|
+
analyze: images.analyze
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A workflow can be a single unit, or an array of units.
|
|
72
|
+
If it's an array, the workflow units are executed in sequence.
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
octets:store:
|
|
76
|
+
workflow:
|
|
77
|
+
- optimize: images.optimize # executed first
|
|
78
|
+
- resize: images.resize # executed second
|
|
79
|
+
analyze: images.analyze # executed in parallel with `resize`
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
If one of the workflow units returns an error, the execution of the workflow is interrupted.
|
|
83
|
+
|
|
84
|
+
### Response
|
|
85
|
+
|
|
86
|
+
The response of the `octets:store` directive is the created Entry.
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
201 Created
|
|
90
|
+
content-type: application/yaml
|
|
91
|
+
|
|
92
|
+
id: eecd837c
|
|
93
|
+
type: image/jpeg
|
|
94
|
+
created: 1698004822358
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
If the `octets:store` directive contains a `workflow`, the response
|
|
98
|
+
is [multipart](protocol.md#multipart-types).
|
|
99
|
+
The first part represents the created Entry, which is sent immediately after the BLOB is stored,
|
|
100
|
+
while subsequent parts are results from the workflow endpoints, sent as soon as they are available.
|
|
101
|
+
|
|
102
|
+
In case a workflow endpoint returns an `Error`, the error part is sent, and the response is closed.
|
|
103
|
+
Error's properties are added to the error part, among with the `step` identifier.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
201 Created
|
|
107
|
+
content-type: multipart/yaml; boundary=cut
|
|
108
|
+
|
|
109
|
+
--cut
|
|
110
|
+
id: eecd837c
|
|
111
|
+
type: image/jpeg
|
|
112
|
+
created: 1698004822358
|
|
113
|
+
--cut
|
|
114
|
+
optimize: null
|
|
115
|
+
--cut
|
|
116
|
+
error:
|
|
117
|
+
step: resize
|
|
118
|
+
code: TOO_SMALL
|
|
119
|
+
message: Image is too small
|
|
120
|
+
--cut--
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## `octets:fetch`
|
|
124
|
+
|
|
125
|
+
Fetches the content of a stored BLOB corresponding to the request path, and returns it as the
|
|
126
|
+
response body with the corresponding `content-type`, `content-length`
|
|
127
|
+
and `etag` ([conditional GET](https://datatracker.ietf.org/doc/html/rfc2616#section-9.3) is
|
|
128
|
+
also supported).
|
|
129
|
+
The `accept` request header is disregarded.
|
|
130
|
+
|
|
131
|
+
The value of the directive is an object with the following properties:
|
|
132
|
+
|
|
133
|
+
- `meta`: `boolean` indicating whether an Entry is accessible.
|
|
134
|
+
Defaults to `false`.
|
|
135
|
+
- `blob`: `boolean` indicating whether the original BLOB is accessible,
|
|
136
|
+
[BLOB variant](/extensions/storages/readme.md#async-fetchpath-string-maybereadable) must be
|
|
137
|
+
specified in the path otherwise.
|
|
138
|
+
Defaults to `true`.
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
/images:
|
|
142
|
+
octets:context: images
|
|
143
|
+
/*:
|
|
144
|
+
GET:
|
|
145
|
+
octets:fetch:
|
|
146
|
+
blob: false # prevent access to the original BLOB
|
|
147
|
+
meta: true # allow access to an Entry
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
To access an Entry, the request path must be suffixed with `:meta`:
|
|
151
|
+
|
|
152
|
+
```http
|
|
153
|
+
GET /images/eecd837c:meta HTTP/1.1
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The `octets:fetch: ~` declaration is equivalent to defaults.
|
|
157
|
+
|
|
158
|
+
## `octets:list`
|
|
159
|
+
|
|
160
|
+
Lists the entries stored under the request path.
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
/images:
|
|
164
|
+
octets:context: images
|
|
165
|
+
GET:
|
|
166
|
+
octets:list: ~
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Responds with a list of entry identifiers.
|
|
170
|
+
|
|
171
|
+
## `octets:delete`
|
|
172
|
+
|
|
173
|
+
Delete the entry corresponding to the request path.
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
176
|
+
/images:
|
|
177
|
+
octets:context: images
|
|
178
|
+
DELETE:
|
|
179
|
+
octets:delete: ~
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## `octets:permute`
|
|
183
|
+
|
|
184
|
+
Performs
|
|
185
|
+
a [permutation](/extensions/storages/readme.md#async-permutepath-string-ids-string-maybevoid) on the
|
|
186
|
+
entries
|
|
187
|
+
under the request path.
|
|
188
|
+
|
|
189
|
+
```yaml
|
|
190
|
+
/images:
|
|
191
|
+
octets:context: images
|
|
192
|
+
PUT:
|
|
193
|
+
octets:permute: ~
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The request body must be a list of entry identifiers.
|
|
@@ -4,15 +4,59 @@
|
|
|
4
4
|
|
|
5
5
|
The following media types are supported for both requests and responses:
|
|
6
6
|
|
|
7
|
-
- `application/json`
|
|
8
|
-
- `application/yaml` using [js-yaml](https://github.com/nodeca/js-yaml)
|
|
9
7
|
- `application/msgpack` using [msgpackr](https://github.com/kriszyp/msgpackr)
|
|
8
|
+
- `application/yaml` using [js-yaml](https://github.com/nodeca/js-yaml)
|
|
9
|
+
- `application/json`
|
|
10
10
|
- `text/plain`
|
|
11
11
|
|
|
12
12
|
The response format is determined by content negotiation
|
|
13
13
|
using [negotiator](https://github.com/jshttp/negotiator).
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
```http
|
|
16
|
+
GET / HTTP/1.1
|
|
17
|
+
accept: application/yaml
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
200 OK
|
|
22
|
+
content-type: application/yaml
|
|
23
|
+
|
|
24
|
+
foo: bar
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Multipart types
|
|
28
|
+
|
|
29
|
+
Multipart responses are endoded using content negotiation,
|
|
30
|
+
and the `content-type` of the response is set to one of the custom `multipart/` subtypes, corresponding to the type of
|
|
31
|
+
the parts:
|
|
32
|
+
|
|
33
|
+
| Response type | Part type |
|
|
34
|
+
|---------------------|-----------------------|
|
|
35
|
+
| `multipart/msgpack` | `application/msgpack` |
|
|
36
|
+
| `multipart/yaml` | `application/yaml` |
|
|
37
|
+
| `multipart/json` | `application/json` |
|
|
38
|
+
| `multipart/text` | `text/plain` |
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
GET /stream/ HTTP/1.1
|
|
44
|
+
accept: application/yaml
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
200 OK
|
|
49
|
+
content-type: multipart/yaml; boundary=cut
|
|
50
|
+
|
|
51
|
+
--cut
|
|
52
|
+
foo: bar
|
|
53
|
+
--cut
|
|
54
|
+
baz: qux
|
|
55
|
+
--cut--
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
See also:
|
|
16
59
|
|
|
17
|
-
|
|
18
|
-
|
|
60
|
+
- [Multipart Content-Type](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html) at W3C
|
|
61
|
+
- [Content-Type: multipart](https://learn.microsoft.com/en-us/previous-versions/office/developer/exchange-server-2010/aa493937(v=exchg.140))
|
|
62
|
+
at Microsoft
|
package/features/access.feature
CHANGED
package/features/errors.feature
CHANGED
|
@@ -191,3 +191,21 @@ Feature: Errors
|
|
|
191
191
|
| debug | response |
|
|
192
192
|
| false | content-length: 0 |
|
|
193
193
|
| true | Error: Broken! |
|
|
194
|
+
|
|
195
|
+
Scenario: Not acceptable request
|
|
196
|
+
Given the annotation:
|
|
197
|
+
"""yaml
|
|
198
|
+
/:
|
|
199
|
+
GET:
|
|
200
|
+
anonymous: true
|
|
201
|
+
dev:stub: hello
|
|
202
|
+
"""
|
|
203
|
+
When the following request is received:
|
|
204
|
+
"""
|
|
205
|
+
GET / HTTP/1.1
|
|
206
|
+
accept: image/jpeg
|
|
207
|
+
"""
|
|
208
|
+
Then the following reply is sent:
|
|
209
|
+
"""
|
|
210
|
+
406 Not Acceptable
|
|
211
|
+
"""
|
|
@@ -181,6 +181,7 @@ Feature: Basic authentication
|
|
|
181
181
|
When the following request is received:
|
|
182
182
|
"""
|
|
183
183
|
POST /identity/basic/ HTTP/1.1
|
|
184
|
+
accept: application/yaml
|
|
184
185
|
content-type: application/yaml
|
|
185
186
|
|
|
186
187
|
username: root
|
|
@@ -224,6 +225,7 @@ Feature: Basic authentication
|
|
|
224
225
|
"""
|
|
225
226
|
PATCH /identity/basic/${{ id }}/ HTTP/1.1
|
|
226
227
|
authorization: Token ${{ token }}
|
|
228
|
+
accept: application/yaml
|
|
227
229
|
content-type: application/yaml
|
|
228
230
|
|
|
229
231
|
username: admin
|