alepha 0.19.3 → 0.19.4

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 (215) hide show
  1. package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
  2. package/dist/api/audits/index.d.ts +8 -8
  3. package/dist/api/invitations/index.d.ts +790 -0
  4. package/dist/api/invitations/index.d.ts.map +1 -0
  5. package/dist/api/invitations/index.js +665 -0
  6. package/dist/api/invitations/index.js.map +1 -0
  7. package/dist/api/jobs/index.browser.js +8 -9
  8. package/dist/api/jobs/index.browser.js.map +1 -1
  9. package/dist/api/jobs/index.d.ts +99 -43
  10. package/dist/api/jobs/index.d.ts.map +1 -1
  11. package/dist/api/jobs/index.js +257 -40
  12. package/dist/api/jobs/index.js.map +1 -1
  13. package/dist/api/keys/index.d.ts +5 -5
  14. package/dist/api/notifications/index.browser.js +0 -1
  15. package/dist/api/notifications/index.browser.js.map +1 -1
  16. package/dist/api/notifications/index.d.ts +3 -3
  17. package/dist/api/notifications/index.d.ts.map +1 -1
  18. package/dist/api/notifications/index.js +0 -1
  19. package/dist/api/notifications/index.js.map +1 -1
  20. package/dist/api/parameters/index.browser.js +112 -1
  21. package/dist/api/parameters/index.browser.js.map +1 -1
  22. package/dist/api/parameters/index.d.ts +90 -3
  23. package/dist/api/parameters/index.d.ts.map +1 -1
  24. package/dist/api/parameters/index.js +79 -12
  25. package/dist/api/parameters/index.js.map +1 -1
  26. package/dist/{billing → api/payments}/index.d.ts +67 -49
  27. package/dist/api/payments/index.d.ts.map +1 -0
  28. package/dist/{billing → api/payments}/index.js +108 -74
  29. package/dist/api/payments/index.js.map +1 -0
  30. package/dist/api/subscriptions/index.d.ts +1692 -0
  31. package/dist/api/subscriptions/index.d.ts.map +1 -0
  32. package/dist/api/subscriptions/index.js +1870 -0
  33. package/dist/api/subscriptions/index.js.map +1 -0
  34. package/dist/api/users/index.d.ts +18 -2
  35. package/dist/api/users/index.d.ts.map +1 -1
  36. package/dist/api/users/index.js +167 -34
  37. package/dist/api/users/index.js.map +1 -1
  38. package/dist/api/verifications/index.d.ts +13 -13
  39. package/dist/api/workflows/index.browser.js +246 -0
  40. package/dist/api/workflows/index.browser.js.map +1 -0
  41. package/dist/api/workflows/index.d.ts +1618 -0
  42. package/dist/api/workflows/index.d.ts.map +1 -0
  43. package/dist/api/workflows/index.js +1504 -0
  44. package/dist/api/workflows/index.js.map +1 -0
  45. package/dist/cli/core/index.d.ts +44 -28
  46. package/dist/cli/core/index.d.ts.map +1 -1
  47. package/dist/cli/core/index.js +16 -61
  48. package/dist/cli/core/index.js.map +1 -1
  49. package/dist/cli/vendor/index.d.ts +31 -8
  50. package/dist/cli/vendor/index.d.ts.map +1 -1
  51. package/dist/cli/vendor/index.js +79 -24
  52. package/dist/cli/vendor/index.js.map +1 -1
  53. package/dist/core/index.browser.js +21 -2
  54. package/dist/core/index.browser.js.map +1 -1
  55. package/dist/core/index.d.ts +33 -2
  56. package/dist/core/index.d.ts.map +1 -1
  57. package/dist/core/index.js +21 -2
  58. package/dist/core/index.js.map +1 -1
  59. package/dist/core/index.native.js +21 -2
  60. package/dist/core/index.native.js.map +1 -1
  61. package/dist/core/index.workerd.js +21 -2
  62. package/dist/core/index.workerd.js.map +1 -1
  63. package/dist/email/smtp/index.js +24 -8
  64. package/dist/email/smtp/index.js.map +1 -1
  65. package/dist/orm/core/index.browser.js +0 -18
  66. package/dist/orm/core/index.browser.js.map +1 -1
  67. package/dist/orm/core/index.bun.js +0 -17
  68. package/dist/orm/core/index.bun.js.map +1 -1
  69. package/dist/orm/core/index.d.ts +1 -13
  70. package/dist/orm/core/index.d.ts.map +1 -1
  71. package/dist/orm/core/index.js +0 -17
  72. package/dist/orm/core/index.js.map +1 -1
  73. package/dist/orm/postgres/index.bun.js +3 -3
  74. package/dist/orm/postgres/index.bun.js.map +1 -1
  75. package/dist/orm/postgres/index.d.ts.map +1 -1
  76. package/dist/orm/postgres/index.js +3 -3
  77. package/dist/orm/postgres/index.js.map +1 -1
  78. package/dist/react/router/index.browser.js +25 -3
  79. package/dist/react/router/index.browser.js.map +1 -1
  80. package/dist/react/router/index.d.ts +16 -1
  81. package/dist/react/router/index.d.ts.map +1 -1
  82. package/dist/react/router/index.js +25 -3
  83. package/dist/react/router/index.js.map +1 -1
  84. package/dist/security/index.d.ts +28 -0
  85. package/dist/security/index.d.ts.map +1 -1
  86. package/dist/security/index.js +28 -0
  87. package/dist/security/index.js.map +1 -1
  88. package/package.json +37 -20
  89. package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
  90. package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
  91. package/src/api/invitations/controllers/InvitationController.ts +84 -0
  92. package/src/api/invitations/entities/invitations.ts +33 -0
  93. package/src/api/invitations/index.ts +65 -0
  94. package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
  95. package/src/api/invitations/providers/InvitationProvider.ts +45 -0
  96. package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
  97. package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
  98. package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
  99. package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
  100. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
  101. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
  102. package/src/api/invitations/services/InvitationService.ts +556 -0
  103. package/src/api/jobs/__tests__/$job.spec.ts +876 -0
  104. package/src/api/jobs/controllers/AdminJobController.ts +44 -0
  105. package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
  106. package/src/api/jobs/index.ts +0 -3
  107. package/src/api/jobs/primitives/$job.ts +22 -11
  108. package/src/api/jobs/providers/JobProvider.ts +229 -19
  109. package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
  110. package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
  111. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
  112. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
  113. package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
  114. package/src/api/jobs/services/JobService.ts +51 -12
  115. package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
  116. package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
  117. package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
  118. package/src/api/parameters/index.browser.ts +12 -0
  119. package/src/api/parameters/primitives/$parameter.ts +20 -3
  120. package/src/api/parameters/services/ParameterProvider.ts +48 -7
  121. package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
  122. package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
  123. package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
  124. package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
  125. package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
  126. package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
  127. package/src/{billing → api/payments}/index.ts +31 -25
  128. package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
  129. package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
  130. package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
  131. package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
  132. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  133. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  134. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  135. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  136. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  137. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  138. package/src/api/subscriptions/index.ts +144 -0
  139. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  140. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  141. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  142. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  143. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  144. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  145. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  146. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  147. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  148. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  149. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  150. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  151. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  152. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  153. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  154. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  155. package/src/api/subscriptions/services/BillingService.ts +437 -0
  156. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  157. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  158. package/src/api/subscriptions/services/UsageService.ts +118 -0
  159. package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
  160. package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
  161. package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
  162. package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
  163. package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
  164. package/src/api/users/__tests__/SessionService.spec.ts +142 -1
  165. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
  166. package/src/api/users/controllers/UserController.ts +3 -8
  167. package/src/api/users/notifications/UserNotifications.ts +23 -0
  168. package/src/api/users/schemas/loginSchema.ts +1 -1
  169. package/src/api/users/services/CredentialService.ts +51 -4
  170. package/src/api/users/services/RegistrationService.ts +38 -9
  171. package/src/api/users/services/SessionService.ts +62 -9
  172. package/src/api/users/services/UserService.ts +21 -12
  173. package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
  174. package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
  175. package/src/api/workflows/entities/workflowExecutions.ts +74 -0
  176. package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
  177. package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
  178. package/src/api/workflows/index.browser.ts +22 -0
  179. package/src/api/workflows/index.ts +124 -0
  180. package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
  181. package/src/api/workflows/primitives/$workflow.ts +202 -0
  182. package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
  183. package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
  184. package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
  185. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
  186. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
  187. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
  188. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
  189. package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
  190. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
  191. package/src/api/workflows/services/WorkflowService.ts +382 -0
  192. package/src/cli/core/templates/webAppRouterTs.ts +5 -58
  193. package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
  194. package/src/cli/vendor/services/VendorService.ts +126 -27
  195. package/src/core/__tests__/TypeProvider.spec.ts +4 -2
  196. package/src/core/providers/SchemaValidator.ts +1 -1
  197. package/src/core/providers/TypeProvider.ts +46 -3
  198. package/src/orm/__tests__/enums.spec.ts +22 -29
  199. package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
  200. package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
  201. package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
  202. package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
  203. package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
  204. package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
  205. package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
  206. package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
  207. package/src/security/primitives/$secure.ts +28 -0
  208. package/dist/billing/index.d.ts.map +0 -1
  209. package/dist/billing/index.js.map +0 -1
  210. package/src/billing/__tests__/BillingService.spec.ts +0 -136
  211. /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
  212. /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
  213. /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
  214. /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
  215. /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
