@robelest/convex-auth 0.0.4-preview.16 → 0.0.4-preview.18
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/authorization/index.d.ts +17 -0
- package/dist/authorization/index.d.ts.map +1 -0
- package/dist/authorization/index.js +17 -0
- package/dist/authorization/index.js.map +1 -0
- package/dist/client/index.js +73 -0
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +52 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/index.js +2 -2
- package/dist/component/model.d.ts +75 -75
- package/dist/component/model.d.ts.map +1 -1
- package/dist/component/model.js +2 -0
- package/dist/component/model.js.map +1 -1
- package/dist/component/public/factors.d.ts.map +1 -1
- package/dist/component/public/factors.js +1 -1
- package/dist/component/public/factors.js.map +1 -1
- package/dist/component/public/groups.d.ts +10 -2
- package/dist/component/public/groups.d.ts.map +1 -1
- package/dist/component/public/groups.js +92 -8
- package/dist/component/public/groups.js.map +1 -1
- package/dist/component/public/identity.d.ts.map +1 -1
- package/dist/component/public/identity.js +3 -3
- package/dist/component/public/identity.js.map +1 -1
- package/dist/component/public/shared.d.ts +4 -4
- package/dist/component/public/shared.d.ts.map +1 -1
- package/dist/component/public.d.ts +3 -3
- package/dist/component/public.js +2 -2
- package/dist/component/schema.d.ts +13 -2
- package/dist/component/schema.js +7 -5
- package/dist/component/schema.js.map +1 -1
- package/dist/component/server/auth.d.ts +4 -25
- package/dist/component/server/auth.d.ts.map +1 -1
- package/dist/component/server/auth.js +4 -10
- package/dist/component/server/auth.js.map +1 -1
- package/dist/component/server/domains/core.js +196 -131
- package/dist/component/server/domains/core.js.map +1 -1
- package/dist/component/server/factory.d.ts +3 -3
- package/dist/component/server/signin.js +20 -1
- package/dist/component/server/signin.js.map +1 -1
- package/dist/server/auth.d.ts +4 -25
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +4 -10
- package/dist/server/auth.js.map +1 -1
- package/dist/server/domains/core.d.ts +73 -58
- package/dist/server/domains/core.d.ts.map +1 -1
- package/dist/server/domains/core.js +196 -131
- package/dist/server/domains/core.js.map +1 -1
- package/dist/server/http.d.ts +2 -2
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/index.d.ts +108 -69
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +138 -54
- package/dist/server/index.js.map +1 -1
- package/dist/server/mutations/code.d.ts +9 -9
- package/dist/server/mutations/index.d.ts +69 -69
- package/dist/server/mutations/invalidate.d.ts +4 -4
- package/dist/server/mutations/invalidate.d.ts.map +1 -1
- package/dist/server/mutations/oauth.d.ts +7 -7
- package/dist/server/mutations/register.d.ts +9 -9
- package/dist/server/mutations/retrieve.d.ts +6 -6
- package/dist/server/mutations/retrieve.d.ts.map +1 -1
- package/dist/server/mutations/signature.d.ts +4 -4
- package/dist/server/mutations/signature.d.ts.map +1 -1
- package/dist/server/mutations/signin.d.ts +5 -5
- package/dist/server/mutations/signin.d.ts.map +1 -1
- package/dist/server/mutations/verify.d.ts +7 -7
- package/dist/server/signin.js +20 -1
- package/dist/server/signin.js.map +1 -1
- package/dist/server/version.d.ts +1 -1
- package/dist/server/version.js +1 -1
- package/dist/server/version.js.map +1 -1
- package/package.json +9 -5
- package/src/authorization/index.ts +37 -0
- package/src/client/index.ts +122 -0
- package/src/component/_generated/component.ts +64 -0
- package/src/component/index.ts +1 -1
- package/src/component/model.ts +2 -0
- package/src/component/public/factors.ts +3 -2
- package/src/component/public/groups.ts +142 -8
- package/src/component/public/identity.ts +9 -6
- package/src/component/schema.ts +14 -2
- package/src/server/auth.ts +9 -61
- package/src/server/domains/core.ts +235 -210
- package/src/server/index.ts +158 -75
- package/src/server/signin.ts +52 -0
- package/src/server/version.ts +1 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Auth, GenericActionCtx, GenericDataModel } from "convex/server";
|
|
2
2
|
import { GenericId } from "convex/values";
|
|
3
3
|
|
|
4
|
-
import { AuthError, Fx } from "../fx";
|
|
5
4
|
import {
|
|
6
5
|
buildScopeChecker,
|
|
7
6
|
checkKeyRateLimit,
|
|
@@ -92,28 +91,27 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
92
91
|
>;
|
|
93
92
|
|
|
94
93
|
const getRoleDefinition = (roleId: string) => {
|
|
95
|
-
|
|
96
|
-
if (!role) {
|
|
97
|
-
throw new AuthError(
|
|
98
|
-
"INVALID_PARAMETERS",
|
|
99
|
-
`Unknown roleId "${roleId}".`,
|
|
100
|
-
).toConvexError();
|
|
101
|
-
}
|
|
102
|
-
return role;
|
|
94
|
+
return roleDefinitions[roleId] ?? null;
|
|
103
95
|
};
|
|
104
96
|
|
|
105
|
-
const normalizeRoleIds = (
|
|
97
|
+
const normalizeRoleIds = (
|
|
98
|
+
roleIds?: string[],
|
|
99
|
+
):
|
|
100
|
+
| { ok: true; roleIds: string[] }
|
|
101
|
+
| { ok: false; invalidRoleIds: string[] } => {
|
|
106
102
|
const normalized = Array.from(new Set(roleIds ?? []));
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
const invalid = normalized.filter((id) => getRoleDefinition(id) === null);
|
|
104
|
+
if (invalid.length > 0) {
|
|
105
|
+
return { ok: false, invalidRoleIds: invalid };
|
|
109
106
|
}
|
|
110
|
-
return normalized;
|
|
107
|
+
return { ok: true, roleIds: normalized };
|
|
111
108
|
};
|
|
112
109
|
|
|
113
110
|
const resolveGrantedPermissions = (roleIds?: string[]) => {
|
|
114
111
|
const grants = new Set<string>();
|
|
115
112
|
for (const roleId of roleIds ?? []) {
|
|
116
113
|
const role = getRoleDefinition(roleId);
|
|
114
|
+
if (role === null) continue;
|
|
117
115
|
for (const grant of role.grants) {
|
|
118
116
|
grants.add(grant);
|
|
119
117
|
}
|
|
@@ -121,8 +119,25 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
121
119
|
return Array.from(grants).sort();
|
|
122
120
|
};
|
|
123
121
|
|
|
122
|
+
// Per-execution cache — attached to ctx so each function invocation has its own.
|
|
123
|
+
// Eliminates redundant cross-component RPCs for the same entity within a handler.
|
|
124
|
+
type CtxCache = {
|
|
125
|
+
users: Map<string, any>;
|
|
126
|
+
groups: Map<string, any>;
|
|
127
|
+
};
|
|
128
|
+
const AUTH_CACHE = Symbol("__convexAuthCache");
|
|
129
|
+
function cache(ctx: any): CtxCache {
|
|
130
|
+
if (!ctx[AUTH_CACHE]) {
|
|
131
|
+
ctx[AUTH_CACHE] = {
|
|
132
|
+
users: new Map(),
|
|
133
|
+
groups: new Map(),
|
|
134
|
+
} satisfies CtxCache;
|
|
135
|
+
}
|
|
136
|
+
return ctx[AUTH_CACHE];
|
|
137
|
+
}
|
|
138
|
+
|
|
124
139
|
const user = {
|
|
125
|
-
|
|
140
|
+
id: async (
|
|
126
141
|
ctx: { auth: Auth } & Partial<ComponentCtx>,
|
|
127
142
|
request?: Request,
|
|
128
143
|
): Promise<string | null> => {
|
|
@@ -135,33 +150,26 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
135
150
|
const authHeader = request.headers.get("Authorization");
|
|
136
151
|
if (authHeader?.startsWith("Bearer sk_")) {
|
|
137
152
|
const rawKey = authHeader.slice(7);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
153
|
+
const result = await getAuth().key.verify(
|
|
154
|
+
ctx as ComponentCtx,
|
|
155
|
+
rawKey,
|
|
156
|
+
);
|
|
157
|
+
if (result.ok) {
|
|
143
158
|
return result.userId;
|
|
144
|
-
} catch {
|
|
145
|
-
return null;
|
|
146
159
|
}
|
|
160
|
+
return null;
|
|
147
161
|
}
|
|
148
162
|
}
|
|
149
163
|
return null;
|
|
150
164
|
},
|
|
151
|
-
require: async (
|
|
152
|
-
ctx: { auth: Auth } & Partial<ComponentCtx>,
|
|
153
|
-
request?: Request,
|
|
154
|
-
): Promise<string> => {
|
|
155
|
-
const userId = await user.current(ctx, request);
|
|
156
|
-
if (userId === null) {
|
|
157
|
-
throw new AuthError("NOT_SIGNED_IN").toConvexError();
|
|
158
|
-
}
|
|
159
|
-
return userId;
|
|
160
|
-
},
|
|
161
165
|
get: async (ctx: ComponentReadCtx, userId: string) => {
|
|
162
|
-
|
|
166
|
+
const c = cache(ctx);
|
|
167
|
+
if (c.users.has(userId)) return c.users.get(userId);
|
|
168
|
+
const result = await ctx.runQuery(config.component.public.userGetById, {
|
|
163
169
|
userId,
|
|
164
170
|
});
|
|
171
|
+
c.users.set(userId, result);
|
|
172
|
+
return result;
|
|
165
173
|
},
|
|
166
174
|
list: async (
|
|
167
175
|
ctx: ComponentReadCtx,
|
|
@@ -176,11 +184,9 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
176
184
|
return await ctx.runQuery(config.component.public.userList, opts);
|
|
177
185
|
},
|
|
178
186
|
viewer: async (ctx: ComponentAuthReadCtx) => {
|
|
179
|
-
const userId = await user.
|
|
187
|
+
const userId = await user.id(ctx);
|
|
180
188
|
if (userId === null) return null;
|
|
181
|
-
return await
|
|
182
|
-
userId,
|
|
183
|
-
});
|
|
189
|
+
return await user.get(ctx, userId);
|
|
184
190
|
},
|
|
185
191
|
update: async (
|
|
186
192
|
ctx: ComponentCtx,
|
|
@@ -266,10 +272,7 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
266
272
|
passkeys.length +
|
|
267
273
|
totps.length;
|
|
268
274
|
if (!cascade && totalLinked > 0) {
|
|
269
|
-
|
|
270
|
-
"INVALID_PARAMETERS",
|
|
271
|
-
`Cannot delete user with ${totalLinked} linked records. Pass { cascade: true } to delete all linked records, or remove them manually first.`,
|
|
272
|
-
).toConvexError();
|
|
275
|
+
return { ok: false as const, code: "INVALID_PARAMETERS" as const };
|
|
273
276
|
}
|
|
274
277
|
const deletions: Promise<unknown>[] = [];
|
|
275
278
|
for (const s of sessions)
|
|
@@ -356,7 +359,7 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
356
359
|
) => {
|
|
357
360
|
const result = await callRetrieveAccountWithCredentials(ctx, args);
|
|
358
361
|
if (typeof result === "string") {
|
|
359
|
-
|
|
362
|
+
return null;
|
|
360
363
|
}
|
|
361
364
|
return result;
|
|
362
365
|
},
|
|
@@ -372,20 +375,14 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
372
375
|
accountId,
|
|
373
376
|
});
|
|
374
377
|
if (doc === null) {
|
|
375
|
-
|
|
376
|
-
"ACCOUNT_NOT_FOUND",
|
|
377
|
-
"Account not found.",
|
|
378
|
-
).toConvexError();
|
|
378
|
+
return { ok: false as const, code: "ACCOUNT_NOT_FOUND" as const };
|
|
379
379
|
}
|
|
380
380
|
const allAccounts = (await ctx.runQuery(
|
|
381
381
|
config.component.public.accountListByUser,
|
|
382
382
|
{ userId: (doc as any).userId },
|
|
383
383
|
)) as Array<{ _id: string }>;
|
|
384
384
|
if (allAccounts.length <= 1) {
|
|
385
|
-
|
|
386
|
-
"INVALID_PARAMETERS",
|
|
387
|
-
"Cannot unlink the user's only account. This would lock them out.",
|
|
388
|
-
).toConvexError();
|
|
385
|
+
return { ok: false as const, code: "INVALID_PARAMETERS" as const };
|
|
389
386
|
}
|
|
390
387
|
await ctx.runMutation(config.component.public.accountDelete, {
|
|
391
388
|
accountId,
|
|
@@ -472,7 +469,13 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
472
469
|
return { ok: true, groupId };
|
|
473
470
|
},
|
|
474
471
|
get: async (ctx: ComponentReadCtx, groupId: string) => {
|
|
475
|
-
|
|
472
|
+
const c = cache(ctx);
|
|
473
|
+
if (c.groups.has(groupId)) return c.groups.get(groupId);
|
|
474
|
+
const result = await ctx.runQuery(config.component.public.groupGet, {
|
|
475
|
+
groupId,
|
|
476
|
+
});
|
|
477
|
+
c.groups.set(groupId, result);
|
|
478
|
+
return result;
|
|
476
479
|
},
|
|
477
480
|
list: async (
|
|
478
481
|
ctx: ComponentReadCtx,
|
|
@@ -564,13 +567,19 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
564
567
|
status?: string;
|
|
565
568
|
extend?: Record<string, unknown>;
|
|
566
569
|
},
|
|
567
|
-
)
|
|
568
|
-
const
|
|
570
|
+
) => {
|
|
571
|
+
const normalized = normalizeRoleIds(data.roleIds);
|
|
572
|
+
if (!normalized.ok)
|
|
573
|
+
return {
|
|
574
|
+
ok: false as const,
|
|
575
|
+
code: "INVALID_ROLE_IDS" as const,
|
|
576
|
+
invalidRoleIds: normalized.invalidRoleIds,
|
|
577
|
+
};
|
|
569
578
|
const memberId = (await ctx.runMutation(
|
|
570
579
|
config.component.public.memberAdd,
|
|
571
|
-
{ ...data, roleIds },
|
|
580
|
+
{ ...data, roleIds: normalized.roleIds },
|
|
572
581
|
)) as string;
|
|
573
|
-
return { ok: true, memberId };
|
|
582
|
+
return { ok: true as const, memberId };
|
|
574
583
|
},
|
|
575
584
|
get: async (ctx: ComponentReadCtx, memberId: string) => {
|
|
576
585
|
return await ctx.runQuery(config.component.public.memberGet, {
|
|
@@ -620,11 +629,18 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
620
629
|
) => {
|
|
621
630
|
const nextData = { ...data };
|
|
622
631
|
if ("roleIds" in nextData) {
|
|
623
|
-
|
|
632
|
+
const normalized = normalizeRoleIds(
|
|
624
633
|
Array.isArray(nextData.roleIds)
|
|
625
634
|
? (nextData.roleIds as string[])
|
|
626
635
|
: undefined,
|
|
627
636
|
);
|
|
637
|
+
if (!normalized.ok)
|
|
638
|
+
return {
|
|
639
|
+
ok: false as const,
|
|
640
|
+
code: "INVALID_ROLE_IDS" as const,
|
|
641
|
+
invalidRoleIds: normalized.invalidRoleIds,
|
|
642
|
+
};
|
|
643
|
+
nextData.roleIds = normalized.roleIds;
|
|
628
644
|
}
|
|
629
645
|
await ctx.runMutation(config.component.public.memberUpdate, {
|
|
630
646
|
memberId,
|
|
@@ -632,7 +648,7 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
632
648
|
});
|
|
633
649
|
return { ok: true as const, memberId };
|
|
634
650
|
},
|
|
635
|
-
|
|
651
|
+
resolve: async (
|
|
636
652
|
ctx: ComponentReadCtx,
|
|
637
653
|
opts: {
|
|
638
654
|
userId: string;
|
|
@@ -642,102 +658,105 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
642
658
|
maxDepth?: number;
|
|
643
659
|
},
|
|
644
660
|
) => {
|
|
645
|
-
const
|
|
661
|
+
const normalized = normalizeRoleIds(opts.roleIds);
|
|
662
|
+
if (!normalized.ok)
|
|
663
|
+
return {
|
|
664
|
+
ok: false as const,
|
|
665
|
+
code: "INVALID_ROLE_IDS" as const,
|
|
666
|
+
invalidRoleIds: normalized.invalidRoleIds,
|
|
667
|
+
};
|
|
668
|
+
const requestedRoleIds = normalized.roleIds;
|
|
646
669
|
const roleFilter =
|
|
647
670
|
requestedRoleIds.length > 0 ? new Set(requestedRoleIds) : null;
|
|
648
671
|
const requiredGrants = Array.from(new Set(opts.grants ?? []));
|
|
649
672
|
const maxDepth = Math.max(0, Math.floor(opts.maxDepth ?? 32));
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (visited.has(currentGroupId)) {
|
|
662
|
-
cycleDetected = true;
|
|
663
|
-
break;
|
|
664
|
-
}
|
|
665
|
-
visited.add(currentGroupId);
|
|
666
|
-
traversedGroupIds.push(currentGroupId);
|
|
667
|
-
const membership = await member.getByUserAndGroup(ctx, {
|
|
668
|
-
userId: opts.userId,
|
|
669
|
-
groupId: currentGroupId,
|
|
670
|
-
});
|
|
671
|
-
const membershipRoleIds = membership?.roleIds ?? [];
|
|
672
|
-
const membershipGrants = resolveGrantedPermissions(membershipRoleIds);
|
|
673
|
-
if (
|
|
674
|
-
membership !== null &&
|
|
675
|
-
(roleFilter === null ||
|
|
676
|
-
membershipRoleIds.some((roleId: string) =>
|
|
677
|
-
roleFilter.has(roleId),
|
|
678
|
-
)) &&
|
|
679
|
-
requiredGrants.every((grant) => membershipGrants.includes(grant))
|
|
680
|
-
) {
|
|
681
|
-
return {
|
|
682
|
-
requestedGroupId: opts.groupId,
|
|
683
|
-
matchedGroupId: currentGroupId,
|
|
684
|
-
membership,
|
|
685
|
-
roleIds: membershipRoleIds,
|
|
686
|
-
grants: membershipGrants,
|
|
687
|
-
missingGrants: [] as string[],
|
|
688
|
-
depth,
|
|
689
|
-
isDirect: depth === 0,
|
|
690
|
-
isInherited: depth > 0,
|
|
691
|
-
traversedGroupIds,
|
|
692
|
-
cycleDetected: false,
|
|
693
|
-
maxDepthReached: false,
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
const doc = await group.get(ctx, currentGroupId);
|
|
697
|
-
if (doc === null || doc.parentGroupId === undefined) break;
|
|
698
|
-
currentGroupId = doc.parentGroupId;
|
|
699
|
-
depth += 1;
|
|
700
|
-
}
|
|
701
|
-
return {
|
|
702
|
-
requestedGroupId: opts.groupId,
|
|
703
|
-
matchedGroupId: null,
|
|
704
|
-
membership: null,
|
|
705
|
-
roleIds: [] as string[],
|
|
706
|
-
grants: [] as string[],
|
|
707
|
-
missingGrants: requiredGrants,
|
|
708
|
-
depth: null,
|
|
709
|
-
isDirect: false,
|
|
710
|
-
isInherited: false,
|
|
711
|
-
traversedGroupIds,
|
|
712
|
-
cycleDetected,
|
|
713
|
-
maxDepthReached,
|
|
714
|
-
};
|
|
715
|
-
},
|
|
716
|
-
require: async (
|
|
717
|
-
ctx: ComponentReadCtx,
|
|
718
|
-
opts: {
|
|
719
|
-
userId: string;
|
|
720
|
-
groupId: string;
|
|
721
|
-
roleIds?: string[];
|
|
722
|
-
grants?: string[];
|
|
723
|
-
maxDepth?: number;
|
|
724
|
-
},
|
|
725
|
-
) => {
|
|
726
|
-
const result = await member.inherit(ctx, opts);
|
|
673
|
+
|
|
674
|
+
// Single cross-component RPC — hierarchy walk happens inside the component
|
|
675
|
+
const result = await ctx.runQuery(config.component.public.memberResolve, {
|
|
676
|
+
userId: opts.userId,
|
|
677
|
+
groupId: opts.groupId,
|
|
678
|
+
maxDepth,
|
|
679
|
+
ancestry: true,
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const traversedGroupIds = result.traversedGroupIds ?? [];
|
|
683
|
+
|
|
727
684
|
if (result.membership === null) {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
685
|
+
return {
|
|
686
|
+
requestedGroupId: opts.groupId,
|
|
687
|
+
matchedGroupId: null,
|
|
688
|
+
membership: null,
|
|
689
|
+
roleIds: [] as string[],
|
|
690
|
+
grants: [] as string[],
|
|
691
|
+
missingGrants: requiredGrants,
|
|
692
|
+
depth: null,
|
|
693
|
+
isDirect: false,
|
|
694
|
+
isInherited: false,
|
|
695
|
+
traversedGroupIds,
|
|
696
|
+
cycleDetected: false,
|
|
697
|
+
maxDepthReached: false,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Grant resolution uses app-defined roles — stays server-side
|
|
702
|
+
const membershipRoleIds = (result.membership as any).roleIds ?? [];
|
|
703
|
+
const membershipGrants = resolveGrantedPermissions(membershipRoleIds);
|
|
704
|
+
|
|
705
|
+
// Check role filter
|
|
706
|
+
if (
|
|
707
|
+
roleFilter !== null &&
|
|
708
|
+
!membershipRoleIds.some((roleId: string) => roleFilter.has(roleId))
|
|
709
|
+
) {
|
|
710
|
+
return {
|
|
711
|
+
requestedGroupId: opts.groupId,
|
|
712
|
+
matchedGroupId: null,
|
|
713
|
+
membership: null,
|
|
714
|
+
roleIds: [] as string[],
|
|
715
|
+
grants: [] as string[],
|
|
716
|
+
missingGrants: requiredGrants,
|
|
717
|
+
depth: null,
|
|
718
|
+
isDirect: false,
|
|
719
|
+
isInherited: false,
|
|
720
|
+
traversedGroupIds,
|
|
721
|
+
cycleDetected: false,
|
|
722
|
+
maxDepthReached: false,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Check required grants
|
|
727
|
+
const missingGrants = requiredGrants.filter(
|
|
728
|
+
(grant) => !membershipGrants.includes(grant),
|
|
729
|
+
);
|
|
730
|
+
if (missingGrants.length > 0) {
|
|
731
|
+
return {
|
|
732
|
+
requestedGroupId: opts.groupId,
|
|
733
|
+
matchedGroupId: result.matchedGroupId,
|
|
734
|
+
membership: result.membership,
|
|
735
|
+
roleIds: membershipRoleIds,
|
|
736
|
+
grants: membershipGrants,
|
|
737
|
+
missingGrants,
|
|
738
|
+
depth: result.depth,
|
|
739
|
+
isDirect: result.isDirect,
|
|
740
|
+
isInherited: result.isInherited,
|
|
741
|
+
traversedGroupIds,
|
|
742
|
+
cycleDetected: false,
|
|
743
|
+
maxDepthReached: false,
|
|
744
|
+
};
|
|
732
745
|
}
|
|
746
|
+
|
|
733
747
|
return {
|
|
734
|
-
|
|
748
|
+
requestedGroupId: opts.groupId,
|
|
735
749
|
matchedGroupId: result.matchedGroupId,
|
|
736
|
-
|
|
737
|
-
|
|
750
|
+
membership: result.membership,
|
|
751
|
+
roleIds: membershipRoleIds,
|
|
752
|
+
grants: membershipGrants,
|
|
753
|
+
missingGrants: [] as string[],
|
|
754
|
+
depth: result.depth,
|
|
738
755
|
isDirect: result.isDirect,
|
|
739
756
|
isInherited: result.isInherited,
|
|
740
|
-
|
|
757
|
+
traversedGroupIds,
|
|
758
|
+
cycleDetected: false,
|
|
759
|
+
maxDepthReached: false,
|
|
741
760
|
};
|
|
742
761
|
},
|
|
743
762
|
};
|
|
@@ -753,12 +772,25 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
753
772
|
},
|
|
754
773
|
) => {
|
|
755
774
|
const requiredGrants = Array.from(new Set(opts.grants));
|
|
756
|
-
const result = await member.
|
|
775
|
+
const result = await member.resolve(ctx, {
|
|
757
776
|
userId: opts.userId,
|
|
758
777
|
groupId: opts.groupId,
|
|
759
778
|
grants: requiredGrants,
|
|
760
779
|
maxDepth: opts.maxDepth,
|
|
761
780
|
});
|
|
781
|
+
if ("code" in result && result.code === "INVALID_ROLE_IDS") {
|
|
782
|
+
return {
|
|
783
|
+
ok: false,
|
|
784
|
+
membership: null,
|
|
785
|
+
matchedGroupId: null,
|
|
786
|
+
roleIds: [] as string[],
|
|
787
|
+
grants: [] as string[],
|
|
788
|
+
missingGrants: requiredGrants,
|
|
789
|
+
isDirect: false,
|
|
790
|
+
isInherited: false,
|
|
791
|
+
depth: null,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
762
794
|
const missingGrants = requiredGrants.filter(
|
|
763
795
|
(grant) => !result.grants.includes(grant),
|
|
764
796
|
);
|
|
@@ -774,24 +806,6 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
774
806
|
depth: result.depth,
|
|
775
807
|
};
|
|
776
808
|
},
|
|
777
|
-
require: async (
|
|
778
|
-
ctx: ComponentReadCtx,
|
|
779
|
-
opts: {
|
|
780
|
-
userId: string;
|
|
781
|
-
groupId: string;
|
|
782
|
-
grants: string[];
|
|
783
|
-
maxDepth?: number;
|
|
784
|
-
},
|
|
785
|
-
) => {
|
|
786
|
-
const result = await access.check(ctx, opts);
|
|
787
|
-
if (!result.ok) {
|
|
788
|
-
throw new AuthError(
|
|
789
|
-
"FORBIDDEN",
|
|
790
|
-
`User ${opts.userId} is missing required grants on group ${opts.groupId}: ${result.missingGrants.join(", ")}.`,
|
|
791
|
-
).toConvexError();
|
|
792
|
-
}
|
|
793
|
-
return result;
|
|
794
|
-
},
|
|
795
809
|
};
|
|
796
810
|
|
|
797
811
|
const invite = {
|
|
@@ -805,8 +819,14 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
805
819
|
expiresTime?: number;
|
|
806
820
|
extend?: Record<string, unknown>;
|
|
807
821
|
},
|
|
808
|
-
)
|
|
809
|
-
const
|
|
822
|
+
) => {
|
|
823
|
+
const normalized = normalizeRoleIds(data.roleIds);
|
|
824
|
+
if (!normalized.ok)
|
|
825
|
+
return {
|
|
826
|
+
ok: false as const,
|
|
827
|
+
code: "INVALID_ROLE_IDS" as const,
|
|
828
|
+
invalidRoleIds: normalized.invalidRoleIds,
|
|
829
|
+
};
|
|
810
830
|
const token = generateRandomString(
|
|
811
831
|
inviteTokenLength,
|
|
812
832
|
inviteTokenAlphabet,
|
|
@@ -814,9 +834,9 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
814
834
|
const tokenHash = await sha256(token);
|
|
815
835
|
const inviteId = (await ctx.runMutation(
|
|
816
836
|
config.component.public.inviteCreate,
|
|
817
|
-
{ ...data, roleIds, tokenHash, status: "pending" },
|
|
837
|
+
{ ...data, roleIds: normalized.roleIds, tokenHash, status: "pending" },
|
|
818
838
|
)) as string;
|
|
819
|
-
return { ok: true, inviteId, token };
|
|
839
|
+
return { ok: true as const, inviteId, token };
|
|
820
840
|
},
|
|
821
841
|
get: async (ctx: ComponentReadCtx, inviteId: string) => {
|
|
822
842
|
return await ctx.runQuery(config.component.public.inviteGet, {
|
|
@@ -923,46 +943,53 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
923
943
|
verify: async (
|
|
924
944
|
ctx: ComponentCtx,
|
|
925
945
|
rawKey: string,
|
|
926
|
-
): Promise<
|
|
946
|
+
): Promise<
|
|
947
|
+
| { ok: true; userId: string; keyId: string; scopes: ScopeChecker }
|
|
948
|
+
| {
|
|
949
|
+
ok: false;
|
|
950
|
+
code:
|
|
951
|
+
| "INVALID_API_KEY"
|
|
952
|
+
| "API_KEY_REVOKED"
|
|
953
|
+
| "API_KEY_EXPIRED"
|
|
954
|
+
| "API_KEY_RATE_LIMITED";
|
|
955
|
+
}
|
|
956
|
+
> => {
|
|
927
957
|
const hashedKey = await hashApiKey(rawKey);
|
|
928
958
|
const doc = (await ctx.runQuery(
|
|
929
959
|
config.component.public.keyGetByHashedKey,
|
|
930
960
|
{ hashedKey },
|
|
931
961
|
)) as KeyDoc | null;
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
};
|
|
964
|
-
}).pipe(Fx.recover((e) => Fx.fatal(e.toConvexError()))),
|
|
965
|
-
);
|
|
962
|
+
if (!doc) {
|
|
963
|
+
return { ok: false as const, code: "INVALID_API_KEY" as const };
|
|
964
|
+
}
|
|
965
|
+
const k = doc;
|
|
966
|
+
if (k.revoked) {
|
|
967
|
+
return { ok: false as const, code: "API_KEY_REVOKED" as const };
|
|
968
|
+
}
|
|
969
|
+
if (k.expiresAt && k.expiresAt < Date.now()) {
|
|
970
|
+
return { ok: false as const, code: "API_KEY_EXPIRED" as const };
|
|
971
|
+
}
|
|
972
|
+
const patchData: Record<string, unknown> = { lastUsedAt: Date.now() };
|
|
973
|
+
if (k.rateLimit) {
|
|
974
|
+
const { limited, newState } = checkKeyRateLimit(
|
|
975
|
+
k.rateLimit,
|
|
976
|
+
k.rateLimitState ?? undefined,
|
|
977
|
+
);
|
|
978
|
+
if (limited) {
|
|
979
|
+
return { ok: false as const, code: "API_KEY_RATE_LIMITED" as const };
|
|
980
|
+
}
|
|
981
|
+
patchData.rateLimitState = newState;
|
|
982
|
+
}
|
|
983
|
+
await ctx.runMutation(config.component.public.keyPatch, {
|
|
984
|
+
keyId: k._id,
|
|
985
|
+
data: patchData,
|
|
986
|
+
});
|
|
987
|
+
return {
|
|
988
|
+
ok: true as const,
|
|
989
|
+
userId: k.userId,
|
|
990
|
+
keyId: k._id,
|
|
991
|
+
scopes: buildScopeChecker(k.scopes),
|
|
992
|
+
};
|
|
966
993
|
},
|
|
967
994
|
list: async (
|
|
968
995
|
ctx: ComponentReadCtx,
|
|
@@ -1027,20 +1054,18 @@ export function createCoreDomains(deps: CoreDeps) {
|
|
|
1027
1054
|
ctx: ComponentCtx,
|
|
1028
1055
|
keyId: string,
|
|
1029
1056
|
opts?: { name?: string; expiresAt?: number },
|
|
1030
|
-
): Promise<
|
|
1057
|
+
): Promise<
|
|
1058
|
+
| { ok: true; keyId: string; secret: string }
|
|
1059
|
+
| { ok: false; code: "INVALID_PARAMETERS" | "API_KEY_REVOKED" }
|
|
1060
|
+
> => {
|
|
1031
1061
|
const existing = await ctx.runQuery(config.component.public.keyGetById, {
|
|
1032
1062
|
keyId,
|
|
1033
1063
|
});
|
|
1034
|
-
if (!existing)
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
"API key not found.",
|
|
1038
|
-
).toConvexError();
|
|
1064
|
+
if (!existing) {
|
|
1065
|
+
return { ok: false as const, code: "INVALID_PARAMETERS" as const };
|
|
1066
|
+
}
|
|
1039
1067
|
if ((existing as any).revoked === true) {
|
|
1040
|
-
|
|
1041
|
-
"API_KEY_REVOKED",
|
|
1042
|
-
"Cannot rotate a key that is already revoked.",
|
|
1043
|
-
).toConvexError();
|
|
1068
|
+
return { ok: false as const, code: "API_KEY_REVOKED" as const };
|
|
1044
1069
|
}
|
|
1045
1070
|
await ctx.runMutation(config.component.public.keyPatch, {
|
|
1046
1071
|
keyId,
|