@spinajs/rbac-http-token 2.0.507

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 (198) hide show
  1. package/README.md +152 -0
  2. package/lib/cjs/actions.d.ts +119 -0
  3. package/lib/cjs/actions.d.ts.map +1 -0
  4. package/lib/cjs/actions.js +257 -0
  5. package/lib/cjs/actions.js.map +1 -0
  6. package/lib/cjs/cli/CreateToken.d.ts +13 -0
  7. package/lib/cjs/cli/CreateToken.d.ts.map +1 -0
  8. package/lib/cjs/cli/CreateToken.js +56 -0
  9. package/lib/cjs/cli/CreateToken.js.map +1 -0
  10. package/lib/cjs/cli/DeleteExpiredTokens.d.ts +11 -0
  11. package/lib/cjs/cli/DeleteExpiredTokens.d.ts.map +1 -0
  12. package/lib/cjs/cli/DeleteExpiredTokens.js +39 -0
  13. package/lib/cjs/cli/DeleteExpiredTokens.js.map +1 -0
  14. package/lib/cjs/cli/DeleteToken.d.ts +7 -0
  15. package/lib/cjs/cli/DeleteToken.d.ts.map +1 -0
  16. package/lib/cjs/cli/DeleteToken.js +36 -0
  17. package/lib/cjs/cli/DeleteToken.js.map +1 -0
  18. package/lib/cjs/cli/GrantTokenRole.d.ts +7 -0
  19. package/lib/cjs/cli/GrantTokenRole.d.ts.map +1 -0
  20. package/lib/cjs/cli/GrantTokenRole.js +37 -0
  21. package/lib/cjs/cli/GrantTokenRole.js.map +1 -0
  22. package/lib/cjs/cli/RevokeTokenRole.d.ts +12 -0
  23. package/lib/cjs/cli/RevokeTokenRole.d.ts.map +1 -0
  24. package/lib/cjs/cli/RevokeTokenRole.js +42 -0
  25. package/lib/cjs/cli/RevokeTokenRole.js.map +1 -0
  26. package/lib/cjs/config/rbac-http-token.d.ts +71 -0
  27. package/lib/cjs/config/rbac-http-token.d.ts.map +1 -0
  28. package/lib/cjs/config/rbac-http-token.js +98 -0
  29. package/lib/cjs/config/rbac-http-token.js.map +1 -0
  30. package/lib/cjs/controllers/AccessTokenController.d.ts +139 -0
  31. package/lib/cjs/controllers/AccessTokenController.d.ts.map +1 -0
  32. package/lib/cjs/controllers/AccessTokenController.js +264 -0
  33. package/lib/cjs/controllers/AccessTokenController.js.map +1 -0
  34. package/lib/cjs/dto/create-token-dto.d.ts +34 -0
  35. package/lib/cjs/dto/create-token-dto.d.ts.map +1 -0
  36. package/lib/cjs/dto/create-token-dto.js +35 -0
  37. package/lib/cjs/dto/create-token-dto.js.map +1 -0
  38. package/lib/cjs/events/AccessTokenCreated.d.ts +4 -0
  39. package/lib/cjs/events/AccessTokenCreated.d.ts.map +1 -0
  40. package/lib/cjs/events/AccessTokenCreated.js +18 -0
  41. package/lib/cjs/events/AccessTokenCreated.js.map +1 -0
  42. package/lib/cjs/events/AccessTokenDeleted.d.ts +4 -0
  43. package/lib/cjs/events/AccessTokenDeleted.d.ts.map +1 -0
  44. package/lib/cjs/events/AccessTokenDeleted.js +18 -0
  45. package/lib/cjs/events/AccessTokenDeleted.js.map +1 -0
  46. package/lib/cjs/events/AccessTokenEvent.d.ts +10 -0
  47. package/lib/cjs/events/AccessTokenEvent.d.ts.map +1 -0
  48. package/lib/cjs/events/AccessTokenEvent.js +26 -0
  49. package/lib/cjs/events/AccessTokenEvent.js.map +1 -0
  50. package/lib/cjs/events/AccessTokenRoleGranted.d.ts +7 -0
  51. package/lib/cjs/events/AccessTokenRoleGranted.d.ts.map +1 -0
  52. package/lib/cjs/events/AccessTokenRoleGranted.js +27 -0
  53. package/lib/cjs/events/AccessTokenRoleGranted.js.map +1 -0
  54. package/lib/cjs/events/AccessTokenRoleRevoked.d.ts +7 -0
  55. package/lib/cjs/events/AccessTokenRoleRevoked.d.ts.map +1 -0
  56. package/lib/cjs/events/AccessTokenRoleRevoked.js +27 -0
  57. package/lib/cjs/events/AccessTokenRoleRevoked.js.map +1 -0
  58. package/lib/cjs/events/index.d.ts +6 -0
  59. package/lib/cjs/events/index.d.ts.map +1 -0
  60. package/lib/cjs/events/index.js +22 -0
  61. package/lib/cjs/events/index.js.map +1 -0
  62. package/lib/cjs/generator.d.ts +12 -0
  63. package/lib/cjs/generator.d.ts.map +1 -0
  64. package/lib/cjs/generator.js +42 -0
  65. package/lib/cjs/generator.js.map +1 -0
  66. package/lib/cjs/index.d.ts +20 -0
  67. package/lib/cjs/index.d.ts.map +1 -0
  68. package/lib/cjs/index.js +39 -0
  69. package/lib/cjs/index.js.map +1 -0
  70. package/lib/cjs/interfaces.d.ts +57 -0
  71. package/lib/cjs/interfaces.d.ts.map +1 -0
  72. package/lib/cjs/interfaces.js +11 -0
  73. package/lib/cjs/interfaces.js.map +1 -0
  74. package/lib/cjs/middlewares.d.ts +76 -0
  75. package/lib/cjs/middlewares.d.ts.map +1 -0
  76. package/lib/cjs/middlewares.js +181 -0
  77. package/lib/cjs/middlewares.js.map +1 -0
  78. package/lib/cjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.d.ts +6 -0
  79. package/lib/cjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.d.ts.map +1 -0
  80. package/lib/cjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.js +38 -0
  81. package/lib/cjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.js.map +1 -0
  82. package/lib/cjs/models/AccessToken.d.ts +55 -0
  83. package/lib/cjs/models/AccessToken.d.ts.map +1 -0
  84. package/lib/cjs/models/AccessToken.js +73 -0
  85. package/lib/cjs/models/AccessToken.js.map +1 -0
  86. package/lib/cjs/package.json +1 -0
  87. package/lib/cjs/policies/NoImpersonationPolicy.d.ts +28 -0
  88. package/lib/cjs/policies/NoImpersonationPolicy.d.ts.map +1 -0
  89. package/lib/cjs/policies/NoImpersonationPolicy.js +45 -0
  90. package/lib/cjs/policies/NoImpersonationPolicy.js.map +1 -0
  91. package/lib/cjs/policies/NoTokenAuthPolicy.d.ts +12 -0
  92. package/lib/cjs/policies/NoTokenAuthPolicy.d.ts.map +1 -0
  93. package/lib/cjs/policies/NoTokenAuthPolicy.js +24 -0
  94. package/lib/cjs/policies/NoTokenAuthPolicy.js.map +1 -0
  95. package/lib/cjs/policies/TokenPolicy.d.ts +49 -0
  96. package/lib/cjs/policies/TokenPolicy.d.ts.map +1 -0
  97. package/lib/cjs/policies/TokenPolicy.js +79 -0
  98. package/lib/cjs/policies/TokenPolicy.js.map +1 -0
  99. package/lib/mjs/actions.d.ts +119 -0
  100. package/lib/mjs/actions.d.ts.map +1 -0
  101. package/lib/mjs/actions.js +242 -0
  102. package/lib/mjs/actions.js.map +1 -0
  103. package/lib/mjs/cli/CreateToken.d.ts +13 -0
  104. package/lib/mjs/cli/CreateToken.d.ts.map +1 -0
  105. package/lib/mjs/cli/CreateToken.js +53 -0
  106. package/lib/mjs/cli/CreateToken.js.map +1 -0
  107. package/lib/mjs/cli/DeleteExpiredTokens.d.ts +11 -0
  108. package/lib/mjs/cli/DeleteExpiredTokens.d.ts.map +1 -0
  109. package/lib/mjs/cli/DeleteExpiredTokens.js +36 -0
  110. package/lib/mjs/cli/DeleteExpiredTokens.js.map +1 -0
  111. package/lib/mjs/cli/DeleteToken.d.ts +7 -0
  112. package/lib/mjs/cli/DeleteToken.d.ts.map +1 -0
  113. package/lib/mjs/cli/DeleteToken.js +33 -0
  114. package/lib/mjs/cli/DeleteToken.js.map +1 -0
  115. package/lib/mjs/cli/GrantTokenRole.d.ts +7 -0
  116. package/lib/mjs/cli/GrantTokenRole.d.ts.map +1 -0
  117. package/lib/mjs/cli/GrantTokenRole.js +34 -0
  118. package/lib/mjs/cli/GrantTokenRole.js.map +1 -0
  119. package/lib/mjs/cli/RevokeTokenRole.d.ts +12 -0
  120. package/lib/mjs/cli/RevokeTokenRole.d.ts.map +1 -0
  121. package/lib/mjs/cli/RevokeTokenRole.js +39 -0
  122. package/lib/mjs/cli/RevokeTokenRole.js.map +1 -0
  123. package/lib/mjs/config/rbac-http-token.d.ts +71 -0
  124. package/lib/mjs/config/rbac-http-token.d.ts.map +1 -0
  125. package/lib/mjs/config/rbac-http-token.js +96 -0
  126. package/lib/mjs/config/rbac-http-token.js.map +1 -0
  127. package/lib/mjs/controllers/AccessTokenController.d.ts +139 -0
  128. package/lib/mjs/controllers/AccessTokenController.d.ts.map +1 -0
  129. package/lib/mjs/controllers/AccessTokenController.js +261 -0
  130. package/lib/mjs/controllers/AccessTokenController.js.map +1 -0
  131. package/lib/mjs/dto/create-token-dto.d.ts +34 -0
  132. package/lib/mjs/dto/create-token-dto.d.ts.map +1 -0
  133. package/lib/mjs/dto/create-token-dto.js +32 -0
  134. package/lib/mjs/dto/create-token-dto.js.map +1 -0
  135. package/lib/mjs/events/AccessTokenCreated.d.ts +4 -0
  136. package/lib/mjs/events/AccessTokenCreated.d.ts.map +1 -0
  137. package/lib/mjs/events/AccessTokenCreated.js +15 -0
  138. package/lib/mjs/events/AccessTokenCreated.js.map +1 -0
  139. package/lib/mjs/events/AccessTokenDeleted.d.ts +4 -0
  140. package/lib/mjs/events/AccessTokenDeleted.d.ts.map +1 -0
  141. package/lib/mjs/events/AccessTokenDeleted.js +15 -0
  142. package/lib/mjs/events/AccessTokenDeleted.js.map +1 -0
  143. package/lib/mjs/events/AccessTokenEvent.d.ts +10 -0
  144. package/lib/mjs/events/AccessTokenEvent.d.ts.map +1 -0
  145. package/lib/mjs/events/AccessTokenEvent.js +23 -0
  146. package/lib/mjs/events/AccessTokenEvent.js.map +1 -0
  147. package/lib/mjs/events/AccessTokenRoleGranted.d.ts +7 -0
  148. package/lib/mjs/events/AccessTokenRoleGranted.d.ts.map +1 -0
  149. package/lib/mjs/events/AccessTokenRoleGranted.js +24 -0
  150. package/lib/mjs/events/AccessTokenRoleGranted.js.map +1 -0
  151. package/lib/mjs/events/AccessTokenRoleRevoked.d.ts +7 -0
  152. package/lib/mjs/events/AccessTokenRoleRevoked.d.ts.map +1 -0
  153. package/lib/mjs/events/AccessTokenRoleRevoked.js +24 -0
  154. package/lib/mjs/events/AccessTokenRoleRevoked.js.map +1 -0
  155. package/lib/mjs/events/index.d.ts +6 -0
  156. package/lib/mjs/events/index.d.ts.map +1 -0
  157. package/lib/mjs/events/index.js +6 -0
  158. package/lib/mjs/events/index.js.map +1 -0
  159. package/lib/mjs/generator.d.ts +12 -0
  160. package/lib/mjs/generator.d.ts.map +1 -0
  161. package/lib/mjs/generator.js +39 -0
  162. package/lib/mjs/generator.js.map +1 -0
  163. package/lib/mjs/index.d.ts +20 -0
  164. package/lib/mjs/index.d.ts.map +1 -0
  165. package/lib/mjs/index.js +23 -0
  166. package/lib/mjs/index.js.map +1 -0
  167. package/lib/mjs/interfaces.d.ts +57 -0
  168. package/lib/mjs/interfaces.d.ts.map +1 -0
  169. package/lib/mjs/interfaces.js +7 -0
  170. package/lib/mjs/interfaces.js.map +1 -0
  171. package/lib/mjs/middlewares.d.ts +76 -0
  172. package/lib/mjs/middlewares.d.ts.map +1 -0
  173. package/lib/mjs/middlewares.js +178 -0
  174. package/lib/mjs/middlewares.js.map +1 -0
  175. package/lib/mjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.d.ts +6 -0
  176. package/lib/mjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.d.ts.map +1 -0
  177. package/lib/mjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.js +35 -0
  178. package/lib/mjs/migrations/RbacHttpTokenInitial_2026_08_11_01_00_00.js.map +1 -0
  179. package/lib/mjs/models/AccessToken.d.ts +55 -0
  180. package/lib/mjs/models/AccessToken.d.ts.map +1 -0
  181. package/lib/mjs/models/AccessToken.js +70 -0
  182. package/lib/mjs/models/AccessToken.js.map +1 -0
  183. package/lib/mjs/package.json +1 -0
  184. package/lib/mjs/policies/NoImpersonationPolicy.d.ts +28 -0
  185. package/lib/mjs/policies/NoImpersonationPolicy.d.ts.map +1 -0
  186. package/lib/mjs/policies/NoImpersonationPolicy.js +41 -0
  187. package/lib/mjs/policies/NoImpersonationPolicy.js.map +1 -0
  188. package/lib/mjs/policies/NoTokenAuthPolicy.d.ts +12 -0
  189. package/lib/mjs/policies/NoTokenAuthPolicy.d.ts.map +1 -0
  190. package/lib/mjs/policies/NoTokenAuthPolicy.js +20 -0
  191. package/lib/mjs/policies/NoTokenAuthPolicy.js.map +1 -0
  192. package/lib/mjs/policies/TokenPolicy.d.ts +49 -0
  193. package/lib/mjs/policies/TokenPolicy.d.ts.map +1 -0
  194. package/lib/mjs/policies/TokenPolicy.js +75 -0
  195. package/lib/mjs/policies/TokenPolicy.js.map +1 -0
  196. package/lib/tsconfig.cjs.tsbuildinfo +1 -0
  197. package/lib/tsconfig.mjs.tsbuildinfo +1 -0
  198. package/package.json +78 -0
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # @spinajs/rbac-http-token
2
+
3
+ Personal access tokens (PAT) for spinajs HTTP routes. Opaque `spt_...` tokens,
4
+ stored hashed (SHA-256), assigned to users, with rbac role intersection and
5
+ optional expiration.
6
+
7
+ ## Securing a route
8
+
9
+ ```ts
10
+ import { BaseController, BasePath, Get, Policy } from '@spinajs/http';
11
+ import { Permission, Resource } from '@spinajs/rbac-http';
12
+ import { TokenPolicy } from '@spinajs/rbac-http-token';
13
+
14
+ @BasePath('api')
15
+ @Resource('my.resource')
16
+ export class ApiController extends BaseController {
17
+ @Get('data')
18
+ @Permission(['readOwn']) // permission metadata + the RbacPolicy group
19
+ @Policy(TokenPolicy) // METHOD level, BELOW @Permission
20
+ public async data() { ... }
21
+ }
22
+ ```
23
+
24
+ The placement is load bearing (see the docblock on `src/policies/TokenPolicy.ts`):
25
+
26
+ - `@Policy(TokenPolicy)` must sit at the **method** level. A class level
27
+ declaration lands in the CONTROLLER policy scope, and the controller scope is
28
+ ANDed with the route scope - it would then have to hold together with the
29
+ `RbacPolicy` group that `@Permission` pushes on the route, and `RbacPolicy`
30
+ demands an authorized session that a token request never has. Every token
31
+ request would 401. Method level puts it in the same scope as `@Permission`,
32
+ and groups within one scope are ORed: a token request satisfies the
33
+ `TokenPolicy` group, a session request satisfies the `RbacPolicy` one.
34
+ - For the same reason it must not be passed as `@Permission`'s `also` argument -
35
+ `@Permission(['readOwn'], TokenPolicy)` bundles both into a single ANDed group.
36
+ - Writing it **below** `@Permission` is deliberate: decorators apply bottom up,
37
+ so when no group holds, the reported rejection is this policy's 403
38
+ ("token role(s) ... do not have permission ...") instead of `RbacPolicy`'s
39
+ misleading "user not logged" 401.
40
+ - Because the two route-scope groups are **ORed**, adding `@Policy(TokenPolicy)`
41
+ *widens* the route: it becomes reachable by a token holder whose effective
42
+ roles carry the grant **and** by a session holder whose active role carries
43
+ it, exactly as before. It is not a "tokens only" switch. Use
44
+ `NoTokenAuthPolicy` (below) for the opposite - a route sessions may use and
45
+ tokens may not.
46
+ - The `@Permission` line stays required - `TokenPolicy` reads the same route
47
+ permission metadata, and a route without it silently inherits the
48
+ controller-level default `['readOwn']`.
49
+
50
+ Clients authenticate with `Authorization: Bearer spt_...` (scheme name is
51
+ case-insensitive) or the configured fallback header (`x-api-key` by default).
52
+
53
+ ## Token semantics
54
+
55
+ - Effective roles at request time = token roles ∩ owner's current roles. Roles
56
+ are re-intersected on every request, so a role revoked on the user takes
57
+ effect immediately for all of their tokens.
58
+ - A token whose effective role set becomes empty stops authenticating.
59
+ - Permission checks run against the **whole** effective role set. A session has
60
+ an *active role* it can switch at runtime (`POST /auth/active-role`) and is
61
+ authorized by that one role; a token has no session and no way to switch, so
62
+ it would be stuck with whichever role happened to be first. Instead
63
+ `TokenAuthMiddleware` clears `req.storage.ActiveRole`, and every consumer
64
+ (`checkRoutePermission`, the orm rbac query middleware) falls back to
65
+ `User.Role` - already narrowed to the effective set.
66
+ - Owner deactivated / banned / soft-deleted => all their tokens stop working.
67
+ - `ExpiresAt` null/absent => token never expires.
68
+ - Plaintext shown exactly once at creation; only its SHA-256 hash is stored, and
69
+ the hash itself is `@Hidden()` so it never reaches a response either.
70
+ - Session-authenticated requests ignore tokens: `TokenAuthMiddleware` runs after
71
+ `RbacMiddleware` and bails out when a session is present, so a request never
72
+ mixes both.
73
+ - Token-authenticated responses are sent with `Cache-Control: no-store`.
74
+ - Tokens cannot manage tokens - the management API is guarded by
75
+ `NoTokenAuthPolicy` (no self-replication).
76
+ - Tokens cannot be minted or managed while impersonating another user -
77
+ `NoImpersonationPolicy` (also exported) rejects such requests with 403. An
78
+ impersonated session is supervised and revocable; a token it minted would
79
+ outlive it, carry the victim's roles, and be invisible to them.
80
+ - A token always keeps at least one role - revoking the last one is refused
81
+ (400 / `E_TOKEN_ROLE_NOT_ALLOWED`). Delete the token to revoke it entirely.
82
+ - A token can never carry a role its owner does not hold, at creation or grant.
83
+ - `LastUsedAt` is stamped on use, throttled to one write per
84
+ `rbac.token.lastUsedUpdateInterval` seconds.
85
+
86
+ ## HTTP API (session auth required)
87
+
88
+ All routes live under the `user.tokens` resource and operate on the caller's own
89
+ tokens only - a foreign uuid simply reads as 404.
90
+
91
+ | Route | Description |
92
+ |---|---|
93
+ | `GET user/tokens` | list own tokens |
94
+ | `POST user/tokens` | create (`{ Name, Roles, ExpiresAt? }`), returns plaintext once |
95
+ | `DELETE user/tokens/:uuid` | revoke (delete) a token |
96
+ | `PUT user/tokens/:uuid/roles/:role` | grant role |
97
+ | `DELETE user/tokens/:uuid/roles/:role` | revoke role |
98
+
99
+ Responses: `400` for role refusals (role not held by the caller, last role
100
+ revoke), `401` without a valid session, `403` when the request is
101
+ token-authenticated or impersonated, `404` for a token the caller does not own.
102
+
103
+ ## CLI
104
+
105
+ ```
106
+ rbac:token-create <userIdOrUuid> -n <name> -r <roles> [-e <iso>]
107
+ rbac:token-delete <uuid>
108
+ rbac:token-grant <uuid> <role>
109
+ rbac:token-revoke <uuid> <role>
110
+ rbac:token-delete-expired # run cyclically from a worker
111
+ ```
112
+
113
+ - `-r, --roles` is a comma separated list, and must be a subset of the owner's
114
+ roles.
115
+ - `-e, --expires` takes an ISO instant. Omitting the flag entirely means the
116
+ token never expires; an empty or unparsable value is refused rather than
117
+ silently treated as "no expiration".
118
+ - `rbac:token-create` prints the plaintext once - it cannot be retrieved later.
119
+ - `rbac:token-delete-expired` is housekeeping only; expired tokens are already
120
+ refused at authentication time.
121
+
122
+ ## Configuration
123
+
124
+ **A consuming app MUST import the package in its bootstrap** -
125
+ `import '@spinajs/rbac-http-token';` - even when it uses nothing from it
126
+ directly. The model, the migration and the auth middleware register themselves
127
+ through decorators when their module is loaded, and the migration in particular
128
+ is *not* found by filesystem scan (see the comment on `system.dirs` in
129
+ `src/config/rbac-http-token.ts` and the note at the bottom of this file). Without
130
+ that import the `rbac_access_tokens` table is never created.
131
+
132
+ See `src/config/rbac-http-token.ts` - `rbac.token.*`:
133
+
134
+ | Key | Default | Meaning |
135
+ |---|---|---|
136
+ | `rbac.token.generation.service` | `SecureRandomTokenProvider` | `AccessTokenGenerationProvider` implementation used to generate/hash tokens |
137
+ | `rbac.token.prefix` | `spt_` | stable plaintext prefix, lets secret scanners recognise leaked tokens |
138
+ | `rbac.token.length` | `32` | random bytes per token (32 = 256 bit entropy) |
139
+ | `rbac.token.headerName` | `x-api-key` | fallback header checked when no `Authorization: Bearer` is present |
140
+ | `rbac.token.lastUsedUpdateInterval` | `60` | seconds between `LastUsedAt` writes for a busy token |
141
+
142
+ The package also ships `rbac.grants` for the `user.tokens` resource and queue
143
+ routing that sends its events (`AccessTokenCreated`, `AccessTokenDeleted`,
144
+ `AccessTokenRoleGranted`, `AccessTokenRoleRevoked`) to rbac's empty connection
145
+ instead of the application's default broker.
146
+
147
+ `system.dirs` ships `cli` and `controllers` only - those are found by filesystem
148
+ scan. `migrations` is deliberately absent: a non-empty `system.dirs.migrations`
149
+ makes the orm replace its build-layout defaults, which would switch off
150
+ migration discovery in every consuming app. The initial migration carries
151
+ `@Migration`, is exported from the package index, and is picked up from the DI
152
+ registry instead.
@@ -0,0 +1,119 @@
1
+ import { DateTime } from 'luxon';
2
+ import { User } from '@spinajs/rbac';
3
+ import { AccessToken } from './models/AccessToken.js';
4
+ /**
5
+ * Failure codes carried by every {@link ErrorCode} this module throws.
6
+ *
7
+ * Named `E_TOKEN_CODES` rather than the shorter `E_CODES` on purpose:
8
+ * `@spinajs/rbac` exports an enum called `E_CODES` too, and both are re-exported
9
+ * from their package index. A downstream barrel that does
10
+ * `export * from '@spinajs/rbac'; export * from '@spinajs/rbac-http-token';`
11
+ * would collide on the NAME while the members underneath carry different
12
+ * numeric values - the kind of clash that resolves silently in a barrel and
13
+ * makes an `err.code === E_CODES.X` comparison answer the wrong question.
14
+ */
15
+ export declare enum E_TOKEN_CODES {
16
+ E_TOKEN_NOT_FOUND = 0,
17
+ E_TOKEN_EXPIRED = 1,
18
+ E_TOKEN_OWNER_INVALID = 2,
19
+ E_TOKEN_ROLE_NOT_ALLOWED = 3
20
+ }
21
+ /**
22
+ * Resolves an AccessToken from an instance or its uuid.
23
+ */
24
+ export declare function _token(token: AccessToken | string): () => Promise<AccessToken>;
25
+ /**
26
+ * Resolves the owning user by instance, numeric id or uuid, with metadata
27
+ * populated ( needed for IsBanned checks downstream ).
28
+ */
29
+ export declare function _owner(user: User | number | string): () => Promise<User>;
30
+ /**
31
+ * Creates a new access token for a user.
32
+ *
33
+ * Roles must be a non-empty subset of the owner's current roles. `expiresAt`
34
+ * null means the token never expires. The plaintext is returned once and
35
+ * never persisted - only its hash is stored.
36
+ *
37
+ * @param user - owner: {@link User} instance, numeric id or uuid
38
+ * @param name - human readable label
39
+ * @param roles - roles carried by the token, subset of the owner's roles
40
+ * @param expiresAt - absolute expiration, or null for a token that never expires
41
+ */
42
+ export declare function createToken(user: User | number | string, name: string, roles: string[], expiresAt: DateTime | null): Promise<{
43
+ Token: AccessToken;
44
+ Plaintext: string;
45
+ }>;
46
+ /**
47
+ * Permanently deletes ( revokes ) a token.
48
+ *
49
+ * @param token - {@link AccessToken} instance or its uuid
50
+ */
51
+ export declare function deleteToken(token: AccessToken | string): Promise<void>;
52
+ /**
53
+ * Adds a role to a token. The role must be held by the token owner.
54
+ *
55
+ * @param token - {@link AccessToken} instance or its uuid
56
+ * @param role - role name to grant
57
+ */
58
+ export declare function grantTokenRole(token: AccessToken | string, role: string): Promise<AccessToken>;
59
+ /**
60
+ * Removes a role from a token.
61
+ *
62
+ * Not symmetric with {@link grantTokenRole}: revoking only ever narrows what a
63
+ * token may do, so it needs no owner check.
64
+ *
65
+ * A token must always keep at least one role - revoking the last one is
66
+ * refused, and full revocation means deleting the token. Beyond being
67
+ * meaningless ( a role-less token authorises nothing ), an empty list is
68
+ * actively corrupting: `@Set()` columns round-trip through `SqlSetConverter`,
69
+ * which stores `[]` as `''` and reads `''` back as `['']`. That phantom empty
70
+ * role then survives every later grant, permanently.
71
+ *
72
+ * @param token - {@link AccessToken} instance or its uuid
73
+ * @param role - role name to revoke
74
+ * @throws ErrorCode E_TOKEN_ROLE_NOT_ALLOWED when `role` is the token's last role
75
+ */
76
+ export declare function revokeTokenRole(token: AccessToken | string, role: string): Promise<AccessToken>;
77
+ export interface ITokenValidationResult {
78
+ User: User;
79
+ Token: AccessToken;
80
+ /**
81
+ * Token roles that the owner still holds. Permission checks run with these.
82
+ */
83
+ EffectiveRoles: string[];
84
+ }
85
+ /**
86
+ * Validates a presented plaintext token.
87
+ *
88
+ * Checks, in order: token exists ( by hash ), token not expired, owner active /
89
+ * not soft-deleted / not banned, effective role intersection non-empty.
90
+ * Throws ErrorCode with {@link E_TOKEN_CODES} on every failure path.
91
+ *
92
+ * The owner checks are spelled out instead of reusing the `isActiveUser` query
93
+ * scope on purpose - a single "no such active user" result cannot say WHY, and
94
+ * callers need to tell an expired token from a banned account.
95
+ *
96
+ * @param plaintext - token exactly as presented by the client
97
+ */
98
+ export declare function validateToken(plaintext: string): Promise<ITokenValidationResult>;
99
+ /**
100
+ * Deletes every token whose expiration has passed. Returns the deleted count.
101
+ *
102
+ * Intended for cyclic execution from a worker ( see `rbac:token-delete-expired` ).
103
+ * Expired tokens are already refused by {@link validateToken}, so this is
104
+ * housekeeping, not enforcement.
105
+ */
106
+ export declare function deleteExpiredTokens(): Promise<number>;
107
+ /**
108
+ * Updates `LastUsedAt`, throttled: writes only when the stamp is absent or
109
+ * older than `intervalSeconds`.
110
+ *
111
+ * Every authenticated request would otherwise write to the token row; the stamp
112
+ * only needs to be accurate enough to spot unused tokens. Callers may
113
+ * fire-and-forget.
114
+ *
115
+ * @param token - token to stamp
116
+ * @param intervalSeconds - minimum time between two writes
117
+ */
118
+ export declare function touchToken(token: AccessToken, intervalSeconds: number): Promise<void>;
119
+ //# sourceMappingURL=actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/actions.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAOrC,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAItD;;;;;;;;;;GAUG;AACH,oBAAY,aAAa;IACvB,iBAAiB,IAAA;IACjB,eAAe,IAAA;IACf,qBAAqB,IAAA;IACrB,wBAAwB,IAAA;CACzB;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,CAM9E;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAUxE;AA2BD;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,QAAQ,GAAG,IAAI,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CA0B7K;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO5E;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAcpG;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAmBrG;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,EAAE,WAAW,CAAC;IAEnB;;OAEG;IACH,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA4CtF;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC,CAU3D;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS3F"}
@@ -0,0 +1,257 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.E_TOKEN_CODES = void 0;
7
+ exports._token = _token;
8
+ exports._owner = _owner;
9
+ exports.createToken = createToken;
10
+ exports.deleteToken = deleteToken;
11
+ exports.grantTokenRole = grantTokenRole;
12
+ exports.revokeTokenRole = revokeTokenRole;
13
+ exports.validateToken = validateToken;
14
+ exports.deleteExpiredTokens = deleteExpiredTokens;
15
+ exports.touchToken = touchToken;
16
+ const lodash_1 = __importDefault(require("lodash"));
17
+ const luxon_1 = require("luxon");
18
+ const rbac_1 = require("@spinajs/rbac");
19
+ const util_1 = require("@spinajs/util");
20
+ const configuration_1 = require("@spinajs/configuration");
21
+ const queue_1 = require("@spinajs/queue");
22
+ const exceptions_1 = require("@spinajs/exceptions");
23
+ const AccessToken_js_1 = require("./models/AccessToken.js");
24
+ const interfaces_js_1 = require("./interfaces.js");
25
+ const index_js_1 = require("./events/index.js");
26
+ /**
27
+ * Failure codes carried by every {@link ErrorCode} this module throws.
28
+ *
29
+ * Named `E_TOKEN_CODES` rather than the shorter `E_CODES` on purpose:
30
+ * `@spinajs/rbac` exports an enum called `E_CODES` too, and both are re-exported
31
+ * from their package index. A downstream barrel that does
32
+ * `export * from '@spinajs/rbac'; export * from '@spinajs/rbac-http-token';`
33
+ * would collide on the NAME while the members underneath carry different
34
+ * numeric values - the kind of clash that resolves silently in a barrel and
35
+ * makes an `err.code === E_CODES.X` comparison answer the wrong question.
36
+ */
37
+ var E_TOKEN_CODES;
38
+ (function (E_TOKEN_CODES) {
39
+ E_TOKEN_CODES[E_TOKEN_CODES["E_TOKEN_NOT_FOUND"] = 0] = "E_TOKEN_NOT_FOUND";
40
+ E_TOKEN_CODES[E_TOKEN_CODES["E_TOKEN_EXPIRED"] = 1] = "E_TOKEN_EXPIRED";
41
+ E_TOKEN_CODES[E_TOKEN_CODES["E_TOKEN_OWNER_INVALID"] = 2] = "E_TOKEN_OWNER_INVALID";
42
+ E_TOKEN_CODES[E_TOKEN_CODES["E_TOKEN_ROLE_NOT_ALLOWED"] = 3] = "E_TOKEN_ROLE_NOT_ALLOWED";
43
+ })(E_TOKEN_CODES || (exports.E_TOKEN_CODES = E_TOKEN_CODES = {}));
44
+ /**
45
+ * Resolves an AccessToken from an instance or its uuid.
46
+ */
47
+ function _token(token) {
48
+ if (lodash_1.default.isString(token)) {
49
+ return () => AccessToken_js_1.AccessToken.where('Uuid', token).firstOrFail();
50
+ }
51
+ return () => Promise.resolve(token);
52
+ }
53
+ /**
54
+ * Resolves the owning user by instance, numeric id or uuid, with metadata
55
+ * populated ( needed for IsBanned checks downstream ).
56
+ */
57
+ function _owner(user) {
58
+ if (lodash_1.default.isString(user)) {
59
+ return () => rbac_1.User.where('Uuid', user).populate('Metadata').firstOrFail();
60
+ }
61
+ if (lodash_1.default.isNumber(user)) {
62
+ return () => rbac_1.User.where('Id', user).populate('Metadata').firstOrFail();
63
+ }
64
+ return () => Promise.resolve(user);
65
+ }
66
+ /**
67
+ * Emits a token-related event through the queue service and forwards the token.
68
+ *
69
+ * @param event - constructor of the {@link AccessTokenEvent} subclass to emit
70
+ * @param args - additional arguments forwarded to the event constructor
71
+ */
72
+ function _token_ev(event, ...args) {
73
+ return async (t) => {
74
+ await (0, queue_1._ev)(new event(t, ...args))();
75
+ return t;
76
+ };
77
+ }
78
+ /**
79
+ * Ensures every role in `roles` is currently held by `owner`.
80
+ * A token must never carry a role its owner does not have.
81
+ */
82
+ function _assert_roles_subset(owner, roles) {
83
+ const missing = roles.filter((r) => !owner.Role.includes(r));
84
+ if (missing.length !== 0) {
85
+ throw new exceptions_1.ErrorCode(E_TOKEN_CODES.E_TOKEN_ROLE_NOT_ALLOWED, `Owner does not hold role(s): ${missing.join(', ')}`, { roles: missing });
86
+ }
87
+ }
88
+ /**
89
+ * Creates a new access token for a user.
90
+ *
91
+ * Roles must be a non-empty subset of the owner's current roles. `expiresAt`
92
+ * null means the token never expires. The plaintext is returned once and
93
+ * never persisted - only its hash is stored.
94
+ *
95
+ * @param user - owner: {@link User} instance, numeric id or uuid
96
+ * @param name - human readable label
97
+ * @param roles - roles carried by the token, subset of the owner's roles
98
+ * @param expiresAt - absolute expiration, or null for a token that never expires
99
+ */
100
+ async function createToken(user, name, roles, expiresAt) {
101
+ name = (0, util_1._check_arg)((0, util_1._trim)(), (0, util_1._non_empty)(), (0, util_1._max_length)(128))(name, 'name');
102
+ roles = (0, util_1._check_arg)((0, util_1._non_nil)(), (0, util_1._non_empty)())(roles, 'roles');
103
+ const generator = await (0, configuration_1._service)('rbac.token.generation', interfaces_js_1.AccessTokenGenerationProvider)();
104
+ const generated = await generator.generate();
105
+ return (0, util_1._chain)(_owner(user), (0, util_1._tap)(async (u) => _assert_roles_subset(u, roles)), async (u) => {
106
+ const token = new AccessToken_js_1.AccessToken({
107
+ Name: name,
108
+ Token: generated.Hash,
109
+ Roles: lodash_1.default.uniq(roles),
110
+ // `ExpiresAt` is optional rather than nullable ( see the model ), so a
111
+ // "never expires" token leaves the property unset instead of holding null.
112
+ ExpiresAt: expiresAt ?? undefined,
113
+ user_id: u.Id,
114
+ });
115
+ await token.insert();
116
+ return token;
117
+ }, _token_ev(index_js_1.AccessTokenCreated), (t) => ({ Token: t, Plaintext: generated.Plaintext }));
118
+ }
119
+ /**
120
+ * Permanently deletes ( revokes ) a token.
121
+ *
122
+ * @param token - {@link AccessToken} instance or its uuid
123
+ */
124
+ async function deleteToken(token) {
125
+ return (0, util_1._chain)(_token(token), (0, util_1._tap)((t) => t.destroy()), _token_ev(index_js_1.AccessTokenDeleted), () => undefined);
126
+ }
127
+ /**
128
+ * Adds a role to a token. The role must be held by the token owner.
129
+ *
130
+ * @param token - {@link AccessToken} instance or its uuid
131
+ * @param role - role name to grant
132
+ */
133
+ async function grantTokenRole(token, role) {
134
+ role = (0, util_1._check_arg)((0, util_1._trim)(), (0, util_1._non_empty)())(role, 'role');
135
+ return (0, util_1._chain)(_token(token), (0, util_1._tap)(async (t) => {
136
+ const owner = await _owner(t.user_id)();
137
+ _assert_roles_subset(owner, [role]);
138
+ t.Roles = lodash_1.default.uniq([...t.Roles, role]);
139
+ await t.update();
140
+ }), _token_ev(index_js_1.AccessTokenRoleGranted, role));
141
+ }
142
+ /**
143
+ * Removes a role from a token.
144
+ *
145
+ * Not symmetric with {@link grantTokenRole}: revoking only ever narrows what a
146
+ * token may do, so it needs no owner check.
147
+ *
148
+ * A token must always keep at least one role - revoking the last one is
149
+ * refused, and full revocation means deleting the token. Beyond being
150
+ * meaningless ( a role-less token authorises nothing ), an empty list is
151
+ * actively corrupting: `@Set()` columns round-trip through `SqlSetConverter`,
152
+ * which stores `[]` as `''` and reads `''` back as `['']`. That phantom empty
153
+ * role then survives every later grant, permanently.
154
+ *
155
+ * @param token - {@link AccessToken} instance or its uuid
156
+ * @param role - role name to revoke
157
+ * @throws ErrorCode E_TOKEN_ROLE_NOT_ALLOWED when `role` is the token's last role
158
+ */
159
+ async function revokeTokenRole(token, role) {
160
+ role = (0, util_1._check_arg)((0, util_1._trim)(), (0, util_1._non_empty)())(role, 'role');
161
+ return (0, util_1._chain)(_token(token), (0, util_1._tap)(async (t) => {
162
+ const remaining = t.Roles.filter((r) => r !== role);
163
+ // checked BEFORE mutating, so a refused revoke leaves the instance and
164
+ // the row exactly as they were
165
+ if (remaining.length === 0) {
166
+ throw new exceptions_1.ErrorCode(E_TOKEN_CODES.E_TOKEN_ROLE_NOT_ALLOWED, 'Cannot revoke the last role from a token - delete the token instead', { token: t.Uuid });
167
+ }
168
+ t.Roles = remaining;
169
+ await t.update();
170
+ }), _token_ev(index_js_1.AccessTokenRoleRevoked, role));
171
+ }
172
+ /**
173
+ * Validates a presented plaintext token.
174
+ *
175
+ * Checks, in order: token exists ( by hash ), token not expired, owner active /
176
+ * not soft-deleted / not banned, effective role intersection non-empty.
177
+ * Throws ErrorCode with {@link E_TOKEN_CODES} on every failure path.
178
+ *
179
+ * The owner checks are spelled out instead of reusing the `isActiveUser` query
180
+ * scope on purpose - a single "no such active user" result cannot say WHY, and
181
+ * callers need to tell an expired token from a banned account.
182
+ *
183
+ * @param plaintext - token exactly as presented by the client
184
+ */
185
+ async function validateToken(plaintext) {
186
+ // Degenerate input is a failed authentication like any other, so it has to
187
+ // leave through the same door. `_check_arg(_trim(), _non_empty())` would
188
+ // throw a raw InvalidArgument here - and a bare TypeError for nil, since
189
+ // `_non_empty` dereferences `.length` - breaking the "always an ErrorCode"
190
+ // contract this function is called under. Reported as E_TOKEN_NOT_FOUND with
191
+ // the unknown-token message on purpose: a caller must not learn WHY.
192
+ if (!lodash_1.default.isString(plaintext) || plaintext.trim().length === 0) {
193
+ throw new exceptions_1.ErrorCode(E_TOKEN_CODES.E_TOKEN_NOT_FOUND, 'Access token not found');
194
+ }
195
+ plaintext = plaintext.trim();
196
+ const generator = await (0, configuration_1._service)('rbac.token.generation', interfaces_js_1.AccessTokenGenerationProvider)();
197
+ const hash = generator.hash(plaintext);
198
+ const token = await AccessToken_js_1.AccessToken.where('Token', hash).first();
199
+ if (!token) {
200
+ throw new exceptions_1.ErrorCode(E_TOKEN_CODES.E_TOKEN_NOT_FOUND, 'Access token not found');
201
+ }
202
+ if (token.IsExpired) {
203
+ throw new exceptions_1.ErrorCode(E_TOKEN_CODES.E_TOKEN_EXPIRED, 'Access token expired', { token: token.Uuid });
204
+ }
205
+ // Metadata carries the ban flag, so it must be populated - without it
206
+ // `IsBanned` silently answers false and every banned owner authenticates.
207
+ const owner = await rbac_1.User.where('Id', token.user_id).populate('Metadata').first();
208
+ // `DeletedAt` is defence in depth: the orm's soft-delete scope already appends
209
+ // `DeletedAt IS NULL` to User selects, so a deleted owner arrives as undefined.
210
+ if (!owner || !owner.IsActive || owner.DeletedAt || owner.IsBanned) {
211
+ throw new exceptions_1.ErrorCode(E_TOKEN_CODES.E_TOKEN_OWNER_INVALID, 'Access token owner is not allowed to authenticate', { token: token.Uuid });
212
+ }
213
+ // roles are re-intersected on every request rather than trusted from the row:
214
+ // a role revoked on the user must take effect immediately, without having to
215
+ // hunt down every token that still carries it
216
+ const effective = token.Roles.filter((r) => owner.Role.includes(r));
217
+ if (effective.length === 0) {
218
+ throw new exceptions_1.ErrorCode(E_TOKEN_CODES.E_TOKEN_ROLE_NOT_ALLOWED, 'Access token has no effective roles', { token: token.Uuid });
219
+ }
220
+ return { User: owner, Token: token, EffectiveRoles: effective };
221
+ }
222
+ /**
223
+ * Deletes every token whose expiration has passed. Returns the deleted count.
224
+ *
225
+ * Intended for cyclic execution from a worker ( see `rbac:token-delete-expired` ).
226
+ * Expired tokens are already refused by {@link validateToken}, so this is
227
+ * housekeeping, not enforcement.
228
+ */
229
+ async function deleteExpiredTokens() {
230
+ // `whereNotNull` is not redundant next to `<=`: a null expiry means "never
231
+ // expires", and null comparisons must not sweep those rows away.
232
+ const expired = await AccessToken_js_1.AccessToken.where('ExpiresAt', '<=', luxon_1.DateTime.now()).whereNotNull('ExpiresAt');
233
+ for (const t of expired) {
234
+ await t.destroy();
235
+ }
236
+ return expired.length;
237
+ }
238
+ /**
239
+ * Updates `LastUsedAt`, throttled: writes only when the stamp is absent or
240
+ * older than `intervalSeconds`.
241
+ *
242
+ * Every authenticated request would otherwise write to the token row; the stamp
243
+ * only needs to be accurate enough to spot unused tokens. Callers may
244
+ * fire-and-forget.
245
+ *
246
+ * @param token - token to stamp
247
+ * @param intervalSeconds - minimum time between two writes
248
+ */
249
+ async function touchToken(token, intervalSeconds) {
250
+ const now = luxon_1.DateTime.now();
251
+ if (token.LastUsedAt && token.LastUsedAt > now.minus({ seconds: intervalSeconds })) {
252
+ return;
253
+ }
254
+ token.LastUsedAt = now;
255
+ await token.update();
256
+ }
257
+ //# sourceMappingURL=actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/actions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { Log } from '@spinajs/log';
2
+ import { CliCommand } from '@spinajs/cli';
3
+ interface ICreateTokenOptions {
4
+ name: string;
5
+ roles: string;
6
+ expires?: string;
7
+ }
8
+ export declare class CreateToken extends CliCommand {
9
+ protected Log: Log;
10
+ execute(userIdOrUuid: string, options: ICreateTokenOptions): Promise<void>;
11
+ }
12
+ export {};
13
+ //# sourceMappingURL=CreateToken.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CreateToken.d.ts","sourceRoot":"","sources":["../../../src/cli/CreateToken.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAU,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAY,UAAU,EAAmB,MAAM,cAAc,CAAC;AAKrE,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAKa,WAAY,SAAQ,UAAU;IAEzC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC;IAEN,OAAO,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BtF;CACF"}
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.CreateToken = void 0;
13
+ const log_1 = require("@spinajs/log");
14
+ const cli_1 = require("@spinajs/cli");
15
+ const luxon_1 = require("luxon");
16
+ const actions_js_1 = require("../actions.js");
17
+ let CreateToken = class CreateToken extends cli_1.CliCommand {
18
+ async execute(userIdOrUuid, options) {
19
+ try {
20
+ // Nil means the flag was not given at all - a token that never expires.
21
+ // Anything else, empty string included, is a value the user meant to be a
22
+ // date and must be validated: a truthiness check here would quietly turn
23
+ // `--expires ""` into an infinite token.
24
+ const expiresAt = options.expires === undefined || options.expires === null ? null : luxon_1.DateTime.fromISO(options.expires);
25
+ if (expiresAt && !expiresAt.isValid) {
26
+ this.Log.error(`Invalid --expires value: ${options.expires}`);
27
+ return;
28
+ }
29
+ const roles = options.roles
30
+ .split(',')
31
+ .map((r) => r.trim())
32
+ .filter((r) => r.length > 0);
33
+ const owner = /^\d+$/.test(userIdOrUuid) ? parseInt(userIdOrUuid, 10) : userIdOrUuid;
34
+ const { Token, Plaintext } = await (0, actions_js_1.createToken)(owner, options.name, roles, expiresAt);
35
+ this.Log.success(`Token created: ${Token.Uuid}`);
36
+ // The single moment the plaintext exists outside the caller's hands.
37
+ this.Log.success(`Token ( copy now, it will not be shown again ): ${Plaintext}`);
38
+ }
39
+ catch (e) {
40
+ this.Log.error(`Error while creating token: ${e.message}`);
41
+ }
42
+ }
43
+ };
44
+ exports.CreateToken = CreateToken;
45
+ __decorate([
46
+ (0, log_1.Logger)('rbac-http-token'),
47
+ __metadata("design:type", log_1.Log)
48
+ ], CreateToken.prototype, "Log", void 0);
49
+ exports.CreateToken = CreateToken = __decorate([
50
+ (0, cli_1.Command)('rbac:token-create', 'Creates an access token for a user'),
51
+ (0, cli_1.Argument)('userIdOrUuid', true, 'numeric id or uuid of the owner'),
52
+ (0, cli_1.Option)('-n, --name <name>', true, 'token label'),
53
+ (0, cli_1.Option)('-r, --roles <roles>', true, 'token roles, comma separated, must be subset of owner roles'),
54
+ (0, cli_1.Option)('-e, --expires <expires>', false, 'ISO expiration instant; omit for a token that never expires')
55
+ ], CreateToken);
56
+ //# sourceMappingURL=CreateToken.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CreateToken.js","sourceRoot":"","sources":["../../../src/cli/CreateToken.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,sCAA2C;AAC3C,sCAAqE;AACrE,iCAAiC;AAEjC,8CAA4C;AAarC,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,gBAAU;IAIlC,KAAK,CAAC,OAAO,CAAC,YAAoB,EAAE,OAA4B;QACrE,IAAI,CAAC;YACH,wEAAwE;YACxE,0EAA0E;YAC1E,yEAAyE;YACzE,yCAAyC;YACzC,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,gBAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACvH,IAAI,SAAS,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;gBACpC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,4BAA4B,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;gBAC9D,OAAO;YACT,CAAC;YAED,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK;iBACxB,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;iBACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC/B,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;YAErF,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,IAAA,wBAAW,EAAC,KAAK,EAAE,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;YAEtF,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,kBAAkB,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;YACjD,qEAAqE;YACrE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,mDAAmD,SAAS,EAAE,CAAC,CAAC;QACnF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,+BAAgC,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QACxE,CAAC;IACH,CAAC;CACF,CAAA;;AA7BW;IADT,IAAA,YAAM,EAAC,iBAAiB,CAAC;8BACX,SAAG;wCAAC;sBAFR,WAAW;IALvB,IAAA,aAAO,EAAC,mBAAmB,EAAE,oCAAoC,CAAC;IAClE,IAAA,cAAQ,EAAC,cAAc,EAAE,IAAI,EAAE,iCAAiC,CAAC;IACjE,IAAA,YAAM,EAAC,mBAAmB,EAAE,IAAI,EAAE,aAAa,CAAC;IAChD,IAAA,YAAM,EAAC,qBAAqB,EAAE,IAAI,EAAE,6DAA6D,CAAC;IAClG,IAAA,YAAM,EAAC,yBAAyB,EAAE,KAAK,EAAE,6DAA6D,CAAC;GAC3F,WAAW,CA+BvB"}
@@ -0,0 +1,11 @@
1
+ import { Log } from '@spinajs/log';
2
+ import { CliCommand } from '@spinajs/cli';
3
+ /**
4
+ * Intended for cyclic execution from a worker process
5
+ * ( eg. cron / task scheduler ) to keep the token table clean.
6
+ */
7
+ export declare class DeleteExpiredTokens extends CliCommand {
8
+ protected Log: Log;
9
+ execute(): Promise<void>;
10
+ }
11
+ //# sourceMappingURL=DeleteExpiredTokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DeleteExpiredTokens.d.ts","sourceRoot":"","sources":["../../../src/cli/DeleteExpiredTokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAU,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAW,MAAM,cAAc,CAAC;AAInD;;;GAGG;AACH,qBACa,mBAAoB,SAAQ,UAAU;IAEjD,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC;IAEN,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAOpC;CACF"}