@toa.io/extensions.exposition 0.20.0-dev.8 → 0.20.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.
Files changed (191) hide show
  1. package/components/context.toa.yaml +15 -0
  2. package/components/identity.bans/manifest.toa.yaml +18 -0
  3. package/components/identity.basic/events/principal.js +9 -0
  4. package/components/identity.basic/manifest.toa.yaml +50 -0
  5. package/components/identity.basic/source/authenticate.ts +29 -0
  6. package/components/identity.basic/source/create.ts +19 -0
  7. package/components/identity.basic/source/transit.ts +64 -0
  8. package/components/identity.basic/source/types.ts +42 -0
  9. package/components/identity.basic/tsconfig.json +9 -0
  10. package/components/identity.roles/manifest.toa.yaml +31 -0
  11. package/components/identity.roles/source/list.ts +7 -0
  12. package/components/identity.roles/source/principal.ts +20 -0
  13. package/components/identity.roles/tsconfig.json +9 -0
  14. package/components/identity.tokens/manifest.toa.yaml +39 -0
  15. package/components/identity.tokens/receivers/identity.bans.updated.js +3 -0
  16. package/components/identity.tokens/source/authenticate.test.ts +56 -0
  17. package/components/identity.tokens/source/authenticate.ts +38 -0
  18. package/components/identity.tokens/source/decrypt.test.ts +59 -0
  19. package/components/identity.tokens/source/decrypt.ts +25 -0
  20. package/components/identity.tokens/source/encrypt.test.ts +35 -0
  21. package/components/identity.tokens/source/encrypt.ts +25 -0
  22. package/components/identity.tokens/source/revoke.ts +5 -0
  23. package/components/identity.tokens/source/types.ts +48 -0
  24. package/components/identity.tokens/tsconfig.json +9 -0
  25. package/cucumber.js +9 -0
  26. package/documentation/.assets/ia3-dark.jpg +0 -0
  27. package/documentation/.assets/ia3-light.jpg +0 -0
  28. package/documentation/.assets/overview-dark.jpg +0 -0
  29. package/documentation/.assets/overview-light.jpg +0 -0
  30. package/documentation/.assets/role-scopes-dark.jpg +0 -0
  31. package/documentation/.assets/role-scopes-light.jpg +0 -0
  32. package/documentation/.assets/rtd-dark.jpg +0 -0
  33. package/documentation/.assets/rtd-light.jpg +0 -0
  34. package/documentation/access.md +256 -0
  35. package/documentation/components.md +276 -0
  36. package/documentation/identity.md +156 -0
  37. package/documentation/notes/sse.md +71 -0
  38. package/documentation/protocol.md +18 -0
  39. package/documentation/query.md +226 -0
  40. package/documentation/tree.md +169 -0
  41. package/features/access.feature +448 -0
  42. package/features/annotation.feature +30 -0
  43. package/features/body.feature +45 -0
  44. package/features/directives.feature +56 -0
  45. package/features/dynamic.feature +99 -0
  46. package/features/errors.feature +193 -0
  47. package/features/identity.basic.feature +276 -0
  48. package/features/identity.feature +61 -0
  49. package/features/identity.roles.feature +51 -0
  50. package/features/identity.tokens.feature +119 -0
  51. package/features/queries.feature +214 -0
  52. package/features/routes.feature +49 -0
  53. package/features/steps/Common.ts +10 -0
  54. package/features/steps/Components.ts +43 -0
  55. package/features/steps/Database.ts +58 -0
  56. package/features/steps/Gateway.ts +113 -0
  57. package/features/steps/HTTP.ts +71 -0
  58. package/features/steps/Parameters.ts +12 -0
  59. package/features/steps/Workspace.ts +40 -0
  60. package/features/steps/components/echo/manifest.toa.yaml +9 -0
  61. package/features/steps/components/echo/operations/affect.js +7 -0
  62. package/features/steps/components/echo/operations/compute.js +7 -0
  63. package/features/steps/components/greeter/manifest.toa.yaml +5 -0
  64. package/features/steps/components/greeter/operations/greet.js +7 -0
  65. package/features/steps/components/pots/manifest.toa.yaml +20 -0
  66. package/features/steps/components/sequences/manifest.toa.yaml +10 -0
  67. package/features/steps/components/sequences/operations/numbers.js +7 -0
  68. package/features/steps/components/sequences/operations/tokens.js +16 -0
  69. package/features/steps/components/users/manifest.toa.yaml +11 -0
  70. package/features/steps/tsconfig.json +9 -0
  71. package/features/streams.feature +26 -0
  72. package/package.json +32 -17
  73. package/readme.md +183 -0
  74. package/schemas/annotation.cos.yaml +5 -0
  75. package/schemas/directive.cos.yaml +3 -0
  76. package/schemas/method.cos.yaml +8 -0
  77. package/schemas/node.cos.yaml +5 -0
  78. package/schemas/query.cos.yaml +17 -0
  79. package/schemas/querystring.cos.yaml +5 -0
  80. package/schemas/range.cos.yaml +2 -0
  81. package/schemas/route.cos.yaml +2 -0
  82. package/source/Annotation.ts +7 -0
  83. package/source/Branch.ts +8 -0
  84. package/source/Composition.ts +57 -0
  85. package/source/Context.ts +6 -0
  86. package/source/Directive.test.ts +91 -0
  87. package/source/Directive.ts +120 -0
  88. package/source/Endpoint.ts +59 -0
  89. package/source/Factory.ts +51 -0
  90. package/source/Gateway.ts +93 -0
  91. package/source/HTTP/Server.fixtures.ts +45 -0
  92. package/source/HTTP/Server.test.ts +221 -0
  93. package/source/HTTP/Server.ts +135 -0
  94. package/source/HTTP/exceptions.ts +77 -0
  95. package/source/HTTP/formats/index.ts +19 -0
  96. package/source/HTTP/formats/json.ts +13 -0
  97. package/source/HTTP/formats/msgpack.ts +10 -0
  98. package/source/HTTP/formats/text.ts +9 -0
  99. package/source/HTTP/formats/yaml.ts +14 -0
  100. package/source/HTTP/index.ts +3 -0
  101. package/source/HTTP/messages.test.ts +116 -0
  102. package/source/HTTP/messages.ts +89 -0
  103. package/source/Mapping.ts +51 -0
  104. package/source/Query.test.ts +37 -0
  105. package/source/Query.ts +105 -0
  106. package/source/RTD/Context.ts +16 -0
  107. package/source/RTD/Directives.ts +9 -0
  108. package/source/RTD/Endpoint.ts +11 -0
  109. package/source/RTD/Match.ts +16 -0
  110. package/source/RTD/Method.ts +24 -0
  111. package/source/RTD/Node.ts +85 -0
  112. package/source/RTD/Route.ts +59 -0
  113. package/source/RTD/Tree.ts +54 -0
  114. package/source/RTD/factory.ts +47 -0
  115. package/source/RTD/index.ts +8 -0
  116. package/source/RTD/segment.test.ts +32 -0
  117. package/source/RTD/segment.ts +25 -0
  118. package/source/RTD/syntax/index.ts +2 -0
  119. package/source/RTD/syntax/parse.test.ts +188 -0
  120. package/source/RTD/syntax/parse.ts +153 -0
  121. package/source/RTD/syntax/types.ts +48 -0
  122. package/source/Remotes.test.ts +42 -0
  123. package/source/Remotes.ts +22 -0
  124. package/source/Tenant.ts +38 -0
  125. package/source/deployment.ts +49 -0
  126. package/source/directives/auth/Anonymous.ts +14 -0
  127. package/source/directives/auth/Echo.ts +12 -0
  128. package/source/directives/auth/Family.ts +145 -0
  129. package/source/directives/auth/Id.ts +19 -0
  130. package/source/directives/auth/Incept.ts +42 -0
  131. package/source/directives/auth/Role.test.ts +62 -0
  132. package/source/directives/auth/Role.ts +56 -0
  133. package/source/directives/auth/Rule.ts +28 -0
  134. package/source/directives/auth/Scheme.ts +26 -0
  135. package/source/directives/auth/index.ts +3 -0
  136. package/source/directives/auth/schemes.ts +8 -0
  137. package/source/directives/auth/split.ts +15 -0
  138. package/source/directives/auth/types.ts +37 -0
  139. package/source/directives/dev/Family.ts +36 -0
  140. package/source/directives/dev/Stub.ts +14 -0
  141. package/source/directives/dev/Throw.ts +14 -0
  142. package/source/directives/dev/index.ts +3 -0
  143. package/source/directives/dev/types.ts +5 -0
  144. package/source/directives/index.ts +5 -0
  145. package/source/discovery.ts +1 -0
  146. package/source/exceptions.ts +17 -0
  147. package/source/index.test.ts +9 -0
  148. package/source/index.ts +6 -0
  149. package/source/manifest.test.ts +59 -0
  150. package/source/manifest.ts +48 -0
  151. package/source/root.ts +38 -0
  152. package/source/schemas.ts +9 -0
  153. package/tsconfig.json +12 -0
  154. package/src/.manifest/index.js +0 -7
  155. package/src/.manifest/normalize.js +0 -58
  156. package/src/.manifest/schema.yaml +0 -71
  157. package/src/.manifest/validate.js +0 -17
  158. package/src/constants.js +0 -3
  159. package/src/deployment.js +0 -23
  160. package/src/exposition.js +0 -68
  161. package/src/factory.js +0 -76
  162. package/src/index.js +0 -9
  163. package/src/manifest.js +0 -12
  164. package/src/query/criteria.js +0 -55
  165. package/src/query/enum.js +0 -35
  166. package/src/query/index.js +0 -17
  167. package/src/query/query.js +0 -60
  168. package/src/query/range.js +0 -28
  169. package/src/query/sort.js +0 -19
  170. package/src/remote.js +0 -88
  171. package/src/server.js +0 -83
  172. package/src/tenant.js +0 -29
  173. package/src/translate/etag.js +0 -14
  174. package/src/translate/index.js +0 -7
  175. package/src/translate/request.js +0 -68
  176. package/src/translate/response.js +0 -62
  177. package/src/tree.js +0 -109
  178. package/test/manifest.normalize.fixtures.js +0 -37
  179. package/test/manifest.normalize.test.js +0 -37
  180. package/test/manifest.validate.test.js +0 -40
  181. package/test/query.range.test.js +0 -18
  182. package/test/tree.fixtures.js +0 -21
  183. package/test/tree.test.js +0 -44
  184. package/types/annotations.d.ts +0 -10
  185. package/types/declarations.d.ts +0 -31
  186. package/types/exposition.d.ts +0 -13
  187. package/types/http.d.ts +0 -13
  188. package/types/query.d.ts +0 -16
  189. package/types/remote.d.ts +0 -19
  190. package/types/server.d.ts +0 -13
  191. package/types/tree.d.ts +0 -33
