@spendgraph/sdk 0.4.0 → 0.5.1

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/dist/client.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Client } from "./core/client/index.js";
2
2
  import type { ClientOptions } from "./core/types.js";
3
- import { Alerts, Credentials, Events, Ingest, Invites, Keys, Models, Playground, Pricing, Projects, Prompts, PromptsAdmin, Stats, Tools } from "./resources/index.js";
3
+ import { Alerts, Cli, Credentials, Events, Ingest, Invites, Keys, Models, Playground, Pricing, Projects, Prompts, PromptsAdmin, Stats, Tools } from "./resources/index.js";
4
4
  export interface SpendgraphOptions extends ClientOptions {
5
5
  /** Scopes writes that accept one. A key is already pinned to its project. */
6
6
  project?: string;
@@ -13,12 +13,14 @@ export interface SpendgraphOptions extends ClientOptions {
13
13
  *
14
14
  * An `apiKey` reaches usage, stats and prompts. The dashboard half — keys,
15
15
  * projects, pricing, credentials — is gated on a signed-in user server-side and
16
- * needs `session`; there is no API-key path to it, which is what stops a leaked
17
- * ingest key from minting more keys or reading a provider secret.
16
+ * needs `session` or an `sgc_` `token`; there is no API-key path to it, which is
17
+ * what stops a leaked ingest key from minting more keys or reading a provider
18
+ * secret. `cli` is how a terminal obtains a token, and how you revoke one.
18
19
  */
19
20
  export declare class Spendgraph {
20
21
  /** The transport. Reach for it only for a route this class does not cover. */
21
22
  readonly http: Client;
23
+ readonly cli: Cli;
22
24
  readonly ingest: Ingest;
23
25
  readonly stats: Stats;
24
26
  readonly events: Events;
package/dist/client.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Client } from "./core/client/index.js";
2
- import { Alerts, Credentials, Events, Ingest, Invites, Keys, Models, Playground, Pricing, Projects, Prompts, PromptsAdmin, Stats, Tools, } from "./resources/index.js";
2
+ import { Alerts, Cli, Credentials, Events, Ingest, Invites, Keys, Models, Playground, Pricing, Projects, Prompts, PromptsAdmin, Stats, Tools, } from "./resources/index.js";
3
3
  export class Spendgraph {
4
4
  http;
5
+ cli;
5
6
  ingest;
6
7
  stats;
7
8
  events;
@@ -18,6 +19,7 @@ export class Spendgraph {
18
19
  tools;
19
20
  constructor(opts) {
20
21
  this.http = new Client(opts);
22
+ this.cli = new Cli(this.http);
21
23
  this.ingest = new Ingest(this.http, opts.project);
22
24
  this.stats = new Stats(this.http);
23
25
  this.events = new Events(this.http);
@@ -17,12 +17,22 @@ export declare class Client {
17
17
  constructor(opts: ClientOptions);
18
18
  /** True once anything is set that the server might accept. */
19
19
  get authenticated(): boolean;
20
- request<T>(path: string, init?: RequestInit): Promise<T>;
20
+ request<T>(path: string, init?: RequestInit, opts?: {
21
+ anonymous?: boolean;
22
+ }): Promise<T>;
21
23
  get<T>(path: string, query?: Query): Promise<T>;
22
24
  post<T>(path: string, body?: unknown, query?: Query): Promise<T>;
23
25
  put<T>(path: string, body?: unknown, query?: Query): Promise<T>;
24
26
  patch<T>(path: string, body?: unknown, query?: Query): Promise<T>;
25
27
  delete<T>(path: string, query?: Query): Promise<T>;
28
+ /**
29
+ * A POST that deliberately carries no credential.
30
+ *
31
+ * Only for the two calls that exist to obtain one. Everything else goes
32
+ * through `post`, so "this request was unauthenticated" stays a decision
33
+ * written at the call site rather than a side effect of having nothing set.
34
+ */
35
+ postAnonymous<T>(path: string, body?: unknown): Promise<T>;
26
36
  private send;
27
37
  /** A 204 and an empty body are success, not a JSON parse failure. */
28
38
  private decode;
@@ -36,14 +36,15 @@ export class Client {
36
36
  ...opts.headers,
37
37
  ...(opts.apiKey ? { "x-api-key": opts.apiKey } : {}),
38
38
  ...(opts.session ? { cookie: opts.session } : {}),
39
+ ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}),
39
40
  };
40
41
  }
41
42
  get authenticated() {
42
43
  return Object.keys(this.auth).length > 0;
43
44
  }
44
- async request(path, init = {}) {
45
- if (!this.authenticated) {
46
- throw new SpendgraphError(401, "no_credentials", "No spendgraph apiKey or session set.");
45
+ async request(path, init = {}, opts = {}) {
46
+ if (!opts.anonymous && !this.authenticated) {
47
+ throw new SpendgraphError(401, "no_credentials", "No spendgraph apiKey, session or token set.");
47
48
  }
48
49
  const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
49
50
  let last;
@@ -90,6 +91,9 @@ export class Client {
90
91
  delete(path, query) {
91
92
  return this.request(`${path}${queryString(query)}`, { method: "DELETE" });
92
93
  }
94
+ postAnonymous(path, body) {
95
+ return this.request(path, { method: "POST", ...(body === undefined ? {} : { body: JSON.stringify(body) }) }, { anonymous: true });
96
+ }
93
97
  send(method, path, body, query) {
94
98
  return this.request(`${path}${queryString(query)}`, {
95
99
  method,
@@ -9,6 +9,12 @@ export interface Credentials {
9
9
  * server rather than a different answer from here.
10
10
  */
11
11
  session?: string;
12
+ /**
13
+ * An `sgc_` terminal token, from `cli.exchange` after somebody approved it in
14
+ * a browser. It stands in for that person's session everywhere a session is
15
+ * accepted, and unlike one it can be revoked — see `cli.revoke`.
16
+ */
17
+ token?: string;
12
18
  /** Sent on every request, under anything set above. */
13
19
  headers?: Record<string, string>;
14
20
  }
@@ -0,0 +1,60 @@
1
+ import type { Client } from "../core/client/index.js";
2
+ export interface TerminalRow {
3
+ id: string;
4
+ label: string;
5
+ createdAt: string;
6
+ lastUsedAt: string | null;
7
+ revokedAt: string | null;
8
+ expiresAt: string;
9
+ }
10
+ export interface DeviceAuth {
11
+ /** The secret the terminal keeps. Never put it in a URL or show it to anyone. */
12
+ deviceCode: string;
13
+ /** The code in the link, formatted for a person to compare against. */
14
+ userCode: string;
15
+ /** Path to open in a browser, joined to your base URL. */
16
+ verifyPath: string;
17
+ expiresAt: string;
18
+ }
19
+ export interface Terminal {
20
+ token: string;
21
+ label: string;
22
+ expiresAt: string;
23
+ }
24
+ /**
25
+ * Signing a terminal in, and signing one out.
26
+ *
27
+ * `start` and `exchange` are the only two calls in this SDK that send no
28
+ * credential: they are how one is obtained. The pair of secrets in `exchange`
29
+ * is the claim being made — the device code proves it is the same terminal that
30
+ * asked, and the one-time code proves a person approved it in a browser.
31
+ */
32
+ export declare class Cli {
33
+ private readonly client;
34
+ constructor(client: Client);
35
+ /** Opens a request. `label` is what the approval page shows the person. */
36
+ start(body: {
37
+ label: string;
38
+ }): Promise<DeviceAuth>;
39
+ /** Redeems an approved request. One token per approval, then the request is spent. */
40
+ exchange(body: {
41
+ deviceCode: string;
42
+ otp: string;
43
+ }): Promise<Terminal>;
44
+ /** Every terminal signed in as you. */
45
+ terminals(): Promise<{
46
+ terminals: TerminalRow[];
47
+ }>;
48
+ /** Signs one out by id. Yours only — somebody else's answers 404. */
49
+ revoke(id: string): Promise<{
50
+ revoked: {
51
+ id: string;
52
+ };
53
+ }>;
54
+ /** Signs out the terminal making this call, which is the one that knows its own token. */
55
+ revokeSelf(): Promise<{
56
+ revoked: {
57
+ id: string;
58
+ };
59
+ }>;
60
+ }
@@ -0,0 +1,21 @@
1
+ export class Cli {
2
+ client;
3
+ constructor(client) {
4
+ this.client = client;
5
+ }
6
+ start(body) {
7
+ return this.client.postAnonymous("/api/v1/cli/auth", body);
8
+ }
9
+ exchange(body) {
10
+ return this.client.postAnonymous("/api/v1/cli/token", body);
11
+ }
12
+ terminals() {
13
+ return this.client.get("/api/v1/cli/tokens");
14
+ }
15
+ revoke(id) {
16
+ return this.client.delete(`/api/v1/cli/tokens/${encodeURIComponent(id)}`);
17
+ }
18
+ revokeSelf() {
19
+ return this.client.delete("/api/v1/cli/tokens/current");
20
+ }
21
+ }
@@ -1,5 +1,7 @@
1
1
  export type { AlertRow } from "./alerts.js";
2
2
  export { Alerts } from "./alerts.js";
3
+ export type { DeviceAuth, Terminal, TerminalRow } from "./cli.js";
4
+ export { Cli } from "./cli.js";
3
5
  export { Credentials } from "./credentials.js";
4
6
  export type { EventsPage, EventsQuery, UsageEventRow } from "./events.js";
5
7
  export { Events } from "./events.js";
@@ -1,4 +1,5 @@
1
1
  export { Alerts } from "./alerts.js";
2
+ export { Cli } from "./cli.js";
2
3
  export { Credentials } from "./credentials.js";
3
4
  export { Events } from "./events.js";
4
5
  export { Ingest, MAX_EVENTS } from "./ingest.js";
@@ -33,9 +33,23 @@ export declare class Projects {
33
33
  removeMember(projectId: string, userId: string): Promise<{
34
34
  ok: boolean;
35
35
  }>;
36
- invite(projectId: string, body: Record<string, unknown>): Promise<{
37
- ok: boolean;
38
- added?: unknown;
36
+ /**
37
+ * Invites by GitHub login, or mints a shareable link when none is given.
38
+ *
39
+ * Typed rather than `Record<string, unknown>`: the route reads `githubLogin`
40
+ * and nothing else, so an untyped body let a caller send `email`, have it
41
+ * silently dropped, and get a link invite it never asked for under a 200.
42
+ */
43
+ invite(projectId: string, body: {
44
+ githubLogin?: string;
45
+ role?: "owner" | "member";
46
+ }): Promise<{
47
+ added: boolean;
48
+ githubLogin: string | null;
49
+ role: "owner" | "member";
50
+ expiresAt?: string;
51
+ /** Present only for a link invite, and only this once. */
52
+ token?: string | null;
39
53
  }>;
40
54
  revokeInvite(projectId: string, inviteId: string): Promise<{
41
55
  ok: boolean;
@@ -68,7 +68,7 @@ export declare class Prompts {
68
68
  list(query?: Scoped & {
69
69
  limit?: number;
70
70
  cursor?: string;
71
- archived?: boolean;
71
+ archived?: "only" | "active" | "all";
72
72
  }): Promise<{
73
73
  prompts: unknown[];
74
74
  nextCursor?: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spendgraph/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Track LLM input/output tokens and cost. Three functions, zero dependencies, fail-open.",
5
5
  "license": "MIT",
6
6
  "repository": {