Skip to content

Commit 73aa542

Browse files
committed
WIP: credentials
Signed-off-by: Grant Linville <[email protected]>
1 parent 7af9fd5 commit 73aa542

File tree

2 files changed

+171
-2
lines changed

2 files changed

+171
-2
lines changed

src/gptscript.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,70 @@ export class GPTScript {
320320
return this._load({toolDefs, disableCache, subTool})
321321
}
322322

323+
async listCredentials(context: string, allContexts: boolean): Promise<Array<Credential>> {
324+
if (!this.ready) {
325+
this.ready = await this.testGPTScriptURL(20)
326+
}
327+
const resp = await fetch(`${GPTScript.serverURL}/credentials`, {
328+
method: "POST",
329+
body: JSON.stringify({context, allContexts})
330+
})
331+
332+
if (resp.status < 200 || resp.status >= 400) {
333+
throw new Error(`Failed to list credentials: ${(await resp.json())["stderr"]}`)
334+
}
335+
336+
const r = await resp.json()
337+
return r["stdout"].map((c: any) => jsonToCredential(JSON.stringify(c)))
338+
}
339+
340+
async createCredential(credential: Credential): Promise<void> {
341+
if (!this.ready) {
342+
this.ready = await this.testGPTScriptURL(20)
343+
}
344+
const resp = await fetch(`${GPTScript.serverURL}/credentials/create`, {
345+
method: "POST",
346+
body: JSON.stringify({
347+
content: credentialToJSON(credential)
348+
})
349+
})
350+
351+
if (resp.status < 200 || resp.status >= 400) {
352+
throw new Error(`Failed to create credential: ${(await resp.json())["stderr"]}`)
353+
}
354+
}
355+
356+
async revealCredential(context: string, name: string): Promise<Credential> {
357+
if (!this.ready) {
358+
this.ready = await this.testGPTScriptURL(20)
359+
}
360+
const resp = await fetch(`${GPTScript.serverURL}/credentials/reveal`, {
361+
method: "POST",
362+
body: JSON.stringify({context, name})
363+
})
364+
365+
if (resp.status < 200 || resp.status >= 400) {
366+
throw new Error(`Failed to reveal credential: ${(await resp.json())["stderr"]}`)
367+
}
368+
369+
const r = await resp.json()
370+
return r["stdout"] as Credential
371+
}
372+
373+
async deleteCredential(context: string, name: string): Promise<void> {
374+
if (!this.ready) {
375+
this.ready = await this.testGPTScriptURL(20)
376+
}
377+
const resp = await fetch(`${GPTScript.serverURL}/credentials/delete`, {
378+
method: "POST",
379+
body: JSON.stringify({context, name})
380+
})
381+
382+
if (resp.status < 200 || resp.status >= 400) {
383+
throw new Error(`Failed to delete credential: ${(await resp.json())["stderr"]}`)
384+
}
385+
}
386+
323387
/**
324388
* Helper method to handle the common logic for loading.
325389
*
@@ -967,3 +1031,48 @@ function parseBlocksFromNodes(nodes: any[]): Block[] {
9671031
function randomId(prefix: string): string {
9681032
return prefix + Math.random().toString(36).substring(2, 12)
9691033
}
1034+
1035+
export enum CredentialType {
1036+
Tool = "tool",
1037+
ModelProvider = "modelProvider",
1038+
}
1039+
1040+
export type Credential = {
1041+
context: string
1042+
name: string
1043+
type: CredentialType
1044+
env: Record<string, string>
1045+
ephemeral: boolean
1046+
expiresAt?: Date | undefined
1047+
refreshToken?: string | undefined
1048+
}
1049+
1050+
// for internal use only
1051+
type cred = {
1052+
context: string
1053+
toolName: string
1054+
type: string
1055+
env: Record<string, string>
1056+
ephemeral: boolean
1057+
expiresAt: string | undefined
1058+
refreshToken: string | undefined
1059+
}
1060+
1061+
export function credentialToJSON(c: Credential): string {
1062+
const expiresAt = c.expiresAt ? c.expiresAt.toISOString() : undefined
1063+
const type = c.type === CredentialType.Tool ? "tool" : "modelProvider"
1064+
return JSON.stringify({context: c.context, toolName: c.name, type: type, env: c.env, ephemeral: c.ephemeral, expiresAt: expiresAt, refreshToken: c.refreshToken} as cred)
1065+
}
1066+
1067+
function jsonToCredential(cred: string): Credential {
1068+
const c = JSON.parse(cred) as cred
1069+
return {
1070+
context: c.context,
1071+
name: c.toolName,
1072+
type: c.type === "tool" ? CredentialType.Tool : CredentialType.ModelProvider,
1073+
env: c.env,
1074+
ephemeral: c.ephemeral,
1075+
expiresAt: c.expiresAt ? new Date(c.expiresAt) : undefined,
1076+
refreshToken: c.refreshToken
1077+
}
1078+
}

tests/gptscript.test.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
import * as gptscript from "../src/gptscript"
2-
import {ArgumentSchemaType, getEnv, PropertyType, RunEventType, TextType, ToolDef, ToolType} from "../src/gptscript"
2+
import {
3+
ArgumentSchemaType,
4+
Credential, CredentialType,
5+
getEnv,
6+
PropertyType,
7+
RunEventType,
8+
TextType,
9+
ToolDef,
10+
ToolType
11+
} from "../src/gptscript"
312
import path from "path"
413
import {fileURLToPath} from "url"
514
import * as fs from "node:fs"
15+
import {randomBytes} from "node:crypto";
616

717
let gFirst: gptscript.GPTScript
818
let g: gptscript.GPTScript
@@ -791,4 +801,54 @@ describe("gptscript module", () => {
791801
expect(err).toEqual(undefined)
792802
expect(out).toEqual("200")
793803
}, 20000)
794-
})
804+
805+
test("credential operations", async () => {
806+
const name = "test-" + randomBytes(10).toString("hex")
807+
const value = randomBytes(10).toString("hex")
808+
809+
// Create
810+
try {
811+
await g.createCredential({
812+
name: name,
813+
context: "default",
814+
env: {"TEST": value},
815+
ephemeral: false,
816+
type: CredentialType.Tool,
817+
})
818+
} catch (e) {
819+
throw new Error("failed to create credential: " + e)
820+
}
821+
822+
// Reveal
823+
try {
824+
const result = await g.revealCredential("default", name)
825+
expect(result.env["TEST"]).toEqual(value)
826+
} catch (e) {
827+
throw new Error("failed to reveal credential: " + e)
828+
}
829+
830+
// List
831+
try {
832+
const result = await g.listCredentials("default", false)
833+
expect(result.length).toBeGreaterThan(0)
834+
expect(result.map(c => c.name)).toContain(name)
835+
} catch (e) {
836+
throw new Error("failed to list credentials: " + e)
837+
}
838+
839+
// Delete
840+
try {
841+
await g.deleteCredential("default", name)
842+
} catch (e) {
843+
throw new Error("failed to delete credential: " + e)
844+
}
845+
846+
// Verify deletion
847+
try {
848+
const result = await g.listCredentials("default", false)
849+
expect(result.map(c => c.name)).not.toContain(name)
850+
} catch (e) {
851+
throw new Error("failed to verify deletion: " + e)
852+
}
853+
})
854+
})

0 commit comments

Comments
 (0)