@makolabs/ripple 3.0.11 → 3.1.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.
@@ -109,6 +109,7 @@ export declare function getPendingUsers(options: GetUsersOptions): Promise<GetUs
109
109
  export declare function approveUser(input: {
110
110
  userId: string;
111
111
  role: string;
112
+ permissions?: string[];
112
113
  }): Promise<{
113
114
  apiKey: string;
114
115
  }>;
@@ -21,6 +21,29 @@ export declare const generateApiKey: import("@sveltejs/kit").RemoteCommand<{
21
21
  apiKey: string;
22
22
  message: string;
23
23
  }>>;
24
+ /**
25
+ * List users who exist in the identity provider but are NOT yet members of
26
+ * ALLOWED_ORG_ID. Treats org membership as the "approved" signal — anyone
27
+ * authenticated against the app without org membership is "pending".
28
+ */
29
+ export declare const getPendingUsers: import("@sveltejs/kit").RemoteCommand<GetUsersOptions, Promise<GetUsersResult>>;
30
+ /**
31
+ * Approve a pending user: add them to ALLOWED_ORG_ID with the chosen Clerk role,
32
+ * then mint an API key carrying the supplied scope set. The caller (the modal)
33
+ * resolves the role's scopes from the `roles` prop and passes them in.
34
+ */
35
+ export declare const approveUser: import("@sveltejs/kit").RemoteCommand<{
36
+ userId: string;
37
+ role: string;
38
+ permissions: string[];
39
+ }, Promise<{
40
+ apiKey: string;
41
+ }>>;
42
+ /**
43
+ * Reject a pending user — permanently deletes them from the identity provider.
44
+ * Destructive and irreversible; the user loses login, history, everything.
45
+ */
46
+ export declare const rejectUser: import("@sveltejs/kit").RemoteCommand<string, Promise<void>>;
24
47
  export declare const verifyToken: import("@sveltejs/kit").RemoteCommand<{
25
48
  apiKey: string;
26
49
  }, Promise<{
@@ -772,6 +772,134 @@ export const generateApiKey = command('unchecked', async (options) => {
772
772
  throw new Error(`Failed to generate API key: ${error instanceof Error ? error.message : 'Unknown error'}`);
773
773
  }
774
774
  });
