@rovela-ai/sdk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/admin/api/accept-invite.d.ts +65 -0
  2. package/dist/admin/api/accept-invite.d.ts.map +1 -0
  3. package/dist/admin/api/accept-invite.js +115 -0
  4. package/dist/admin/api/accept-invite.js.map +1 -0
  5. package/dist/admin/api/categories.d.ts.map +1 -1
  6. package/dist/admin/api/categories.js +21 -28
  7. package/dist/admin/api/categories.js.map +1 -1
  8. package/dist/admin/api/customers.d.ts.map +1 -1
  9. package/dist/admin/api/customers.js +17 -25
  10. package/dist/admin/api/customers.js.map +1 -1
  11. package/dist/admin/api/forgot-password.d.ts +39 -0
  12. package/dist/admin/api/forgot-password.d.ts.map +1 -0
  13. package/dist/admin/api/forgot-password.js +66 -0
  14. package/dist/admin/api/forgot-password.js.map +1 -0
  15. package/dist/admin/api/index.d.ts +6 -0
  16. package/dist/admin/api/index.d.ts.map +1 -1
  17. package/dist/admin/api/index.js +9 -0
  18. package/dist/admin/api/index.js.map +1 -1
  19. package/dist/admin/api/me.d.ts +72 -0
  20. package/dist/admin/api/me.d.ts.map +1 -0
  21. package/dist/admin/api/me.js +177 -0
  22. package/dist/admin/api/me.js.map +1 -0
  23. package/dist/admin/api/orders.d.ts.map +1 -1
  24. package/dist/admin/api/orders.js +21 -28
  25. package/dist/admin/api/orders.js.map +1 -1
  26. package/dist/admin/api/products.d.ts.map +1 -1
  27. package/dist/admin/api/products.js +33 -37
  28. package/dist/admin/api/products.js.map +1 -1
  29. package/dist/admin/api/refund.d.ts.map +1 -1
  30. package/dist/admin/api/refund.js +5 -7
  31. package/dist/admin/api/refund.js.map +1 -1
  32. package/dist/admin/api/reset-password.d.ts +49 -0
  33. package/dist/admin/api/reset-password.d.ts.map +1 -0
  34. package/dist/admin/api/reset-password.js +99 -0
  35. package/dist/admin/api/reset-password.js.map +1 -0
  36. package/dist/admin/api/return.d.ts.map +1 -1
  37. package/dist/admin/api/return.js +9 -12
  38. package/dist/admin/api/return.js.map +1 -1
  39. package/dist/admin/api/settings.d.ts.map +1 -1
  40. package/dist/admin/api/settings.js +9 -12
  41. package/dist/admin/api/settings.js.map +1 -1
  42. package/dist/admin/api/shipping.d.ts.map +1 -1
  43. package/dist/admin/api/shipping.js +65 -61
  44. package/dist/admin/api/shipping.js.map +1 -1
  45. package/dist/admin/api/stats.d.ts.map +1 -1
  46. package/dist/admin/api/stats.js +5 -7
  47. package/dist/admin/api/stats.js.map +1 -1
  48. package/dist/admin/api/stripe-status.d.ts.map +1 -1
  49. package/dist/admin/api/stripe-status.js +5 -7
  50. package/dist/admin/api/stripe-status.js.map +1 -1
  51. package/dist/admin/api/tax-zones.d.ts.map +1 -1
  52. package/dist/admin/api/tax-zones.js +21 -28
  53. package/dist/admin/api/tax-zones.js.map +1 -1
  54. package/dist/admin/api/users.d.ts +142 -0
  55. package/dist/admin/api/users.d.ts.map +1 -0
  56. package/dist/admin/api/users.js +356 -0
  57. package/dist/admin/api/users.js.map +1 -0
  58. package/dist/admin/components/AdminAcceptInviteForm.d.ts +3 -0
  59. package/dist/admin/components/AdminAcceptInviteForm.d.ts.map +1 -0
  60. package/dist/admin/components/AdminAcceptInviteForm.js +137 -0
  61. package/dist/admin/components/AdminAcceptInviteForm.js.map +1 -0
  62. package/dist/admin/components/AdminAccountPage.d.ts +10 -0
  63. package/dist/admin/components/AdminAccountPage.d.ts.map +1 -0
  64. package/dist/admin/components/AdminAccountPage.js +123 -0
  65. package/dist/admin/components/AdminAccountPage.js.map +1 -0
  66. package/dist/admin/components/AdminForgotPasswordForm.d.ts +8 -0
  67. package/dist/admin/components/AdminForgotPasswordForm.d.ts.map +1 -0
  68. package/dist/admin/components/AdminForgotPasswordForm.js +59 -0
  69. package/dist/admin/components/AdminForgotPasswordForm.js.map +1 -0
  70. package/dist/admin/components/AdminNav.d.ts.map +1 -1
  71. package/dist/admin/components/AdminNav.js +39 -8
  72. package/dist/admin/components/AdminNav.js.map +1 -1
  73. package/dist/admin/components/AdminResetPasswordForm.d.ts +12 -0
  74. package/dist/admin/components/AdminResetPasswordForm.d.ts.map +1 -0
  75. package/dist/admin/components/AdminResetPasswordForm.js +134 -0
  76. package/dist/admin/components/AdminResetPasswordForm.js.map +1 -0
  77. package/dist/admin/components/AdminUserMenu.d.ts.map +1 -1
  78. package/dist/admin/components/AdminUserMenu.js +2 -2
  79. package/dist/admin/components/AdminUserMenu.js.map +1 -1
  80. package/dist/admin/components/InviteUserDialog.d.ts +3 -0
  81. package/dist/admin/components/InviteUserDialog.d.ts.map +1 -0
  82. package/dist/admin/components/InviteUserDialog.js +127 -0
  83. package/dist/admin/components/InviteUserDialog.js.map +1 -0
  84. package/dist/admin/components/UsersTable.d.ts +3 -0
  85. package/dist/admin/components/UsersTable.d.ts.map +1 -0
  86. package/dist/admin/components/UsersTable.js +399 -0
  87. package/dist/admin/components/UsersTable.js.map +1 -0
  88. package/dist/admin/components/index.d.ts +9 -0
  89. package/dist/admin/components/index.d.ts.map +1 -1
  90. package/dist/admin/components/index.js +9 -0
  91. package/dist/admin/components/index.js.map +1 -1
  92. package/dist/admin/config.d.ts.map +1 -1
  93. package/dist/admin/config.js +23 -1
  94. package/dist/admin/config.js.map +1 -1
  95. package/dist/admin/hooks/index.d.ts +4 -0
  96. package/dist/admin/hooks/index.d.ts.map +1 -1
  97. package/dist/admin/hooks/index.js +3 -0
  98. package/dist/admin/hooks/index.js.map +1 -1
  99. package/dist/admin/hooks/useAdminMe.d.ts +31 -0
  100. package/dist/admin/hooks/useAdminMe.d.ts.map +1 -0
  101. package/dist/admin/hooks/useAdminMe.js +103 -0
  102. package/dist/admin/hooks/useAdminMe.js.map +1 -0
  103. package/dist/admin/hooks/useAdminPermissions.d.ts +3 -0
  104. package/dist/admin/hooks/useAdminPermissions.d.ts.map +1 -0
  105. package/dist/admin/hooks/useAdminPermissions.js +51 -0
  106. package/dist/admin/hooks/useAdminPermissions.js.map +1 -0
  107. package/dist/admin/hooks/useAdminUsers.d.ts +3 -0
  108. package/dist/admin/hooks/useAdminUsers.d.ts.map +1 -0
  109. package/dist/admin/hooks/useAdminUsers.js +240 -0
  110. package/dist/admin/hooks/useAdminUsers.js.map +1 -0
  111. package/dist/admin/index.d.ts +4 -4
  112. package/dist/admin/index.d.ts.map +1 -1
  113. package/dist/admin/index.js +20 -2
  114. package/dist/admin/index.js.map +1 -1
  115. package/dist/admin/permissions.d.ts +92 -0
  116. package/dist/admin/permissions.d.ts.map +1 -0
  117. package/dist/admin/permissions.js +201 -0
  118. package/dist/admin/permissions.js.map +1 -0
  119. package/dist/admin/server/admin-invite.d.ts +122 -0
  120. package/dist/admin/server/admin-invite.d.ts.map +1 -0
  121. package/dist/admin/server/admin-invite.js +235 -0
  122. package/dist/admin/server/admin-invite.js.map +1 -0
  123. package/dist/admin/server/admin-password-reset.d.ts +87 -0
  124. package/dist/admin/server/admin-password-reset.d.ts.map +1 -0
  125. package/dist/admin/server/admin-password-reset.js +220 -0
  126. package/dist/admin/server/admin-password-reset.js.map +1 -0
  127. package/dist/admin/server/admin-self-service.d.ts +86 -0
  128. package/dist/admin/server/admin-self-service.d.ts.map +1 -0
  129. package/dist/admin/server/admin-self-service.js +188 -0
  130. package/dist/admin/server/admin-self-service.js.map +1 -0
  131. package/dist/admin/server/admin-service.d.ts.map +1 -1
  132. package/dist/admin/server/admin-service.js +21 -2
  133. package/dist/admin/server/admin-service.js.map +1 -1
  134. package/dist/admin/server/admin-session.d.ts +126 -0
  135. package/dist/admin/server/admin-session.d.ts.map +1 -0
  136. package/dist/admin/server/admin-session.js +215 -0
  137. package/dist/admin/server/admin-session.js.map +1 -0
  138. package/dist/admin/server/index.d.ts +7 -0
  139. package/dist/admin/server/index.d.ts.map +1 -1
  140. package/dist/admin/server/index.js +20 -0
  141. package/dist/admin/server/index.js.map +1 -1
  142. package/dist/admin/server/user-management.d.ts +223 -0
  143. package/dist/admin/server/user-management.d.ts.map +1 -0
  144. package/dist/admin/server/user-management.js +846 -0
  145. package/dist/admin/server/user-management.js.map +1 -0
  146. package/dist/admin/types.d.ts +153 -2
  147. package/dist/admin/types.d.ts.map +1 -1
  148. package/dist/core/db/queries.d.ts +19 -13
  149. package/dist/core/db/queries.d.ts.map +1 -1
  150. package/dist/core/db/schema.d.ts +327 -9
  151. package/dist/core/db/schema.d.ts.map +1 -1
  152. package/dist/core/db/schema.js +80 -3
  153. package/dist/core/db/schema.js.map +1 -1
  154. package/dist/core/types.d.ts +19 -3
  155. package/dist/core/types.d.ts.map +1 -1
  156. package/dist/emails/index.d.ts +2 -2
  157. package/dist/emails/index.d.ts.map +1 -1
  158. package/dist/emails/index.js +3 -1
  159. package/dist/emails/index.js.map +1 -1
  160. package/dist/emails/send/admin-auth.d.ts +94 -0
  161. package/dist/emails/send/admin-auth.d.ts.map +1 -0
  162. package/dist/emails/send/admin-auth.js +118 -0
  163. package/dist/emails/send/admin-auth.js.map +1 -0
  164. package/dist/emails/send/index.d.ts +2 -0
  165. package/dist/emails/send/index.d.ts.map +1 -1
  166. package/dist/emails/send/index.js +4 -0
  167. package/dist/emails/send/index.js.map +1 -1
  168. package/dist/emails/templates/admin-invite.d.ts +40 -0
  169. package/dist/emails/templates/admin-invite.d.ts.map +1 -0
  170. package/dist/emails/templates/admin-invite.js +62 -0
  171. package/dist/emails/templates/admin-invite.js.map +1 -0
  172. package/dist/emails/templates/index.d.ts +1 -0
  173. package/dist/emails/templates/index.d.ts.map +1 -1
  174. package/dist/emails/templates/index.js +4 -0
  175. package/dist/emails/templates/index.js.map +1 -1
  176. package/dist/emails/types.d.ts +22 -1
  177. package/dist/emails/types.d.ts.map +1 -1
  178. package/package.json +21 -1
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @rovela/sdk/admin/permissions
3
+ *
4
+ * Role-based permission matrix for store admins.
5
+ *
6
+ * # Four-role hierarchy (strict)
7
+ *
8
+ * owner (rank 0) can do everything, including managing other owners.
9
+ * Subject to "at least one active owner always exists".
10
+ * administrator (rank 1) can do everything except manage owners or other administrators.
11
+ * Prevents lateral privilege escalation.
12
+ * manager (rank 2) full day-to-day operational access (products, orders, customers,
13
+ * returns, refunds). Cannot touch settings, billing, or users.
14
+ * user (rank 3) read-only on most resources + write access to orders only
15
+ * (for warehouse/fulfillment staff updating tracking).
16
+ *
17
+ * # Legacy 'admin' role
18
+ *
19
+ * Stores created before the four-role system shipped have rows with role='admin'.
20
+ * We never rewrite those rows (Postgres enums can't remove values anyway). The
21
+ * permission map treats 'admin' as an alias for 'administrator' — same exact
22
+ * permission set, same `canManageUser` behavior.
23
+ *
24
+ * # Client vs server
25
+ *
26
+ * All checks in this file are pure functions over static data. They are safe to
27
+ * import from both server and client code. The server uses them as the source
28
+ * of truth for authorization; the client uses them for UX gating (hide/disable
29
+ * buttons). The client is always advisory — the server is the gate.
30
+ */
31
+ import type { AdminRole } from '../core/types';
32
+ /**
33
+ * Every action an admin can perform, grouped by resource. Adding a new
34
+ * permission here means:
35
+ * 1. Add it to this type union
36
+ * 2. Assign it to one or more roles in the PERMISSIONS map below
37
+ * 3. Reference it via `hasPermission(role, 'foo.bar')` at the relevant API handler
38
+ */
39
+ export type Permission = 'products.read' | 'products.write' | 'products.delete' | 'orders.read' | 'orders.write' | 'orders.refund' | 'returns.approve' | 'returns.reject' | 'customers.read' | 'customers.write' | 'customers.delete' | 'settings.read' | 'settings.write' | 'billing.read' | 'billing.write' | 'users.read' | 'users.write' | 'users.delete';
40
+ /**
41
+ * Returns true if `actorRole` is at least as powerful as `minRole`.
42
+ *
43
+ * @example
44
+ * meetsMinRole('owner', 'manager') // true
45
+ * meetsMinRole('user', 'manager') // false
46
+ */
47
+ export declare function meetsMinRole(actorRole: AdminRole | null | undefined, minRole: AdminRole): boolean;
48
+ /**
49
+ * Does this role have a specific permission?
50
+ *
51
+ * Safe to call with `undefined` / `null` actor — returns `false`, which is what
52
+ * unauthenticated requests want.
53
+ *
54
+ * @example
55
+ * hasPermission('owner', 'users.delete') // true
56
+ * hasPermission('administrator', 'users.delete') // false
57
+ * hasPermission(undefined, 'products.read') // false
58
+ */
59
+ export declare function hasPermission(role: AdminRole | null | undefined, permission: Permission): boolean;
60
+ /**
61
+ * Can `actor` modify `target` as a user-management action?
62
+ *
63
+ * Rules:
64
+ * - Owners can manage anyone, including other owners. The "at least one
65
+ * active owner must remain" invariant is enforced separately at the
66
+ * service layer, not here — this helper only answers "does the actor
67
+ * have the authority in principle?"
68
+ * - Administrators can manage managers and users only. They cannot touch
69
+ * owners or other administrators. (Prevents lateral escalation.)
70
+ * - Managers and users cannot manage anyone.
71
+ * - Actor cannot manage themselves via this helper. Self-actions (password
72
+ * change, profile update) go through /api/admin/me/* endpoints and have
73
+ * their own rules.
74
+ */
75
+ export declare function canManageUser(actor: {
76
+ id: string;
77
+ role: AdminRole;
78
+ }, target: {
79
+ id: string;
80
+ role: AdminRole;
81
+ }): boolean;
82
+ /**
83
+ * Human-readable label for a role. Maps the legacy 'admin' to
84
+ * 'Administrator' so UI never leaks the old value.
85
+ */
86
+ export declare function roleLabel(role: AdminRole): string;
87
+ /**
88
+ * Canonical role for a given stored value. Collapses 'admin' → 'administrator'
89
+ * so callers don't need to think about the legacy alias.
90
+ */
91
+ export declare function canonicalRole(role: AdminRole): Exclude<AdminRole, 'admin'>;
92
+ //# sourceMappingURL=permissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/admin/permissions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAM9C;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAElB,eAAe,GACf,gBAAgB,GAChB,iBAAiB,GAEjB,aAAa,GACb,cAAc,GACd,eAAe,GAEf,iBAAiB,GACjB,gBAAgB,GAEhB,gBAAgB,GAChB,iBAAiB,GACjB,kBAAkB,GAElB,eAAe,GACf,gBAAgB,GAEhB,cAAc,GACd,eAAe,GAEf,YAAY,GACZ,aAAa,GACb,cAAc,CAAA;AAmBlB;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,EACvC,OAAO,EAAE,SAAS,GACjB,OAAO,CAGT;AAoFD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,EAClC,UAAU,EAAE,UAAU,GACrB,OAAO,CAIT;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,EACtC,MAAM,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GACtC,OAAO,CAaT;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAYjD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAE1E"}
@@ -0,0 +1,201 @@
1
+ /**
2
+ * @rovela/sdk/admin/permissions
3
+ *
4
+ * Role-based permission matrix for store admins.
5
+ *
6
+ * # Four-role hierarchy (strict)
7
+ *
8
+ * owner (rank 0) can do everything, including managing other owners.
9
+ * Subject to "at least one active owner always exists".
10
+ * administrator (rank 1) can do everything except manage owners or other administrators.
11
+ * Prevents lateral privilege escalation.
12
+ * manager (rank 2) full day-to-day operational access (products, orders, customers,
13
+ * returns, refunds). Cannot touch settings, billing, or users.
14
+ * user (rank 3) read-only on most resources + write access to orders only
15
+ * (for warehouse/fulfillment staff updating tracking).
16
+ *
17
+ * # Legacy 'admin' role
18
+ *
19
+ * Stores created before the four-role system shipped have rows with role='admin'.
20
+ * We never rewrite those rows (Postgres enums can't remove values anyway). The
21
+ * permission map treats 'admin' as an alias for 'administrator' — same exact
22
+ * permission set, same `canManageUser` behavior.
23
+ *
24
+ * # Client vs server
25
+ *
26
+ * All checks in this file are pure functions over static data. They are safe to
27
+ * import from both server and client code. The server uses them as the source
28
+ * of truth for authorization; the client uses them for UX gating (hide/disable
29
+ * buttons). The client is always advisory — the server is the gate.
30
+ */
31
+ // =============================================================================
32
+ // Role ranks + hierarchy helpers
33
+ // =============================================================================
34
+ /**
35
+ * Lower number = higher power. Used by `meetsMinRole` to enforce "must be at
36
+ * least an administrator" style checks. 'admin' is explicitly an alias for
37
+ * 'administrator' at rank 1.
38
+ */
39
+ const ROLE_RANK = {
40
+ owner: 0,
41
+ administrator: 1,
42
+ admin: 1, // legacy alias — identical to 'administrator'
43
+ manager: 2,
44
+ user: 3,
45
+ };
46
+ /**
47
+ * Returns true if `actorRole` is at least as powerful as `minRole`.
48
+ *
49
+ * @example
50
+ * meetsMinRole('owner', 'manager') // true
51
+ * meetsMinRole('user', 'manager') // false
52
+ */
53
+ export function meetsMinRole(actorRole, minRole) {
54
+ if (!actorRole)
55
+ return false;
56
+ return ROLE_RANK[actorRole] <= ROLE_RANK[minRole];
57
+ }
58
+ // =============================================================================
59
+ // Permission matrix
60
+ // =============================================================================
61
+ const OWNER_PERMISSIONS = [
62
+ 'products.read',
63
+ 'products.write',
64
+ 'products.delete',
65
+ 'orders.read',
66
+ 'orders.write',
67
+ 'orders.refund',
68
+ 'returns.approve',
69
+ 'returns.reject',
70
+ 'customers.read',
71
+ 'customers.write',
72
+ 'customers.delete',
73
+ 'settings.read',
74
+ 'settings.write',
75
+ 'billing.read',
76
+ 'billing.write',
77
+ 'users.read',
78
+ 'users.write',
79
+ 'users.delete',
80
+ ];
81
+ const ADMINISTRATOR_PERMISSIONS = [
82
+ 'products.read',
83
+ 'products.write',
84
+ 'products.delete',
85
+ 'orders.read',
86
+ 'orders.write',
87
+ 'orders.refund',
88
+ 'returns.approve',
89
+ 'returns.reject',
90
+ 'customers.read',
91
+ 'customers.write',
92
+ 'customers.delete',
93
+ 'settings.read',
94
+ 'settings.write',
95
+ 'billing.read',
96
+ 'billing.write',
97
+ 'users.read',
98
+ 'users.write',
99
+ // NOTE: 'users.delete' is intentionally owner-only. Administrators can
100
+ // deactivate (via users.write + canManageUser) but not hard-delete.
101
+ ];
102
+ const MANAGER_PERMISSIONS = [
103
+ 'products.read',
104
+ 'products.write',
105
+ 'orders.read',
106
+ 'orders.write',
107
+ 'orders.refund',
108
+ 'returns.approve',
109
+ 'returns.reject',
110
+ 'customers.read',
111
+ 'customers.write',
112
+ 'settings.read',
113
+ ];
114
+ const USER_PERMISSIONS = [
115
+ 'products.read',
116
+ 'orders.read',
117
+ 'orders.write', // fulfillment staff update tracking/status
118
+ ];
119
+ /**
120
+ * The canonical permission map. Each role points at a Set<Permission> for O(1)
121
+ * membership checks. 'admin' shares the administrator set (legacy alias).
122
+ */
123
+ const PERMISSIONS = {
124
+ owner: new Set(OWNER_PERMISSIONS),
125
+ administrator: new Set(ADMINISTRATOR_PERMISSIONS),
126
+ admin: new Set(ADMINISTRATOR_PERMISSIONS), // legacy alias
127
+ manager: new Set(MANAGER_PERMISSIONS),
128
+ user: new Set(USER_PERMISSIONS),
129
+ };
130
+ // =============================================================================
131
+ // Public API
132
+ // =============================================================================
133
+ /**
134
+ * Does this role have a specific permission?
135
+ *
136
+ * Safe to call with `undefined` / `null` actor — returns `false`, which is what
137
+ * unauthenticated requests want.
138
+ *
139
+ * @example
140
+ * hasPermission('owner', 'users.delete') // true
141
+ * hasPermission('administrator', 'users.delete') // false
142
+ * hasPermission(undefined, 'products.read') // false
143
+ */
144
+ export function hasPermission(role, permission) {
145
+ if (!role)
146
+ return false;
147
+ const perms = PERMISSIONS[role];
148
+ return perms ? perms.has(permission) : false;
149
+ }
150
+ /**
151
+ * Can `actor` modify `target` as a user-management action?
152
+ *
153
+ * Rules:
154
+ * - Owners can manage anyone, including other owners. The "at least one
155
+ * active owner must remain" invariant is enforced separately at the
156
+ * service layer, not here — this helper only answers "does the actor
157
+ * have the authority in principle?"
158
+ * - Administrators can manage managers and users only. They cannot touch
159
+ * owners or other administrators. (Prevents lateral escalation.)
160
+ * - Managers and users cannot manage anyone.
161
+ * - Actor cannot manage themselves via this helper. Self-actions (password
162
+ * change, profile update) go through /api/admin/me/* endpoints and have
163
+ * their own rules.
164
+ */
165
+ export function canManageUser(actor, target) {
166
+ // No self-management through this helper.
167
+ if (actor.id === target.id)
168
+ return false;
169
+ if (actor.role === 'owner') {
170
+ return true;
171
+ }
172
+ if (actor.role === 'administrator' || actor.role === 'admin') {
173
+ return target.role === 'manager' || target.role === 'user';
174
+ }
175
+ return false;
176
+ }
177
+ /**
178
+ * Human-readable label for a role. Maps the legacy 'admin' to
179
+ * 'Administrator' so UI never leaks the old value.
180
+ */
181
+ export function roleLabel(role) {
182
+ switch (role) {
183
+ case 'owner':
184
+ return 'Owner';
185
+ case 'admin':
186
+ case 'administrator':
187
+ return 'Administrator';
188
+ case 'manager':
189
+ return 'Manager';
190
+ case 'user':
191
+ return 'User';
192
+ }
193
+ }
194
+ /**
195
+ * Canonical role for a given stored value. Collapses 'admin' → 'administrator'
196
+ * so callers don't need to think about the legacy alias.
197
+ */
198
+ export function canonicalRole(role) {
199
+ return role === 'admin' ? 'administrator' : role;
200
+ }
201
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../src/admin/permissions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @rovela/sdk/admin/server/admin-invite
3
+ *
4
+ * Admin invite token lifecycle: validation, acceptance, and housekeeping.
5
+ *
6
+ * Paired with `user-management.ts::inviteAdmin` which creates the token +
7
+ * sends the email. This file handles the INVITEE's side of the flow: they
8
+ * click the email link, land on `/admin/accept-invite?token=X`, and this
9
+ * module validates + consumes the token.
10
+ *
11
+ * # Failure modes (all graceful — no exceptions leak out)
12
+ *
13
+ * - Token unknown → `{valid: false}` / `INVALID_TOKEN`
14
+ * - Token expired → delete row, return `{valid: false}` with "expired"
15
+ * - Admin deleted → cascaded delete already removed the token;
16
+ * caller will see `{valid: false}`
17
+ * - Admin already accepted → `{valid: false}` with "already accepted"
18
+ * (protects against double-clicks)
19
+ * - Admin was deactivated → same as "already accepted" from UX point of view
20
+ * - Concurrent acceptance → atomic UPDATE returns 0 rows → `INVALID_STATE`
21
+ *
22
+ * # Design notes
23
+ *
24
+ * The structure mirrors `admin-password-reset.ts` from Phase 1 exactly:
25
+ * 1. `validate*` is non-destructive (opportunistically cleans up expired
26
+ * tokens but doesn't touch the admin row).
27
+ * 2. `accept*` is destructive — it updates the password, flips status,
28
+ * and deletes ALL tokens for the admin (both invite + reset tokens,
29
+ * so a stale reset link can't resurrect access).
30
+ * 3. `delete*ForAdmin` helper used by `cancelInvite` / `resendInvite` to
31
+ * wipe stale tokens before issuing a new one.
32
+ * 4. `cleanupExpired` is an optional cron hook.
33
+ */
34
+ import type { AdminRole } from '../../core/types';
35
+ /**
36
+ * Invite token lifetime: 72 hours. Matches industry standard
37
+ * (Google Workspace, GitHub, Shopify).
38
+ */
39
+ export declare const INVITE_EXPIRY_MS: number;
40
+ /** Token length — nanoid(32) = ~192 bits of entropy, URL-safe. */
41
+ export declare const INVITE_TOKEN_LENGTH = 32;
42
+ /** Display string for the expiry duration. */
43
+ export declare const INVITE_EXPIRY_HOURS = "72";
44
+ export interface AdminInviteSnapshot {
45
+ id: string;
46
+ email: string;
47
+ name: string;
48
+ role: AdminRole;
49
+ }
50
+ export interface ValidateInviteResult {
51
+ valid: boolean;
52
+ error?: string;
53
+ admin?: AdminInviteSnapshot;
54
+ }
55
+ export interface AcceptInviteResult {
56
+ success: boolean;
57
+ error?: string;
58
+ }
59
+ /**
60
+ * Create a new invite token for an existing `invited` admin row.
61
+ *
62
+ * The caller is responsible for:
63
+ * - Validating the admin exists and is in `invited` status
64
+ * - Deleting any existing invite tokens for this admin (via
65
+ * `deleteAdminInviteTokens`) before calling this to keep "one active
66
+ * token" semantics
67
+ *
68
+ * @returns The plain-text token that should be embedded in the email link.
69
+ */
70
+ export declare function createInviteToken(params: {
71
+ adminId: string;
72
+ invitedBy: string;
73
+ }): Promise<{
74
+ token: string;
75
+ expires: Date;
76
+ }>;
77
+ /**
78
+ * Validate an invite token without consuming it.
79
+ *
80
+ * Used by the accept-invite page on mount to decide between rendering the
81
+ * password form or an "invalid/expired" error view.
82
+ *
83
+ * Side effect: expired tokens are opportunistically deleted as part of the
84
+ * check. All other failure modes leave the DB untouched.
85
+ */
86
+ export declare function validateInviteToken(token: string): Promise<ValidateInviteResult>;
87
+ /**
88
+ * Consume an invite token and activate the invited admin.
89
+ *
90
+ * Atomically:
91
+ * 1. Validates the token + admin row.
92
+ * 2. Hashes + stores the new password via the existing
93
+ * `updateAdminPassword` helper.
94
+ * 3. Flips status from 'invited' → 'active' via an atomic guarded UPDATE
95
+ * (if two clicks race, the second one fails with INVALID_STATE).
96
+ * 4. Deletes all invite tokens for this admin (single-use batch).
97
+ * 5. Defensively deletes any stale password reset tokens too — a
98
+ * previously abandoned reset flow shouldn't grant access after the
99
+ * user has taken ownership of the account via accept-invite.
100
+ */
101
+ export declare function acceptAdminInvite(token: string, newPassword: string): Promise<AcceptInviteResult>;
102
+ /**
103
+ * Delete all invite tokens for a specific admin.
104
+ *
105
+ * Called from:
106
+ * - `user-management.ts::inviteAdmin` — not called; the admin is newly
107
+ * created so there are no tokens to delete
108
+ * - `user-management.ts::resendAdminInvite` — clear stale tokens before
109
+ * creating a new one
110
+ * - `acceptAdminInvite` above — invalidate the used batch
111
+ * - `user-management.ts::cancelAdminInvite` — cascade handles this
112
+ * automatically via FK, but we call explicitly for clarity
113
+ */
114
+ export declare function deleteAdminInviteTokens(adminId: string): Promise<void>;
115
+ /**
116
+ * Delete all expired invite tokens. Optional hook for a periodic cleanup
117
+ * job. Safe to call anytime — no-op if nothing is expired.
118
+ *
119
+ * @returns Number of tokens deleted
120
+ */
121
+ export declare function cleanupExpiredInviteTokens(): Promise<number>;
122
+ //# sourceMappingURL=admin-invite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-invite.d.ts","sourceRoot":"","sources":["../../../src/admin/server/admin-invite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AASH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAMjD;;;GAGG;AACH,eAAO,MAAM,gBAAgB,QAAsB,CAAA;AAEnD,kEAAkE;AAClE,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC,8CAA8C;AAC9C,eAAO,MAAM,mBAAmB,OAAO,CAAA;AAMvC,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,SAAS,CAAA;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,mBAAmB,CAAA;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAMD;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE;IAC9C,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,IAAI,CAAA;CAAE,CAAC,CAa5C;AAMD;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,oBAAoB,CAAC,CA+D/B;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,kBAAkB,CAAC,CA8C7B;AAMD;;;;;;;;;;;GAWG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK5E;AAED;;;;;GAKG;AACH,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,MAAM,CAAC,CAOlE"}
@@ -0,0 +1,235 @@
1
+ /**
2
+ * @rovela/sdk/admin/server/admin-invite
3
+ *
4
+ * Admin invite token lifecycle: validation, acceptance, and housekeeping.
5
+ *
6
+ * Paired with `user-management.ts::inviteAdmin` which creates the token +
7
+ * sends the email. This file handles the INVITEE's side of the flow: they
8
+ * click the email link, land on `/admin/accept-invite?token=X`, and this
9
+ * module validates + consumes the token.
10
+ *
11
+ * # Failure modes (all graceful — no exceptions leak out)
12
+ *
13
+ * - Token unknown → `{valid: false}` / `INVALID_TOKEN`
14
+ * - Token expired → delete row, return `{valid: false}` with "expired"
15
+ * - Admin deleted → cascaded delete already removed the token;
16
+ * caller will see `{valid: false}`
17
+ * - Admin already accepted → `{valid: false}` with "already accepted"
18
+ * (protects against double-clicks)
19
+ * - Admin was deactivated → same as "already accepted" from UX point of view
20
+ * - Concurrent acceptance → atomic UPDATE returns 0 rows → `INVALID_STATE`
21
+ *
22
+ * # Design notes
23
+ *
24
+ * The structure mirrors `admin-password-reset.ts` from Phase 1 exactly:
25
+ * 1. `validate*` is non-destructive (opportunistically cleans up expired
26
+ * tokens but doesn't touch the admin row).
27
+ * 2. `accept*` is destructive — it updates the password, flips status,
28
+ * and deletes ALL tokens for the admin (both invite + reset tokens,
29
+ * so a stale reset link can't resurrect access).
30
+ * 3. `delete*ForAdmin` helper used by `cancelInvite` / `resendInvite` to
31
+ * wipe stale tokens before issuing a new one.
32
+ * 4. `cleanupExpired` is an optional cron hook.
33
+ */
34
+ import { eq, lt } from 'drizzle-orm';
35
+ import { nanoid } from 'nanoid';
36
+ import { getDb } from '../../core/db/client';
37
+ import * as schema from '../../core/db/schema';
38
+ import { findAdminById, updateAdminPassword } from './admin-service';
39
+ import { deleteAdminPasswordResetTokens } from './admin-password-reset';
40
+ import { validatePassword } from '../../auth/server/password';
41
+ // =============================================================================
42
+ // Constants
43
+ // =============================================================================
44
+ /**
45
+ * Invite token lifetime: 72 hours. Matches industry standard
46
+ * (Google Workspace, GitHub, Shopify).
47
+ */
48
+ export const INVITE_EXPIRY_MS = 72 * 60 * 60 * 1000;
49
+ /** Token length — nanoid(32) = ~192 bits of entropy, URL-safe. */
50
+ export const INVITE_TOKEN_LENGTH = 32;
51
+ /** Display string for the expiry duration. */
52
+ export const INVITE_EXPIRY_HOURS = '72';
53
+ // =============================================================================
54
+ // Token creation (used by inviteAdmin + resendAdminInvite in user-management.ts)
55
+ // =============================================================================
56
+ /**
57
+ * Create a new invite token for an existing `invited` admin row.
58
+ *
59
+ * The caller is responsible for:
60
+ * - Validating the admin exists and is in `invited` status
61
+ * - Deleting any existing invite tokens for this admin (via
62
+ * `deleteAdminInviteTokens`) before calling this to keep "one active
63
+ * token" semantics
64
+ *
65
+ * @returns The plain-text token that should be embedded in the email link.
66
+ */
67
+ export async function createInviteToken(params) {
68
+ const db = getDb();
69
+ const token = nanoid(INVITE_TOKEN_LENGTH);
70
+ const expires = new Date(Date.now() + INVITE_EXPIRY_MS);
71
+ await db.insert(schema.adminInviteTokens).values({
72
+ adminId: params.adminId,
73
+ token,
74
+ expires,
75
+ invitedBy: params.invitedBy,
76
+ });
77
+ return { token, expires };
78
+ }
79
+ // =============================================================================
80
+ // Validate (non-destructive)
81
+ // =============================================================================
82
+ /**
83
+ * Validate an invite token without consuming it.
84
+ *
85
+ * Used by the accept-invite page on mount to decide between rendering the
86
+ * password form or an "invalid/expired" error view.
87
+ *
88
+ * Side effect: expired tokens are opportunistically deleted as part of the
89
+ * check. All other failure modes leave the DB untouched.
90
+ */
91
+ export async function validateInviteToken(token) {
92
+ if (!token) {
93
+ return {
94
+ valid: false,
95
+ error: 'Invite link is missing a token. Please request a new invite.',
96
+ };
97
+ }
98
+ const db = getDb();
99
+ const [record] = await db
100
+ .select()
101
+ .from(schema.adminInviteTokens)
102
+ .where(eq(schema.adminInviteTokens.token, token))
103
+ .limit(1);
104
+ if (!record) {
105
+ return {
106
+ valid: false,
107
+ error: 'Invalid or expired invite link. Please request a new one.',
108
+ };
109
+ }
110
+ if (new Date() > record.expires) {
111
+ // Opportunistic cleanup
112
+ await db
113
+ .delete(schema.adminInviteTokens)
114
+ .where(eq(schema.adminInviteTokens.id, record.id));
115
+ return {
116
+ valid: false,
117
+ error: 'Invite link has expired. Please request a new one.',
118
+ };
119
+ }
120
+ const admin = await findAdminById(record.adminId);
121
+ if (!admin) {
122
+ // Cascaded delete should have prevented this — defensive
123
+ return {
124
+ valid: false,
125
+ error: 'Invalid or expired invite link. Please request a new one.',
126
+ };
127
+ }
128
+ if (admin.status !== 'invited') {
129
+ return {
130
+ valid: false,
131
+ error: admin.status === 'active'
132
+ ? 'This invite has already been accepted. Please sign in with your password.'
133
+ : 'This invite is no longer valid.',
134
+ };
135
+ }
136
+ return {
137
+ valid: true,
138
+ admin: {
139
+ id: admin.id,
140
+ email: admin.email,
141
+ name: admin.name,
142
+ role: admin.role,
143
+ },
144
+ };
145
+ }
146
+ // =============================================================================
147
+ // Accept (destructive — consumes token, activates admin)
148
+ // =============================================================================
149
+ /**
150
+ * Consume an invite token and activate the invited admin.
151
+ *
152
+ * Atomically:
153
+ * 1. Validates the token + admin row.
154
+ * 2. Hashes + stores the new password via the existing
155
+ * `updateAdminPassword` helper.
156
+ * 3. Flips status from 'invited' → 'active' via an atomic guarded UPDATE
157
+ * (if two clicks race, the second one fails with INVALID_STATE).
158
+ * 4. Deletes all invite tokens for this admin (single-use batch).
159
+ * 5. Defensively deletes any stale password reset tokens too — a
160
+ * previously abandoned reset flow shouldn't grant access after the
161
+ * user has taken ownership of the account via accept-invite.
162
+ */
163
+ export async function acceptAdminInvite(token, newPassword) {
164
+ // 1. Re-validate (fresh read — the validate* function is idempotent)
165
+ const validation = await validateInviteToken(token);
166
+ if (!validation.valid || !validation.admin) {
167
+ return {
168
+ success: false,
169
+ error: validation.error || 'Invalid or expired invite link.',
170
+ };
171
+ }
172
+ const adminId = validation.admin.id;
173
+ // 2. Password strength check
174
+ const passwordCheck = validatePassword(newPassword);
175
+ if (!passwordCheck.valid) {
176
+ return { success: false, error: passwordCheck.error };
177
+ }
178
+ const db = getDb();
179
+ // 3. Hash + store password (updateAdminPassword handles hashing internally)
180
+ await updateAdminPassword(adminId, newPassword);
181
+ // 4. Flip status — guarded so a concurrent accept gets rejected cleanly
182
+ const statusUpdate = await db
183
+ .update(schema.storeAdmins)
184
+ .set({ status: 'active' })
185
+ .where(eq(schema.storeAdmins.id, adminId))
186
+ .returning({ id: schema.storeAdmins.id, status: schema.storeAdmins.status });
187
+ if (statusUpdate.length === 0) {
188
+ // Target was deleted between validate and update — race condition
189
+ return {
190
+ success: false,
191
+ error: 'Admin state changed while accepting invite. Please try again.',
192
+ };
193
+ }
194
+ // 5. Wipe all invite tokens for this admin (single-use batch)
195
+ await deleteAdminInviteTokens(adminId);
196
+ // 6. Defensive: wipe any stale password reset tokens too
197
+ await deleteAdminPasswordResetTokens(adminId);
198
+ return { success: true };
199
+ }
200
+ // =============================================================================
201
+ // Housekeeping
202
+ // =============================================================================
203
+ /**
204
+ * Delete all invite tokens for a specific admin.
205
+ *
206
+ * Called from:
207
+ * - `user-management.ts::inviteAdmin` — not called; the admin is newly
208
+ * created so there are no tokens to delete
209
+ * - `user-management.ts::resendAdminInvite` — clear stale tokens before
210
+ * creating a new one
211
+ * - `acceptAdminInvite` above — invalidate the used batch
212
+ * - `user-management.ts::cancelAdminInvite` — cascade handles this
213
+ * automatically via FK, but we call explicitly for clarity
214
+ */
215
+ export async function deleteAdminInviteTokens(adminId) {
216
+ const db = getDb();
217
+ await db
218
+ .delete(schema.adminInviteTokens)
219
+ .where(eq(schema.adminInviteTokens.adminId, adminId));
220
+ }
221
+ /**
222
+ * Delete all expired invite tokens. Optional hook for a periodic cleanup
223
+ * job. Safe to call anytime — no-op if nothing is expired.
224
+ *
225
+ * @returns Number of tokens deleted
226
+ */
227
+ export async function cleanupExpiredInviteTokens() {
228
+ const db = getDb();
229
+ const result = await db
230
+ .delete(schema.adminInviteTokens)
231
+ .where(lt(schema.adminInviteTokens.expires, new Date()))
232
+ .returning({ id: schema.adminInviteTokens.id });
233
+ return result.length;
234
+ }
235
+ //# sourceMappingURL=admin-invite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-invite.js","sourceRoot":"","sources":["../../../src/admin/server/admin-invite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAC5C,OAAO,KAAK,MAAM,MAAM,sBAAsB,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AACpE,OAAO,EAAE,8BAA8B,EAAE,MAAM,wBAAwB,CAAA;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAG7D,gFAAgF;AAChF,YAAY;AACZ,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAEnD,kEAAkE;AAClE,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAA;AAErC,8CAA8C;AAC9C,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAA;AAwBvC,gFAAgF;AAChF,iFAAiF;AACjF,gFAAgF;AAEhF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAGvC;IACC,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;IAClB,MAAM,KAAK,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAA;IACzC,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,gBAAgB,CAAC,CAAA;IAEvD,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,MAAM,CAAC;QAC/C,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,KAAK;QACL,OAAO;QACP,SAAS,EAAE,MAAM,CAAC,SAAS;KAC5B,CAAC,CAAA;IAEF,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;AAC3B,CAAC;AAED,gFAAgF;AAChF,6BAA6B;AAC7B,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,KAAa;IAEb,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,KAAK,EAAE,8DAA8D;SACtE,CAAA;IACH,CAAC;IAED,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;IAElB,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE;SACtB,MAAM,EAAE;SACR,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC;SAC9B,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;SAChD,KAAK,CAAC,CAAC,CAAC,CAAA;IAEX,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,KAAK,EAAE,2DAA2D;SACnE,CAAA;IACH,CAAC;IAED,IAAI,IAAI,IAAI,EAAE,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;QAChC,wBAAwB;QACxB,MAAM,EAAE;aACL,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC;aAChC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAA;QAEpD,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,KAAK,EAAE,oDAAoD;SAC5D,CAAA;IACH,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACjD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,yDAAyD;QACzD,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,KAAK,EAAE,2DAA2D;SACnE,CAAA;IACH,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,KAAK,EACH,KAAK,CAAC,MAAM,KAAK,QAAQ;gBACvB,CAAC,CAAC,2EAA2E;gBAC7E,CAAC,CAAC,iCAAiC;SACxC,CAAA;IACH,CAAC;IAED,OAAO;QACL,KAAK,EAAE,IAAI;QACX,KAAK,EAAE;YACL,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAiB;SAC9B;KACF,CAAA;AACH,CAAC;AAED,gFAAgF;AAChF,yDAAyD;AACzD,gFAAgF;AAEhF;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAAa,EACb,WAAmB;IAEnB,qEAAqE;IACrE,MAAM,UAAU,GAAG,MAAM,mBAAmB,CAAC,KAAK,CAAC,CAAA;IACnD,IAAI,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QAC3C,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,UAAU,CAAC,KAAK,IAAI,iCAAiC;SAC7D,CAAA;IACH,CAAC;IACD,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,EAAE,CAAA;IAEnC,6BAA6B;IAC7B,MAAM,aAAa,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAA;IACnD,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC;QACzB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,CAAC,KAAK,EAAE,CAAA;IACvD,CAAC;IAED,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;IAElB,4EAA4E;IAC5E,MAAM,mBAAmB,CAAC,OAAO,EAAE,WAAW,CAAC,CAAA;IAE/C,wEAAwE;IACxE,MAAM,YAAY,GAAG,MAAM,EAAE;SAC1B,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC;SAC1B,GAAG,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;SACzB,KAAK,CACJ,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,EAAE,OAAO,CAAC,CACnC;SACA,SAAS,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,CAAA;IAE9E,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,kEAAkE;QAClE,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,+DAA+D;SACvE,CAAA;IACH,CAAC;IAED,8DAA8D;IAC9D,MAAM,uBAAuB,CAAC,OAAO,CAAC,CAAA;IAEtC,yDAAyD;IACzD,MAAM,8BAA8B,CAAC,OAAO,CAAC,CAAA;IAE7C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC1B,CAAC;AAED,gFAAgF;AAChF,eAAe;AACf,gFAAgF;AAEhF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,OAAe;IAC3D,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;IAClB,MAAM,EAAE;SACL,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC;SAChC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAA;AACzD,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B;IAC9C,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;IAClB,MAAM,MAAM,GAAG,MAAM,EAAE;SACpB,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC;SAChC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;SACvD,SAAS,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,iBAAiB,CAAC,EAAE,EAAE,CAAC,CAAA;IACjD,OAAO,MAAM,CAAC,MAAM,CAAA;AACtB,CAAC"}