@cedarjs/tenancy 8.0.0-canary.3539 → 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
@@ -100,6 +100,39 @@ rather than an organization: it needs a signed-in user, and checks `roles`
100
100
  against `currentUser.roles`. `hasOrgRole` and `hasUserRole` are the boolean
101
101
  checks to match. There's no `requireAuth` or `hasRole`.
102
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)
126
+ ```
127
+
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`.
135
+
103
136
  ## Web
104
137
 
105
138
  `@cedarjs/tenancy/web` exports `OrgScope`, `useCurrentOrg` and
@@ -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,5 +1,5 @@
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;
@@ -12,9 +12,15 @@ export interface OrgScopeProps {
12
12
  */
13
13
  roles?: string | string[];
14
14
  /**
15
- * The name of the route a member without one of `roles` is redirected to.
16
- * It gets the `orgSlug` param when its path has one, so it can be a page
17
- * of the same organization. Without it, they get `notAMember`.
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`.
18
24
  */
19
25
  unauthorized?: string;
20
26
  /**
@@ -36,7 +42,9 @@ export interface OrgScopeProps {
36
42
  * that organization (or not one with `roles`, see `unauthorized`);
37
43
  * otherwise wraps `children` in that organization's own `ApolloProvider`,
38
44
  * so every Cell, `useQuery` and `useMutation` under it carries the
39
- * `cedar-org` header and reads from that organization's cache.
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.
40
48
  */
41
- export declare function OrgScope({ orgSlug: orgSlugProp, notAMember, roles, unauthorized, 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;
42
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;AAqB5C,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;;;;OAIG;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;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,EACvB,OAAO,EAAE,WAAW,EACpB,UAAiB,EACjB,KAAK,EACL,YAAY,EACZ,OAAmB,EACnB,QAAQ,EACR,QAAQ,GACT,EAAE,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CA4JnC"}
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,7 +1,7 @@
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";
4
+ import { HasPermissionContext, useNoAuth } from "@cedarjs/auth";
5
5
  import {
6
6
  navigate,
7
7
  Redirect,
@@ -37,6 +37,7 @@ function OrgScope({
37
37
  orgSlug: orgSlugProp,
38
38
  notAMember = null,
39
39
  roles,
40
+ permission,
40
41
  unauthorized,
41
42
  useAuth = useNoAuth,
42
43
  onSetOrg,
@@ -100,10 +101,16 @@ function OrgScope({
100
101
  [onSetOrg, params, location.pathname, location.search, location.hash]
101
102
  );
102
103
  const membership = orgSlug ? memberships.find((m) => m.organization.slug === orgSlug) : void 0;
104
+ const membershipPermissions = membership?.permissions;
105
+ const hasPermission = React.useCallback(
106
+ (permissionToCheck) => membershipPermissions?.includes(permissionToCheck) ?? false,
107
+ [membershipPermissions]
108
+ );
103
109
  if (!membership || !userId || !orgSlug) {
104
110
  return /* @__PURE__ */ jsx(Fragment, { children: notAMember });
105
111
  }
106
- if (roles !== void 0 && !toList(roles).includes(membership.role)) {
112
+ const allowed = (roles === void 0 || toList(roles).includes(membership.role)) && (permission === void 0 || hasPermission(permission));
113
+ if (!allowed) {
107
114
  if (!unauthorized) {
108
115
  return /* @__PURE__ */ jsx(Fragment, { children: notAMember });
109
116
  }
@@ -118,7 +125,8 @@ function OrgScope({
118
125
  slug: membership.organization.slug,
119
126
  name: membership.organization.name,
120
127
  role: membership.role,
121
- membershipId: membership.id
128
+ membershipId: membership.id,
129
+ permissions: membership.permissions ?? []
122
130
  };
123
131
  const orgClient = getOrgClient({
124
132
  userId,
@@ -133,7 +141,7 @@ function OrgScope({
133
141
  hasOrgRole: (roles2, organizationId) => hasOrgRole(memberships, roles2, organizationId ?? org.id),
134
142
  setOrg
135
143
  };
136
- 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 }) }) });
137
145
  }
138
146
  export {
139
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") {
@@ -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,6 +29,8 @@ 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
36
  * What `OrgScope` provides below it, read through `useCurrentOrg()`.
@@ -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.3539",
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.3539",
42
- "@cedarjs/context": "8.0.0-canary.3539",
43
- "@cedarjs/graphql-server": "8.0.0-canary.3539"
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.3539",
49
- "@cedarjs/framework-tools": "8.0.0-canary.3539",
50
- "@cedarjs/router": "8.0.0-canary.3539",
51
- "@cedarjs/web": "8.0.0-canary.3539",
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.3539",
78
- "@cedarjs/router": "8.0.0-canary.3539",
79
- "@cedarjs/web": "8.0.0-canary.3539",
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
  },