@toa.io/extensions.exposition 1.0.0-alpha.330 → 1.0.0-alpha.332

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 (53) hide show
  1. package/components/exposition.octets/package.json +2 -2
  2. package/components/identity.basic/manifest.toa.yaml +3 -1
  3. package/components/identity.basic/operations/transit.js +2 -2
  4. package/components/identity.basic/package.json +1 -1
  5. package/components/identity.basic/source/transit.ts +1 -1
  6. package/components/identity.basic/types/toa.d.ts +1 -0
  7. package/components/identity.otp/manifest.toa.yaml +30 -1
  8. package/components/identity.otp/operations/authenticate.js +14 -18
  9. package/components/identity.otp/operations/confirm.js +23 -0
  10. package/components/identity.otp/operations/issue.js +4 -10
  11. package/components/identity.otp/operations/lib/index.js +2 -2
  12. package/components/identity.otp/operations/lib/redeem.js +28 -0
  13. package/components/identity.otp/source/authenticate.ts +14 -23
  14. package/components/identity.otp/source/confirm.ts +41 -0
  15. package/components/identity.otp/source/issue.ts +3 -10
  16. package/components/identity.otp/source/lib/index.ts +1 -0
  17. package/components/identity.otp/source/lib/redeem.ts +49 -0
  18. package/components/identity.otp/types/toa.d.ts +8 -0
  19. package/components/identity.tokens/manifest.toa.yaml +5 -0
  20. package/components/identity.tokens/operations/authenticate.js +10 -3
  21. package/components/identity.tokens/source/authenticate.ts +11 -2
  22. package/components/identity.tokens/types/toa.d.ts +3 -1
  23. package/package.json +9 -9
  24. package/transpiled/directives/auth/Authorization.js +3 -1
  25. package/transpiled/directives/auth/Authorization.js.map +1 -1
  26. package/transpiled/directives/auth/Refresh.d.ts +16 -0
  27. package/transpiled/directives/auth/Refresh.js +44 -0
  28. package/transpiled/directives/auth/Refresh.js.map +1 -0
  29. package/transpiled/root.js +26 -0
  30. package/transpiled/root.js.map +1 -1
  31. package/ui/dist/_app/immutable/chunks/BfkEoJWo.js +3 -0
  32. package/ui/dist/_app/immutable/chunks/C2Pl6xgG.js +90 -0
  33. package/ui/dist/_app/immutable/chunks/{Ct2RV88Z.js → CXABSZYQ.js} +2 -2
  34. package/ui/dist/_app/immutable/chunks/{CpefnqEp.js → MJkgCRen.js} +1 -1
  35. package/ui/dist/_app/immutable/chunks/doOvBfYz.js +1 -0
  36. package/ui/dist/_app/immutable/entry/app.DIki71EZ.js +2 -0
  37. package/ui/dist/_app/immutable/entry/start.DLcriI5g.js +1 -0
  38. package/ui/dist/_app/immutable/nodes/0.Bsma_XlH.js +1 -0
  39. package/ui/dist/_app/immutable/nodes/{1.lNr__ktV.js → 1.CPfTvL2Y.js} +1 -1
  40. package/ui/dist/_app/immutable/nodes/2.DyvXex_x.js +8 -0
  41. package/ui/dist/_app/immutable/nodes/{3.tmoa7-_k.js → 3.ClsZXJWk.js} +1 -1
  42. package/ui/dist/_app/immutable/nodes/{4.C6CClEtZ.js → 4.yPslz9Nb.js} +1 -1
  43. package/ui/dist/_app/immutable/nodes/{5.CX8sVMjt.js → 5.BZxbRAPf.js} +1 -1
  44. package/ui/dist/_app/immutable/nodes/{6.D-kQrGiV.js → 6.J6MdfI1j.js} +1 -1
  45. package/ui/dist/_app/version.json +1 -1
  46. package/ui/dist/index.html +11 -11
  47. package/ui/dist/_app/immutable/chunks/BVDJTrf4.js +0 -3
  48. package/ui/dist/_app/immutable/chunks/B_xKDxop.js +0 -90
  49. package/ui/dist/_app/immutable/chunks/CZaFFKON.js +0 -1
  50. package/ui/dist/_app/immutable/entry/app.CqwgUhww.js +0 -2
  51. package/ui/dist/_app/immutable/entry/start.B46VRLuJ.js +0 -1
  52. package/ui/dist/_app/immutable/nodes/0.D5Wva3zJ.js +0 -1
  53. package/ui/dist/_app/immutable/nodes/2.BRk85phb.js +0 -8
@@ -3,8 +3,8 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@aws-sdk/client-s3": "3.1148.0",
7
- "@aws-sdk/lib-storage": "3.1148.0",
6
+ "@aws-sdk/client-s3": "3.1152.0",
7
+ "@aws-sdk/lib-storage": "3.1152.0",
8
8
  "cloudinary": "2.11.0"
9
9
  }
10
10
  }
@@ -165,8 +165,10 @@ configuration:
165
165
  type: object
166
166
  properties:
167
167
  rounds:
168
+ description: bcrypt cost
168
169
  type: integer
169
- minimum: 1
170
+ minimum: 4
171
+ maximum: 31
170
172
  pepper:
171
173
  description: Hashing pepper, a secret
172
174
  type: string
@@ -46,7 +46,7 @@ export class Transition {
46
46
  if (invalid(input.password, this.passwordRx))
47
47
  return ERR_INVALID_PASSWORD;
48
48
  const spicy = input.password + this.pepper;
49
- object.password = await hash(spicy, this.rounds);
49
+ object.password = await hash(spicy, { cost: this.rounds });
50
50
  }
51
51
  return { id: object.id };
52
52
  }
@@ -61,4 +61,4 @@ const ERR_PRINCIPAL_LOCKED = new Error('PRINCIPAL_LOCKED');
61
61
  const ERR_INVALID_USERNAME = new Error('INVALID_USERNAME');
62
62
  const ERR_INVALID_PASSWORD = new Error('INVALID_PASSWORD');
63
63
  const ERR_EXISTS = new Error('EXISTS');
