@canopy-io/node 0.2.0 → 0.3.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/README.md CHANGED
@@ -16,7 +16,7 @@ Despite the name, it is not Node-only: the client is `fetch` and nothing else, s
16
16
 
17
17
  ## Features
18
18
 
19
- - **Typed resource wrappers** for `permissions`, `identities`, `roles` and `assignments` — the four every integration touches.
19
+ - **Typed resource wrappers** for `permissions`, `identities`, `roles`, `assignments` and `organizations` — the ones every integration touches.
20
20
  - **The whole API, typed.** Any operation without a wrapper is reachable through `canopy.client.request` with the same envelope handling, error typing and retry policy.
21
21
  - **Both credential types.** An API key (`cnpy_…`, sent as `X-API-Key`) for server-to-server calls, or an identity or portal JWT sent as a bearer token.
22
22
  - **Envelope unwrapping.** All five response shapes are handled, so a call returns the payload rather than a wrapper.
@@ -113,6 +113,43 @@ new TokenVerifier({ audience: process.env.CANOPY_OAUTH_CLIENT_ID });
113
113
  Self-hosted instances set `issuer`. Getting it wrong fails closed: tokens are
114
114
  rejected, never mistakenly accepted.
115
115
 
116
+ With an Environment's **organizations** container on, an identity token also
117
+ names the organization the session is acting in and the one role held there. Read the pair through `orgContext` rather than off the raw claims —
118
+ Canopy mints the two together, and the helper refuses a half-present pair:
119
+
120
+ ```ts
121
+ import { orgContext } from "@canopy-io/node";
122
+
123
+ const org = orgContext(claims);
124
+
125
+ if (org) {
126
+ org.orgId; // the organization — also a hierarchy node id
127
+ org.orgRole; // the single role held there, by name
128
+ }
129
+ ```
130
+
131
+ `null` is an ordinary answer: every token from an Environment without the
132
+ container on, and every identity that belongs to no organization yet.
133
+
134
+ Tokens also carry `amr`, how the session was proven (`pwd`, `otp` or `sso`,
135
+ plus `mfa` once a second factor was verified), so a backend can insist on
136
+ `claims.amr?.includes("mfa")` before a sensitive action.
137
+
138
+ The organizations themselves are wrapped on the client: provision a tenant,
139
+ manage its members and invitations, tighten its sign-in policy, and bind its
140
+ identity provider.
141
+
142
+ ```ts
143
+ const acme = await canopy.organizations.create({ name: "Acme Corp" });
144
+
145
+ await canopy.organizations.addMember(acme.id, {
146
+ identity_id: identityId,
147
+ role_id: ownerRoleId,
148
+ });
149
+
150
+ await canopy.organizations.updatePolicy(acme.id, { mfa_required: true });
151
+ ```
152
+
116
153
  ### Authorizing without a call per request
117
154
 
118
155
  Asking "may this identity act _here_" on every request puts Canopy in your
package/dist/index.cjs CHANGED
@@ -619,6 +619,162 @@ function withConcurrency(options = {}) {
619
619
  return { headers: { "If-Match": options.ifMatch } };
620
620
  }
621
621
 
622
+ // src/resources/organizations.ts
623
+ var Organizations = class {
624
+ constructor(client) {
625
+ this.client = client;
626
+ }
627
+ client;
628
+ list(query = {}) {
629
+ return paginate(
630
+ (params) => this.client.request("GET", "/api/v1/organizations", {
631
+ query: params
632
+ }),
633
+ { ...query }
634
+ );
635
+ }
636
+ get(id) {
637
+ return this.client.request("GET", `/api/v1/organizations/${enc(id)}`);
638
+ }
639
+ create(input) {
640
+ return this.client.request("POST", "/api/v1/organizations", {
641
+ body: input
642
+ });
643
+ }
644
+ /**
645
+ * Pass `ifMatch` with the organization's current `version` to make a
646
+ * read-modify-write safe — a concurrent edit answers 409 instead of being
647
+ * silently overwritten.
648
+ */
649
+ update(id, input, options = {}) {
650
+ return this.client.request("PATCH", `/api/v1/organizations/${enc(id)}`, {
651
+ body: input,
652
+ ...withConcurrency(options)
653
+ });
654
+ }
655
+ /**
656
+ * Removes the organization with its memberships, pending invitations,
657
+ * policy, connection bindings, and the tree beneath it. Member identities
658
+ * survive.
659
+ */
660
+ delete(id, options = {}) {
661
+ return this.client.request(
662
+ "DELETE",
663
+ `/api/v1/organizations/${enc(id)}`,
664
+ withConcurrency(options)
665
+ );
666
+ }
667
+ /** Empties the container: every organization goes, the step before switching it off. */
668
+ deleteAll() {
669
+ return this.client.request("DELETE", "/api/v1/organizations");
670
+ }
671
+ // ── Members: one role per member, held inside the organization only ──
672
+ listMembers(id, query = {}) {
673
+ return paginate(
674
+ (params) => this.client.request("GET", `/api/v1/organizations/${enc(id)}/members`, {
675
+ query: params
676
+ }),
677
+ { ...query }
678
+ );
679
+ }
680
+ addMember(id, input) {
681
+ return this.client.request(
682
+ "POST",
683
+ `/api/v1/organizations/${enc(id)}/members`,
684
+ { body: input }
685
+ );
686
+ }
687
+ changeMemberRole(id, identityId, input) {
688
+ return this.client.request(
689
+ "PATCH",
690
+ `/api/v1/organizations/${enc(id)}/members/${enc(identityId)}`,
691
+ { body: input }
692
+ );
693
+ }
694
+ /** Revokes that organization's one role; the identity and its other memberships are untouched. */
695
+ removeMember(id, identityId) {
696
+ return this.client.request(
697
+ "DELETE",
698
+ `/api/v1/organizations/${enc(id)}/members/${enc(identityId)}`
699
+ );
700
+ }
701
+ // ── Invitations: pending membership, carrying the role the recipient will hold ──
702
+ listInvites(id, query = {}) {
703
+ return paginate(
704
+ (params) => this.client.request("GET", `/api/v1/organizations/${enc(id)}/invites`, {
705
+ query: params
706
+ }),
707
+ { ...query }
708
+ );
709
+ }
710
+ createInvite(id, input) {
711
+ return this.client.request(
712
+ "POST",
713
+ `/api/v1/organizations/${enc(id)}/invites`,
714
+ { body: input }
715
+ );
716
+ }
717
+ revokeInvite(id, inviteId) {
718
+ return this.client.request(
719
+ "DELETE",
720
+ `/api/v1/organizations/${enc(id)}/invites/${enc(inviteId)}`
721
+ );
722
+ }
723
+ // ── Policy: the organization's tightening of the Environment's sign-in rules ──
724
+ /**
725
+ * The organization's own values (null inherits), the Environment baseline
726
+ * they tighten from, and the effective policy its members sign in under.
727
+ */
728
+ getPolicy(id) {
729
+ return this.client.request(
730
+ "GET",
731
+ `/api/v1/organizations/${enc(id)}/policy`
732
+ );
733
+ }
734
+ /**
735
+ * Can only tighten the Environment: a value that would loosen it answers
736
+ * 400 `organization.policy_loosens`. Pass `ifMatch` with the policy's
737
+ * `version` (0 until the organization sets one) for a safe
738
+ * read-modify-write.
739
+ */
740
+ updatePolicy(id, input, options = {}) {
741
+ return this.client.request(
742
+ "PATCH",
743
+ `/api/v1/organizations/${enc(id)}/policy`,
744
+ { body: input, ...withConcurrency(options) }
745
+ );
746
+ }
747
+ // ── SSO: the organization's own identity provider ──
748
+ listSsoConnections(id) {
749
+ return this.client.request(
750
+ "GET",
751
+ `/api/v1/organizations/${enc(id)}/sso-connections`
752
+ );
753
+ }
754
+ /**
755
+ * Sign-ins through the connection then land in this organization, joining
756
+ * as a member with `default_role_id`. The connection must already be bound
757
+ * to the organization's Environment, and a connection binds to one
758
+ * organization per Environment.
759
+ */
760
+ bindSsoConnection(id, input) {
761
+ return this.client.request(
762
+ "POST",
763
+ `/api/v1/organizations/${enc(id)}/sso-connections`,
764
+ { body: input }
765
+ );
766
+ }
767
+ unbindSsoConnection(id, connectionId) {
768
+ return this.client.request(
769
+ "DELETE",
770
+ `/api/v1/organizations/${enc(id)}/sso-connections/${enc(connectionId)}`
771
+ );
772
+ }
773
+ };
774
+ function enc(segment) {
775
+ return encodeURIComponent(segment);
776
+ }
777
+
622
778
  // src/resources/permissions.ts
623
779
  var Permissions = class {
624
780
  constructor(client) {
@@ -780,12 +936,14 @@ var Canopy = class {
780
936
  identities;
781
937
  roles;
782
938
  assignments;
939
+ organizations;
783
940
  constructor(options) {
784
941
  this.client = new CanopyClient(options);
785
942
  this.permissions = new Permissions(this.client);
786
943
  this.identities = new Identities(this.client);
787
944
  this.roles = new Roles(this.client);
788
945
  this.assignments = new Assignments(this.client);
946
+ this.organizations = new Organizations(this.client);
789
947
  }
790
948
  };
791
949
 
@@ -1010,6 +1168,13 @@ var DEFAULT_JWKS_MIN_REFETCH_INTERVAL_MS = 30 * 1e3;
1010
1168
  var DEFAULT_JWKS_TIMEOUT_MS = 5e3;
1011
1169
  var PRINCIPAL_TYPES = /* @__PURE__ */ new Set(["user", "identity", "api_key", "platform"]);
1012
1170
  var DEFAULT_CLOCK_TOLERANCE_SEC = 60;
1171
+ function orgContext(claims) {
1172
+ const { org_id, org_role } = claims;
1173
+ if (typeof org_id !== "string" || org_id === "" || typeof org_role !== "string" || org_role === "") {
1174
+ return null;
1175
+ }
1176
+ return { orgId: org_id, orgRole: org_role };
1177
+ }
1013
1178
  function decodeBase64Url(value) {
1014
1179
  const padded = value.replace(/-/g, "+").replace(/_/g, "/");
1015
1180
  const binary = atob(padded.padEnd(Math.ceil(padded.length / 4) * 4, "="));
@@ -1329,6 +1494,7 @@ exports.CanopyError = CanopyError;
1329
1494
  exports.CanopyTokenError = CanopyTokenError;
1330
1495
  exports.Identities = Identities;
1331
1496
  exports.LocalAuthorizer = LocalAuthorizer;
1497
+ exports.Organizations = Organizations;
1332
1498
  exports.Paginator = Paginator;
1333
1499
  exports.Permissions = Permissions;
1334
1500
  exports.Roles = Roles;
@@ -1338,6 +1504,7 @@ exports.isCanopyConnectionError = isCanopyConnectionError;
1338
1504
  exports.isCanopyError = isCanopyError;
1339
1505
  exports.isCanopyTokenError = isCanopyTokenError;
1340
1506
  exports.isCursorPagination = isCursorPagination;
1507
+ exports.orgContext = orgContext;
1341
1508
  exports.paginate = paginate;
1342
1509
  exports.withConcurrency = withConcurrency;
1343
1510
  //# sourceMappingURL=index.cjs.map