@@ -0,0 +1,945 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Alepha } from "alepha";
3
+ import {
4
+ $action,
5
+ AlephaServer,
6
+ ForbiddenError,
7
+ UnauthorizedError,
8
+ } from "alepha/server";
9
+ import { describe, expect, it } from "vitest";
10
+ import { $issuer, $secure, AlephaSecurity } from "../index.ts";
11
+
12
+ // -----------------------------------------------------------------------------------------------------------------
13
+ // Shared setup
14
+ // -----------------------------------------------------------------------------------------------------------------
15
+
16
+ function setup() {
17
+ class TestApp {
18
+ main = $issuer({
19
+ secret: "test-main",
20
+ roles: [
21
+ {
22
+ name: "admin",
23
+ permissions: [{ name: "*" }],
24
+ },
25
+ {
26
+ name: "editor",
27
+ permissions: [
28
+ { name: "posts:read" },
29
+ { name: "posts:create" },
30
+ { name: "posts:update" },
31
+ ],
32
+ },
33
+ {
34
+ name: "viewer",
35
+ permissions: [{ name: "posts:read" }],
36
+ },
37
+ {
38
+ name: "moderator",
39
+ permissions: [
40
+ { name: "posts:read" },
41
+ { name: "posts:delete" },
42
+ { name: "comments:delete" },
43
+ ],
44
+ },
45
+ ],
46
+ });
47
+
48
+ external = $issuer({
49
+ secret: "test-external",
50
+ roles: [
51
+ {
52
+ name: "partner",
53
+ permissions: [{ name: "posts:read" }],
54
+ },
55
+ ],
56
+ });
57
+
58
+ // ---------------------------------------------------------------------------------------------------------------
59
+ // Actions with single options
60
+ // ---------------------------------------------------------------------------------------------------------------
61
+
62
+ authOnly = $action({
63
+ use: [$secure()],
64
+ handler: () => "auth-only",
65
+ });
66
+
67
+ requireViewer = $action({
68
+ use: [$secure({ roles: ["viewer"] })],
69
+ handler: () => "viewer",
70
+ });
71
+
72
+ requireRead = $action({
73
+ use: [$secure({ permissions: ["posts:read"] })],
74
+ handler: () => "read",
75
+ });
76
+
77
+ requireMainIssuer = $action({
78
+ use: [$secure({ issuers: ["main"] })],
79
+ handler: () => "main-issuer",
80
+ });
81
+
82
+ // ---------------------------------------------------------------------------------------------------------------
83
+ // Actions with two-option combinations
84
+ // ---------------------------------------------------------------------------------------------------------------
85
+
86
+ issuerAndRole = $action({
87
+ use: [$secure({ issuers: ["main"], roles: ["editor"] })],
88
+ handler: () => "issuer+role",
89
+ });
90
+
91
+ issuerAndPermission = $action({
92
+ use: [$secure({ issuers: ["main"], permissions: ["posts:create"] })],
93
+ handler: () => "issuer+permission",
94
+ });
95
+
96
+ issuerAndGuard = $action({
97
+ use: [
98
+ $secure({
99
+ issuers: ["main"],
100
+ guard: (user) => user.email === "allowed@test.com",
101
+ }),
102
+ ],
103
+ handler: () => "issuer+guard",
104
+ });
105
+
106
+ roleAndPermission = $action({
107
+ use: [$secure({ roles: ["editor"], permissions: ["posts:create"] })],
108
+ handler: () => "role+permission",
109
+ });
110
+
111
+ roleAndGuard = $action({
112
+ use: [
113
+ $secure({
114
+ roles: ["editor"],
115
+ guard: (user) => user.email === "allowed@test.com",
116
+ }),
117
+ ],
118
+ handler: () => "role+guard",
119
+ });
120
+
121
+ permissionAndGuard = $action({
122
+ use: [
123
+ $secure({
124
+ permissions: ["posts:read"],
125
+ guard: (user) => user.email === "allowed@test.com",
126
+ }),
127
+ ],
128
+ handler: () => "permission+guard",
129
+ });
130
+
131
+ // ---------------------------------------------------------------------------------------------------------------
132
+ // Actions with three-option combinations
133
+ // ---------------------------------------------------------------------------------------------------------------
134
+
135
+ issuerRolePermission = $action({
136
+ use: [
137
+ $secure({
138
+ issuers: ["main"],
139
+ roles: ["editor"],
140
+ permissions: ["posts:create"],
141
+ }),
142
+ ],
143
+ handler: () => "issuer+role+permission",
144
+ });
145
+
146
+ issuerRoleGuard = $action({
147
+ use: [
148
+ $secure({
149
+ issuers: ["main"],
150
+ roles: ["editor"],
151
+ guard: (user) => user.email === "allowed@test.com",
152
+ }),
153
+ ],
154
+ handler: () => "issuer+role+guard",
155
+ });
156
+
157
+ issuerPermissionGuard = $action({
158
+ use: [
159
+ $secure({
160
+ issuers: ["main"],
161
+ permissions: ["posts:create"],
162
+ guard: (user) => user.email === "allowed@test.com",
163
+ }),
164
+ ],
165
+ handler: () => "issuer+permission+guard",
166
+ });
167
+
168
+ rolePermissionGuard = $action({
169
+ use: [
170
+ $secure({
171
+ roles: ["editor"],
172
+ permissions: ["posts:create"],
173
+ guard: (user) => user.email === "allowed@test.com",
174
+ }),
175
+ ],
176
+ handler: () => "role+permission+guard",
177
+ });
178
+
179
+ // ---------------------------------------------------------------------------------------------------------------
180
+ // All four options
181
+ // ---------------------------------------------------------------------------------------------------------------
182
+
183
+ allOptions = $action({
184
+ use: [
185
+ $secure({
186
+ issuers: ["main"],
187
+ roles: ["editor"],
188
+ permissions: ["posts:create"],
189
+ guard: (user) => user.email === "allowed@test.com",
190
+ }),
191
+ ],
192
+ handler: () => "all",
193
+ });
194
+
195
+ // ---------------------------------------------------------------------------------------------------------------
196
+ // Multiple permissions (AND logic)
197
+ // ---------------------------------------------------------------------------------------------------------------
198
+
199
+ multiplePermissions = $action({
200
+ use: [$secure({ permissions: ["posts:read", "posts:delete"] })],
201
+ handler: () => "multi-perm",
202
+ });
203
+
204
+ // ---------------------------------------------------------------------------------------------------------------
205
+ // Multiple issuers (OR logic)
206
+ // ---------------------------------------------------------------------------------------------------------------
207
+
208
+ multipleIssuers = $action({
209
+ use: [$secure({ issuers: ["main", "external"] })],
210
+ handler: () => "multi-issuer",
211
+ });
212
+
213
+ // ---------------------------------------------------------------------------------------------------------------
214
+ // Multiple roles (OR logic)
215
+ // ---------------------------------------------------------------------------------------------------------------
216
+
217
+ multipleRoles = $action({
218
+ use: [$secure({ roles: ["editor", "moderator"] })],
219
+ handler: () => "multi-role",
220
+ });
221
+ }
222
+
223
+ const alepha = Alepha.create().with(AlephaServer).with(AlephaSecurity);
224
+ const app = alepha.inject(TestApp);
225
+
226
+ const users = {
227
+ admin: {
228
+ id: randomUUID(),
229
+ roles: ["admin"],
230
+ realm: "main",
231
+ email: "admin@test.com",
232
+ name: "Admin",
233
+ },
234
+ editor: {
235
+ id: randomUUID(),
236
+ roles: ["editor"],
237
+ realm: "main",
238
+ email: "allowed@test.com",
239
+ name: "Editor",
240
+ },
241
+ editorWrongEmail: {
242
+ id: randomUUID(),
243
+ roles: ["editor"],
244
+ realm: "main",
245
+ email: "other@test.com",
246
+ name: "Editor2",
247
+ },
248
+ viewer: {
249
+ id: randomUUID(),
250
+ roles: ["viewer"],
251
+ realm: "main",
252
+ email: "viewer@test.com",
253
+ name: "Viewer",
254
+ },
255
+ moderator: {
256
+ id: randomUUID(),
257
+ roles: ["moderator"],
258
+ realm: "main",
259
+ email: "mod@test.com",
260
+ name: "Moderator",
261
+ },
262
+ partner: {
263
+ id: randomUUID(),
264
+ roles: ["partner"],
265
+ realm: "external",
266
+ email: "partner@test.com",
267
+ name: "Partner",
268
+ },
269
+ noRoles: {
270
+ id: randomUUID(),
271
+ roles: [] as string[],
272
+ realm: "main",
273
+ email: "noroles@test.com",
274
+ name: "NoRoles",
275
+ },
276
+ noRealm: {
277
+ id: randomUUID(),
278
+ roles: ["editor"],
279
+ email: "norealm@test.com",
280
+ name: "NoRealm",
281
+ },
282
+ editorAndModerator: {
283
+ id: randomUUID(),
284
+ roles: ["editor", "moderator"],
285
+ realm: "main",
286
+ email: "allowed@test.com",
287
+ name: "EditorMod",
288
+ },
289
+ };
290
+
291
+ return { alepha, app, users };
292
+ }
293
+
294
+ // -----------------------------------------------------------------------------------------------------------------
295
+ // Two-option combinations
296
+ // -----------------------------------------------------------------------------------------------------------------
297
+
298
+ describe("$secure combinations", () => {
299
+ describe("issuers + roles", () => {
300
+ it("should allow when both issuer and role match", async () => {
301
+ const { alepha, app, users } = setup();
302
+ await alepha.start();
303
+
304
+ expect(await app.issuerAndRole.run({}, { user: users.editor })).toBe(
305
+ "issuer+role",
306
+ );
307
+ });
308
+
309
+ it("should deny when issuer matches but role does not", async () => {
310
+ const { alepha, app, users } = setup();
311
+ await alepha.start();
312
+
313
+ await expect(
314
+ app.issuerAndRole.run({}, { user: users.viewer }),
315
+ ).rejects.toThrowError(ForbiddenError);
316
+ });
317
+
318
+ it("should deny when role matches but issuer does not", async () => {
319
+ const { alepha, app, users } = setup();
320
+ await alepha.start();
321
+
322
+ const externalEditor = {
323
+ ...users.editor,
324
+ id: randomUUID(),
325
+ realm: "external",
326
+ };
327
+ await expect(
328
+ app.issuerAndRole.run({}, { user: externalEditor }),
329
+ ).rejects.toThrowError(ForbiddenError);
330
+ });
331
+ });
332
+
333
+ describe("issuers + permissions", () => {
334
+ it("should allow when both issuer and permission match", async () => {
335
+ const { alepha, app, users } = setup();
336
+ await alepha.start();
337
+
338
+ expect(
339
+ await app.issuerAndPermission.run({}, { user: users.editor }),
340
+ ).toBe("issuer+permission");
341
+ });
342
+
343
+ it("should deny when issuer matches but permission does not", async () => {
344
+ const { alepha, app, users } = setup();
345
+ await alepha.start();
346
+
347
+ await expect(
348
+ app.issuerAndPermission.run({}, { user: users.viewer }),
349
+ ).rejects.toThrowError(ForbiddenError);
350
+ });
351
+
352
+ it("should deny when permission would match but issuer does not", async () => {
353
+ const { alepha, app, users } = setup();
354
+ await alepha.start();
355
+
356
+ const externalEditor = {
357
+ ...users.editor,
358
+ id: randomUUID(),
359
+ realm: "external",
360
+ };
361
+ await expect(
362
+ app.issuerAndPermission.run({}, { user: externalEditor }),
363
+ ).rejects.toThrowError(ForbiddenError);
364
+ });
365
+ });
366
+
367
+ describe("issuers + guard", () => {
368
+ it("should allow when both issuer and guard pass", async () => {
369
+ const { alepha, app, users } = setup();
370
+ await alepha.start();
371
+
372
+ expect(await app.issuerAndGuard.run({}, { user: users.editor })).toBe(
373
+ "issuer+guard",
374
+ );
375
+ });
376
+
377
+ it("should deny when issuer matches but guard fails", async () => {
378
+ const { alepha, app, users } = setup();
379
+ await alepha.start();
380
+
381
+ await expect(
382
+ app.issuerAndGuard.run({}, { user: users.editorWrongEmail }),
383
+ ).rejects.toThrowError(ForbiddenError);
384
+ });
385
+
386
+ it("should deny when guard would pass but issuer does not", async () => {
387
+ const { alepha, app, users } = setup();
388
+ await alepha.start();
389
+
390
+ const externalAllowed = {
391
+ ...users.editor,
392
+ id: randomUUID(),
393
+ realm: "external",
394
+ };
395
+ await expect(
396
+ app.issuerAndGuard.run({}, { user: externalAllowed }),
397
+ ).rejects.toThrowError(ForbiddenError);
398
+ });
399
+ });
400
+
401
+ describe("roles + permissions", () => {
402
+ it("should allow when both role and permission match", async () => {
403
+ const { alepha, app, users } = setup();
404
+ await alepha.start();
405
+
406
+ expect(await app.roleAndPermission.run({}, { user: users.editor })).toBe(
407
+ "role+permission",
408
+ );
409
+ });
410
+
411
+ it("should deny when role matches but permission does not", async () => {
412
+ const { alepha, app, users } = setup();
413
+ await alepha.start();
414
+
415
+ // viewer role doesn't have posts:create, but we need a user WITH editor role but WITHOUT the permission
416
+ // Actually: editor role has posts:create. So use a viewer who somehow has editor role name... no.
417
+ // The check is: roles check is OR on role NAME, permissions check is on role PERMISSIONS.
418
+ // A user with roles: ["editor"] has posts:create. A user with roles: ["viewer"] does NOT have posts:create.
419
+ // But viewer also doesn't have the "editor" role name. So both checks fail.
420
+ // To test "role matches but permission doesn't": we'd need a role that exists by name but lacks the permission.
421
+ // viewer role exists, has posts:read, but NOT posts:create.
422
+ // So: $secure({ roles: ["viewer"], permissions: ["posts:create"] }) with a viewer user would fail on permission.
423
+
424
+ // Let's just verify viewer fails on roleAndPermission (requires editor role)
425
+ await expect(
426
+ app.roleAndPermission.run({}, { user: users.viewer }),
427
+ ).rejects.toThrowError(ForbiddenError);
428
+ });
429
+
430
+ it("should deny when user has no roles", async () => {
431
+ const { alepha, app, users } = setup();
432
+ await alepha.start();
433
+
434
+ await expect(
435
+ app.roleAndPermission.run({}, { user: users.noRoles }),
436
+ ).rejects.toThrowError(ForbiddenError);
437
+ });
438
+ });
439
+
440
+ describe("roles + guard", () => {
441
+ it("should allow when both role and guard pass", async () => {
442
+ const { alepha, app, users } = setup();
443
+ await alepha.start();
444
+
445
+ expect(await app.roleAndGuard.run({}, { user: users.editor })).toBe(
446
+ "role+guard",
447
+ );
448
+ });
449
+
450
+ it("should deny when role matches but guard fails", async () => {
451
+ const { alepha, app, users } = setup();
452
+ await alepha.start();
453
+
454
+ await expect(
455
+ app.roleAndGuard.run({}, { user: users.editorWrongEmail }),
456
+ ).rejects.toThrowError(ForbiddenError);
457
+ });
458
+
459
+ it("should deny when guard would pass but role does not", async () => {
460
+ const { alepha, app, users } = setup();
461
+ await alepha.start();
462
+
463
+ // viewer with the "allowed" email — guard would pass, but role check fails
464
+ const viewerAllowed = { ...users.viewer, email: "allowed@test.com" };
465
+ await expect(
466
+ app.roleAndGuard.run({}, { user: viewerAllowed }),
467
+ ).rejects.toThrowError(ForbiddenError);
468
+ });
469
+ });
470
+
471
+ describe("permissions + guard", () => {
472
+ it("should allow when both permission and guard pass", async () => {
473
+ const { alepha, app, users } = setup();
474
+ await alepha.start();
475
+
476
+ expect(await app.permissionAndGuard.run({}, { user: users.editor })).toBe(
477
+ "permission+guard",
478
+ );
479
+ });
480
+
481
+ it("should deny when permission matches but guard fails", async () => {
482
+ const { alepha, app, users } = setup();
483
+ await alepha.start();
484
+
485
+ // viewer has posts:read but wrong email
486
+ await expect(
487
+ app.permissionAndGuard.run({}, { user: users.viewer }),
488
+ ).rejects.toThrowError(ForbiddenError);
489
+ });
490
+
491
+ it("should deny when guard would pass but permission does not", async () => {
492
+ const { alepha, app, users } = setup();
493
+ await alepha.start();
494
+
495
+ // user with correct email but no posts:read permission
496
+ const noPermsAllowed = { ...users.noRoles, email: "allowed@test.com" };
497
+ await expect(
498
+ app.permissionAndGuard.run({}, { user: noPermsAllowed }),
499
+ ).rejects.toThrowError(ForbiddenError);
500
+ });
501
+ });
502
+ });
503
+
504
+ // -----------------------------------------------------------------------------------------------------------------
505
+ // Three-option combinations
506
+ // -----------------------------------------------------------------------------------------------------------------
507
+
508
+ describe("$secure three-option combinations", () => {
509
+ describe("issuers + roles + permissions", () => {
510
+ it("should allow when all three match", async () => {
511
+ const { alepha, app, users } = setup();
512
+ await alepha.start();
513
+
514
+ expect(
515
+ await app.issuerRolePermission.run({}, { user: users.editor }),
516
+ ).toBe("issuer+role+permission");
517
+ });
518
+
519
+ it("should deny when issuer fails", async () => {
520
+ const { alepha, app, users } = setup();
521
+ await alepha.start();
522
+
523
+ const externalEditor = {
524
+ ...users.editor,
525
+ id: randomUUID(),
526
+ realm: "external",
527
+ };
528
+ await expect(
529
+ app.issuerRolePermission.run({}, { user: externalEditor }),
530
+ ).rejects.toThrowError(ForbiddenError);
531
+ });
532
+
533
+ it("should deny when role fails", async () => {
534
+ const { alepha, app, users } = setup();
535
+ await alepha.start();
536
+
537
+ await expect(
538
+ app.issuerRolePermission.run({}, { user: users.viewer }),
539
+ ).rejects.toThrowError(ForbiddenError);
540
+ });
541
+
542
+ it("should deny when permission fails", async () => {
543
+ const { alepha, app, users } = setup();
544
+ await alepha.start();
545
+
546
+ // moderator role doesn't have posts:create
547
+ await expect(
548
+ app.issuerRolePermission.run({}, { user: users.moderator }),
549
+ ).rejects.toThrowError(ForbiddenError);
550
+ });
551
+ });
552
+
553
+ describe("issuers + roles + guard", () => {
554
+ it("should allow when all three match", async () => {
555
+ const { alepha, app, users } = setup();
556
+ await alepha.start();
557
+
558
+ expect(await app.issuerRoleGuard.run({}, { user: users.editor })).toBe(
559
+ "issuer+role+guard",
560
+ );
561
+ });
562
+
563
+ it("should deny when guard fails (issuer and role pass)", async () => {
564
+ const { alepha, app, users } = setup();
565
+ await alepha.start();
566
+
567
+ await expect(
568
+ app.issuerRoleGuard.run({}, { user: users.editorWrongEmail }),
569
+ ).rejects.toThrowError(ForbiddenError);
570
+ });
571
+ });
572
+
573
+ describe("issuers + permissions + guard", () => {
574
+ it("should allow when all three match", async () => {
575
+ const { alepha, app, users } = setup();
576
+ await alepha.start();
577
+
578
+ expect(
579
+ await app.issuerPermissionGuard.run({}, { user: users.editor }),
580
+ ).toBe("issuer+permission+guard");
581
+ });
582
+
583
+ it("should deny when permission fails (issuer and guard would pass)", async () => {
584
+ const { alepha, app, users } = setup();
585
+ await alepha.start();
586
+
587
+ // viewer has posts:read but not posts:create
588
+ const viewerAllowed = { ...users.viewer, email: "allowed@test.com" };
589
+ await expect(
590
+ app.issuerPermissionGuard.run({}, { user: viewerAllowed }),
591
+ ).rejects.toThrowError(ForbiddenError);
592
+ });
593
+ });
594
+
595
+ describe("roles + permissions + guard", () => {
596
+ it("should allow when all three match", async () => {
597
+ const { alepha, app, users } = setup();
598
+ await alepha.start();
599
+
600
+ expect(
601
+ await app.rolePermissionGuard.run({}, { user: users.editor }),
602
+ ).toBe("role+permission+guard");
603
+ });
604
+
605
+ it("should deny when only guard fails", async () => {
606
+ const { alepha, app, users } = setup();
607
+ await alepha.start();
608
+
609
+ await expect(
610
+ app.rolePermissionGuard.run({}, { user: users.editorWrongEmail }),
611
+ ).rejects.toThrowError(ForbiddenError);
612
+ });
613
+
614
+ it("should deny when only role fails", async () => {
615
+ const { alepha, app, users } = setup();
616
+ await alepha.start();
617
+
618
+ // admin has all permissions and correct email but not "editor" role
619
+ const adminAllowed = { ...users.admin, email: "allowed@test.com" };
620
+ await expect(
621
+ app.rolePermissionGuard.run({}, { user: adminAllowed }),
622
+ ).rejects.toThrowError(ForbiddenError);
623
+ });
624
+ });
625
+ });
626
+
627
+ // -----------------------------------------------------------------------------------------------------------------
628
+ // All four options
629
+ // -----------------------------------------------------------------------------------------------------------------
630
+
631
+ describe("$secure all options combined", () => {
632
+ it("should allow when issuer + role + permission + guard all pass", async () => {
633
+ const { alepha, app, users } = setup();
634
+ await alepha.start();
635
+
636
+ expect(await app.allOptions.run({}, { user: users.editor })).toBe("all");
637
+ });
638
+
639
+ it("should deny when only issuer fails", async () => {
640
+ const { alepha, app, users } = setup();
641
+ await alepha.start();
642
+
643
+ const externalEditor = {
644
+ ...users.editor,
645
+ id: randomUUID(),
646
+ realm: "external",
647
+ };
648
+ await expect(
649
+ app.allOptions.run({}, { user: externalEditor }),
650
+ ).rejects.toThrowError(ForbiddenError);
651
+ });
652
+
653
+ it("should deny when only role fails", async () => {
654
+ const { alepha, app, users } = setup();
655
+ await alepha.start();
656
+
657
+ const viewerAllowed = { ...users.viewer, email: "allowed@test.com" };
658
+ await expect(
659
+ app.allOptions.run({}, { user: viewerAllowed }),
660
+ ).rejects.toThrowError(ForbiddenError);
661
+ });
662
+
663
+ it("should deny when only permission fails", async () => {
664
+ const { alepha, app, users } = setup();
665
+ await alepha.start();
666
+
667
+ // moderator role has posts:delete and comments:delete, but not posts:create
668
+ const modAllowed = {
669
+ ...users.moderator,
670
+ roles: ["moderator"],
671
+ email: "allowed@test.com",
672
+ };
673
+ await expect(
674
+ app.allOptions.run({}, { user: modAllowed }),
675
+ ).rejects.toThrowError(ForbiddenError);
676
+ });
677
+
678
+ it("should deny when only guard fails", async () => {
679
+ const { alepha, app, users } = setup();
680
+ await alepha.start();
681
+
682
+ await expect(
683
+ app.allOptions.run({}, { user: users.editorWrongEmail }),
684
+ ).rejects.toThrowError(ForbiddenError);
685
+ });
686
+
687
+ it("should deny unauthenticated user", async () => {
688
+ const { alepha, app } = setup();
689
+ await alepha.start();
690
+
691
+ await expect(app.allOptions.run({})).rejects.toThrowError(
692
+ UnauthorizedError,
693
+ );
694
+ });
695
+ });
696
+
697
+ // -----------------------------------------------------------------------------------------------------------------
698
+ // Multiple permissions (AND logic)
699
+ // -----------------------------------------------------------------------------------------------------------------
700
+
701
+ describe("$secure multiple permissions (AND)", () => {
702
+ it("should allow when user has all required permissions", async () => {
703
+ const { alepha, app, users } = setup();
704
+ await alepha.start();
705
+
706
+ // moderator has posts:read AND posts:delete
707
+ expect(
708
+ await app.multiplePermissions.run({}, { user: users.moderator }),
709
+ ).toBe("multi-perm");
710
+ });
711
+
712
+ it("should deny when user has only some of the required permissions", async () => {
713
+ const { alepha, app, users } = setup();
714
+ await alepha.start();
715
+
716
+ // viewer has posts:read but NOT posts:delete
717
+ await expect(
718
+ app.multiplePermissions.run({}, { user: users.viewer }),
719
+ ).rejects.toThrowError(ForbiddenError);
720
+ });
721
+
722
+ it("should deny when user has none of the required permissions", async () => {
723
+ const { alepha, app, users } = setup();
724
+ await alepha.start();
725
+
726
+ await expect(
727
+ app.multiplePermissions.run({}, { user: users.noRoles }),
728
+ ).rejects.toThrowError(ForbiddenError);
729
+ });
730
+
731
+ it("should allow admin with wildcard permission", async () => {
732
+ const { alepha, app, users } = setup();
733
+ await alepha.start();
734
+
735
+ expect(await app.multiplePermissions.run({}, { user: users.admin })).toBe(
736
+ "multi-perm",
737
+ );
738
+ });
739
+ });
740
+
741
+ // -----------------------------------------------------------------------------------------------------------------
742
+ // Multiple issuers (OR logic)
743
+ // -----------------------------------------------------------------------------------------------------------------
744
+
745
+ describe("$secure multiple issuers (OR)", () => {
746
+ it("should allow user from first issuer", async () => {
747
+ const { alepha, app, users } = setup();
748
+ await alepha.start();
749
+
750
+ expect(await app.multipleIssuers.run({}, { user: users.editor })).toBe(
751
+ "multi-issuer",
752
+ );
753
+ });
754
+
755
+ it("should allow user from second issuer", async () => {
756
+ const { alepha, app, users } = setup();
757
+ await alepha.start();
758
+
759
+ expect(await app.multipleIssuers.run({}, { user: users.partner })).toBe(
760
+ "multi-issuer",
761
+ );
762
+ });
763
+
764
+ it("should deny user from unknown issuer", async () => {
765
+ const { alepha, app } = setup();
766
+ await alepha.start();
767
+
768
+ const unknownRealmUser = {
769
+ id: randomUUID(),
770
+ roles: ["admin"],
771
+ realm: "unknown",
772
+ name: "Unknown",
773
+ };
774
+ await expect(
775
+ app.multipleIssuers.run({}, { user: unknownRealmUser }),
776
+ ).rejects.toThrowError(ForbiddenError);
777
+ });
778
+ });
779
+
780
+ // -----------------------------------------------------------------------------------------------------------------
781
+ // Multiple roles (OR logic)
782
+ // -----------------------------------------------------------------------------------------------------------------
783
+
784
+ describe("$secure multiple roles (OR)", () => {
785
+ it("should allow user with first matching role", async () => {
786
+ const { alepha, app, users } = setup();
787
+ await alepha.start();
788
+
789
+ expect(await app.multipleRoles.run({}, { user: users.editor })).toBe(
790
+ "multi-role",
791
+ );
792
+ });
793
+
794
+ it("should allow user with second matching role", async () => {
795
+ const { alepha, app, users } = setup();
796
+ await alepha.start();
797
+
798
+ expect(await app.multipleRoles.run({}, { user: users.moderator })).toBe(
799
+ "multi-role",
800
+ );
801
+ });
802
+
803
+ it("should allow user with both roles", async () => {
804
+ const { alepha, app, users } = setup();
805
+ await alepha.start();
806
+
807
+ expect(
808
+ await app.multipleRoles.run({}, { user: users.editorAndModerator }),
809
+ ).toBe("multi-role");
810
+ });
811
+
812
+ it("should deny user with none of the required roles", async () => {
813
+ const { alepha, app, users } = setup();
814
+ await alepha.start();
815
+
816
+ await expect(
817
+ app.multipleRoles.run({}, { user: users.viewer }),
818
+ ).rejects.toThrowError(ForbiddenError);
819
+ });
820
+ });
821
+
822
+ // -----------------------------------------------------------------------------------------------------------------
823
+ // Check order: issuer → role → permission → guard
824
+ // -----------------------------------------------------------------------------------------------------------------
825
+
826
+ describe("$secure check order", () => {
827
+ it("should check issuer before role", async () => {
828
+ const { alepha, app, users } = setup();
829
+ await alepha.start();
830
+
831
+ // User from wrong issuer with wrong role — error message should be about issuer, not role
832
+ const wrongBoth = {
833
+ id: randomUUID(),
834
+ roles: ["viewer"],
835
+ realm: "external",
836
+ name: "Wrong",
837
+ };
838
+ await expect(
839
+ app.issuerAndRole.run({}, { user: wrongBoth }),
840
+ ).rejects.toThrowError(/issuer/i);
841
+ });
842
+
843
+ it("should check role before permission", async () => {
844
+ const { alepha, app, users } = setup();
845
+ await alepha.start();
846
+
847
+ // User with wrong role and wrong permission — error should be about role
848
+ await expect(
849
+ app.roleAndPermission.run({}, { user: users.viewer }),
850
+ ).rejects.toThrowError(/role.*required/i);
851
+ });
852
+
853
+ it("should check permission before guard", async () => {
854
+ const { alepha, app, users } = setup();
855
+ await alepha.start();
856
+
857
+ // User with correct email (guard would pass) but no permission
858
+ const noPermsAllowed = { ...users.noRoles, email: "allowed@test.com" };
859
+ await expect(
860
+ app.permissionAndGuard.run({}, { user: noPermsAllowed }),
861
+ ).rejects.toThrowError(/permission.*required/i);
862
+ });
863
+
864
+ it("should reach guard only after all other checks pass", async () => {
865
+ const { alepha, app, users } = setup();
866
+ await alepha.start();
867
+
868
+ // Editor with wrong email — issuer/role/permission all pass, only guard fails
869
+ await expect(
870
+ app.allOptions.run({}, { user: users.editorWrongEmail }),
871
+ ).rejects.toThrowError(/access denied/i);
872
+ });
873
+ });
874
+
875
+ // -----------------------------------------------------------------------------------------------------------------
876
+ // Edge cases
877
+ // -----------------------------------------------------------------------------------------------------------------
878
+
879
+ describe("$secure edge cases", () => {
880
+ it("should deny user with empty roles array against role check", async () => {
881
+ const { alepha, app, users } = setup();
882
+ await alepha.start();
883
+
884
+ await expect(
885
+ app.requireViewer.run({}, { user: users.noRoles }),
886
+ ).rejects.toThrowError(ForbiddenError);
887
+ });
888
+
889
+ it("should deny user with undefined roles against role check", async () => {
890
+ const { alepha, app } = setup();
891
+ await alepha.start();
892
+
893
+ const noRolesUndefined = {
894
+ id: randomUUID(),
895
+ realm: "main",
896
+ name: "NoRoles",
897
+ };
898
+ await expect(
899
+ app.requireViewer.run({}, { user: noRolesUndefined }),
900
+ ).rejects.toThrowError(ForbiddenError);
901
+ });
902
+
903
+ it("should deny user with no realm against issuer check", async () => {
904
+ const { alepha, app, users } = setup();
905
+ await alepha.start();
906
+
907
+ await expect(
908
+ app.requireMainIssuer.run({}, { user: users.noRealm }),
909
+ ).rejects.toThrowError(ForbiddenError);
910
+ });
911
+
912
+ it("should deny user with empty roles array against permission check", async () => {
913
+ const { alepha, app, users } = setup();
914
+ await alepha.start();
915
+
916
+ await expect(
917
+ app.requireRead.run({}, { user: users.noRoles }),
918
+ ).rejects.toThrowError(ForbiddenError);
919
+ });
920
+
921
+ it("should allow auth-only action for user with no roles", async () => {
922
+ const { alepha, app, users } = setup();
923
+ await alepha.start();
924
+
925
+ expect(await app.authOnly.run({}, { user: users.noRoles })).toBe(
926
+ "auth-only",
927
+ );
928
+ });
929
+
930
+ it("should allow auth-only action for user with no realm", async () => {
931
+ const { alepha, app, users } = setup();
932
+ await alepha.start();
933
+
934
+ expect(await app.authOnly.run({}, { user: users.noRealm })).toBe(
935
+ "auth-only",
936
+ );
937
+ });
938
+
939
+ it("should allow admin wildcard through any permission check", async () => {
940
+ const { alepha, app, users } = setup();
941
+ await alepha.start();
942
+
943
+ expect(await app.requireRead.run({}, { user: users.admin })).toBe("read");
944
+ });
945
+ });