64
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHJhbnNpdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NvdXJjZS90cmFuc2l0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQTtBQVd0QyxNQUFNLE9BQU8sVUFBVTtJQUNiLE1BQU0sR0FBVyxFQUFFLENBQUE7SUFDbkIsTUFBTSxHQUFXLEVBQUUsQ0FBQTtJQUNuQixTQUFTLENBQVk7SUFDckIsTUFBTSxHQUFXLFNBQThCLENBQUE7SUFDL0MsSUFBSSxHQUFTLFNBQTRCLENBQUE7SUFDekMsVUFBVSxHQUFhLEVBQUUsQ0FBQTtJQUN6QixVQUFVLEdBQWEsRUFBRSxDQUFBO0lBRTFCLEtBQUssQ0FBQyxPQUFnQjtRQUMzQixJQUFJLENBQUMsTUFBTSxHQUFHLE9BQU8sQ0FBQyxhQUFhLENBQUMsTUFBTSxDQUFBO1FBQzFDLElBQUksQ0FBQyxNQUFNLEdBQUcsT0FBTyxDQUFDLGFBQWEsQ0FBQyxNQUFNLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBRSxDQUFBO1FBQzFELElBQUksQ0FBQyxTQUFTLEdBQUcsT0FBTyxDQUFDLGFBQWEsQ0FBQyxTQUFTLENBQUE7UUFDaEQsSUFBSSxDQUFDLE1BQU0sR0FBRyxPQUFPLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUE7UUFDNUMsSUFBSSxDQUFDLElBQUksR0FBRyxPQUFPLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUE7UUFFeEMsSUFBSSxDQUFDLFVBQVUsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQTtRQUN0RCxJQUFJLENBQUMsVUFBVSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFBO0lBQ3hELENBQUM7SUFFTyxNQUFNLENBQUMsTUFBYztRQUMzQixPQUFPLENBQ0wsSUFBSSxDQUFDLFNBQVMsS0FBSyxT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
64
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHJhbnNpdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NvdXJjZS90cmFuc2l0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQTtBQVd0QyxNQUFNLE9BQU8sVUFBVTtJQUNiLE1BQU0sR0FBVyxFQUFFLENBQUE7SUFDbkIsTUFBTSxHQUFXLEVBQUUsQ0FBQTtJQUNuQixTQUFTLENBQVk7SUFDckIsTUFBTSxHQUFXLFNBQThCLENBQUE7SUFDL0MsSUFBSSxHQUFTLFNBQTRCLENBQUE7SUFDekMsVUFBVSxHQUFhLEVBQUUsQ0FBQTtJQUN6QixVQUFVLEdBQWEsRUFBRSxDQUFBO0lBRTFCLEtBQUssQ0FBQyxPQUFnQjtRQUMzQixJQUFJLENBQUMsTUFBTSxHQUFHLE9BQU8sQ0FBQyxhQUFhLENBQUMsTUFBTSxDQUFBO1FBQzFDLElBQUksQ0FBQyxNQUFNLEdBQUcsT0FBTyxDQUFDLGFBQWEsQ0FBQyxNQUFNLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBRSxDQUFBO1FBQzFELElBQUksQ0FBQyxTQUFTLEdBQUcsT0FBTyxDQUFDLGFBQWEsQ0FBQyxTQUFTLENBQUE7UUFDaEQsSUFBSSxDQUFDLE1BQU0sR0FBRyxPQUFPLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUE7UUFDNUMsSUFBSSxDQUFDLElBQUksR0FBRyxPQUFPLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUE7UUFFeEMsSUFBSSxDQUFDLFVBQVUsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQTtRQUN0RCxJQUFJLENBQUMsVUFBVSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFBO0lBQ3hELENBQUM7SUFFTyxNQUFNLENBQUMsTUFBYztRQUMzQixPQUFPLENBQ0wsSUFBSSxDQUFDLFNBQVMsS0FBSyxT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
@@ -3,6 +3,6 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@node-rs/bcrypt": "1.10.9"
6
+ "@node-rs/bcrypt": "2.0.0"
7
7
  }
8
8
  }
@@ -68,7 +68,7 @@ export class Transition implements Operation {
68
68
 
69
69
  const spicy = input.password + this.pepper
70
70
 
71
- object.password = await hash(spicy, this.rounds)
71
+ object.password = await hash(spicy, { cost: this.rounds })
72
72
  }
73
73
 
74
74
  return { id: object.id }
@@ -111,6 +111,7 @@ export interface Component {
111
111
  }
112
112
 
