@cedarjs/tenancy 8.0.0-canary.3538 → 8.0.0-canary.3540

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/README.md CHANGED
@@ -76,23 +76,71 @@ import {
76
76
  ## Auth helpers
77
77
 
78
78
  ```ts
79
- import { hasOrgRole, requireMembership } from '@cedarjs/tenancy'
79
+ // api/src/lib/auth.ts, in place of the single-tenant hasRole/requireAuth
80
+ import { createTenancyAuth } from '@cedarjs/tenancy'
81
+
82
+ export const { hasUserRole, hasOrgRole, requireUserAuth, requireOrgAuth } =
83
+ createTenancyAuth()
80
84
 
81
85
  // In a service:
82
- requireMembership({ roles: ['owner', 'admin'] })
86
+ requireOrgAuth({ roles: ['owner', 'admin'] }) // in the current organization
87
+ requireUserAuth() // signed in, no organization needed
83
88
 
84
89
  // Anywhere:
85
- if (hasOrgRole('owner')) { ... }
90
+ if (hasOrgRole('owner')) { ... } // in the current organization
91
+ if (hasUserRole('superuser')) { ... } // currentUser.roles
92
+ ```
93
+
94
+ `requireOrgAuth` (and the `@requireOrgAuth` directive `setup tenancy` adds) is
95
+ about the current organization: it throws `AuthenticationError` with no
96
+ current user, and `ForbiddenError` with no current organization or when the
97
+ user's role there isn't one of `roles` when they're given. With no `roles`,
98
+ any member gets in. `requireUserAuth` (and `@requireUserAuth`) is for fields about the user
99
+ rather than an organization: it needs a signed-in user, and checks `roles`
100
+ against `currentUser.roles`. `hasOrgRole` and `hasUserRole` are the boolean
101
+ checks to match. There's no `requireAuth` or `hasRole`.
102
+
103
+ ## Permissions
104
+
105
+ ```ts
106
+ // packages/permissions/src/index.ts, shared by the api and web sides
107
+ import { definePermissions } from '@cedarjs/tenancy/permissions'
108
+
109
+ export const permissions = definePermissions({
110
+ permissions: ['projects.read', 'projects.edit', 'members.manage'],
111
+ roles: {
112
+ owner: ['projects.read', 'projects.edit', 'members.manage'],
113
+ member: ['projects.read'],
114
+ },
115
+ })
116
+ ```
117
+
118
+ ```ts
119
+ // api/src/lib/permissions.ts
120
+ import { createPermissions } from '@cedarjs/tenancy'
121
+
122
+ import { permissions } from '@my-app/permissions'
123
+
124
+ export const { hasPermission, requirePermission, withPermissions } =
125
+ createPermissions(permissions)
86
126
  ```
87
127
 
88
- `requireMembership` throws `AuthenticationError` with no current user and
89
- `ForbiddenError` with no current organization or a role mismatch — the same
90
- error types `requireAuth` uses.
128
+ Roles map to permissions in one place; fields ask for a permission with
129
+ `@requirePermission(permission: "projects.edit")` or `requirePermission()`.
130
+ A role only gets what its list says, so a new role is one entry. Permission
131
+ names are typed from the definitions. `withPermissions` in `getCurrentUser` gives each
132
+ membership its permissions, which `useAuth().hasPermission()` checks inside
133
+ `OrgScope`. Every permission is about an organization: fields about the user
134
+ rather than an organization use `@requireUserAuth`.
91
135
 
92
136
  ## Web
93
137
 
94
- `@cedarjs/tenancy/web` exports `OrgScope`, `useCurrentOrg`, `OrgContext` and
138
+ `@cedarjs/tenancy/web` exports `OrgScope`, `useCurrentOrg` and
95
139
  `getMemberships`/`hasOrgRole` for the client side: a per-organization Apollo
96
140
  client provided under organization routes, keyed by the immutable organization
97
- id so switching organizations never mixes caches. See the how-to for the full
141
+ id so switching organizations never mixes caches. `useCurrentOrg().hasOrgRole`
142
+ checks the user's role in the organization, and `OrgScope`'s `roles` and
143
+ `unauthorized` props guard routes by it. `useAuth().hasUserRole` checks
144
+ `currentUser.roles`; there's no `useAuth().hasRole` or `<PrivateSet roles>` in
145
+ an app with tenancy. See the how-to for the full
98
146
  `<Set wrap={OrgScope}>` setup `yarn cedar setup tenancy` generates.
package/dist/auth.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ type Roles = string | readonly string[];
1
2
  /**
2
3
  * Whether the current request holds one of `roles` in an organization.
3
4
  *
@@ -9,20 +10,56 @@
9
10
  *
10
11
  * Returns `false` when there's no authenticated user, no matching
11
12
  * organization, or no matching membership. An empty `roles` array returns
12
- * `true` as long as a matching membership exists, mirroring `hasRole()`'s
13
- * behavior for global roles.
13
+ * `true` as long as a matching membership exists.
14
14
  */
15
- export declare function hasOrgRole(roles: string | string[], organizationId?: string): boolean;
15
+ export declare function hasOrgRole(roles: Roles, organizationId?: string): boolean;
16
+ export interface TenancyAuth {
17
+ /**
18
+ * Whether the signed-in user holds one of `roles` in `currentUser.roles`,
19
+ * in any organization or none. What the single-tenant `hasRole()` checked;
20
+ * `hasOrgRole()` checks the role in an organization.
21
+ */
22
+ hasUserRole(roles: Roles): boolean;
23
+ /**
24
+ * Guards a service or directive on the current organization: signed in,
25
+ * a member of the current organization and, when `roles` are given, with
26
+ * one of them there. With no `roles`, any member gets in, as any
27
+ * signed-in user gets past the single-tenant `requireAuth()`: use `roles`
28
+ * or `requirePermission()` for anything narrower.
29
+ *
30
+ * Throws `AuthenticationError` when no one is signed in, and
31
+ * `ForbiddenError` otherwise.
32
+ */
33
+ requireOrgAuth(options?: {
34
+ roles?: Roles;
35
+ }): void;
36
+ /**
37
+ * Guards a service or directive on the signed-in user alone, in any
38
+ * organization or none: their account, their memberships, accepting an
39
+ * invitation. When `roles` are given, the user needs one of them in
40
+ * `currentUser.roles`.
41
+ *
42
+ * Throws `AuthenticationError` when no one is signed in, and
43
+ * `ForbiddenError` otherwise.
44
+ */
45
+ requireUserAuth(options?: {
46
+ roles?: Roles;
47
+ }): void;
48
+ /** The standalone `hasOrgRole`: with no roles, any membership */
49
+ hasOrgRole(roles?: Roles, organizationId?: string): boolean;
50
+ }
16
51
  /**
17
- * Guards a service or directive on the current request having a membership
18
- * in the current organization, optionally with one of `roles`.
52
+ * The auth checks for an app with tenancy, which `api/src/lib/auth.ts`
53
+ * exports in place of its single-tenant `hasRole` and `requireAuth`:
19
54
  *
20
- * Throws `AuthenticationError` when there's no authenticated user, and
21
- * `ForbiddenError` when there's no current organization or the membership's
22
- * role doesn't match — the same error types `requireAuth` uses, so existing
23
- * error handling on the web side applies unchanged.
55
+ * ```ts
56
+ * export const { hasUserRole, hasOrgRole, requireUserAuth, requireOrgAuth } =
57
+ * createTenancyAuth()
58
+ * ```
59
+ *
60
+ * There's no `hasRole` or `requireAuth`: every check says whether it's
61
+ * about the signed-in user alone or the current organization.
24
62
  */
25
- export declare function requireMembership(options?: {
26
- roles?: string | string[];
27
- }): void;
63
+ export declare function createTenancyAuth(): TenancyAuth;
64
+ export {};
28
65
  //# sourceMappingURL=auth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CA4BT;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAAO,GAC1C,IAAI,CAwBN"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAKA,KAAK,KAAK,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,CAAA;AAwBvC;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CA4BzE;AAED,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAA;IAClC;;;;;;;;;OASG;IACH,cAAc,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE,GAAG,IAAI,CAAA;IACjD;;;;;;;;OAQG;IACH,eAAe,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE,GAAG,IAAI,CAAA;IAClD,iEAAiE;IACjE,UAAU,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAC5D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,IAAI,WAAW,CA8C/C"}
package/dist/auth.js CHANGED
@@ -1,8 +1,19 @@
1
1
  import { context } from "@cedarjs/context";
2
2
  import { AuthenticationError, ForbiddenError } from "@cedarjs/graphql-server";
3
3
  import { getCurrentOrg, isUserWithMemberships } from "./context.js";
4
+ const toList = (roles) => roles === void 0 ? [] : typeof roles === "string" ? [roles] : [...roles];
5
+ function userRoles() {
6
+ const roles = context.currentUser?.roles;
7
+ if (typeof roles === "string") {
8
+ return [roles];
9
+ }
10
+ if (Array.isArray(roles)) {
11
+ return roles.filter((role) => typeof role === "string");
12
+ }
13
+ return [];
14
+ }
4
15
  function hasOrgRole(roles, organizationId) {
5
- const roleList = Array.isArray(roles) ? roles : [roles];
16
+ const roleList = toList(roles);
6
17
  if (organizationId) {
7
18
  const currentUser = context.currentUser;
8
19
  if (!isUserWithMemberships(currentUser)) {
@@ -22,26 +33,40 @@ function hasOrgRole(roles, organizationId) {
22
33
  }
23
34
  return roleList.length === 0 || roleList.includes(currentOrg.role);
24
35
  }
25
- function requireMembership(options = {}) {
26
- if (!context.currentUser) {
27
- throw new AuthenticationError("You must be logged in to do that.");
28
- }
29
- const currentOrg = getCurrentOrg();
30
- if (!currentOrg) {
31
- throw new ForbiddenError(
32
- "You must be a member of an organization to do that."
33
- );
34
- }
35
- const { roles } = options;
36
- if (roles === void 0) {
37
- return;
38
- }
39
- const roleList = Array.isArray(roles) ? roles : [roles];
40
- if (roleList.length > 0 && !roleList.includes(currentOrg.role)) {
41
- throw new ForbiddenError("You do not have permission to do that.");
42
- }
36
+ function createTenancyAuth() {
37
+ return {
38
+ hasUserRole(roles) {
39
+ const roleList = toList(roles);
40
+ return !!context.currentUser && userRoles().some((role) => roleList.includes(role));
41
+ },
42
+ requireUserAuth({ roles } = {}) {
43
+ if (!context.currentUser) {
44
+ throw new AuthenticationError("You don't have permission to do that.");
45
+ }
46
+ if (roles && !userRoles().some((role) => toList(roles).includes(role))) {
47
+ throw new ForbiddenError("You don't have access to do that.");
48
+ }
49
+ },
50
+ requireOrgAuth({ roles } = {}) {
51
+ if (!context.currentUser) {
52
+ throw new AuthenticationError("You don't have permission to do that.");
53
+ }
54
+ const currentOrg = getCurrentOrg();
55
+ if (!currentOrg) {
56
+ throw new ForbiddenError(
57
+ "You must be a member of an organization to do that."
58
+ );
59
+ }
60
+ if (roles && !toList(roles).includes(currentOrg.role)) {
61
+ throw new ForbiddenError("You don't have access to do that.");
62
+ }
63
+ },
64
+ hasOrgRole(roles = [], organizationId) {
65
+ return hasOrgRole(roles, organizationId);
66
+ }
67
+ };
43
68
  }
44
69
  export {
45
- hasOrgRole,
46
- requireMembership
70
+ createTenancyAuth,
71
+ hasOrgRole
47
72
  };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Every permission an app checks, and what the built-in roles may do.
3
+ *
4
+ * Plain data with no api code in it, so both sides can import it: the api to
5
+ * check permissions (`createPermissions`), and the web for the names.
6
+ */
7
+ export interface PermissionDefinitions<Name extends string = string> {
8
+ /**
9
+ * Every permission the app checks, by name. Fields, services and the web
10
+ * ask for these names, so they're defined in code. A name is a stable id,
11
+ * so it can also key what an app shows for it, such as translations.
12
+ */
13
+ permissions: readonly Name[];
14
+ /**
15
+ * What each built-in role may do in an organization. A role that isn't
16
+ * listed has no permissions.
17
+ */
18
+ roles: Readonly<Record<string, readonly Name[]>>;
19
+ }
20
+ /** The permission names in `definitions` */
21
+ export type PermissionName<Definitions> = Definitions extends PermissionDefinitions<infer Name> ? Name : never;
22
+ /**
23
+ * Throws when a role lists a permission that `permissions` doesn't define. The types catch this in TypeScript; this is for
24
+ * JavaScript, and for definitions built at runtime.
25
+ */
26
+ export declare function assertPermissionDefinitions(definitions: PermissionDefinitions): void;
27
+ /**
28
+ * Defines an app's permissions, and what each built-in role may do.
29
+ * Permission names are typed from `permissions`, so a role listing a name
30
+ * that isn't defined is a type error.
31
+ *
32
+ * ```ts
33
+ * export const permissions = definePermissions({
34
+ * permissions: ['projects.read', 'members.manage'],
35
+ * roles: {
36
+ * owner: ['projects.read', 'members.manage'],
37
+ * member: ['projects.read'],
38
+ * },
39
+ * })
40
+ * ```
41
+ *
42
+ * Pass the result to `createPermissions` on the api.
43
+ */
44
+ export declare function definePermissions<const Name extends string>(definitions: {
45
+ permissions: readonly Name[];
46
+ roles: Readonly<Record<string, readonly NoInfer<Name>[]>>;
47
+ }): PermissionDefinitions<Name>;
48
+ //# sourceMappingURL=definePermissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definePermissions.d.ts","sourceRoot":"","sources":["../src/definePermissions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IACjE;;;;OAIG;IACH,WAAW,EAAE,SAAS,IAAI,EAAE,CAAA;IAC5B;;;OAGG;IACH,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC,CAAA;CACjD;AAED,4CAA4C;AAC5C,MAAM,MAAM,cAAc,CAAC,WAAW,IACpC,WAAW,SAAS,qBAAqB,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,CAAA;AAEtE;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,qBAAqB,QAanC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,IAAI,SAAS,MAAM,EAAE,WAAW,EAAE;IACxE,WAAW,EAAE,SAAS,IAAI,EAAE,CAAA;IAC5B,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;CAC1D,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAI9B"}
@@ -0,0 +1,20 @@
1
+ function assertPermissionDefinitions(definitions) {
2
+ const defined = new Set(definitions.permissions);
3
+ for (const [role, names] of Object.entries(definitions.roles)) {
4
+ for (const name of names) {
5
+ if (!defined.has(name)) {
6
+ throw new Error(
7
+ `Unknown permission "${name}" in role "${role}". Add it to \`permissions\` in your permission definitions first.`
8
+ );
9
+ }
10
+ }
11
+ }
12
+ }
13
+ function definePermissions(definitions) {
14
+ assertPermissionDefinitions(definitions);
15
+ return definitions;
16
+ }
17
+ export {
18
+ assertPermissionDefinitions,
19
+ definePermissions
20
+ };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './errors.js';
2
2
  export * from './context.js';
3
3
  export * from './auth.js';
4
+ export * from './permissions.js';
4
5
  export { createTenancyExtension } from './prismaExtension.js';
5
6
  export type { ModelNamesFor, TenancyConfig } from './prismaExtension.js';
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,kBAAkB,CAAA;AAChC,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./errors.js";
2
2
  export * from "./context.js";
3
3
  export * from "./auth.js";
4
+ export * from "./permissions.js";
4
5
  import { createTenancyExtension } from "./prismaExtension.js";
5
6
  export {
6
7
  createTenancyExtension
@@ -0,0 +1,69 @@
1
+ import type { PermissionDefinitions } from './definePermissions.js';
2
+ export { definePermissions } from './definePermissions.js';
3
+ export type { PermissionDefinitions, PermissionName, } from './definePermissions.js';
4
+ /** What `permissionsOf` needs to know about a membership */
5
+ export interface MembershipForPermissions {
6
+ role: string;
7
+ }
8
+ /** A membership as `getCurrentUser` returns it, with its permissions */
9
+ export type WithPermissions<Membership, Permission extends string> = Membership & {
10
+ permissions: Permission[];
11
+ };
12
+ export interface Permissions<Permission extends string> {
13
+ /**
14
+ * Whether the signed-in user may do `permission` in this request: their
15
+ * membership in the current organization has it. False with no current
16
+ * organization.
17
+ */
18
+ hasPermission(permission: Permission): boolean;
19
+ /**
20
+ * Guards a service or directive on `hasPermission(permission)`. Throws
21
+ * `AuthenticationError` when no one is signed in, and `ForbiddenError`
22
+ * when they may not.
23
+ */
24
+ requirePermission(options: {
25
+ permission: Permission;
26
+ }): void;
27
+ /**
28
+ * What a membership may do: its role's permissions. The one place a membership's permissions are worked out, which
29
+ * `hasPermission`, `requirePermission` and `withPermissions` all use.
30
+ */
31
+ permissionsOf(membership: MembershipForPermissions): Permission[];
32
+ /**
33
+ * `user` with each of its memberships' permissions added, for
34
+ * `getCurrentUser` to return, so the web gets them with the memberships
35
+ * (`useAuth().hasPermission`).
36
+ */
37
+ withPermissions<User extends {
38
+ memberships: MembershipForPermissions[];
39
+ }>(user: User): Omit<User, 'memberships'> & {
40
+ memberships: WithPermissions<User['memberships'][number], Permission>[];
41
+ };
42
+ withPermissions<User extends {
43
+ memberships: MembershipForPermissions[];
44
+ }>(user: User | null): (Omit<User, 'memberships'> & {
45
+ memberships: WithPermissions<User['memberships'][number], Permission>[];
46
+ }) | null;
47
+ }
48
+ /**
49
+ * Permissions on top of roles: roles map to permissions in one place, and
50
+ * fields and services ask for a permission instead of listing roles, so
51
+ * adding a role is one entry in the definitions instead of an edit to every
52
+ * field it should reach.
53
+ *
54
+ * ```ts
55
+ * // api/src/lib/permissions.ts
56
+ * import { permissions } from '@my-app/permissions'
57
+ *
58
+ * export const { hasPermission, requirePermission, withPermissions } =
59
+ * createPermissions(permissions)
60
+ * ```
61
+ *
62
+ * `permissions` comes from `definePermissions`. Fails closed: a role without
63
+ * the permission is refused, as is a role the definitions don't list.
64
+ * Permission names are typed, so a typo in a service is a type error; a
65
+ * name that isn't defined (a typo in an SDL directive) throws instead of
66
+ * quietly refusing everyone.
67
+ */
68
+ export declare function createPermissions<const Permission extends string>(definitions: PermissionDefinitions<Permission>): Permissions<Permission>;
69
+ //# sourceMappingURL=permissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAEnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,YAAY,EACV,qBAAqB,EACrB,cAAc,GACf,MAAM,wBAAwB,CAAA;AAE/B,4DAA4D;AAC5D,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAA;CACb;AAED,wEAAwE;AACxE,MAAM,MAAM,eAAe,CACzB,UAAU,EACV,UAAU,SAAS,MAAM,IACvB,UAAU,GAAG;IAAE,WAAW,EAAE,UAAU,EAAE,CAAA;CAAE,CAAA;AAE9C,MAAM,WAAW,WAAW,CAAC,UAAU,SAAS,MAAM;IACpD;;;;OAIG;IACH,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAA;IAC9C;;;;OAIG;IACH,iBAAiB,CAAC,OAAO,EAAE;QAAE,UAAU,EAAE,UAAU,CAAA;KAAE,GAAG,IAAI,CAAA;IAC5D;;;OAGG;IACH,aAAa,CAAC,UAAU,EAAE,wBAAwB,GAAG,UAAU,EAAE,CAAA;IACjE;;;;OAIG;IACH,eAAe,CAAC,IAAI,SAAS;QAAE,WAAW,EAAE,wBAAwB,EAAE,CAAA;KAAE,EACtE,IAAI,EAAE,IAAI,GACT,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,GAAG;QAC7B,WAAW,EAAE,eAAe,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,EAAE,CAAA;KACxE,CAAA;IACD,eAAe,CAAC,IAAI,SAAS;QAAE,WAAW,EAAE,wBAAwB,EAAE,CAAA;KAAE,EACtE,IAAI,EAAE,IAAI,GAAG,IAAI,GAEf,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,GAAG;QAC3B,WAAW,EAAE,eAAe,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,EAAE,CAAA;KACxE,CAAC,GACF,IAAI,CAAA;CACT;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,UAAU,SAAS,MAAM,EAC/D,WAAW,EAAE,qBAAqB,CAAC,UAAU,CAAC,GAC7C,WAAW,CAAC,UAAU,CAAC,CAwFzB"}
@@ -0,0 +1,68 @@
1
+ import { context } from "@cedarjs/context";
2
+ import { AuthenticationError, ForbiddenError } from "@cedarjs/graphql-server";
3
+ import { getCurrentOrg, isUserWithMemberships } from "./context.js";
4
+ import { assertPermissionDefinitions } from "./definePermissions.js";
5
+ import { definePermissions } from "./definePermissions.js";
6
+ function createPermissions(definitions) {
7
+ assertPermissionDefinitions(definitions);
8
+ const assertDefined = (permission) => {
9
+ if (!definitions.permissions.includes(permission)) {
10
+ throw new Error(
11
+ `Unknown permission "${permission}". Add it to \`permissions\` in your permission definitions before using it.`
12
+ );
13
+ }
14
+ };
15
+ const permissionsOf = (membership) => Object.hasOwn(definitions.roles, membership.role) ? [...definitions.roles[membership.role]] : [];
16
+ const currentMembership = () => {
17
+ const currentOrg = getCurrentOrg();
18
+ if (!currentOrg) {
19
+ return void 0;
20
+ }
21
+ const currentUser = context.currentUser;
22
+ const membership = isUserWithMemberships(currentUser) ? currentUser.memberships.find((m) => m.id === currentOrg.membershipId) : void 0;
23
+ return { ...membership, role: currentOrg.role };
24
+ };
25
+ const hasPermission = (permission) => {
26
+ assertDefined(permission);
27
+ if (!context.currentUser) {
28
+ return false;
29
+ }
30
+ const membership = currentMembership();
31
+ return !!membership && permissionsOf(membership).includes(permission);
32
+ };
33
+ function withPermissions(user) {
34
+ if (!user) {
35
+ return null;
36
+ }
37
+ return {
38
+ ...user,
39
+ memberships: user.memberships.map((membership) => ({
40
+ ...membership,
41
+ permissions: permissionsOf(membership)
42
+ }))
43
+ };
44
+ }
45
+ return {
46
+ hasPermission,
47
+ permissionsOf,
48
+ withPermissions,
49
+ requirePermission({ permission }) {
50
+ assertDefined(permission);
51
+ if (!context.currentUser) {
52
+ throw new AuthenticationError("You don't have permission to do that.");
53
+ }
54
+ if (!getCurrentOrg()) {
55
+ throw new ForbiddenError(
56
+ "You must be a member of an organization to do that."
57
+ );
58
+ }
59
+ if (!hasPermission(permission)) {
60
+ throw new ForbiddenError("You don't have access to do that.");
61
+ }
62
+ }
63
+ };
64
+ }
65
+ export {
66
+ createPermissions,
67
+ definePermissions
68
+ };
@@ -1,10 +1,28 @@
1
1
  import React from 'react';
2
- import type { UseAuth } from '@cedarjs/auth';
2
+ import type { PermissionName, UseAuth } from '@cedarjs/auth';
3
3
  export interface OrgScopeProps {
4
4
  /** Overrides the `orgSlug` route param. */
5
5
  orgSlug?: string;
6
6
  /** Rendered when the user has no membership matching the slug. */
7
7
  notAMember?: React.ReactNode;
8
+ /**
9
+ * Only members with one of these roles in the organization get in.
10
+ * Checked after the organization is resolved, which `<PrivateSet roles>`
11
+ * around `OrgScope` can't do.
12
+ */
13
+ roles?: string | string[];
14
+ /**
15
+ * Only members whose membership has this permission get in (see
16
+ * `createPermissions` in `@cedarjs/tenancy`). Checked like `roles`.
17
+ */
18
+ permission?: PermissionName;
19
+ /**
20
+ * The name of the route a member without one of `roles`, or without
21
+ * `permission`, is redirected to. It gets the `orgSlug` param when its
22
+ * path has one, so it can be a page of the same organization. Without
23
+ * it, they get `notAMember`.
24
+ */
25
+ unauthorized?: string;
8
26
  /**
9
27
  * The app's `useAuth`, so `OrgScope` can read `currentUser.memberships`
10
28
  * without importing app code. Defaults to `useNoAuth`.
@@ -21,9 +39,12 @@ export interface OrgScopeProps {
21
39
  * Provides the per-organization Apollo client and `OrgContext` for the
22
40
  * organization matching `orgSlug` (a prop, or else the `orgSlug` route
23
41
  * param). Renders `notAMember` when the current user has no membership in
24
- * that organization; otherwise wraps `children` in that organization's own
25
- * `ApolloProvider`, so every Cell, `useQuery` and `useMutation` under it
26
- * carries the `cedar-org` header and reads from that organization's cache.
42
+ * that organization (or not one with `roles`, see `unauthorized`);
43
+ * otherwise wraps `children` in that organization's own `ApolloProvider`,
44
+ * so every Cell, `useQuery` and `useMutation` under it carries the
45
+ * `cedar-org` header and reads from that organization's cache. Under it,
46
+ * `useAuth().hasPermission` checks the user's membership in that
47
+ * organization.
27
48
  */
28
- export declare function OrgScope({ orgSlug: orgSlugProp, notAMember, useAuth, onSetOrg, children, }: OrgScopeProps): React.JSX.Element;
49
+ export declare function OrgScope({ orgSlug: orgSlugProp, notAMember, roles, permission, unauthorized, useAuth, onSetOrg, children, }: OrgScopeProps): React.JSX.Element;
29
50
  //# sourceMappingURL=OrgScope.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"OrgScope.d.ts","sourceRoot":"","sources":["../../src/web/OrgScope.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAe5C,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kEAAkE;IAClE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC5B;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B;AAYD;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,EACvB,OAAO,EAAE,WAAW,EACpB,UAAiB,EACjB,OAAmB,EACnB,QAAQ,EACR,QAAQ,GACT,EAAE,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAyInC"}
1
+ {"version":3,"file":"OrgScope.d.ts","sourceRoot":"","sources":["../../src/web/OrgScope.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAqB5D,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kEAAkE;IAClE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC5B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACzB;;;OAGG;IACH,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B;AAwCD;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,EACvB,OAAO,EAAE,WAAW,EACpB,UAAiB,EACjB,KAAK,EACL,UAAU,EACV,YAAY,EACZ,OAAmB,EACnB,QAAQ,EACR,QAAQ,GACT,EAAE,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CA8KnC"}
@@ -1,20 +1,44 @@
1
1
  import { Fragment, jsx } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import { ApolloProvider } from "@apollo/client/react";
4
- import { useNoAuth } from "@cedarjs/auth";
5
- import { navigate, useLocation, useParams } from "@cedarjs/router";
4
+ import { HasPermissionContext, useNoAuth } from "@cedarjs/auth";
5
+ import {
6
+ navigate,
7
+ Redirect,
8
+ routes,
9
+ useLocation,
10
+ useParams
11
+ } from "@cedarjs/router";
6
12
  import { useCreateApolloClient } from "@cedarjs/web/apollo";
7
13
  import { getMemberships } from "./getMemberships.js";
8
14
  import { hasOrgRole } from "./hasOrgRole.js";
9
15
  import { clearOrgClients, dropOrgClient, getOrgClient } from "./orgClients.js";
10
16
  import { OrgContext } from "./OrgContext.js";
11
17
  const CEDAR_ORG_HEADER = "cedar-org";
18
+ const toList = (roles) => Array.isArray(roles) ? roles : [roles];
19
+ const REPLACE = { replace: true };
20
+ function isNamedRoute(value) {
21
+ return typeof value === "function";
22
+ }
23
+ function unauthorizedPath(route, orgSlug) {
24
+ try {
25
+ return route();
26
+ } catch (e) {
27
+ if (e instanceof Error && e.message.includes("Missing parameter")) {
28
+ return route({ orgSlug });
29
+ }
30
+ throw e;
31
+ }
32
+ }
12
33
  function membershipRoleMap(memberships) {
13
34
  return new Map(memberships.map((m) => [m.organizationId, m.role]));
14
35
  }
15
36
  function OrgScope({
16
37
  orgSlug: orgSlugProp,
17
38
  notAMember = null,
39
+ roles,
40
+ permission,
41
+ unauthorized,
18
42
  useAuth = useNoAuth,
19
43
  onSetOrg,
20
44
  children
@@ -77,15 +101,32 @@ function OrgScope({
77
101
  [onSetOrg, params, location.pathname, location.search, location.hash]
78
102
  );
79
103
  const membership = orgSlug ? memberships.find((m) => m.organization.slug === orgSlug) : void 0;
80
- if (!membership || !userId) {
104
+ const membershipPermissions = membership?.permissions;
105
+ const hasPermission = React.useCallback(
106
+ (permissionToCheck) => membershipPermissions?.includes(permissionToCheck) ?? false,
107
+ [membershipPermissions]
108
+ );
109
+ if (!membership || !userId || !orgSlug) {
81
110
  return /* @__PURE__ */ jsx(Fragment, { children: notAMember });
82
111
  }
112
+ const allowed = (roles === void 0 || toList(roles).includes(membership.role)) && (permission === void 0 || hasPermission(permission));
113
+ if (!allowed) {
114
+ if (!unauthorized) {
115
+ return /* @__PURE__ */ jsx(Fragment, { children: notAMember });
116
+ }
117
+ const route = Object.hasOwn(routes, unauthorized) ? Reflect.get(routes, unauthorized) : void 0;
118
+ if (!isNamedRoute(route)) {
119
+ throw new Error(`We could not find a route named ${unauthorized}`);
120
+ }
121
+ return /* @__PURE__ */ jsx(Redirect, { to: unauthorizedPath(route, orgSlug), options: REPLACE });
122
+ }
83
123
  const org = {
84
124
  id: membership.organizationId,
85
125
  slug: membership.organization.slug,
86
126
  name: membership.organization.name,
87
127
  role: membership.role,
88
- membershipId: membership.id
128
+ membershipId: membership.id,
129
+ permissions: membership.permissions ?? []
89
130
  };
90
131
  const orgClient = getOrgClient({
91
132
  userId,
@@ -97,10 +138,10 @@ function OrgScope({
97
138
  const contextValue = {
98
139
  org,
99
140
  memberships,
100
- hasOrgRole: (roles, organizationId) => hasOrgRole(memberships, roles, organizationId ?? org.id),
141
+ hasOrgRole: (roles2, organizationId) => hasOrgRole(memberships, roles2, organizationId ?? org.id),
101
142
  setOrg
102
143
  };
103
- return /* @__PURE__ */ jsx(ApolloProvider, { client: orgClient, children: /* @__PURE__ */ jsx(OrgContext.Provider, { value: contextValue, children }) });
144
+ return /* @__PURE__ */ jsx(ApolloProvider, { client: orgClient, children: /* @__PURE__ */ jsx(OrgContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(HasPermissionContext.Provider, { value: hasPermission, children }) }) });
104
145
  }
105
146
  export {
106
147
  OrgScope
@@ -1 +1 @@
1
- {"version":3,"file":"getMemberships.d.ts","sourceRoot":"","sources":["../../src/web/getMemberships.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AA+B/C;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,OAAO,GAAG,aAAa,EAAE,CAYpE"}
1
+ {"version":3,"file":"getMemberships.d.ts","sourceRoot":"","sources":["../../src/web/getMemberships.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAmC/C;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,OAAO,GAAG,aAAa,EAAE,CAYpE"}
@@ -8,7 +8,8 @@ function isOrgMembership(value) {
8
8
  return false;
9
9
  }
10
10
  const org = organization;
11
- return typeof membership["id"] === "string" && typeof membership["organizationId"] === "string" && typeof membership["role"] === "string" && typeof org["id"] === "string" && typeof org["slug"] === "string" && typeof org["name"] === "string";
11
+ const permissions = membership["permissions"];
12
+ return (permissions === void 0 || Array.isArray(permissions) && permissions.every((permission) => typeof permission === "string")) && typeof membership["id"] === "string" && typeof membership["organizationId"] === "string" && typeof membership["role"] === "string" && typeof org["id"] === "string" && typeof org["slug"] === "string" && typeof org["name"] === "string";
12
13
  }
13
14
  function getMemberships(currentUser) {
14
15
  if (!currentUser || typeof currentUser !== "object") {
@@ -1,6 +1,5 @@
1
1
  export { getMemberships } from './getMemberships.js';
2
2
  export { hasOrgRole } from './hasOrgRole.js';
3
- export { OrgContext } from './OrgContext.js';
4
3
  export type { OrgScopeProps } from './OrgScope.js';
5
4
  export { OrgScope } from './OrgScope.js';
6
5
  export { clearOrgClients } from './orgClients.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/web/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,YAAY,EACV,iBAAiB,EACjB,eAAe,EACf,aAAa,GACd,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/web/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,YAAY,EACV,iBAAiB,EACjB,eAAe,EACf,aAAa,GACd,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA"}
package/dist/web/index.js CHANGED
@@ -1,11 +1,9 @@
1
1
  import { getMemberships } from "./getMemberships.js";
2
2
  import { hasOrgRole } from "./hasOrgRole.js";
3
- import { OrgContext } from "./OrgContext.js";
4
3
  import { OrgScope } from "./OrgScope.js";
5
4
  import { clearOrgClients } from "./orgClients.js";
6
5
  import { useCurrentOrg } from "./useCurrentOrg.js";
7
6
  export {
8
- OrgContext,
9
7
  OrgScope,
10
8
  clearOrgClients,
11
9
  getMemberships,
@@ -7,6 +7,12 @@ export interface OrgMembership {
7
7
  id: string;
8
8
  organizationId: string;
9
9
  role: string;
10
+ /**
11
+ * What the membership may do: `withPermissions` from the app's
12
+ * permissions map adds these in `getCurrentUser`. Missing when the app
13
+ * doesn't use permissions.
14
+ */
15
+ permissions?: string[];
10
16
  organization: {
11
17
  id: string;
12
18
  slug: string;
@@ -23,9 +29,11 @@ export interface CurrentOrgSummary {
23
29
  name: string;
24
30
  role: string;
25
31
  membershipId: string;
32
+ /** The membership's permissions; empty when the app doesn't use them */
33
+ permissions: string[];
26
34
  }
27
35
  /**
28
- * The value `OrgContext` carries, read through `useCurrentOrg()`.
36
+ * What `OrgScope` provides below it, read through `useCurrentOrg()`.
29
37
  */
30
38
  export interface OrgContextValue {
31
39
  org: CurrentOrgSummary | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/web/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE;QACZ,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;CACF;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,MAAM,CAAA;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,iBAAiB,GAAG,SAAS,CAAA;IAClC,WAAW,EAAE,aAAa,EAAE,CAAA;IAC5B,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACtE,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/web/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,YAAY,EAAE;QACZ,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;CACF;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,MAAM,CAAA;IACpB,wEAAwE;IACxE,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,iBAAiB,GAAG,SAAS,CAAA;IAClC,WAAW,EAAE,aAAa,EAAE,CAAA;IAC5B,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACtE,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/tenancy",
3
- "version": "8.0.0-canary.3538",
3
+ "version": "8.0.0-canary.3540",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/cedarjs/cedar.git",
@@ -15,6 +15,12 @@
15
15
  "default": "./dist/index.js"
16
16
  }
17
17
  },
18
+ "./permissions": {
19
+ "default": {
20
+ "types": "./dist/definePermissions.d.ts",
21
+ "default": "./dist/definePermissions.js"
22
+ }
23
+ },
18
24
  "./web": {
19
25
  "default": {
20
26
  "types": "./dist/web/index.d.ts",
@@ -38,17 +44,17 @@
38
44
  "test:watch": "vitest watch"
39
45
  },
40
46
  "dependencies": {
41
- "@cedarjs/api": "8.0.0-canary.3538",
42
- "@cedarjs/context": "8.0.0-canary.3538",
43
- "@cedarjs/graphql-server": "8.0.0-canary.3538"
47
+ "@cedarjs/api": "8.0.0-canary.3540",
48
+ "@cedarjs/context": "8.0.0-canary.3540",
49
+ "@cedarjs/graphql-server": "8.0.0-canary.3540"
44
50
  },
45
51
  "devDependencies": {
46
52
  "@apollo/client": "4.3.5",
47
53
  "@arethetypeswrong/cli": "0.18.5",
48
- "@cedarjs/auth": "8.0.0-canary.3538",
49
- "@cedarjs/framework-tools": "8.0.0-canary.3538",
50
- "@cedarjs/router": "8.0.0-canary.3538",
51
- "@cedarjs/web": "8.0.0-canary.3538",
54
+ "@cedarjs/auth": "8.0.0-canary.3540",
55
+ "@cedarjs/framework-tools": "8.0.0-canary.3540",
56
+ "@cedarjs/router": "8.0.0-canary.3540",
57
+ "@cedarjs/web": "8.0.0-canary.3540",
52
58
  "@prisma/adapter-better-sqlite3": "7.10.1",
53
59
  "@prisma/client": "7.10.1",
54
60
  "@testing-library/dom": "10.4.2",
@@ -74,9 +80,9 @@
74
80
  },
75
81
  "peerDependencies": {
76
82
  "@apollo/client": "4.3.5",
77
- "@cedarjs/auth": "8.0.0-canary.3538",
78
- "@cedarjs/router": "8.0.0-canary.3538",
79
- "@cedarjs/web": "8.0.0-canary.3538",
83
+ "@cedarjs/auth": "8.0.0-canary.3540",
84
+ "@cedarjs/router": "8.0.0-canary.3540",
85
+ "@cedarjs/web": "8.0.0-canary.3540",
80
86
  "@prisma/client": "7.10.1",
81
87
  "react": "19.3.1"
82
88
  },