diff --git a/README.md b/README.md
index 9c9f6ae..5d49e9d 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,9 @@ export default handler(request, response) {
### Verification
-#### `async verifyRequestByKeyId(rawBody, signature, keyId, options)`
+
+
+#### `async verifyRequestByKeyId(rawBody, signature, keyId, requestOptions)`
Verify the request payload using the provided signature and key ID. The method will request the public key from GitHub's API for the given keyId and then verify the payload.
@@ -202,6 +204,87 @@ import { createDoneEvent } from "@copilot-extensions/preview-sdk";
response.write(createDoneEvent().toString());
```
+### Parsing
+
+
+
+#### `parseRequestBody(body)`
+
+Parses the raw request body and returns an object with type support.
+
+⚠️ **It's well possible that the type is not 100% correct. Please send pull requests to `index.d.ts` to improve it**
+
+```js
+import { parseRequestBody } from "@copilot-extensions/preview-sdk";
+
+const payload = parseRequestBody(rawBody);
+// When your IDE supports types, typing "payload." should prompt the available keys and their types.
+```
+
+#### `transformPayloadForOpenAICompatibility()`
+
+For cases when you want to pipe a user request directly to OpenAI, use this method to remove Copilot-specific fields from the request payload.
+
+```js
+import { transformPayloadForOpenAICompatibility } from "@copilot-extensions/preview-sdk";
+import { OpenAI } from "openai";
+
+const openaiPayload = transformPayloadForOpenAICompatibility(payload);
+
+const openai = new OpenAI({ apiKey: OPENAI_API_KEY });
+const stream = openai.beta.chat.completions.stream({
+ ...openaiPayload,
+ model: "gpt-4-1106-preview",
+ stream: true,
+});
+```
+
+#### `verifyAndParseRequest()`
+
+Convenience method to verify and parse a request in one go. It calls [`verifyRequestByKeyId()`](#verifyRequestByKeyId) and [`parseRequestBody()`](#parseRequestBody) internally.
+
+```js
+import { verifyAndParseRequest } from "@copilot-extensions/preview-sdk";
+
+const { isValidRequest, payload } = await verifyAndParseRequest(
+ request,
+ signature,
+ key
+);
+
+if (!isValidRequest) {
+ throw new Error("Request could not be verified");
+}
+
+// `payload` has type support.
+```
+
+#### `getUserMessage()`
+
+Convencience method to get the user's message from the request payload.
+
+```js
+import { getUserMessage } from "@copilot-extensions/preview-sdk";
+
+const userMessage = getUserMessage(payload);
+```
+
+#### `getUserConfirmation()`
+
+Convencience method to get the user's confirmation from the request payload (in case the user's last response was a confirmation).
+
+```js
+import { getUserConfirmation } from "@copilot-extensions/preview-sdk";
+
+const userConfirmation = getUserConfirmation(payload);
+
+if (userConfirmation) {
+ console.log("Received a user confirmation", userConfirmation);
+} else {
+ // The user's last response was not a confirmation
+}
+```
+
## Dreamcode
While implementing the lower-level functionality, we also dream big: what would our dream SDK for Coplitot extensions look like? Please have a look and share your thoughts and ideas:
diff --git a/index.d.ts b/index.d.ts
index 0f25568..ee3f478 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,5 +1,7 @@
import { request } from "@octokit/request";
+// verification types
+
type RequestInterface = typeof request;
type RequestOptions = {
request?: RequestInterface;
@@ -34,6 +36,8 @@ interface VerifyRequestByKeyIdInterface {
): Promise;
}
+// response types
+
export interface CreateAckEventInterface {
(): ResponseEvent<"ack">
}
@@ -126,6 +130,122 @@ interface CopilotReference {
};
}
+// parse types
+
+export interface CopilotRequestPayload {
+ copilot_thread_id: string
+ messages: Message[]
+ stop: any
+ top_p: number
+ temperature: number
+ max_tokens: number
+ presence_penalty: number
+ frequency_penalty: number
+ copilot_skills: any[]
+ agent: string
+}
+
+export interface OpenAICompatibilityPayload {
+ messages: {
+ role: string
+ name?: string
+ content: string
+ }[]
+}
+
+export interface Message {
+ role: string
+ content: string
+ copilot_references: MessageCopilotReference[]
+ copilot_confirmations?: MessageCopilotConfirmation[]
+ name?: string
+}
+
+export interface MessageCopilotReference {
+ type: string
+ data: CopilotReferenceData
+ id: string
+ is_implicit: boolean
+ metadata: CopilotReferenceMetadata
+}
+
+export interface CopilotReferenceData {
+ type: string
+ id: number
+ name?: string
+ ownerLogin?: string
+ ownerType?: string
+ readmePath?: string
+ description?: string
+ commitOID?: string
+ ref?: string
+ refInfo?: CopilotReferenceDataRefInfo
+ visibility?: string
+ languages?: CopilotReferenceDataLanguage[]
+ login?: string
+ avatarURL?: string
+ url?: string
+}
+
+export interface CopilotReferenceDataRefInfo {
+ name: string
+ type: string
+}
+
+export interface CopilotReferenceDataLanguage {
+ name: string
+ percent: number
+}
+
+export interface CopilotReferenceMetadata {
+ display_name: string
+ display_icon: string
+ display_url: string
+}
+
+export interface MessageCopilotConfirmation {
+ state: "dismissed" | "accepted"
+ confirmation: {
+ id: string
+ [key: string]: unknown
+ }
+}
+
+export interface ParseRequestBodyInterface {
+ (body: string): CopilotRequestPayload
+}
+
+export interface TransformPayloadForOpenAICompatibilityInterface {
+ (payload: CopilotRequestPayload): OpenAICompatibilityPayload
+}
+
+
+export interface VerifyAndParseRequestInterface {
+ (
+ body: string,
+ signature: string,
+ keyID: string,
+ requestOptions?: RequestOptions,
+ ): Promise<{ isValidRequest: boolean; payload: CopilotRequestPayload }>;
+}
+
+
+export interface GetUserMessageInterface {
+ (payload: CopilotRequestPayload): string;
+}
+
+export type UserConfirmation = {
+ accepted: boolean;
+ id?: string;
+ metadata: Record;
+}
+
+export interface GetUserConfirmationInterface {
+ (payload: CopilotRequestPayload): UserConfirmation | undefined;
+}
+
+// exported methods
+
export declare const verifyRequest: VerifyRequestInterface;
export declare const fetchVerificationKeys: FetchVerificationKeysInterface;
export declare const verifyRequestByKeyId: VerifyRequestByKeyIdInterface;
@@ -136,3 +256,9 @@ export declare const createDoneEvent: CreateDoneEventInterface;
export declare const createErrorsEvent: CreateErrorsEventInterface;
export declare const createReferencesEvent: CreateReferencesEventInterface;
export declare const createTextEvent: CreateTextEventInterface;
+
+export declare const parseRequestBody: ParseRequestBodyInterface;
+export declare const transformPayloadForOpenAICompatibility: TransformPayloadForOpenAICompatibilityInterface;
+export declare const verifyAndParseRequest: VerifyAndParseRequestInterface;
+export declare const getUserMessage: GetUserMessageInterface;
+export declare const getUserConfirmation: GetUserConfirmationInterface;
\ No newline at end of file
diff --git a/index.js b/index.js
index 686156a..71259ca 100644
--- a/index.js
+++ b/index.js
@@ -1,4 +1,5 @@
// @ts-check
-export * from "./lib/verification.js";
+export * from "./lib/parse.js";
export * from "./lib/response.js";
+export * from "./lib/verification.js";
diff --git a/index.test-d.ts b/index.test-d.ts
index 14413b7..3e6ed22 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -11,16 +11,18 @@ import {
fetchVerificationKeys,
verifyRequest,
verifyRequestByKeyId,
+ parseRequestBody,
+ transformPayloadForOpenAICompatibility,
+ verifyAndParseRequest,
+ getUserMessage,
+ getUserConfirmation,
type VerificationPublicKey,
+ CopilotRequestPayload,
} from "./index.js";
-const rawBody = "";
-const signature = "";
-const keyId = "";
-const key = ""
const token = "";
-export async function verifyRequestByKeyIdTest() {
+export async function verifyRequestByKeyIdTest(rawBody: string, signature: string, keyId: string) {
const result = await verifyRequestByKeyId(rawBody, signature, keyId);
expectType(result);
@@ -43,7 +45,7 @@ export async function verifyRequestByKeyIdTest() {
await verifyRequestByKeyId(rawBody, signature, keyId, { request });
}
-export async function verifyRequestTest() {
+export async function verifyRequestTest(rawBody: string, signature: string, key: string) {
const result = await verifyRequest(rawBody, signature, key);
expectType(result);
@@ -227,3 +229,41 @@ export function createDoneEventTest() {
// @ts-expect-error - .event is required
event.event
}
+
+export function parseRequestBodyTest(body: string) {
+ const result = parseRequestBody(body)
+ expectType(result);
+}
+
+export function transformPayloadForOpenAICompatibilityTest(payload: CopilotRequestPayload) {
+ const result = transformPayloadForOpenAICompatibility(payload)
+ expectType<{
+ messages: {
+ content: string;
+ role: string;
+ name?: string
+ }[]
+ }
+ >(result);
+}
+
+export async function verifyAndParseRequestTest(rawBody: string, signature: string, keyId: string) {
+ const result = await verifyAndParseRequest(rawBody, signature, keyId)
+ expectType<{ isValidRequest: boolean, payload: CopilotRequestPayload }>(result);
+}
+
+export function getUserMessageTest(payload: CopilotRequestPayload) {
+ const result = getUserMessage(payload)
+ expectType(result)
+}
+
+export function getUserConfirmationTest(payload: CopilotRequestPayload) {
+ const result = getUserConfirmation(payload)
+
+ if (result === undefined) {
+ expectType(result)
+ return
+ }
+
+ expectType<{ accepted: boolean; id?: string; metadata: Record }>(result)
+}
diff --git a/lib/parse.js b/lib/parse.js
new file mode 100644
index 0000000..83d84d2
--- /dev/null
+++ b/lib/parse.js
@@ -0,0 +1,57 @@
+// @ts-check
+
+import { verifyRequestByKeyId } from "./verification.js";
+
+/** @type {import('..').ParseRequestBodyInterface} */
+export function parseRequestBody(body) {
+ return JSON.parse(body);
+}
+
+/** @type {import('..').TransformPayloadForOpenAICompatibilityInterface} */
+export function transformPayloadForOpenAICompatibility(payload) {
+ return {
+ messages: payload.messages.map((message) => {
+ return {
+ role: message.role,
+ name: message.name,
+ content: message.content,
+ };
+ }),
+ };
+}
+
+/** @type {import('..').VerifyAndParseRequestInterface} */
+export async function verifyAndParseRequest(body, signature, keyID, options) {
+ const isValidRequest = await verifyRequestByKeyId(
+ body,
+ signature,
+ keyID,
+ options
+ );
+
+ return {
+ isValidRequest,
+ payload: parseRequestBody(body),
+ };
+}
+
+/** @type {import('..').GetUserMessageInterface} */
+export function getUserMessage(payload) {
+ return payload.messages[payload.messages.length - 1].content;
+}
+
+/** @type {import('..').GetUserConfirmationInterface} */
+export function getUserConfirmation(payload) {
+ const confirmation =
+ payload.messages[payload.messages.length - 1].copilot_confirmations?.[0];
+
+ if (!confirmation) return;
+
+ const { id, ...metadata } = confirmation.confirmation;
+
+ return {
+ accepted: confirmation.state === "accepted",
+ id,
+ metadata,
+ };
+}
diff --git a/test/parse.test.js b/test/parse.test.js
new file mode 100644
index 0000000..fe55173
--- /dev/null
+++ b/test/parse.test.js
@@ -0,0 +1,129 @@
+import { test, suite } from "node:test";
+
+import { MockAgent } from "undici";
+import { request as defaultRequest } from "@octokit/request";
+
+import {
+ getUserConfirmation,
+ getUserMessage,
+ parseRequestBody,
+ transformPayloadForOpenAICompatibility,
+ verifyAndParseRequest,
+} from "../index.js";
+import {
+ CURRENT_PUBLIC_KEY,
+ KEY_ID,
+ RAW_BODY,
+ SIGNATURE,
+} from "./verification.test.js";
+
+suite("request parsing", () => {
+ test("parseRequestBody()", (t) => {
+ // parseRequestBody() does not check for structure. We assume it adheres
+ // to the expected structure when we verify that request came indeed
+ // from GitHub
+ const payload = parseRequestBody('{"messages": []}');
+ t.assert.deepStrictEqual(payload.messages, []);
+ });
+
+ test("transformPayloadForOpenAICompatibility()", (t) => {
+ const payload = transformPayloadForOpenAICompatibility({
+ messages: [],
+ someCopilotKey: "value",
+ });
+ t.assert.deepStrictEqual(payload.messages, []);
+ });
+
+ test("verifyAndParseRequest()", async (t) => {
+ const mockAgent = new MockAgent();
+ function fetchMock(url, opts) {
+ opts ||= {};
+ opts.dispatcher = mockAgent;
+ return fetch(url, opts);
+ }
+
+ mockAgent.disableNetConnect();
+ const mockPool = mockAgent.get("https://api.github.com");
+ mockPool
+ .intercept({
+ method: "get",
+ path: `/meta/public_keys/copilot_api`,
+ })
+ .reply(
+ 200,
+ {
+ public_keys: [
+ {
+ key: CURRENT_PUBLIC_KEY,
+ key_identifier: KEY_ID,
+ is_current: true,
+ },
+ ],
+ },
+ {
+ headers: {
+ "content-type": "application/json",
+ "x-request-id": "",
+ },
+ }
+ );
+ const testRequest = defaultRequest.defaults({
+ request: { fetch: fetchMock },
+ });
+
+ const result = await verifyAndParseRequest(RAW_BODY, SIGNATURE, KEY_ID, {
+ request: testRequest,
+ });
+
+ t.assert.deepStrictEqual(
+ { isValidRequest: true, payload: JSON.parse(RAW_BODY) },
+ result
+ );
+ });
+
+ test("getUserMessage()", (t) => {
+ const payload = {
+ messages: [
+ {
+ content: "Some previous message",
+ },
+ {
+ content: "Hello, world!",
+ },
+ ],
+ };
+ const result = getUserMessage(payload);
+ t.assert.equal("Hello, world!", result);
+ });
+
+ test("getUserConfirmation()", (t) => {
+ const payload = {
+ messages: [
+ {
+ content: "Some previous message",
+ },
+ {
+ content: "Hello, world!",
+ copilot_confirmations: [
+ {
+ state: "accepted",
+ confirmation: {
+ id: "some-confirmation-id",
+ someConfirmationMetadata: "value",
+ },
+ },
+ ],
+ },
+ ],
+ };
+ const result = getUserConfirmation(payload);
+ t.assert.deepStrictEqual(
+ {
+ accepted: true,
+ id: "some-confirmation-id",
+ metadata: { someConfirmationMetadata: "value" },
+ },
+ result
+ );
+ });
+});
diff --git a/test/response.test.js b/test/response.test.js
index 3f1a660..3b66b23 100644
--- a/test/response.test.js
+++ b/test/response.test.js
@@ -1,5 +1,3 @@
-// @ts-check
-
import { test, suite } from "node:test";
import {
diff --git a/test/verification.test.js b/test/verification.test.js
index 2484371..4ac89a9 100644
--- a/test/verification.test.js
+++ b/test/verification.test.js
@@ -10,17 +10,17 @@ import {
verifyRequestByKeyId,
} from "../index.js";
-const RAW_BODY = `{"copilot_thread_id":"9a1cc23a-ab73-498b-87a5-96c94cb7e3f3","messages":[{"role":"user","content":"@gr2m hi","copilot_references":[{"type":"github.repository","data":{"type":"repository","id":102985470,"name":"sandbox","ownerLogin":"gr2m","ownerType":"User","readmePath":"README.md","description":"@gr2m's little sandbox to play","commitOID":"9b04fffccbb818b2e317394463731b66f1ec5e89","ref":"refs/heads/main","refInfo":{"name":"main","type":"branch"},"visibility":"public","languages":[{"name":"JavaScript","percent":100}]},"id":"gr2m/sandbox","is_implicit":false,"metadata":{"display_name":"gr2m/sandbox","display_icon":"","display_url":""}}],"copilot_confirmations":null},{"role":"user","content":"@gr2m test","copilot_references":[{"type":"github.repository","data":{"type":"repository","id":102985470,"name":"sandbox","ownerLogin":"gr2m","ownerType":"User","readmePath":"README.md","description":"@gr2m's little sandbox to play","commitOID":"9b04fffccbb818b2e317394463731b66f1ec5e89","ref":"refs/heads/main","refInfo":{"name":"main","type":"branch"},"visibility":"public","languages":[{"name":"JavaScript","percent":100}]},"id":"gr2m/sandbox","is_implicit":false,"metadata":{"display_name":"gr2m/sandbox","display_icon":"","display_url":""}}],"copilot_confirmations":null},{"role":"user","content":"@gr2m test","copilot_references":[{"type":"github.repository","data":{"type":"repository","id":102985470,"name":"sandbox","ownerLogin":"gr2m","ownerType":"User","readmePath":"README.md","description":"@gr2m's little sandbox to play","commitOID":"9b04fffccbb818b2e317394463731b66f1ec5e89","ref":"refs/heads/main","refInfo":{"name":"main","type":"branch"},"visibility":"public","languages":[{"name":"JavaScript","percent":100}]},"id":"gr2m/sandbox","is_implicit":false,"metadata":{"display_name":"gr2m/sandbox","display_icon":"","display_url":""}}],"copilot_confirmations":null},{"role":"user","content":"Current Date and Time (UTC): 2024-08-26 19:43:13\\nUser's Current URL: https://github.com/gr2m/sandbox\\nCurrent User's Login: gr2m\\n","name":"_session","copilot_references":[],"copilot_confirmations":null},{"role":"user","content":"","copilot_references":[{"type":"github.repository","data":{"type":"repository","id":102985470,"name":"sandbox","ownerLogin":"gr2m","ownerType":"User","readmePath":"README.md","description":"@gr2m's little sandbox to play","commitOID":"9b04fffccbb818b2e317394463731b66f1ec5e89","ref":"refs/heads/main","refInfo":{"name":"main","type":"branch"},"visibility":"public","languages":[{"name":"JavaScript","percent":100}]},"id":"gr2m/sandbox","is_implicit":false,"metadata":{"display_name":"gr2m/sandbox","display_icon":"","display_url":""}}],"copilot_confirmations":null},{"role":"user","content":"test","copilot_references":[],"copilot_confirmations":[]}],"stop":null,"top_p":0,"temperature":0,"max_tokens":0,"presence_penalty":0,"frequency_penalty":0,"copilot_skills":null,"agent":"gr2m"}`;
-const KEY_ID =
+export const RAW_BODY = `{"copilot_thread_id":"9a1cc23a-ab73-498b-87a5-96c94cb7e3f3","messages":[{"role":"user","content":"@gr2m hi","copilot_references":[{"type":"github.repository","data":{"type":"repository","id":102985470,"name":"sandbox","ownerLogin":"gr2m","ownerType":"User","readmePath":"README.md","description":"@gr2m's little sandbox to play","commitOID":"9b04fffccbb818b2e317394463731b66f1ec5e89","ref":"refs/heads/main","refInfo":{"name":"main","type":"branch"},"visibility":"public","languages":[{"name":"JavaScript","percent":100}]},"id":"gr2m/sandbox","is_implicit":false,"metadata":{"display_name":"gr2m/sandbox","display_icon":"","display_url":""}}],"copilot_confirmations":null},{"role":"user","content":"@gr2m test","copilot_references":[{"type":"github.repository","data":{"type":"repository","id":102985470,"name":"sandbox","ownerLogin":"gr2m","ownerType":"User","readmePath":"README.md","description":"@gr2m's little sandbox to play","commitOID":"9b04fffccbb818b2e317394463731b66f1ec5e89","ref":"refs/heads/main","refInfo":{"name":"main","type":"branch"},"visibility":"public","languages":[{"name":"JavaScript","percent":100}]},"id":"gr2m/sandbox","is_implicit":false,"metadata":{"display_name":"gr2m/sandbox","display_icon":"","display_url":""}}],"copilot_confirmations":null},{"role":"user","content":"@gr2m test","copilot_references":[{"type":"github.repository","data":{"type":"repository","id":102985470,"name":"sandbox","ownerLogin":"gr2m","ownerType":"User","readmePath":"README.md","description":"@gr2m's little sandbox to play","commitOID":"9b04fffccbb818b2e317394463731b66f1ec5e89","ref":"refs/heads/main","refInfo":{"name":"main","type":"branch"},"visibility":"public","languages":[{"name":"JavaScript","percent":100}]},"id":"gr2m/sandbox","is_implicit":false,"metadata":{"display_name":"gr2m/sandbox","display_icon":"","display_url":""}}],"copilot_confirmations":null},{"role":"user","content":"Current Date and Time (UTC): 2024-08-26 19:43:13\\nUser's Current URL: https://github.com/gr2m/sandbox\\nCurrent User's Login: gr2m\\n","name":"_session","copilot_references":[],"copilot_confirmations":null},{"role":"user","content":"","copilot_references":[{"type":"github.repository","data":{"type":"repository","id":102985470,"name":"sandbox","ownerLogin":"gr2m","ownerType":"User","readmePath":"README.md","description":"@gr2m's little sandbox to play","commitOID":"9b04fffccbb818b2e317394463731b66f1ec5e89","ref":"refs/heads/main","refInfo":{"name":"main","type":"branch"},"visibility":"public","languages":[{"name":"JavaScript","percent":100}]},"id":"gr2m/sandbox","is_implicit":false,"metadata":{"display_name":"gr2m/sandbox","display_icon":"","display_url":""}}],"copilot_confirmations":null},{"role":"user","content":"test","copilot_references":[],"copilot_confirmations":[]}],"stop":null,"top_p":0,"temperature":0,"max_tokens":0,"presence_penalty":0,"frequency_penalty":0,"copilot_skills":null,"agent":"gr2m"}`;
+export const KEY_ID =
"4fe6b016179b74078ade7581abf4e84fb398c6fae4fb973972235b84fcd70ca3";
-const CURRENT_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
+export const CURRENT_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELPuPiLVQbHY/clvpNnY+0BzYIXgo
S0+XhEkTWUZEEznIVpS3rQseDTG6//gEWr4j9fY35+dGOxwOx3Z9mK3i7w==
-----END PUBLIC KEY-----
`;
-const SIGNATURE =
+export const SIGNATURE =
"MEYCIQC8aEmkYA/4EQrXEOi2OL9nfpbnrCxkMc6HrH7b6SogKgIhAIYBThcpzkCCswiV1+pOaPI+zFQF9ShG61puoKs9rJjq";
test("smoke", (t) => {