113
113
  export interface Configuration {
114
+ /** bcrypt cost */
114
115
  rounds: number
115
116
  /** Hashing pepper, a secret */
116
117
  pepper?: Secret
@@ -38,6 +38,30 @@ operations:
38
38
  - authority
39
39
  - username
40
40
  type: object
41
+ confirm:
42
+ input:
43
+ properties:
44
+ authority:
45
+ type: string
46
+ username:
47
+ type: string
48
+ minLength: 1
49
+ maxLength: 256
50
+ code:
51
+ type: string
52
+ identity:
53
+ type: string
54
+ pattern: ^[0-9a-f]{32}$
55
+ required:
56
+ - authority
57
+ - username
58
+ - code
59
+ - identity
60
+ type: object
61
+ errors:
62
+ - EXPIRED
63
+ - TOO_MANY_ATTEMPTS
64
+ - EXISTS
41
65
  authenticate:
42
66
  input:
43
67
  properties:
@@ -86,11 +110,16 @@ stash: ~
86
110
  exposition:
87
111
  /:identity:
88
112
  auth:id: identity
89
- auth:role: system:identity:otp
90
113
  POST:
114
+ auth:role: system:identity:otp
91
115
  io:input: []
92
116
  io:output: [code]
93
117
  map:authority: authority
94
118
  map:segments:
95
119
  username: identity
96
120
  endpoint: issue
121
+ PUT:
122
+ io:input: [username, code]
123
+ io:output: false
124
+ map:authority: authority
125
+ endpoint: confirm
@@ -1,23 +1,21 @@
1
+ import { redeem, SIGN_IN } from './lib/index.js';
1
2
  export async function effect(input, context) {
2
3
  const { authority, credentials } = input;
3
4
  const [username, code] = Buffer.from(credentials, 'base64').toString().split(':');
4
5
  if (code === undefined)
5
6
  return ERR_INVALID_CREDENTIALS;
6
- const attempts = `${authority}:${username}:attempts`;
7
- const attempt = await context.stash.incr(attempts);
8
- if (attempt === 1)
9
- await context.stash.expire(attempts, context.configuration.lifetime);
10
- if (attempt > context.configuration.attempts) {
11
- context.logs.debug('OTP attempts exceeded', { authority, username, attempt });
12
- return ERR_TOO_MANY_ATTEMPTS;
13
- }
14
- const key = `${authority}:${username}:${code}`;
15
- const n = await context.stash.del(key);
16
- if (n === 0) {
17
- context.logs.debug('OTP code not found', { authority, username, attempt });
18
- return ERR_EXPIRED;
19
- }
20
- await context.stash.del(attempts);
7
+ // a code issued with an identity signs in only where the username is that identity's own id:
8
+ // any other is for `confirm`, and would tie the username to an identity of someone else's choosing
9
+ const issued = await redeem({
10
+ authority,
11
+ username,
12
+ code,
13
+ accepts: (value) => value === SIGN_IN || value === username
14
+ }, context);
15
+ if (issued instanceof Error)
16
+ return issued;
17
+ if (issued === username)
18
+ return { identity: { id: issued } };
21
19
  const entry = await context.local.ensure({
22
20
  entity: {
23
21
  authority,
@@ -30,7 +28,5 @@ export async function effect(input, context) {
30
28
  return { identity: { id } };
31
29
  }
32
30
  const ERR_INVALID_CREDENTIALS = new Error('INVALID_CREDENTIALS');
33
- const ERR_EXPIRED = new Error('EXPIRED');
34
- const ERR_TOO_MANY_ATTEMPTS = new Error('TOO_MANY_ATTEMPTS');
35
31
  const ERR_NOT_FOUND = new Error('NOT_FOUND');
36
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0aGVudGljYXRlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc291cmNlL2F1dGhlbnRpY2F0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFFQSxNQUFNLENBQUMsS0FBSyxVQUFVLE1BQU0sQ0FBQyxLQUFZLEVBQUUsT0FBZ0I7SUFDekQsTUFBTSxFQUFFLFNBQVMsRUFBRSxXQUFXLEVBQUUsR0FBRyxLQUFLLENBQUE7SUFDeEMsTUFBTSxDQUFDLFFBQVEsRUFBRSxJQUFJLENBQUMsR0FBRyxNQUFNLENBQUMsSUFBSSxDQUFDLFdBQVcsRUFBRSxRQUFRLENBQUMsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUE7SUFFakYsSUFBSSxJQUFJLEtBQUssU0FBUztRQUFFLE9BQU8sdUJBQXVCLENBQUE7SUFFdEQsTUFBTSxRQUFRLEdBQUcsR0FBRyxTQUFTLElBQUksUUFBUSxXQUFXLENBQUE7SUFDcEQsTUFBTSxPQUFPLEdBQUcsTUFBTSxPQUFPLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQTtJQUVsRCxJQUFJLE9BQU8sS0FBSyxDQUFDO1FBQUUsTUFBTSxPQUFPLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxRQUFRLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQTtJQUV2RixJQUFJLE9BQU8sR0FBRyxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsRUFBRSxDQUFDO1FBQzdDLE9BQU8sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLHVCQUF1QixFQUFFLEVBQUUsU0FBUyxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFBO1FBRTdFLE9BQU8scUJBQXFCLENBQUE7SUFDOUIsQ0FBQztJQUVELE1BQU0sR0FBRyxHQUFHLEdBQUcsU0FBUyxJQUFJLFFBQVEsSUFBSSxJQUFJLEVBQUUsQ0FBQTtJQUM5QyxNQUFNLENBQUMsR0FBRyxNQUFNLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFBO0lBRXRDLElBQUksQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQ1osT0FBTyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsb0JBQW9CLEVBQUUsRUFBRSxTQUFTLEVBQUUsUUFBUSxFQUFFLE9BQU8sRUFBRSxDQUFDLENBQUE7UUFFMUUsT0FBTyxXQUFXLENBQUE7SUFDcEIsQ0FBQztJQUVELE1BQU0sT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLENBQUE7SUFFakMsTUFBTSxLQUFLLEdBQUcsTUFBTSxPQUFPLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQztRQUN2QyxNQUFNLEVBQUU7WUFDTixTQUFTO1lBQ1QsUUFBUTtTQUNUO0tBQ0YsQ0FBQyxDQUFBO0lBRUYsSUFBSSxLQUFLLEtBQUssSUFBSTtRQUFFLE9BQU8sYUFBYSxDQUFBO0lBRXhDLE1BQU0sRUFBRSxHQUFHLEtBQUssQ0FBQyxRQUFRLElBQUksS0FBSyxDQUFDLEVBQUUsQ0FBQSxDQUFDLHFCQUFxQjtJQUUzRCxPQUFPLEVBQUUsUUFBUSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsQ0FBQTtBQUM3QixDQUFDO0FBRUQsTUFBTSx1QkFBdUIsR0FBRyxJQUFJLEtBQUssQ0FBQyxxQkFBcUIsQ0FBQyxDQUFBO0FBRWhFLE1BQU0sV0FBVyxHQUFHLElBQUksS0FBSyxDQUFDLFNBQVMsQ0FBQyxDQUFBO0FBRXhDLE1BQU0scUJBQXFCLEdBQUcsSUFBSSxLQUFLLENBQUMsbUJBQW1CLENBQUMsQ0FBQTtBQUU1RCxNQUFNLGFBQWEsR0FBRyxJQUFJLEtBQUssQ0FBQyxXQUFXLENBQUMsQ0FBQSJ9
32
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0aGVudGljYXRlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc291cmNlL2F1dGhlbnRpY2F0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsTUFBTSxFQUFFLE9BQU8sRUFBRSxNQUFNLGdCQUFnQixDQUFBO0FBR2hELE1BQU0sQ0FBQyxLQUFLLFVBQVUsTUFBTSxDQUFDLEtBQVksRUFBRSxPQUFnQjtJQUN6RCxNQUFNLEVBQUUsU0FBUyxFQUFFLFdBQVcsRUFBRSxHQUFHLEtBQUssQ0FBQTtJQUN4QyxNQUFNLENBQUMsUUFBUSxFQUFFLElBQUksQ0FBQyxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsV0FBVyxFQUFFLFFBQVEsQ0FBQyxDQUFDLFFBQVEsRUFBRSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQTtJQUVqRixJQUFJLElBQUksS0FBSyxTQUFTO1FBQUUsT0FBTyx1QkFBdUIsQ0FBQTtJQUV0RCw2RkFBNkY7SUFDN0YsbUdBQW1HO0lBQ25HLE1BQU0sTUFBTSxHQUFHLE1BQU0sTUFBTSxDQUN6QjtRQUNFLFNBQVM7UUFDVCxRQUFRO1FBQ1IsSUFBSTtRQUNKLE9BQU8sRUFBRSxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsS0FBSyxLQUFLLE9BQU8sSUFBSSxLQUFLLEtBQUssUUFBUTtLQUM1RCxFQUNELE9BQU8sQ0FDUixDQUFBO0lBRUQsSUFBSSxNQUFNLFlBQVksS0FBSztRQUFFLE9BQU8sTUFBTSxDQUFBO0lBRTFDLElBQUksTUFBTSxLQUFLLFFBQVE7UUFBRSxPQUFPLEVBQUUsUUFBUSxFQUFFLEVBQUUsRUFBRSxFQUFFLE1BQU0sRUFBRSxFQUFFLENBQUE7SUFFNUQsTUFBTSxLQUFLLEdBQUcsTUFBTSxPQUFPLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQztRQUN2QyxNQUFNLEVBQUU7WUFDTixTQUFTO1lBQ1QsUUFBUTtTQUNUO0tBQ0YsQ0FBQyxDQUFBO0lBRUYsSUFBSSxLQUFLLEtBQUssSUFBSTtRQUFFLE9BQU8sYUFBYSxDQUFBO0lBRXhDLE1BQU0sRUFBRSxHQUFHLEtBQUssQ0FBQyxRQUFRLElBQUksS0FBSyxDQUFDLEVBQUUsQ0FBQSxDQUFDLHFCQUFxQjtJQUUzRCxPQUFPLEVBQUUsUUFBUSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsQ0FBQTtBQUM3QixDQUFDO0FBRUQsTUFBTSx1QkFBdUIsR0FBRyxJQUFJLEtBQUssQ0FBQyxxQkFBcUIsQ0FBQyxDQUFBO0FBRWhFLE1BQU0sYUFBYSxHQUFHLElBQUksS0FBSyxDQUFDLFdBQVcsQ0FBQyxDQUFBIn0=
@@ -0,0 +1,23 @@
1
+ import { quote } from '@toa.io/generic';
2
+ import { redeem } from './lib/index.js';
3
+ export async function effect(input, context) {
4
+ const { authority, username, code, identity } = input;
5
+ const issued = await redeem({ authority, username, code, accepts: (value) => value === identity }, context);
6
+ if (issued instanceof Error)
7
+ return issued;
8
+ const entry = await context.local.ensure({
9
+ query: { criteria: `authority==${quote(authority)};username==${quote(username)}` },
10
+ entity: { authority, username, identity }
11
+ });
12
+ const bound = entry.identity ?? entry.id; // identity inception
13
+ if (bound !== identity) {
14
+ context.logs.debug('OTP username is tied to another identity', {
15
+ authority,
16
+ username
17
+ });
18
+ return ERR_EXISTS;
19
+ }
20
+ return undefined;
21
+ }
22
+ const ERR_EXISTS = new Error('EXISTS');
23
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlybS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NvdXJjZS9jb25maXJtLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQTtBQUN2QyxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sZ0JBQWdCLENBQUE7QUFHdkMsTUFBTSxDQUFDLEtBQUssVUFBVSxNQUFNLENBQUMsS0FBWSxFQUFFLE9BQWdCO0lBQ3pELE1BQU0sRUFBRSxTQUFTLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUUsR0FBRyxLQUFLLENBQUE7SUFFckQsTUFBTSxNQUFNLEdBQUcsTUFBTSxNQUFNLENBQ3pCLEVBQUUsU0FBUyxFQUFFLFFBQVEsRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxLQUFLLEtBQUssUUFBUSxFQUFFLEVBQ3JFLE9BQU8sQ0FDUixDQUFBO0lBRUQsSUFBSSxNQUFNLFlBQVksS0FBSztRQUFFLE9BQU8sTUFBTSxDQUFBO0lBRTFDLE1BQU0sS0FBSyxHQUFHLE1BQU0sT0FBTyxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUM7UUFDdkMsS0FBSyxFQUFFLEVBQUUsUUFBUSxFQUFFLGNBQWMsS0FBSyxDQUFDLFNBQVMsQ0FBQyxjQUFjLEtBQUssQ0FBQyxRQUFRLENBQUMsRUFBRSxFQUFFO1FBQ2xGLE1BQU0sRUFBRSxFQUFFLFNBQVMsRUFBRSxRQUFRLEVBQUUsUUFBUSxFQUFFO0tBQzFDLENBQUMsQ0FBQTtJQUVGLE1BQU0sS0FBSyxHQUFHLEtBQUssQ0FBQyxRQUFRLElBQUksS0FBSyxDQUFDLEVBQUUsQ0FBQSxDQUFDLHFCQUFxQjtJQUU5RCxJQUFJLEtBQUssS0FBSyxRQUFRLEVBQUUsQ0FBQztRQUN2QixPQUFPLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQywwQ0FBMEMsRUFBRTtZQUM3RCxTQUFTO1lBQ1QsUUFBUTtTQUNULENBQUMsQ0FBQTtRQUVGLE9BQU8sVUFBVSxDQUFBO0lBQ25CLENBQUM7SUFFRCxPQUFPLFNBQVMsQ0FBQTtBQUNsQixDQUFDO0FBRUQsTUFBTSxVQUFVLEdBQUcsSUFBSSxLQUFLLENBQUMsUUFBUSxDQUFDLENBQUEifQ==
@@ -1,19 +1,13 @@
1
1
  import { randomInt } from 'node:crypto';
2
+ import { SIGN_IN } from './lib/index.js';
2
3
  export async function effect(input, context) {
3
4
  const { authority, username, identity } = input;
4
5
  const code = randomInt(100000, 1000000).toString();
5
6
  const key = `${authority}:${username}:${code}`;
6
7
  const lifetime = input.lifetime ?? context.configuration.lifetime;
7
- if (identity !== undefined)
8
- await context.local.ensure({
9
- entity: {
10
- authority,
11
- username,
12
- identity
13
- }
14
- });
15
8
  context.logs.debug('Issue OTP', { authority, username, identity, lifetime });
16
- await context.stash.set(key, 1, 'EX', lifetime);
9
+ // the username is tied to the identity once `confirm` is given the code, not before
10
+ await context.stash.set(key, identity ?? SIGN_IN, 'EX', lifetime);
17
11
  return { code };
18
12
  }
19
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaXNzdWUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zb3VyY2UvaXNzdWUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLGFBQWEsQ0FBQTtBQUd2QyxNQUFNLENBQUMsS0FBSyxVQUFVLE1BQU0sQ0FBQyxLQUFZLEVBQUUsT0FBZ0I7SUFDekQsTUFBTSxFQUFFLFNBQVMsRUFBRSxRQUFRLEVBQUUsUUFBUSxFQUFFLEdBQUcsS0FBSyxDQUFBO0lBQy9DLE1BQU0sSUFBSSxHQUFHLFNBQVMsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUMsUUFBUSxFQUFFLENBQUE7SUFDbEQsTUFBTSxHQUFHLEdBQUcsR0FBRyxTQUFTLElBQUksUUFBUSxJQUFJLElBQUksRUFBRSxDQUFBO0lBQzlDLE1BQU0sUUFBUSxHQUFHLEtBQUssQ0FBQyxRQUFRLElBQUksT0FBTyxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUE7SUFFakUsSUFBSSxRQUFRLEtBQUssU0FBUztRQUN4QixNQUFNLE9BQU8sQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDO1lBQ3pCLE1BQU0sRUFBRTtnQkFDTixTQUFTO2dCQUNULFFBQVE7Z0JBQ1IsUUFBUTthQUNUO1NBQ0YsQ0FBQyxDQUFBO0lBRUosT0FBTyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsV0FBVyxFQUFFLEVBQUUsU0FBUyxFQUFFLFFBQVEsRUFBRSxRQUFRLEVBQUUsUUFBUSxFQUFFLENBQUMsQ0FBQTtJQUU1RSxNQUFNLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxDQUFDLEVBQUUsSUFBSSxFQUFFLFFBQVEsQ0FBQyxDQUFBO0lBRS9DLE9BQU8sRUFBRSxJQUFJLEVBQUUsQ0FBQTtBQUNqQixDQUFDIn0=
13
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaXNzdWUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zb3VyY2UvaXNzdWUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLGFBQWEsQ0FBQTtBQUN2QyxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sZ0JBQWdCLENBQUE7QUFHeEMsTUFBTSxDQUFDLEtBQUssVUFBVSxNQUFNLENBQUMsS0FBWSxFQUFFLE9BQWdCO0lBQ3pELE1BQU0sRUFBRSxTQUFTLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxHQUFHLEtBQUssQ0FBQTtJQUMvQyxNQUFNLElBQUksR0FBRyxTQUFTLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxDQUFDLFFBQVEsRUFBRSxDQUFBO0lBQ2xELE1BQU0sR0FBRyxHQUFHLEdBQUcsU0FBUyxJQUFJLFFBQVEsSUFBSSxJQUFJLEVBQUUsQ0FBQTtJQUM5QyxNQUFNLFFBQVEsR0FBRyxLQUFLLENBQUMsUUFBUSxJQUFJLE9BQU8sQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFBO0lBRWpFLE9BQU8sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLFdBQVcsRUFBRSxFQUFFLFNBQVMsRUFBRSxRQUFRLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUE7SUFFNUUsb0ZBQW9GO0lBQ3BGLE1BQU0sT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLFFBQVEsSUFBSSxPQUFPLEVBQUUsSUFBSSxFQUFFLFFBQVEsQ0FBQyxDQUFBO0lBRWpFLE9BQU8sRUFBRSxJQUFJLEVBQUUsQ0FBQTtBQUNqQixDQUFDIn0=
@@ -1,2 +1,2 @@
1
- export {};
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zb3VyY2UvbGliL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiIifQ==
1
+ export { redeem, SIGN_IN } from './redeem.js';
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zb3VyY2UvbGliL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFBRSxNQUFNLEVBQUUsT0FBTyxFQUFFLE1BQU0sYUFBYSxDQUFBIn0=
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Spends a code that `accepts` takes and answers what it was issued with: `SIGN_IN`, or the
3
+ * identity the username is to be tied to. A code it does not take stays for the operation that
4
+ * does, and counts as a wrong one.
5
+ */
6
+ export async function redeem({ authority, username, code, accepts }, context) {
7
+ const attempts = `${authority}:${username}:attempts`;
8
+ const attempt = await context.stash.incr(attempts);
9
+ if (attempt === 1)
10
+ await context.stash.expire(attempts, context.configuration.lifetime);
11
+ if (attempt > context.configuration.attempts) {
12
+ context.logs.debug('OTP attempts exceeded', { authority, username, attempt });
13
+ return ERR_TOO_MANY_ATTEMPTS;
14
+ }
15
+ const key = `${authority}:${username}:${code}`;
16
+ const issued = await context.stash.get(key);
17
+ // of two requests with one code, the one that deletes it is the one it is spent on
18
+ if (issued === null || !accepts(issued) || (await context.stash.del(key)) === 0) {
19
+ context.logs.debug('OTP code not found', { authority, username, attempt });
20
+ return ERR_EXPIRED;
21
+ }
22
+ await context.stash.del(attempts);
23
+ return issued;
24
+ }
25
+ export const SIGN_IN = '1';
26
+ const ERR_EXPIRED = new Error('EXPIRED');
27
+ const ERR_TOO_MANY_ATTEMPTS = new Error('TOO_MANY_ATTEMPTS');
28
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVkZWVtLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc291cmNlL2xpYi9yZWRlZW0udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUE7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsTUFBTSxDQUMxQixFQUFFLFNBQVMsRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFBYyxFQUNsRCxPQUFnQjtJQUVoQixNQUFNLFFBQVEsR0FBRyxHQUFHLFNBQVMsSUFBSSxRQUFRLFdBQVcsQ0FBQTtJQUNwRCxNQUFNLE9BQU8sR0FBRyxNQUFNLE9BQU8sQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFBO0lBRWxELElBQUksT0FBTyxLQUFLLENBQUM7UUFBRSxNQUFNLE9BQU8sQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLFFBQVEsRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFBO0lBRXZGLElBQUksT0FBTyxHQUFHLE9BQU8sQ0FBQyxhQUFhLENBQUMsUUFBUSxFQUFFLENBQUM7UUFDN0MsT0FBTyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsdUJBQXVCLEVBQUUsRUFBRSxTQUFTLEVBQUUsUUFBUSxFQUFFLE9BQU8sRUFBRSxDQUFDLENBQUE7UUFFN0UsT0FBTyxxQkFBcUIsQ0FBQTtJQUM5QixDQUFDO0lBRUQsTUFBTSxHQUFHLEdBQUcsR0FBRyxTQUFTLElBQUksUUFBUSxJQUFJLElBQUksRUFBRSxDQUFBO0lBQzlDLE1BQU0sTUFBTSxHQUFHLE1BQU0sT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUE7SUFFM0MsbUZBQW1GO0lBQ25GLElBQUksTUFBTSxLQUFLLElBQUksSUFBSSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLE1BQU0sT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUNoRixPQUFPLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxvQkFBb0IsRUFBRSxFQUFFLFNBQVMsRUFBRSxRQUFRLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQTtRQUUxRSxPQUFPLFdBQVcsQ0FBQTtJQUNwQixDQUFDO0lBRUQsTUFBTSxPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUMsQ0FBQTtJQUVqQyxPQUFPLE1BQU0sQ0FBQTtBQUNmLENBQUM7QUFFRCxNQUFNLENBQUMsTUFBTSxPQUFPLEdBQUcsR0FBRyxDQUFBO0FBRTFCLE1BQU0sV0FBVyxHQUFHLElBQUksS0FBSyxDQUFDLFNBQVMsQ0FBQyxDQUFBO0FBRXhDLE1BQU0scUJBQXFCLEdBQUcsSUFBSSxLQUFLLENBQUMsbUJBQW1CLENBQUMsQ0FBQSJ9
@@ -1,3 +1,4 @@
1
+ import { redeem, SIGN_IN } from './lib/index.ts'
1
2
  import type { Context } from './lib/index.ts'
2
3
 
3
4
  export async function effect(input: Input, context: Context): Promise<Output | Error> {
@@ -6,27 +7,21 @@ export async function effect(input: Input, context: Context): Promise<Output | E
6
7
 
7
8
  if (code === undefined) return ERR_INVALID_CREDENTIALS
8
9
 
9
- const attempts = `${authority}:${username}:attempts`
10
- const attempt = await context.stash.incr(attempts)
11
-
12
- if (attempt === 1) await context.stash.expire(attempts, context.configuration.lifetime)
13
-
14
- if (attempt > context.configuration.attempts) {
15
- context.logs.debug('OTP attempts exceeded', { authority, username, attempt })
16
-
17
- return ERR_TOO_MANY_ATTEMPTS
18
- }
19
-
20
- const key = `${authority}:${username}:${code}`
21
- const n = await context.stash.del(key)
22
-
23
- if (n === 0) {
24
- context.logs.debug('OTP code not found', { authority, username, attempt })
10
+ // a code issued with an identity signs in only where the username is that identity's own id:
11
+ // any other is for `confirm`, and would tie the username to an identity of someone else's choosing
12
+ const issued = await redeem(
13
+ {
14
+ authority,
15
+ username,
16
+ code,
17
+ accepts: (value) => value === SIGN_IN || value === username
18
+ },
19
+ context
20
+ )
25
21
 
26
- return ERR_EXPIRED
27
- }
22
+ if (issued instanceof Error) return issued
28
23
 
29
- await context.stash.del(attempts)
24
+ if (issued === username) return { identity: { id: issued } }
30
25
 
31
26
  const entry = await context.local.ensure({
32
27
  entity: {
@@ -44,10 +39,6 @@ export async function effect(input: Input, context: Context): Promise<Output | E
44
39
 
45
40
  const ERR_INVALID_CREDENTIALS = new Error('INVALID_CREDENTIALS')
46
41
 
47
- const ERR_EXPIRED = new Error('EXPIRED')
48
-
49
- const ERR_TOO_MANY_ATTEMPTS = new Error('TOO_MANY_ATTEMPTS')
50
-
51
42
  const ERR_NOT_FOUND = new Error('NOT_FOUND')
52
43
 
53
44
  interface Input {
@@ -0,0 +1,41 @@
1
+ import { quote } from '@toa.io/generic'
2
+ import { redeem } from './lib/index.ts'
3
+ import type { Context } from './lib/index.ts'
4
+
5
+ export async function effect(input: Input, context: Context): Promise<void | Error> {
6
+ const { authority, username, code, identity } = input
7
+
8
+ const issued = await redeem(
9
+ { authority, username, code, accepts: (value) => value === identity },
10
+ context
11
+ )
12
+
13
+ if (issued instanceof Error) return issued
14
+
15
+ const entry = await context.local.ensure({
16
+ query: { criteria: `authority==${quote(authority)};username==${quote(username)}` },
17
+ entity: { authority, username, identity }
18
+ })
19
+
20
+ const bound = entry.identity ?? entry.id // identity inception
21
+
22
+ if (bound !== identity) {
23
+ context.logs.debug('OTP username is tied to another identity', {
24
+ authority,
25
+ username
26
+ })
27
+
28
+ return ERR_EXISTS
29
+ }
30
+
31
+ return undefined
32
+ }
33
+
34
+ const ERR_EXISTS = new Error('EXISTS')
35
+
36
+ interface Input {
37
+ authority: string
38
+ username: string
39
+ code: string
40
+ identity: string
41
+ }
@@ -1,4 +1,5 @@
1
1
  import { randomInt } from 'node:crypto'
2
+ import { SIGN_IN } from './lib/index.ts'
2
3
  import type { Context } from './lib/index.ts'
3
4
 
4
5
  export async function effect(input: Input, context: Context): Promise<Output> {
@@ -7,18 +8,10 @@ export async function effect(input: Input, context: Context): Promise<Output> {
7
8
  const key = `${authority}:${username}:${code}`
8
9
  const lifetime = input.lifetime ?? context.configuration.lifetime
9
10
 
10
- if (identity !== undefined)
11
- await context.local.ensure({
12
- entity: {
13
- authority,
14
- username,
15
- identity
16
- }
17
- })
18
-
19
11
  context.logs.debug('Issue OTP', { authority, username, identity, lifetime })
20
12
 
21
- await context.stash.set(key, 1, 'EX', lifetime)
13
+ // the username is tied to the identity once `confirm` is given the code, not before
14
+ await context.stash.set(key, identity ?? SIGN_IN, 'EX', lifetime)
22
15
 
23
16
  return { code }
24
17
  }
@@ -1 +1,2 @@
1
1
  export type { Context } from './Context.ts'
2
+ export { redeem, SIGN_IN } from './redeem.ts'
@@ -0,0 +1,49 @@
1
+ import type { Context } from './Context.ts'
2
+
3
+ /**
4
+ * Spends a code that `accepts` takes and answers what it was issued with: `SIGN_IN`, or the
5
+ * identity the username is to be tied to. A code it does not take stays for the operation that
6
+ * does, and counts as a wrong one.
7
+ */
8
+ export async function redeem(
9
+ { authority, username, code, accepts }: Redemption,
10
+ context: Context
11
+ ): Promise<string | Error> {
12
+ const attempts = `${authority}:${username}:attempts`
13
+ const attempt = await context.stash.incr(attempts)
14
+
15
+ if (attempt === 1) await context.stash.expire(attempts, context.configuration.lifetime)
16
+
17
+ if (attempt > context.configuration.attempts) {
18
+ context.logs.debug('OTP attempts exceeded', { authority, username, attempt })
19
+
20
+ return ERR_TOO_MANY_ATTEMPTS
21
+ }
22
+
23
+ const key = `${authority}:${username}:${code}`
24
+ const issued = await context.stash.get(key)
25
+
26
+ // of two requests with one code, the one that deletes it is the one it is spent on
27
+ if (issued === null || !accepts(issued) || (await context.stash.del(key)) === 0) {
28
+ context.logs.debug('OTP code not found', { authority, username, attempt })
29
+
30
+ return ERR_EXPIRED
31
+ }
32
+
33
+ await context.stash.del(attempts)
34
+
35
+ return issued
36
+ }
37
+
38
+ export const SIGN_IN = '1'
39
+
40
+ const ERR_EXPIRED = new Error('EXPIRED')
41
+
42
+ const ERR_TOO_MANY_ATTEMPTS = new Error('TOO_MANY_ATTEMPTS')
43
+
44
+ interface Redemption {
45
+ authority: string
46
+ username: string
47
+ code: string
48
+ accepts: (issued: string) => boolean
49
+ }
@@ -29,6 +29,13 @@ export type IssueInput = {
29
29
  lifetime?: number
30
30
  }
31
31
 
32
+ export type ConfirmInput = {
33
+ authority: string
34
+ username: string
35
+ code: string
36
+ identity: string
37
+ }
38
+
32
39
  export type AuthenticateInput = {
33
40
  authority: string
34
41
  credentials: string
@@ -42,6 +49,7 @@ export type AuthenticateOutput = {
42
49
 
43
50
  export interface Component {
44
51
  issue: (request: { input: IssueInput, entity?: Partial<Entity>, task?: boolean }) => Promise<unknown>
52
+ confirm: (request: { input: ConfirmInput, entity?: Partial<Entity>, task?: boolean }) => Promise<{} | null | undefined | CodedError<"EXPIRED" | "TOO_MANY_ATTEMPTS" | "EXISTS">>
45
53
  authenticate: (request: { input: AuthenticateInput, entity?: Partial<Entity>, task?: boolean }) => Promise<AuthenticateOutput | CodedError<"INVALID_CREDENTIALS" | "EXPIRED" | "TOO_MANY_ATTEMPTS" | "NOT_FOUND">>
46
54
  assign: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<Entity>
47
55
  ensure: (request: { input?: null, query?: Query<Entity>, entity?: Partial<Entity>, task?: boolean }) => Promise<Entity>
@@ -105,6 +105,9 @@ operations:
105
105
  type: string
106
106
  credentials:
107
107
  type: string
108
+ force:
109
+ description: Refresh a token whatever its age, and refuse one that cannot be
110
+ type: boolean
108
111
  type: object
109
112
  required:
110
113
  - authority
@@ -130,6 +133,8 @@ operations:
130
133
  - UNRECOGNIZED
131
134
  - AUTHORITY_MISMATCH
132
135
  - TOKEN_REVOKED
136
+ # a custom token asked to be refreshed
137
+ - UNREFRESHABLE
133
138
  # what `decrypt` refuses with, passed on
134
139
  - INVALID_TOKEN
135
140
  - INVALID_KEY
@@ -18,17 +18,23 @@ export class Computation {
18
18
  return claims;
19
19
  if (claims.iss !== input.authority)
20
20
  return ERR_AUTHORITY;
21
+ const force = input.force === true;
22
+ // re-issued as a session token, it would lose what restricts it and outlive its key
23
+ if (force && claims.custom)
24
+ return ERR_UNREFRESHABLE;
21
25
  const identity = claims.identity;
22
26
  const iat = new Date(claims.iat).getTime();
23
27
  const transient = claims.exp !== undefined;
24
28
  const aged = transient && iat + this.refresh < Date.now();
25
- if (aged) {
29
+ // a forced refresh checks whatever the age, or it would exchange a revoked token for one
30
+ // issued after the revocation
31
+ if (aged || force) {
26
32
  const revocation = await this.observe({ query: { id: identity.id } });
27
33
  if (revocation?.revokedAt !== undefined && iat < revocation.revokedAt)
28
34
  return ERR_TOKEN_REVOKED;
29
35
  }
30
36
  // a custom token is invalidated by deleting its key, not replaced
31
- const refresh = (aged && !claims.custom) || claims.refresh;
37
+ const refresh = force || (aged && !claims.custom) || claims.refresh;
32
38
  return {
33
39
  identity: claims.aud === undefined ? identity : { ...identity, aud: list(claims.aud) },
34
40
  refresh
@@ -43,4 +49,5 @@ function list(aud) {
43
49
  const ERR_UNRECOGNIZED = new Error('UNRECOGNIZED');
44
50
  const ERR_AUTHORITY = new Error('AUTHORITY_MISMATCH');
45
51
  const ERR_TOKEN_REVOKED = new Error('TOKEN_REVOKED');
46
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0aGVudGljYXRlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc291cmNlL2F1dGhlbnRpY2F0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFFQSxPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0sZ0JBQWdCLENBQUE7QUFHckMsTUFBTSxPQUFPLFdBQVc7SUFDZCxPQUFPLEdBQVcsQ0FBQyxDQUFBO0lBQ25CLE9BQU8sQ0FBOEI7SUFDckMsT0FBTyxDQUE4QjtJQUV0QyxLQUFLLENBQUMsT0FBZ0I7UUFDM0IsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUMsYUFBYSxDQUFDLE9BQU8sR0FBRyxJQUFJLENBQUE7UUFDbkQsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQTtRQUNwQyxJQUFJLENBQUMsT0FBTyxHQUFHLE9BQU8sQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFBO0lBQ3RDLENBQUM7SUFFTSxLQUFLLENBQUMsT0FBTyxDQUFDLEtBQXdCO1FBQzNDLHNGQUFzRjtRQUN0RiwrRUFBK0U7UUFDL0UsSUFBSSxJQUFJLENBQUMsS0FBSyxDQUFDLFdBQVcsQ0FBQyxLQUFLLElBQUk7WUFBRSxPQUFPLGdCQUFnQixDQUFBO1FBRTdELE1BQU0sTUFBTSxHQUFHLE1BQU0sSUFBSSxDQUFDLE9BQU8sQ0FBQyxFQUFFLEtBQUssRUFBRSxLQUFLLENBQUMsV0FBVyxFQUFFLENBQUMsQ0FBQTtRQUUvRCxJQUFJLE1BQU0sWUFBWSxLQUFLO1lBQUUsT0FBTyxNQUFNLENBQUE7UUFFMUMsSUFBSSxNQUFNLENBQUMsR0FBRyxLQUFLLEtBQUssQ0FBQyxTQUFTO1lBQUUsT0FBTyxhQUFhLENBQUE7UUFFeEQsTUFBTSxRQUFRLEdBQUcsTUFBTSxDQUFDLFFBQVEsQ0FBQTtRQUNoQyxNQUFNLEdBQUcsR0FBRyxJQUFJLElBQUksQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsT0FBTyxFQUFFLENBQUE7UUFDMUMsTUFBTSxTQUFTLEdBQUcsTUFBTSxDQUFDLEdBQUcsS0FBSyxTQUFTLENBQUE7UUFDMUMsTUFBTSxJQUFJLEdBQUcsU0FBUyxJQUFJLEdBQUcsR0FBRyxJQUFJLENBQUMsT0FBTyxHQUFHLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQTtRQUV6RCxJQUFJLElBQUksRUFBRSxDQUFDO1lBQ1QsTUFBTSxVQUFVLEdBQUcsTUFBTSxJQUFJLENBQUMsT0FBTyxDQUFDLEVBQUUsS0FBSyxFQUFFLEVBQUUsRUFBRSxFQUFFLFFBQVEsQ0FBQyxFQUFFLEVBQUUsRUFBRSxDQUFDLENBQUE7WUFFckUsSUFBSSxVQUFVLEVBQUUsU0FBUyxLQUFLLFNBQVMsSUFBSSxHQUFHLEdBQUcsVUFBVSxDQUFDLFNBQVM7Z0JBQ25FLE9BQU8saUJBQWlCLENBQUE7UUFDNUIsQ0FBQztRQUVELGtFQUFrRTtRQUNsRSxNQUFNLE9BQU8sR0FBRyxDQUFDLElBQUksSUFBSSxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsSUFBSSxNQUFNLENBQUMsT0FBTyxDQUFBO1FBRTFELE9BQU87WUFDTCxRQUFRLEVBQ04sTUFBTSxDQUFDLEdBQUcsS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsRUFBRSxHQUFHLFFBQVEsRUFBRSxHQUFHLEVBQUUsSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsRUFBRTtZQUM5RSxPQUFPO1NBQ1IsQ0FBQTtJQUNILENBQUM7Q0FDRjtBQUVELFNBQVMsSUFBSSxDQUFDLEdBQXVCO0lBQ25DLElBQUksT0FBTyxHQUFHLEtBQUssUUFBUTtRQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQTtJQUV6QyxPQUFPLEdBQUcsQ0FBQyxNQUFNLENBQUMsQ0FBQyxLQUFLLEVBQW1CLEVBQUUsQ0FBQyxPQUFPLEtBQUssS0FBSyxRQUFRLENBQUMsQ0FBQTtBQUMxRSxDQUFDO0FBRUQsTUFBTSxnQkFBZ0IsR0FBRyxJQUFJLEtBQUssQ0FBQyxjQUFjLENBQUMsQ0FBQTtBQUVsRCxNQUFNLGFBQWEsR0FBRyxJQUFJLEtBQUssQ0FBQyxvQkFBb0IsQ0FBQyxDQUFBO0FBRXJELE1BQU0saUJBQWlCLEdBQUcsSUFBSSxLQUFLLENBQUMsZUFBZSxDQUFDLENBQUEifQ==
52
+ const ERR_UNREFRESHABLE = new Error('UNREFRESHABLE');
53
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0aGVudGljYXRlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc291cmNlL2F1dGhlbnRpY2F0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFFQSxPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0sZ0JBQWdCLENBQUE7QUFHckMsTUFBTSxPQUFPLFdBQVc7SUFDZCxPQUFPLEdBQVcsQ0FBQyxDQUFBO0lBQ25CLE9BQU8sQ0FBOEI7SUFDckMsT0FBTyxDQUE4QjtJQUV0QyxLQUFLLENBQUMsT0FBZ0I7UUFDM0IsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUMsYUFBYSxDQUFDLE9BQU8sR0FBRyxJQUFJLENBQUE7UUFDbkQsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQTtRQUNwQyxJQUFJLENBQUMsT0FBTyxHQUFHLE9BQU8sQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFBO0lBQ3RDLENBQUM7SUFFTSxLQUFLLENBQUMsT0FBTyxDQUFDLEtBQXdCO1FBQzNDLHNGQUFzRjtRQUN0RiwrRUFBK0U7UUFDL0UsSUFBSSxJQUFJLENBQUMsS0FBSyxDQUFDLFdBQVcsQ0FBQyxLQUFLLElBQUk7WUFBRSxPQUFPLGdCQUFnQixDQUFBO1FBRTdELE1BQU0sTUFBTSxHQUFHLE1BQU0sSUFBSSxDQUFDLE9BQU8sQ0FBQyxFQUFFLEtBQUssRUFBRSxLQUFLLENBQUMsV0FBVyxFQUFFLENBQUMsQ0FBQTtRQUUvRCxJQUFJLE1BQU0sWUFBWSxLQUFLO1lBQUUsT0FBTyxNQUFNLENBQUE7UUFFMUMsSUFBSSxNQUFNLENBQUMsR0FBRyxLQUFLLEtBQUssQ0FBQyxTQUFTO1lBQUUsT0FBTyxhQUFhLENBQUE7UUFFeEQsTUFBTSxLQUFLLEdBQUcsS0FBSyxDQUFDLEtBQUssS0FBSyxJQUFJLENBQUE7UUFFbEMsb0ZBQW9GO1FBQ3BGLElBQUksS0FBSyxJQUFJLE1BQU0sQ0FBQyxNQUFNO1lBQUUsT0FBTyxpQkFBaUIsQ0FBQTtRQUVwRCxNQUFNLFFBQVEsR0FBRyxNQUFNLENBQUMsUUFBUSxDQUFBO1FBQ2hDLE1BQU0sR0FBRyxHQUFHLElBQUksSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQTtRQUMxQyxNQUFNLFNBQVMsR0FBRyxNQUFNLENBQUMsR0FBRyxLQUFLLFNBQVMsQ0FBQTtRQUMxQyxNQUFNLElBQUksR0FBRyxTQUFTLElBQUksR0FBRyxHQUFHLElBQUksQ0FBQyxPQUFPLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFBO1FBRXpELHlGQUF5RjtRQUN6Riw4QkFBOEI7UUFDOUIsSUFBSSxJQUFJLElBQUksS0FBSyxFQUFFLENBQUM7WUFDbEIsTUFBTSxVQUFVLEdBQUcsTUFBTSxJQUFJLENBQUMsT0FBTyxDQUFDLEVBQUUsS0FBSyxFQUFFLEVBQUUsRUFBRSxFQUFFLFFBQVEsQ0FBQyxFQUFFLEVBQUUsRUFBRSxDQUFDLENBQUE7WUFFckUsSUFBSSxVQUFVLEVBQUUsU0FBUyxLQUFLLFNBQVMsSUFBSSxHQUFHLEdBQUcsVUFBVSxDQUFDLFNBQVM7Z0JBQ25FLE9BQU8saUJBQWlCLENBQUE7UUFDNUIsQ0FBQztRQUVELGtFQUFrRTtRQUNsRSxNQUFNLE9BQU8sR0FBRyxLQUFLLElBQUksQ0FBQyxJQUFJLElBQUksQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLElBQUksTUFBTSxDQUFDLE9BQU8sQ0FBQTtRQUVuRSxPQUFPO1lBQ0wsUUFBUSxFQUNOLE1BQU0sQ0FBQyxHQUFHLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEVBQUUsR0FBRyxRQUFRLEVBQUUsR0FBRyxFQUFFLElBQUksQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLEVBQUU7WUFDOUUsT0FBTztTQUNSLENBQUE7SUFDSCxDQUFDO0NBQ0Y7QUFFRCxTQUFTLElBQUksQ0FBQyxHQUF1QjtJQUNuQyxJQUFJLE9BQU8sR0FBRyxLQUFLLFFBQVE7UUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUE7SUFFekMsT0FBTyxHQUFHLENBQUMsTUFBTSxDQUFDLENBQUMsS0FBSyxFQUFtQixFQUFFLENBQUMsT0FBTyxLQUFLLEtBQUssUUFBUSxDQUFDLENBQUE7QUFDMUUsQ0FBQztBQUVELE1BQU0sZ0JBQWdCLEdBQUcsSUFBSSxLQUFLLENBQUMsY0FBYyxDQUFDLENBQUE7QUFFbEQsTUFBTSxhQUFhLEdBQUcsSUFBSSxLQUFLLENBQUMsb0JBQW9CLENBQUMsQ0FBQTtBQUVyRCxNQUFNLGlCQUFpQixHQUFHLElBQUksS0FBSyxDQUFDLGVBQWUsQ0FBQyxDQUFBO0FBRXBELE1BQU0saUJBQWlCLEdBQUcsSUFBSSxLQUFLLENBQUMsZUFBZSxDQUFDLENBQUEifQ==
@@ -25,12 +25,19 @@ export class Computation implements Operation {
25
25
 
26
26
  if (claims.iss !== input.authority) return ERR_AUTHORITY
27
27
 
28
+ const force = input.force === true
29
+
30
+ // re-issued as a session token, it would lose what restricts it and outlive its key
31
+ if (force && claims.custom) return ERR_UNREFRESHABLE
32
+
28
33
  const identity = claims.identity
29
34
  const iat = new Date(claims.iat).getTime()
30
35
  const transient = claims.exp !== undefined
31
36
  const aged = transient && iat + this.refresh < Date.now()
32
37
 
33
- if (aged) {
38
+ // a forced refresh checks whatever the age, or it would exchange a revoked token for one
39
+ // issued after the revocation
40
+ if (aged || force) {
34
41
  const revocation = await this.observe({ query: { id: identity.id } })
35
42
 
36
43
  if (revocation?.revokedAt !== undefined && iat < revocation.revokedAt)
@@ -38,7 +45,7 @@ export class Computation implements Operation {
38
45
  }
39
46
 
40
47
  // a custom token is invalidated by deleting its key, not replaced
41
- const refresh = (aged && !claims.custom) || claims.refresh
48
+ const refresh = force || (aged && !claims.custom) || claims.refresh
42
49
 
43
50
  return {
44
51
  identity:
@@ -59,3 +66,5 @@ const ERR_UNRECOGNIZED = new Error('UNRECOGNIZED')
59
66
  const ERR_AUTHORITY = new Error('AUTHORITY_MISMATCH')
60
67
 
61
68
  const ERR_TOKEN_REVOKED = new Error('TOKEN_REVOKED')
69
+
70
+ const ERR_UNREFRESHABLE = new Error('UNREFRESHABLE')
@@ -60,6 +60,8 @@ export type DecryptOutput = {
60
60
  export type AuthenticateInput = {
61
61
  authority: string
62
62
  credentials: string
63
+ /** Refresh a token whatever its age, and refuse one that cannot be */
64
+ force?: boolean
63
65
  }
64
66
 
65
67
  export type AuthenticateOutput = {
@@ -93,7 +95,7 @@ export type RevokeOutput = null
93
95
  export interface Component {
94
96
  encrypt: (request: { input: EncryptInput, entity?: Partial<Entity>, task?: boolean }) => Promise<EncryptOutput | CodedError<"INACCESSIBLE_SCOPE">>
95
97
  decrypt: (request: { input: DecryptInput, task?: boolean }) => Promise<DecryptOutput | CodedError<"INVALID_TOKEN" | "INVALID_KEY" | "FORGED_KEY" | "REVOKED_KEY">>
96
- authenticate: (request: { input: AuthenticateInput, task?: boolean }) => Promise<AuthenticateOutput | CodedError<"UNRECOGNIZED" | "AUTHORITY_MISMATCH" | "TOKEN_REVOKED" | "INVALID_TOKEN" | "INVALID_KEY" | "FORGED_KEY" | "REVOKED_KEY">>
98
+ authenticate: (request: { input: AuthenticateInput, task?: boolean }) => Promise<AuthenticateOutput | CodedError<"UNRECOGNIZED" | "AUTHORITY_MISMATCH" | "TOKEN_REVOKED" | "UNREFRESHABLE" | "INVALID_TOKEN" | "INVALID_KEY" | "FORGED_KEY" | "REVOKED_KEY">>
97
99
  issue: (request: { input: IssueInput, entity?: Partial<Entity>, task?: boolean }) => Promise<IssueOutput | CodedError<"INACCESSIBLE_SCOPE">>
98
100
  revoke: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<RevokeOutput>
99
101
  assign: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<Entity>