@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.
- package/dist/funcs/mock-user-management.d.ts +1 -0
- package/dist/funcs/user-management.remote.d.ts +23 -0
- package/dist/funcs/user-management.remote.js +128 -0
- package/dist/user-management/UserApproveModal.svelte +6 -1
- package/dist/user-management/UserManagement.svelte +10 -2
- package/dist/user-management/user-management-types.d.ts +8 -3
- package/package.json +1 -1
|
@@ -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
|
|
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({
|
|
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
|
|
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
|
|
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
|
}>;
|