@@ -0,0 +1,15 @@
1
+ # used for development only
2
+
3
+ name: exposition
4
+ packages: '*'
5
+ registry: localhost
6
+
7
+ amqp: amqp://localhost
8
+ mongodb: mongodb://localhost
9
+
10
+ configuration:
11
+ identity.tokens:
12
+ key0: k3.local.pIZT8-9Fa6U_QtfQHOSStfGtmyzPINyKQq2Xk-hd7vA
13
+
14
+ exposition:
15
+ debug: true
@@ -0,0 +1,18 @@
1
+ namespace: identity
2
+ name: bans
3
+
4
+ entity:
5
+ schema:
6
+ banned: false
7
+ initialized: true
8
+
9
+ operations:
10
+ assign:
11
+ input:
12
+ banned: true
13
+
14
+ exposition:
15
+ isolated: true
16
+ /:id:
17
+ auth:role: system:identity:bans
18
+ PUT: assign
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ exports.condition = function (event, context) {
4
+ return event.state.username === context.configuration.principal
5
+ }
6
+
7
+ exports.payload = function (event) {
8
+ return { id: event.state.id }
9
+ }
@@ -0,0 +1,50 @@
1
+ namespace: identity
2
+ name: basic
3
+
4
+ entity:
5
+ schema:
6
+ username*: string
7
+ password*: string
8
+ initialized: true
9
+
10
+ operations:
11
+ transit:
12
+ concurrency: retry
13
+ input:
14
+ username: string
15
+ password: string
16
+ incept:
17
+ forward: transit
18
+ query: false
19
+ input:
20
+ username*: string
21
+ password*: string
22
+ create:
23
+ input:
24
+ id*: string
25
+ credentials*: string
26
+ output:
27
+ id: string
28
+ authenticate:
29
+ input: string
30
+ output:
31
+ identity:
32
+ id: string
33
+
34
+ configuration:
35
+ rounds: 10
36
+ pepper: ''
37
+ principal: string
38
+ username+: ^\S{1,16}$
39
+ password+: ^\S{8,32}$
40
+
41
+ exposition:
42
+ isolated: true
43
+ /:
44
+ anonymous: true
45
+ POST: incept
46
+ /:id:
47
+ auth:role: system:identity:basic
48
+ auth:scheme: basic
49
+ auth:id: id
50
+ PATCH: transit
@@ -0,0 +1,29 @@
1
+ import { atob } from 'buffer'
2
+ import { compare } from 'bcryptjs'
3
+ import { type Query } from '@toa.io/types'
4
+ import { Nope, type Nopeable } from 'nopeable'
5
+ import { type Context } from './types'
6
+
7
+ export async function computation (input: string, context: Context): Promise<Nopeable<Output>> {
8
+ const [username, password] = atob(input).split(':')
9
+ const query: Query = { criteria: `username==${username}` }
10
+ const credentials = await context.local.observe({ query })
11
+
12
+ if (credentials instanceof Nope)
13
+ return credentials
14
+
15
+ if (credentials === null)
16
+ return new Nope('NOT_FOUND')
17
+
18
+ const spicy = password + context.configuration.pepper
19
+ const match = await compare(spicy, credentials.password)
20
+
21
+ if (match) return { identity: { id: credentials.id } }
22
+ else return new Nope('PASSWORD_MISMATCH')
23
+ }
24
+
25
+ interface Output {
26
+ identity: {
27
+ id: string
28
+ }
29
+ }
@@ -0,0 +1,19 @@
1
+ import { type Nopeable } from 'nopeable'
2
+ import { type Context } from './types'
3
+
4
+ export async function effect
5
+ (input: CreateInput, context: Context): Promise<Nopeable<CreateOutput>> {
6
+ const [username, password] = atob(input.credentials).split(':')
7
+ const request = { input: { username, password }, query: { id: input.id } }
8
+
9
+ return await context.local.transit(request)
10
+ }
11
+
12
+ interface CreateInput {
13
+ id: string
14
+ credentials: string
15
+ }
16
+
17
+ interface CreateOutput {
18
+ id: string
19
+ }
@@ -0,0 +1,64 @@
1
+ import { genSalt, hash } from 'bcryptjs'
2
+ import { type Operation } from '@toa.io/types'
3
+ import { Nope, type Nopeable } from 'nopeable'
4
+ import { type Context, type Entity, type TransitInput, type TransitOutput } from './types'
5
+
6
+ export class Transition implements Operation {
7
+ private rounds: number = 10
8
+ private pepper: string = ''
9
+ private principal?: string
10
+ private tokens: Tokens = undefined as unknown as Tokens
11
+ private usernameRx: RegExp[] = []
12
+ private passwrodRx: RegExp[] = []
13
+
14
+ public mount (context: Context): void {
15
+ this.rounds = context.configuration.rounds
16
+ this.pepper = context.configuration.pepper
17
+ this.principal = context.configuration.principal
18
+ this.tokens = context.remote.identity.tokens
19
+
20
+ this.usernameRx = toRx(context.configuration.username)
21
+ this.passwrodRx = toRx(context.configuration.password)
22
+ }
23
+
24
+ public async execute (input: TransitInput, object: Entity): Promise<Nopeable<TransitOutput>> {
25
+ const existent = object._version !== 0
26
+
27
+ if (existent)
28
+ await this.tokens.revoke({ query: { id: object.id } })
29
+
30
+ if (input.username !== undefined) {
31
+ if (existent && object.username === this.principal)
32
+ return new Nope('PRINCIPAL_LOCKED', 'Principal username cannot be changed.')
33
+
34
+ if (invalid(input.username, this.usernameRx))
35
+ return new Nope('INVALID_USERNAME', 'Username is not meeting the requirements.')
36
+
37
+ object.username = input.username
38
+ }
39
+
40
+ if (input.password !== undefined) {
41
+ if (invalid(input.password, this.passwrodRx))
42
+ return new Nope('INVALID_PASSWORD', 'Password is not meeting the requirements.')
43
+
44
+ const salt = await genSalt(this.rounds)
45
+ const spicy = input.password + this.pepper
46
+
47
+ object.password = await hash(spicy, salt)
48
+ }
49
+
50
+ return { id: object.id }
51
+ }
52
+ }
53
+
54
+ function toRx (input: string | string[]): RegExp[] {
55
+ const expressions = typeof input === 'string' ? [input] : input
56
+
57
+ return expressions.map((expression) => new RegExp(expression))
58
+ }
59
+
60
+ function invalid (value: string, expressions: RegExp[]): boolean {
61
+ return expressions.some((expression) => !expression.test(value))
62
+ }
63
+
64
+ type Tokens = Context['remote']['identity']['tokens']
@@ -0,0 +1,42 @@
1
+ import { type Call, type Observation, type Query } from '@toa.io/types'
2
+
3
+ export interface Context {
4
+ local: {
5
+ observe: Observation<Entity>
6
+ transit: Call<TransitOutput, TransitInput>
7
+ }
8
+ remote: {
9
+ identity: {
10
+ tokens: {
11
+ revoke: Call<void, IdentityTokensRevokeInput>
12
+ }
13
+ }
14
+ }
15
+ configuration: {
16
+ readonly rounds: number
17
+ readonly pepper: string
18
+ readonly principal?: string
19
+ readonly username: string | string[]
20
+ readonly password: string | string[]
21
+ }
22
+ }
23
+
24
+ export interface Entity {
25
+ readonly id: string
26
+ readonly _version: number
27
+ username: string
28
+ password: string
29
+ }
30
+
31
+ export interface TransitInput {
32
+ username?: string
33
+ password?: string
34
+ }
35
+
36
+ export interface TransitOutput {
37
+ id: string
38
+ }
39
+
40
+ interface IdentityTokensRevokeInput {
41
+ query: Query
42
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./operations",
5
+ },
6
+ "include": [
7
+ "source"
8
+ ]
9
+ }
@@ -0,0 +1,31 @@
1
+ namespace: identity
2
+ name: roles
3
+
4
+ entity:
5
+ schema:
6
+ identity*: string
7
+ role*: string
8
+
9
+ operations:
10
+ transit:
11
+ query: false
12
+ input:
13
+ identity*: string
14
+ role*: string
15
+ list:
16
+ output: [string]
17
+ principal:
18
+ input:
19
+ id: string
20
+
21
+ receivers:
22
+ identity.basic.principal: principal
23
+
24
+ exposition:
25
+ isolated: true
26
+ /:identity:
27
+ auth:role: system:identity:roles
28
+ POST: transit
29
+ GET:
30
+ auth:id: identity
31
+ endpoint: list
@@ -0,0 +1,7 @@
1
+ export function observation (_: unknown, objects: Entity[]): string[] {
2
+ return objects.map(({ role }) => role)
3
+ }
4
+
5
+ interface Entity {
6
+ role: string
7
+ }
@@ -0,0 +1,20 @@
1
+ import { type Call } from '@toa.io/types'
2
+
3
+ export async function effect (input: Identity, context: Context): Promise<void> {
4
+ await context.local.transit({ input: { identity: input.id, role: 'system' } })
5
+ }
6
+
7
+ interface Identity {
8
+ id: string
9
+ }
10
+
11
+ export interface Context {
12
+ local: {
13
+ transit: Call<void, TransitInput>
14
+ }
15
+ }
16
+
17
+ interface TransitInput {
18
+ identity: string
19
+ role: string
20
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./operations",
5
+ },
6
+ "include": [
7
+ "source"
8
+ ]
9
+ }
@@ -0,0 +1,39 @@
1
+ namespace: identity
2
+ name: tokens
3
+
4
+ entity:
5
+ schema:
6
+ revokedAt: number # timestamp
7
+ initialized: true
8
+
9
+ operations:
10
+ encrypt:
11
+ input:
12
+ identity*: &identity
13
+ id: string
14
+ ...: true
15
+ lifetime: number # seconds
16
+ output: string
17
+ decrypt:
18
+ input: string
19
+ output:
20
+ identity: *identity
21
+ iat: string
22
+ exp: string
23
+ refresh: boolean
24
+ authenticate:
25
+ input: string
26
+ output:
27
+ identity: *identity
28
+ refresh: boolean
29
+ revoke:
30
+ concurrency: retry
31
+
32
+ receivers:
33
+ identity.bans.updated: revoke
34
+
35
+ configuration:
36
+ key0*: string
37
+ key1: string
38
+ lifetime: 2592000 # seconds, 30 days
39
+ refresh: 600 # seconds, 10 minutes
@@ -0,0 +1,3 @@
1
+ 'use strict'
2
+
3
+ exports.request = (payload) => ({ query: { id: payload.id } })
@@ -0,0 +1,56 @@
1
+ import { generate } from 'randomstring'
2
+ import { type Configuration, type Context, type DecryptOutput, type Identity } from './types'
3
+ import { Computation as Authenticate } from './authenticate'
4
+
5
+ let configuration: Configuration
6
+ let context: Context
7
+ let output: DecryptOutput
8
+ let authenticate: Authenticate
9
+
10
+ const identity: Identity = { id: generate() }
11
+
12
+ beforeEach(() => {
13
+ configuration = {
14
+ key0: 'k3.local.m28p8SrbS467t-2IUjQuSOqmjvi24TbXhyjAW_dOrog',
15
+ lifetime: 2592000,
16
+ refresh: 600
17
+ }
18
+
19
+ context = {
20
+ configuration,
21
+ local: {
22
+ decrypt: jest.fn(async () => (output)),
23
+ observe: jest.fn(async () => null)
24
+ }
25
+ }
26
+
27
+ authenticate = new Authenticate()
28
+ authenticate.mount(context)
29
+ })
30
+
31
+ it.each([
32
+ [true, -50],
33
+ [false, +50]
34
+ ])('should mark as stale: %s', async (expected: boolean, shift: number) => {
35
+ const now = Date.now()
36
+ const iat = new Date(now - configuration.refresh * 1000 + shift).toISOString()
37
+ const exp = new Date(now + 1000).toISOString()
38
+
39
+ output = { identity, exp, iat, refresh: false }
40
+
41
+ const result = await authenticate.execute('')
42
+
43
+ expect(result).toEqual({ identity, refresh: expected })
44
+ })
45
+
46
+ it.each([true, false])('should return stale: %s',
47
+ async (refresh) => {
48
+ const iat = new Date().toISOString()
49
+ const exp = new Date(Date.now() + 1000).toISOString()
50
+
51
+ output = { identity, exp, iat, refresh }
52
+
53
+ const result = await authenticate.execute('')
54
+
55
+ expect(result).toEqual({ identity, refresh })
56
+ })
@@ -0,0 +1,38 @@
1
+ import { Nope, type Nopeable } from 'nopeable'
2
+ import { type Operation } from '@toa.io/types'
3
+ import { type AuthenticateOutput, type Context } from './types'
4
+
5
+ export class Computation implements Operation {
6
+ private refresh: number = 0
7
+ private decrypt: Context['local']['decrypt'] = undefined as unknown as Context['local']['decrypt']
8
+ private observe: Context['local']['observe'] = undefined as unknown as Context['local']['observe']
9
+
10
+ public mount (context: Context): void {
11
+ this.refresh = context.configuration.refresh * 1000
12
+ this.decrypt = context.local.decrypt
13
+ this.observe = context.local.observe
14
+ }
15
+
16
+ public async execute (token: string): Promise<Nopeable<AuthenticateOutput>> {
17
+ const claim = await this.decrypt({ input: token })
18
+
19
+ if (claim instanceof Nope)
20
+ return claim
21
+
22
+ const identity = claim.identity
23
+ const iat = new Date(claim.iat).getTime()
24
+ const transient = claim.exp !== undefined
25
+ const stale = transient && (iat + this.refresh < Date.now())
26
+
27
+ if (stale) {
28
+ const revocation = await this.observe({ query: { id: identity.id } })
29
+
30
+ if (revocation !== null && iat < revocation.revokedAt)
31
+ return new Nope('REVOKED')
32
+ }
33
+
34
+ const refresh = stale || claim.refresh
35
+
36
+ return { identity, refresh }
37
+ }
38
+ }
@@ -0,0 +1,59 @@
1
+ import { generate } from 'randomstring'
2
+ import { Effect as Encrypt } from './encrypt'
3
+ import { computation as decrypt } from './decrypt'
4
+ import { type Configuration, type Context, type Identity } from './types'
5
+
6
+ let configuration: Configuration
7
+ let context: Context
8
+ let encrypt: Encrypt
9
+
10
+ beforeEach(() => {
11
+ configuration = {
12
+ key0: 'k3.local.m28p8SrbS467t-2IUjQuSOqmjvi24TbXhyjAW_dOrog',
13
+ key1: 'k3.local.-498jfWenrZH-Dqw3-zQJih_hKzDgBgUMfe37OCqSOA',
14
+ lifetime: 1000,
15
+ refresh: 500
16
+ }
17
+
18
+ context = { configuration } as unknown as Context
19
+
20
+ encrypt = new Encrypt()
21
+ encrypt.mount(context)
22
+ })
23
+
24
+ it('should decrypt', async () => {
25
+ const identity: Identity = { id: generate() }
26
+ const lifetime = 100
27
+
28
+ const reply = await encrypt.execute({ identity, lifetime })
29
+
30
+ if (reply === undefined)
31
+ throw new Error('?')
32
+
33
+ const decrypted = await decrypt(reply, context)
34
+
35
+ expect(decrypted).toMatchObject({ identity, refresh: false })
36
+ })
37
+
38
+ it('should decrypt with key1', async () => {
39
+ const k1context = {
40
+ configuration: {
41
+ key0: configuration.key1
42
+ }
43
+ } as unknown as Context
44
+
45
+ encrypt = new Encrypt()
46
+ encrypt.mount(k1context)
47
+
48
+ const identity: Identity = { id: generate() }
49
+ const lifetime = 100
50
+
51
+ const encrypted = await encrypt.execute({ identity, lifetime })
52
+
53
+ if (encrypted === undefined)
54
+ throw new Error('?')
55
+
56
+ const decrypted = await decrypt(encrypted, context)
57
+
58
+ expect(decrypted).toMatchObject({ identity, refresh: true })
59
+ })
@@ -0,0 +1,25 @@
1
+ import { V3 } from 'paseto'
2
+ import { Nope, type Nopeable } from 'nopeable'
3
+ import { type Context, type Claim, type DecryptOutput } from './types'
4
+
5
+ export async function computation (token: string, context: Context):
6
+ Promise<Nopeable<DecryptOutput>> {
7
+ let refresh = false
8
+ let claim = await decrypt(token, context.configuration.key0)
9
+
10
+ if (claim === null && context.configuration.key1 !== undefined) {
11
+ refresh = true
12
+ claim = await decrypt(token, context.configuration.key1)
13
+ }
14
+
15
+ if (claim === null) return new Nope('INVALID_TOKEN', 'Invalid token')
16
+ else return { identity: claim.identity, iat: claim.iat, exp: claim.exp, refresh }
17
+ }
18
+
19
+ async function decrypt (token: string, key: string): Promise<Claim | null> {
20
+ try {
21
+ return await V3.decrypt<Claim>(token, key)
22
+ } catch {
23
+ return null
24
+ }
25
+ }
@@ -0,0 +1,35 @@
1
+ import { generate } from 'randomstring'
2
+ import { timeout } from '@toa.io/generic'
3
+ import { Effect as Encrypt } from './encrypt'
4
+ import { computation as decrypt } from './decrypt'
5
+ import { type Context, type Identity } from './types'
6
+
7
+ let encrypt: Encrypt
8
+
9
+ const context: Context = {} as unknown as Context
10
+
11
+ beforeEach(() => {
12
+ context.configuration = {
13
+ key0: 'k3.local.m28p8SrbS467t-2IUjQuSOqmjvi24TbXhyjAW_dOrog',
14
+ lifetime: 1000,
15
+ refresh: 2
16
+ }
17
+
18
+ encrypt = new Encrypt()
19
+ encrypt.mount(context)
20
+ })
21
+
22
+ it('should encrypt with given lifetime', async () => {
23
+ const identity: Identity = { id: generate() }
24
+ const lifetime = 0.1
25
+ const encrypted = await encrypt.execute({ identity, lifetime })
26
+
27
+ if (encrypted === undefined)
28
+ throw new Error('?')
29
+
30
+ await expect(decrypt(encrypted, context)).resolves.toMatchObject({ identity })
31
+
32
+ await timeout(lifetime * 1000)
33
+
34
+ await expect(decrypt(encrypted, context)).resolves.toMatchObject({ code: 'INVALID_TOKEN' })
35
+ })
@@ -0,0 +1,25 @@
1
+ import { V3 } from 'paseto'
2
+ import { type Operation } from '@toa.io/types'
3
+ import { type Claim, type Context, type EncryptInput } from './types'
4
+
5
+ export class Effect implements Operation {
6
+ private key: string = ''
7
+ private lifetime: number = 0
8
+
9
+ public mount (context: Context): void {
10
+ this.key = context.configuration.key0
11
+ this.lifetime = context.configuration.lifetime * 1000
12
+ }
13
+
14
+ public async execute (input: EncryptInput): Promise<string> {
15
+ const lifetime = input.lifetime === undefined ? this.lifetime : input.lifetime * 1000
16
+
17
+ const exp = lifetime === 0
18
+ ? undefined
19
+ : new Date(Date.now() + lifetime).toISOString()
20
+
21
+ const payload: Partial<Claim> = { identity: input.identity, exp }
22
+
23
+ return await V3.encrypt(payload, this.key)
24
+ }
25
+ }
@@ -0,0 +1,5 @@
1
+ import { type Entity } from './types'
2
+
3
+ export function transition (_: never, object: Entity): void {
4
+ object.revokedAt = Date.now()
5
+ }
@@ -0,0 +1,48 @@
1
+ import { type Call, type Observation } from '@toa.io/types'
2
+
3
+ export interface Context {
4
+ local: {
5
+ observe: Observation<Entity>
6
+ decrypt: Call<DecryptOutput, string>
7
+ }
8
+ configuration: Configuration
9
+ }
10
+
11
+ export interface Configuration {
12
+ readonly key0: string
13
+ readonly key1?: string
14
+ readonly lifetime: number
15
+ readonly refresh: number
16
+ }
17
+
18
+ export interface Entity {
19
+ identity: string
20
+ revokedAt: number
21
+ }
22
+
23
+ export interface Identity extends Record<string, any> {
24
+ id: string
25
+ }
26
+
27
+ export interface AuthenticateOutput {
28
+ identity: Identity
29
+ refresh: boolean
30
+ }
31
+
32
+ export interface EncryptInput {
33
+ identity: Identity
34
+ lifetime?: number
35
+ }
36
+
37
+ export interface DecryptOutput {
38
+ identity: Identity
39
+ iat: string
40
+ exp?: string
41
+ refresh: boolean
42
+ }
43
+
44
+ export interface Claim {
45
+ identity: Identity
46
+ iat: string
47
+ exp?: string
48
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./operations",
5
+ },
6
+ "include": [
7
+ "source"
8
+ ]
9
+ }