775
+ /**
776
+ * Fetch the set of Clerk user IDs that already belong to ALLOWED_ORG_ID.
777
+ * Pages through Clerk's memberships endpoint until exhausted.
778
+ */
779
+ async function fetchOrgMemberIds() {
780
+ if (!ORGANIZATION_ID) {
781
+ throw new Error('ALLOWED_ORG_ID environment variable is required');
782
+ }
783
+ const memberIds = new Set();
784
+ const limit = 500;
785
+ let offset = 0;
786
+ while (true) {
787
+ const data = await makeClerkRequest(`/organizations/${ORGANIZATION_ID}/memberships?limit=${limit}&offset=${offset}`);
788
+ const list = Array.isArray(data)
789
+ ? data
790
+ : (data?.data ?? []);
791
+ for (const m of list) {
792
+ const uid = m?.public_user_data?.user_id;
793
+ if (uid)
794
+ memberIds.add(uid);
795
+ }
796
+ if (list.length < limit)
797
+ break;
798
+ offset += limit;
799
+ }
800
+ return memberIds;
801
+ }
802
+ /**
803
+ * List users who exist in the identity provider but are NOT yet members of
804
+ * ALLOWED_ORG_ID. Treats org membership as the "approved" signal — anyone
805
+ * authenticated against the app without org membership is "pending".
806
+ */
807
+ export const getPendingUsers = command('unchecked', async (options) => {
808
+ log.trace('getPendingUsers', 'Called with options:', options);
809
+ try {
810
+ const memberIds = await fetchOrgMemberIds();
811
+ // Pull all users (paginated) — Clerk has no "not in org" filter, so
812
+ // we filter client-side. For typical org sizes this is one or two API calls.
813
+ const all = [];
814
+ const limit = 500;
815
+ let offset = 0;
816
+ while (true) {
817
+ const params = new URLSearchParams({
818
+ limit: limit.toString(),
819
+ offset: offset.toString(),
820
+ order_by: '-created_at'
821
+ });
822
+ if (options.query)
823
+ params.append('query', options.query);
824
+ const page = await makeClerkRequest(`/users?${params}`);
825
+ const users = Array.isArray(page) ? page : (page?.data ?? []);
826
+ all.push(...users);
827
+ if (users.length < limit)
828
+ break;
829
+ offset += limit;
830
+ }
831
+ const pending = all.filter((u) => !memberIds.has(u.id));
832
+ const start = (options.page - 1) * options.pageSize;
833
+ const slice = pending.slice(start, start + options.pageSize);
834
+ log.trace('getPendingUsers', 'Total users:', all.length, 'org members:', memberIds.size, 'pending:', pending.length);
835
+ return JSON.parse(JSON.stringify({
836
+ users: slice,
837
+ totalUsers: pending.length
838
+ }));
839
+ }
840
+ catch (error) {
841
+ log.error('getPendingUsers', 'Error:', error);
842
+ throw new Error(`Failed to fetch pending users: ${error instanceof Error ? error.message : 'Unknown error'}`);
843
+ }
844
+ });
845
+ /**
846
+ * Approve a pending user: add them to ALLOWED_ORG_ID with the chosen Clerk role,
847
+ * then mint an API key carrying the supplied scope set. The caller (the modal)
848
+ * resolves the role's scopes from the `roles` prop and passes them in.
849
+ */
850
+ export const approveUser = command('unchecked', async (input) => {
851
+ log.trace('approveUser', 'Called for userId:', input.userId, 'role:', input.role, 'permissions:', input.permissions);
852
+ if (!input.role?.trim()) {
853
+ throw new Error('Role is required to approve a user');
854
+ }
855
+ if (!input.permissions?.length) {
856
+ throw new Error('At least one permission scope is required to approve a user');
857
+ }
858
+ try {
859
+ const clerkRole = input.role.startsWith('org:') ? input.role : `org:${input.role}`;
860
+ await makeClerkRequest(`/organizations/${ORGANIZATION_ID}/memberships`, {
861
+ method: 'POST',
862
+ body: JSON.stringify({ user_id: input.userId, role: clerkRole })
863
+ });
864
+ const user = await makeClerkRequest(`/users/${input.userId}`);
865
+ const email = user.email_addresses?.[0]?.email_address;
866
+ if (!email) {
867
+ throw new Error('User has no email address');
868
+ }
869
+ const adminKeyResult = await createUserPermissions(email, input.permissions);
870
+ const apiKey = adminKeyResult?.data?.key;
871
+ if (!apiKey) {
872
+ throw new Error('Failed to mint API key during approval');
873
+ }
874
+ await makeClerkRequest(`/users/${input.userId}`, {
875
+ method: 'PATCH',
876
+ body: JSON.stringify({
877
+ private_metadata: { ...(user.private_metadata || {}), mako_api_key: apiKey }
878
+ })
879
+ });
880
+ log.trace('approveUser', 'Approved and key issued');
881
+ return JSON.parse(JSON.stringify({ apiKey }));
882
+ }
883
+ catch (error) {
884
+ log.error('approveUser', 'Error:', error);
885
+ handleClerkError(error, 'Failed to approve user');
886
+ }
887
+ });
888
+ /**
889
+ * Reject a pending user — permanently deletes them from the identity provider.
890
+ * Destructive and irreversible; the user loses login, history, everything.
891
+ */
892
+ export const rejectUser = command('unchecked', async (userId) => {
893
+ log.trace('rejectUser', 'Called for userId:', userId);
894
+ try {
895
+ await makeClerkRequest(`/users/${userId}`, { method: 'DELETE' });
896
+ log.trace('rejectUser', 'Deleted successfully');
897
+ }
898
+ catch (error) {
899
+ log.error('rejectUser', 'Error:', error);
900
+ throw new Error(`Failed to reject user: ${error instanceof Error ? error.message : 'Unknown error'}`);
901
+ }
902
+ });
775
903
  export const verifyToken = command('unchecked', async (options) => {
776
904
  log.trace('verifyToken', 'Called');
777
905
  try {
@@ -32,7 +32,12 @@
32
32
  if (!user || !selectedRole) return;
33
33
  approving = true;
34
34
  try {
35
- const result = await onapprove({ userId: user.id, role: selectedRole });
35
+ const permissions = roles.find((r) => r.value === selectedRole)?.permissions ?? [];
36
+ const result = await onapprove({
37
+ userId: user.id,
38
+ role: selectedRole,
39
+ permissions: [...permissions]
40
+ });
36
41
  issuedKey = result.apiKey;
37
42
  } catch (error) {
38
43
  toast.error(error instanceof Error ? error.message : 'Failed to approve user');
@@ -320,11 +320,19 @@
320
320
  showApproveModal = true;
321
321
  }
322
322
 
323
- async function handleApproveUser({ userId, role }: { userId: string; role: string }) {
323
+ async function handleApproveUser({
324
+ userId,
325
+ role,
326
+ permissions
327
+ }: {
328
+ userId: string;
329
+ role: string;
330
+ permissions: string[];
331
+ }) {
324
332
  if (!adapter.approveUser) {
325
333
  throw new Error('approveUser is not configured on the adapter');
326
334
  }
327
- const result = await adapter.approveUser({ userId, role });
335
+ const result = await adapter.approveUser({ userId, role, permissions });
328
336
  // The user is approved AND we hold their one-time API key. A refresh failure
329
337
  // here must NOT lose that key — surface a soft warning instead of throwing.
330
338
  try {
@@ -214,12 +214,15 @@ export interface UserManagementAdapter {
214
214
  getPendingUsers?: (options: GetUsersOptions) => PromiseLike<GetUsersResult>;
215
215
  /**
216
216
  * Optional. Approve a pending user — assigns them to the org with the
217
- * selected role and generates their first API key. Returns the new key
218
- * for one-time reveal in the UI.
217
+ * selected role and generates their first API key with the given scopes.
218
+ * Returns the new key for one-time reveal in the UI. The `permissions`
219
+ * array is the resolved scope list for the chosen role; the modal looks
220
+ * it up from the `roles` prop so adapters don't need a role→scope map.
219
221
  */
220
222
  approveUser?: (input: {
221
223
  userId: string;
222
224
  role: string;
225
+ permissions: string[];
223
226
  }) => PromiseLike<{
224
227
  apiKey: string;
225
228
  }>;
@@ -238,12 +241,14 @@ export interface UserApproveModalProps {
238
241
  user: User | null;
239
242
  roles?: Role[];
240
243
  /**
241
- * Approval callback. Receives the userId and chosen role; should resolve
244
+ * Approval callback. Receives the userId, chosen role, and the resolved
245
+ * scope list for that role (looked up from the `roles` prop). Resolves
242
246
  * with the newly generated API key for one-time display.
243
247
  */
244
248
  onapprove: (input: {
245
249
  userId: string;
246
250
  role: string;
251
+ permissions: string[];
247
252
  }) => Promise<{
248
253
  apiKey: string;
249
254
  }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makolabs/ripple",
3
- "version": "3.0.11",
3
+ "version": "3.1.0",
4
4
  "description": "Simple Svelte 5 powered component library ✨",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {