@canopy-io/node 0.3.4 → 0.3.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @canopy-io/node
2
2
 
3
+ ## 0.3.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 0708712: Typed webhook deliveries.
8
+
9
+ - Canopy now publishes a schema per webhook event, and the types are regenerated from it: 37 `Webhook…Event` schemas on the shared envelope, plus `payload_schema` and `resource_types` on `GET /api/v1/webhooks/event-types`. No operation was added or removed.
10
+ - New `WebhookDelivery`, `WebhookEventName`, `WebhookEvent<Name>` and `WebhookEventData<Name>` types, and an `isWebhookEvent(delivery, name)` guard, so a handler reads one event's `metadata` without a cast.
11
+ - `metadata` is now always an object on every delivery.
12
+
13
+ ## 0.3.5
14
+
15
+ ### Patch Changes
16
+
17
+ - bd5a478: Set up an organization's directory sync (SCIM) from your backend.
18
+
19
+ - `organizations.setDirectoryGrantableRoles` and `listDirectoryGrantableRoles` wrap the two new operations, `PUT`/`GET /api/v1/organizations/{id}/directory/grantable-roles`. Naming the roles an organization's identity provider may grant, and the one people arrive with, is what opens directory sync for it. Only your API key can set the list; an organization admin's token can read it and is refused changing it.
20
+ - The rest of an organization's directory is wrapped too: `getDirectory`, `createDirectory`, `removeDirectory`, `listDirectoryTokens`, `mintDirectoryToken`, `rotateDirectoryToken`, `revokeDirectoryToken`, `listDirectoryActivity`, `listDirectoryGroups`, `mapDirectoryGroup` and `unmapDirectoryGroup`. They were already typed and reachable through `client.request`.
21
+ - `base_url` on a directory and on a minted token is now the full URL, including the host, so it can be handed to a customer as it is. If you were putting your API host in front of it, stop.
22
+
23
+ The published spec gained 2 operations (132 to 134), both additive, so this is a patch.
24
+
3
25
  ## 0.3.4
4
26
 
5
27
  ### Patch Changes
package/README.md CHANGED
@@ -136,8 +136,8 @@ plus `mfa` once a second factor was verified), so a backend can insist on
136
136
  `claims.amr?.includes("mfa")` before a sensitive action.
137
137
 
138
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.
139
+ manage its members and invitations, tighten its sign-in policy, bind its
140
+ identity provider, and set up its directory sync.
141
141
 
142
142
  ```ts
143
143
  const acme = await canopy.organizations.create({ name: "Acme Corp" });
@@ -150,6 +150,51 @@ await canopy.organizations.addMember(acme.id, {
150
150
  await canopy.organizations.updatePolicy(acme.id, { mfa_required: true });
151
151
  ```
152
152
 
153
+ Directory sync (SCIM) opens when you name the roles an organization's identity
154
+ provider may grant; the organization's own admin can read that list but not
155
+ change it. The token is returned once, with the full base URL its connector
156
+ needs.
157
+
158
+ ```ts
159
+ await canopy.organizations.setDirectoryGrantableRoles(acme.id, {
160
+ role_ids: [memberRoleId, leadRoleId],
161
+ default_role_id: memberRoleId,
162
+ });
163
+
164
+ await canopy.organizations.createDirectory(acme.id);
165
+
166
+ const { token, base_url } = await canopy.organizations.mintDirectoryToken(
167
+ acme.id,
168
+ { name: "Okta" },
169
+ );
170
+ ```
171
+
172
+ ### Handling webhooks
173
+
174
+ Every webhook event has a published schema, and the SDK types deliveries from
175
+ it. Narrow by event name and read that event's `metadata` without a cast; a
176
+ field the API adds arrives with the next `npm run generate`.
177
+
178
+ ```ts
179
+ import { isWebhookEvent, type WebhookDelivery } from "@canopy-io/node";
180
+
181
+ app.post("/webhooks/canopy", (req, res) => {
182
+ const delivery = req.body as WebhookDelivery;
183
+
184
+ if (isWebhookEvent(delivery, "organization.member.added")) {
185
+ const { identity_id, role_id } = delivery.data.metadata;
186
+ // ...
187
+ }
188
+
189
+ res.sendStatus(204);
190
+ });
191
+ ```
192
+
193
+ `WebhookEvent<"assignment.updated">` is the delivery of one event, and
194
+ `WebhookEventName` is every event a subscription can receive. The API's
195
+ `GET /api/v1/webhooks/event-types` names each event's schema in
196
+ `payload_schema`.
197
+
153
198
  ### Authorizing without a call per request
154
199
 
155
200
  Asking "may this identity act _here_" on every request puts Canopy in your
package/dist/index.cjs CHANGED
@@ -770,6 +770,127 @@ var Organizations = class {
770
770
  `/api/v1/organizations/${enc(id)}/sso-connections/${enc(connectionId)}`
771
771
  );
772
772
  }
