@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 +22 -0
- package/README.md +47 -2
- package/dist/index.cjs +127 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1126 -5
- package/dist/index.d.ts +1126 -5
- package/dist/index.js +127 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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;
|