@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 +38 -1
- package/dist/index.cjs +167 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2246 -30
- package/dist/index.d.ts +2246 -30
- package/dist/index.js +166 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 `
|
|
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
|