773
+ // ── Directory sync: the organization's own SCIM connection ──
774
+ /**
775
+ * The roles this organization's identity provider may grant, and the one
776
+ * people arrive with. An empty list means directory sync is not open for
777
+ * the organization.
778
+ */
779
+ listDirectoryGrantableRoles(id) {
780
+ return this.client.request(
781
+ "GET",
782
+ `/api/v1/organizations/${enc(id)}/directory/grantable-roles`
783
+ );
784
+ }
785
+ /**
786
+ * Replace the list in one act, which is how directory sync is opened for
787
+ * an organization. Exactly one role is `default_role_id`, the one people
788
+ * arrive with. Withdrawing a role unmaps the groups naming it and keeps
789
+ * their members in the organization; an empty list is refused while a
790
+ * directory is connected. Your API key only: an organization admin's
791
+ * token can read the list but is refused changing it.
792
+ */
793
+ setDirectoryGrantableRoles(id, input) {
794
+ return this.client.request(
795
+ "PUT",
796
+ `/api/v1/organizations/${enc(id)}/directory/grantable-roles`,
797
+ { body: input }
798
+ );
799
+ }
800
+ /**
801
+ * The organization's directory, its full SCIM base URL, and the roles it
802
+ * may grant. `id` is null until a directory is connected.
803
+ */
804
+ getDirectory(id) {
805
+ return this.client.request(
806
+ "GET",
807
+ `/api/v1/organizations/${enc(id)}/directory`
808
+ );
809
+ }
810
+ /** Connect a directory. Refused until the organization's roles are set. */
811
+ createDirectory(id) {
812
+ return this.client.request(
813
+ "POST",
814
+ `/api/v1/organizations/${enc(id)}/directory`
815
+ );
816
+ }
817
+ /**
818
+ * End the connection. The people it provisioned keep their accounts and
819
+ * their membership; what ends is the provider's ability to push.
820
+ */
821
+ removeDirectory(id) {
822
+ return this.client.request(
823
+ "DELETE",
824
+ `/api/v1/organizations/${enc(id)}/directory`
825
+ );
826
+ }
827
+ listDirectoryTokens(id) {
828
+ return this.client.request(
829
+ "GET",
830
+ `/api/v1/organizations/${enc(id)}/directory/tokens`
831
+ );
832
+ }
833
+ /**
834
+ * The raw token is in this answer and nowhere else, alongside the full
835
+ * base URL the identity provider's connector needs with it.
836
+ */
837
+ mintDirectoryToken(id, input = {}) {
838
+ return this.client.request(
839
+ "POST",
840
+ `/api/v1/organizations/${enc(id)}/directory/tokens`,
841
+ { body: input }
842
+ );
843
+ }
844
+ /**
845
+ * A replacement token, shown once. The old one keeps working for a short
846
+ * overlap, so the provider can be moved across without an outage.
847
+ */
848
+ rotateDirectoryToken(id, tokenId, input = {}) {
849
+ return this.client.request(
850
+ "POST",
851
+ `/api/v1/organizations/${enc(id)}/directory/tokens/${enc(tokenId)}/rotate`,
852
+ { body: input }
853
+ );
854
+ }
855
+ revokeDirectoryToken(id, tokenId) {
856
+ return this.client.request(
857
+ "DELETE",
858
+ `/api/v1/organizations/${enc(id)}/directory/tokens/${enc(tokenId)}`
859
+ );
860
+ }
861
+ /** The organization's own provisioning events, newest first. */
862
+ listDirectoryActivity(id) {
863
+ return this.client.request(
864
+ "GET",
865
+ `/api/v1/organizations/${enc(id)}/directory/activity`
866
+ );
867
+ }
868
+ /** The groups the provider has pushed, each with its role mapping. */
869
+ listDirectoryGroups(id) {
870
+ return this.client.request(
871
+ "GET",
872
+ `/api/v1/organizations/${enc(id)}/directory/groups`
873
+ );
874
+ }
875
+ /**
876
+ * Grant a listed role to every member of a pushed group, at the
877
+ * organization. A role not on the organization's list is refused with
878
+ * `scim.role_not_grantable`.
879
+ */
880
+ mapDirectoryGroup(id, groupId, input) {
881
+ return this.client.request(
882
+ "PUT",
883
+ `/api/v1/organizations/${enc(id)}/directory/groups/${enc(groupId)}/mapping`,
884
+ { body: input }
885
+ );
886
+ }
887
+ /** Members fall back to the arrival role and stay in the organization. */
888
+ unmapDirectoryGroup(id, groupId) {
889
+ return this.client.request(
890
+ "DELETE",
891
+ `/api/v1/organizations/${enc(id)}/directory/groups/${enc(groupId)}/mapping`
892
+ );
893
+ }
773
894
  };
774
895
  function enc(segment) {
775
896
  return encodeURIComponent(segment);
@@ -1485,6 +1606,11 @@ var TokenVerifier = class {
1485
1606
  }
1486
1607
  };
1487
1608
 
1609
+ // src/webhooks.ts
1610
+ function isWebhookEvent(delivery, name) {
1611
+ return delivery.event === name;
1612
+ }
1613
+
1488
1614
  exports.Assignments = Assignments;
1489
1615
  exports.Canopy = Canopy;
1490
1616
  exports.CanopyAuthorizerError = CanopyAuthorizerError;
@@ -1504,6 +1630,7 @@ exports.isCanopyConnectionError = isCanopyConnectionError;
1504
1630
  exports.isCanopyError = isCanopyError;
1505
1631
  exports.isCanopyTokenError = isCanopyTokenError;
1506
1632
  exports.isCursorPagination = isCursorPagination;
1633
+ exports.isWebhookEvent = isWebhookEvent;
1507
1634
  exports.orgContext = orgContext;
1508
1635
  exports.paginate = paginate;
1509
1636
  exports.withConcurrency = withConcurrency;