toolpass-client 0.11.3 → 0.13.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
@@ -48,7 +48,10 @@ Without a wrapper:
48
48
  ```ts
49
49
  const d = await tp.check("dana@example.com", "jira-main", "DELETE_ISSUES", "issue:PAY-123");
50
50
  d.decision, d.reason; // "deny", "denied: ..."
51
+ d.identity; // the account the email resolved to (a GitHub login), "" if none
51
52
  await tp.require(...); // rejects with PermissionDenied unless allow
53
+ // The fifth argument: the user's groups, or { groups, fresh, token } (token: the user's own
54
+ // ID token, for a connection that reads the groups from it: oidc_issuer).
52
55
  ```
53
56
 
54
57
  Framework examples: [examples/agent-ts](https://github.com/roee-hersh/toolpass/tree/main/examples/agent-ts).
package/dist/index.d.ts CHANGED
@@ -38,13 +38,15 @@ export type DecisionValue = typeof ALLOW | typeof DENY | typeof UNKNOWN;
38
38
  * `decision` is `allow`, `deny` or `unknown`. `reason` is `"<code>: <text>"`
39
39
  * as toolpass sent it, or `"client_error: ..."` when the request never
40
40
  * produced a usable answer. `status` is the HTTP status, 0 when no response
41
- * arrived.
41
+ * arrived. `identity` is the account the user's email resolved to in the
42
+ * system (a GitHub login), "" when none was resolved.
42
43
  */
43
44
  export declare class Decision {
44
45
  readonly decision: DecisionValue;
45
46
  readonly reason: string;
46
47
  readonly status: number;
47
- constructor(decision: DecisionValue, reason: string, status?: number);
48
+ readonly identity: string;
49
+ constructor(decision: DecisionValue, reason: string, status?: number, identity?: string);
48
50
  /** True only for a positive `allow`. `unknown` is not allowed. */
49
51
  get allowed(): boolean;
50
52
  /** The machine-readable code in front of the colon, e.g. `user_not_found`. */
@@ -89,8 +91,10 @@ export declare class Toolpass {
89
91
  /**
90
92
  * Ask toolpass. Never rejects on transport: every failure becomes an
91
93
  * `unknown` decision. The fifth argument is the user's groups, or an
92
- * options object: `{ groups, fresh }`. Groups left out or null are
93
- * omitted from the request.
94
+ * options object: `{ groups, fresh, token }`. Groups left out or null are
95
+ * omitted from the request. `token` is the user's own login token (an
96
+ * OIDC ID token), for a connection that reads the groups from it
97
+ * (`oidc_issuer`).
94
98
  *
95
99
  * `fresh: true` asks for an answer straight from the upstream system,
96
100
  * skipping toolpass's caches. Use it for destructive actions. It narrows
@@ -107,6 +111,8 @@ export declare class Toolpass {
107
111
  export interface CheckOptions {
108
112
  /** The user's groups; left out or null, none are sent. */
109
113
  groups?: readonly string[] | null;
114
+ /** The user's own login token (an OIDC ID token); left out or null, none is sent. */
115
+ token?: string | null;
110
116
  /** Skip toolpass's caches and ask the upstream system now. */
111
117
  fresh?: boolean;
112
118
  }
package/dist/index.js CHANGED
@@ -38,16 +38,19 @@ export const UNKNOWN = "unknown";
38
38
  * `decision` is `allow`, `deny` or `unknown`. `reason` is `"<code>: <text>"`
39
39
  * as toolpass sent it, or `"client_error: ..."` when the request never
40
40
  * produced a usable answer. `status` is the HTTP status, 0 when no response
41
- * arrived.
41
+ * arrived. `identity` is the account the user's email resolved to in the
42
+ * system (a GitHub login), "" when none was resolved.
42
43
  */
43
44
  export class Decision {
44
45
  decision;
45
46
  reason;
46
47
  status;
47
- constructor(decision, reason, status = 200) {
48
+ identity;
49
+ constructor(decision, reason, status = 200, identity = "") {
48
50
  this.decision = decision;
49
51
  this.reason = reason;
50
52
  this.status = status;
53
+ this.identity = identity;
51
54
  Object.freeze(this);
52
55
  }
53
56
  /** True only for a positive `allow`. `unknown` is not allowed. */
@@ -105,8 +108,10 @@ export class Toolpass {
105
108
  /**
106
109
  * Ask toolpass. Never rejects on transport: every failure becomes an
107
110
  * `unknown` decision. The fifth argument is the user's groups, or an
108
- * options object: `{ groups, fresh }`. Groups left out or null are
109
- * omitted from the request.
111
+ * options object: `{ groups, fresh, token }`. Groups left out or null are
112
+ * omitted from the request. `token` is the user's own login token (an
113
+ * OIDC ID token), for a connection that reads the groups from it
114
+ * (`oidc_issuer`).
110
115
  *
111
116
  * `fresh: true` asks for an answer straight from the upstream system,
112
117
  * skipping toolpass's caches. Use it for destructive actions. It narrows
@@ -114,7 +119,7 @@ export class Toolpass {
114
119
  * two; it does not close it.
115
120
  */
116
121
  async check(user, connection, action, resource, options) {
117
- const { groups, fresh } = checkOptions(options);
122
+ const { groups, fresh, token } = checkOptions(options);
118
123
  const body = { user, connection, action, resource };
119
124
  if (groups != null) {
120
125
  body.groups = groupList(groups);
@@ -122,6 +127,9 @@ export class Toolpass {
122
127
  if (fresh) {
123
128
  body.fresh = true;
124
129
  }
130
+ if (token) {
131
+ body.token = token;
132
+ }
125
133
  let response;
126
134
  let raw;
127
135
  try {
@@ -203,28 +211,31 @@ function isLoopback(hostname) {
203
211
  }
204
212
  function checkOptions(o) {
205
213
  if (o == null) {
206
- return { groups: null, fresh: false };
214
+ return { groups: null, fresh: false, token: "" };
207
215
  }
208
216
  if (Array.isArray(o)) {
209
- return { groups: o, fresh: false };
217
+ return { groups: o, fresh: false, token: "" };
210
218
  }
211
219
  if (typeof o === "boolean") {
212
- throw new TypeError("the fifth argument is the groups array or { groups, fresh }; pass { fresh: true } for a fresh check");
220
+ throw new TypeError("the fifth argument is the groups array or { groups, fresh, token }; pass { fresh: true } for a fresh check");
213
221
  }
214
222
  if (typeof o !== "object" || Symbol.iterator in o) {
215
223
  // A Set or another iterable of groups is not an array of them.
216
224
  throw new TypeError("groups must be an array of strings");
217
225
  }
218
226
  for (const key of Object.keys(o)) {
219
- if (key !== "groups" && key !== "fresh") {
220
- throw new TypeError(`unknown check option ${JSON.stringify(key)}; the options are groups and fresh`);
227
+ if (key !== "groups" && key !== "fresh" && key !== "token") {
228
+ throw new TypeError(`unknown check option ${JSON.stringify(key)}; the options are groups, fresh and token`);
221
229
  }
222
230
  }
223
231
  const opts = o;
224
232
  if (opts.fresh !== undefined && typeof opts.fresh !== "boolean") {
225
233
  throw new TypeError("fresh must be a boolean");
226
234
  }
227
- return { groups: opts.groups ?? null, fresh: opts.fresh === true };
235
+ if (opts.token != null && typeof opts.token !== "string") {
236
+ throw new TypeError("token must be a string");
237
+ }
238
+ return { groups: opts.groups ?? null, fresh: opts.fresh === true, token: opts.token ?? "" };
228
239
  }
229
240
  function groupList(groups) {
230
241
  if (typeof groups === "string" || !Array.isArray(groups) || !groups.every((g) => typeof g === "string")) {
@@ -251,7 +262,7 @@ function parse(status, raw) {
251
262
  if (typeof data !== "object" || data === null || !("decision" in data)) {
252
263
  return new Decision(UNKNOWN, `client_error: unexpected response (HTTP ${status})`, status);
253
264
  }
254
- const { decision, reason } = data;
265
+ const { decision, reason, identity } = data;
255
266
  if (decision !== ALLOW && decision !== DENY && decision !== UNKNOWN) {
256
267
  return new Decision(UNKNOWN, `client_error: unexpected decision ${JSON.stringify(decision)}`, status);
257
268
  }
@@ -259,7 +270,7 @@ function parse(status, raw) {
259
270
  // toolpass never allows with a non-200 status; do not trust a proxy that does.
260
271
  return new Decision(UNKNOWN, `client_error: allow with HTTP ${status}`, status);
261
272
  }
262
- return new Decision(decision, reason === undefined ? "" : String(reason), status);
273
+ return new Decision(decision, reason === undefined ? "" : String(reason), status, typeof identity === "string" ? identity : "");
263
274
  }
264
275
  /**
265
276
  * Resolve a user or groups source now.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolpass-client",
3
- "version": "0.11.3",
3
+ "version": "0.13.0",
4
4
  "description": "Check with toolpass whether a user may do something before your AI agent does it for them.",
5
5
  "keywords": [
6
6
  "ai-agents",