@copilot-extensions/preview-sdk
Advanced tools
Comparing version 1.0.0 to 2.0.0
@@ -8,7 +8,26 @@ import { request } from "@octokit/request"; | ||
}; | ||
export type VerificationPublicKey = { | ||
key_identifier: string; | ||
key: string; | ||
is_current: boolean; | ||
}; | ||
interface VerifyInterface { | ||
interface VerifyRequestInterface { | ||
( | ||
rawBody: string, | ||
signature: string, | ||
key: string | ||
): Promise<boolean>; | ||
} | ||
interface FetchVerificationKeysInterface { | ||
( | ||
requestOptions?: RequestOptions, | ||
): Promise<VerificationPublicKey[]>; | ||
} | ||
interface VerifyRequestByKeyIdInterface { | ||
( | ||
rawBody: string, | ||
signature: string, | ||
keyId: string, | ||
@@ -19,2 +38,4 @@ requestOptions?: RequestOptions, | ||
export declare const verify: VerifyInterface; | ||
export declare const verifyRequest: VerifyRequestInterface; | ||
export declare const fetchVerificationKeys: FetchVerificationKeysInterface; | ||
export declare const verifyRequestByKeyId: VerifyRequestByKeyIdInterface; |
72
index.js
@@ -8,16 +8,24 @@ // @ts-check | ||
/** @type {import('.').VerifyInterface} */ | ||
export async function verify( | ||
rawBody, | ||
signature, | ||
keyId, | ||
{ token = "", request = defaultRequest } = { request: defaultRequest }, | ||
) { | ||
/** @type {import('.').VerifyRequestByKeyIdInterface} */ | ||
export async function verifyRequest(rawBody, signature, key) { | ||
// verify arguments | ||
assertValidString(rawBody, "Invalid payload"); | ||
assertValidString(signature, "Invalid signature"); | ||
assertValidString(keyId, "Invalid keyId"); | ||
assertValidString(key, "Invalid key"); | ||
// receive valid public keys from GitHub | ||
const requestOptions = request.endpoint("GET /meta/public_keys/copilot_api", { | ||
const verify = createVerify("SHA256").update(rawBody); | ||
// verify signature | ||
try { | ||
return verify.verify(key, signature, "base64"); | ||
} catch { | ||
return false; | ||
} | ||
} | ||
/** @type {import('.').FetchVerificationKeysInterface} */ | ||
export async function fetchVerificationKeys( | ||
{ token = "", request = defaultRequest } = { request: defaultRequest } | ||
) { | ||
const { data } = await request("GET /meta/public_keys/copilot_api", { | ||
headers: token | ||
@@ -29,24 +37,38 @@ ? { | ||
}); | ||
const response = await request(requestOptions); | ||
const { data: keys } = response; | ||
return data.public_keys; | ||
} | ||
/** @type {import('.').VerifyRequestByKeyIdInterface} */ | ||
export async function verifyRequestByKeyId( | ||
rawBody, | ||
signature, | ||
keyId, | ||
requestOptions | ||
) { | ||
// verify arguments | ||
assertValidString(rawBody, "Invalid payload"); | ||
assertValidString(signature, "Invalid signature"); | ||
assertValidString(keyId, "Invalid keyId"); | ||
// receive valid public keys from GitHub | ||
const keys = await fetchVerificationKeys(requestOptions); | ||
// verify provided key Id | ||
const publicKey = keys.public_keys.find( | ||
(key) => key.key_identifier === keyId, | ||
); | ||
const publicKey = keys.find((key) => key.key_identifier === keyId); | ||
if (!publicKey) { | ||
throw new RequestError( | ||
"[@copilot-extensions/preview-sdk] No public key found matching key identifier", | ||
404, | ||
const keyNotFoundError = Object.assign( | ||
new Error( | ||
"[@copilot-extensions/preview-sdk] No public key found matching key identifier" | ||
), | ||
{ | ||
request: requestOptions, | ||
response, | ||
}, | ||
keyId, | ||
keys, | ||
} | ||
); | ||
throw keyNotFoundError; | ||
} | ||
const verify = createVerify("SHA256").update(rawBody); | ||
// verify signature | ||
return verify.verify(publicKey.key, signature, "base64"); | ||
return verifyRequest(rawBody, signature, publicKey.key); | ||
} | ||
@@ -53,0 +75,0 @@ |
import { expectType } from "tsd"; | ||
import { request } from "@octokit/request"; | ||
import { verify } from "./index.js"; | ||
import { | ||
fetchVerificationKeys, | ||
verifyRequest, | ||
verifyRequestByKeyId, | ||
type VerificationPublicKey, | ||
} from "./index.js"; | ||
@@ -9,25 +14,54 @@ const rawBody = ""; | ||
const keyId = ""; | ||
const key = "" | ||
const token = ""; | ||
export async function verifyTest() { | ||
const result = await verify(rawBody, signature, keyId); | ||
export async function verifyRequestByKeyIdTest() { | ||
const result = await verifyRequestByKeyId(rawBody, signature, keyId); | ||
expectType<boolean>(result); | ||
// @ts-expect-error - first 3 arguments are required | ||
verify(rawBody, signature); | ||
verifyRequestByKeyId(rawBody, signature); | ||
// @ts-expect-error - rawBody must be a string | ||
await verify(1, signature, keyId); | ||
await verifyRequestByKeyId(1, signature, keyId); | ||
// @ts-expect-error - signature must be a string | ||
await verify(rawBody, 1, keyId); | ||
await verifyRequestByKeyId(rawBody, 1, keyId); | ||
// @ts-expect-error - keyId must be a string | ||
await verify(rawBody, signature, 1); | ||
await verifyRequestByKeyId(rawBody, signature, 1); | ||
// accepts a token argument | ||
await verify(rawBody, signature, keyId, { token }); | ||
await verifyRequestByKeyId(rawBody, signature, keyId, { token }); | ||
// accepts a request argument | ||
await verify(rawBody, signature, keyId, { request }); | ||
await verifyRequestByKeyId(rawBody, signature, keyId, { request }); | ||
} | ||
export async function verifyRequestTest() { | ||
const result = await verifyRequest(rawBody, signature, key); | ||
expectType<boolean>(result); | ||
// @ts-expect-error - first 3 arguments are required | ||
verifyRequest(rawBody, signature); | ||
// @ts-expect-error - rawBody must be a string | ||
await verifyRequest(1, signature, key); | ||
// @ts-expect-error - signature must be a string | ||
await verifyRequest(rawBody, 1, key); | ||
// @ts-expect-error - key must be a string | ||
await verifyRequest(rawBody, signature, 1); | ||
} | ||
export async function fetchVerificationKeysTest() { | ||
const result = await fetchVerificationKeys(); | ||
expectType<VerificationPublicKey[]>(result); | ||
// accepts a token argument | ||
await fetchVerificationKeys({ token }); | ||
// accepts a request argument | ||
await fetchVerificationKeys({ request }); | ||
} |
@@ -8,3 +8,3 @@ { | ||
"type": "module", | ||
"version": "1.0.0", | ||
"version": "2.0.0", | ||
"keywords": [ | ||
@@ -11,0 +11,0 @@ "ai", |
@@ -5,17 +5,85 @@ # `@copilot-extensions/preview-sdk` | ||
⚠️ **This SDK is a preview and subjetct to change**. We will however adhere to [semantic versioning](https://semver.org/), so it's save to use for early experimentation. Just beware there will be breaking changes. Best to watch this repository's releases for updates. | ||
⚠️ **This SDK is a preview and subject to change**. We will however adhere to [semantic versioning](https://semver.org/), so it's save to use for early experimentation. Just beware there will be breaking changes. Best to watch this repository's releases for updates. | ||
## Usage | ||
### `verify(rawBody, signature, keyId, options)` | ||
### Verify a request | ||
```js | ||
import { verifyRequestByKeyId } from "@copilot-extensions/preview-sdk"; | ||
const payloadIsVerified = await verifyRequestByKeyId( | ||
request.body, | ||
signature, | ||
key, | ||
{ | ||
token: process.env.GITHUB_TOKEN, | ||
} | ||
); | ||
// true or false | ||
``` | ||
## API | ||
### `async verifyRequestByKeyId(rawBody, signature, keyId, options)` | ||
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. | ||
The `options` argument is optional. It can contain a `token` to authenticate the request to GitHub's API, or a custom `request` instance to use for the request. | ||
```js | ||
import { verifyRequestByKeyId } from "@copilot-extensions/preview-sdk"; | ||
const payloadIsVerified = await verifyRequestByKeyId( | ||
request.body, | ||
signature, | ||
key | ||
); | ||
// with token | ||
await verifyRequestByKeyId(request.body, signature, key, { token: "ghp_1234" }); | ||
// with custom octokit request instance | ||
await verifyRequestByKeyId(request.body, signature, key, { request }); | ||
``` | ||
### `async fetchVerificationKeys(options)` | ||
Fetches public keys for verifying copilot extension requests [from GitHub's API](https://api.github.com/meta/public_keys/copilot_api) | ||
and returns them as an array. The request can be made without authentication, with a token, or with a custom [octokit request](https://github.com/octokit/request.js) instance. | ||
```js | ||
import { fetchVerificationKeys } from "@copilot-extensions/preview-sdk"; | ||
// fetch without authentication | ||
const [current] = await fetchVerificationKeys(); | ||
// with token | ||
const [current] = await fetchVerificationKeys({ token: "ghp_1234" }); | ||
// with custom octokit request instance | ||
const [current] = await fetchVerificationKeys({ request });) | ||
``` | ||
### `async verifyRequestPayload(rawBody, signature, keyId)` | ||
Verify the request payload using the provided signature and key. Note that the raw body as received by GitHub must be passed, before any parsing. | ||
```js | ||
import { verify } from "@copilot-extensions/preview-sdk"; | ||
const payloadIsVerified = await verify(request.body, signature, keyId, { | ||
token, | ||
}); | ||
const payloadIsVerified = await verifyRequestPayload( | ||
request.body, | ||
signature, | ||
key | ||
); | ||
// true or false | ||
``` | ||
## 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: | ||
[dreamcode.md](./dreamcode.md) | ||
## Contributing | ||
@@ -22,0 +90,0 @@ |
@@ -7,3 +7,7 @@ import { test } from "node:test"; | ||
import { verify } from "../index.js"; | ||
import { | ||
fetchVerificationKeys, | ||
verifyRequest, | ||
verifyRequestByKeyId, | ||
} from "../index.js"; | ||
@@ -24,6 +28,6 @@ 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"}`; | ||
test("smoke", (t) => { | ||
assert.equal(typeof verify, "function"); | ||
assert.equal(typeof verifyRequestByKeyId, "function"); | ||
}); | ||
test("minimal usage", async (t) => { | ||
test("verifyRequestByKeyId()", async (t) => { | ||
const mockAgent = new MockAgent(); | ||
@@ -59,3 +63,3 @@ function fetchMock(url, opts) { | ||
}, | ||
}, | ||
} | ||
); | ||
@@ -66,3 +70,3 @@ const testRequest = defaultRequest.defaults({ | ||
const result = await verify(RAW_BODY, SIGNATURE, KEY_ID, { | ||
const result = await verifyRequestByKeyId(RAW_BODY, SIGNATURE, KEY_ID, { | ||
request: testRequest, | ||
@@ -74,4 +78,4 @@ }); | ||
test("invalid arguments", (t) => { | ||
assert.rejects(verify(RAW_BODY, SIGNATURE), { | ||
test("verifyRequestByKeyId() - invalid arguments", (t) => { | ||
assert.rejects(verifyRequestByKeyId(RAW_BODY, SIGNATURE), { | ||
name: "Error", | ||
@@ -81,3 +85,3 @@ message: "[@copilot-extensions/preview-sdk] Invalid keyId", | ||
assert.rejects(verify("", SIGNATURE, KEY_ID), { | ||
assert.rejects(verifyRequestByKeyId("", SIGNATURE, KEY_ID), { | ||
name: "Error", | ||
@@ -87,3 +91,3 @@ message: "[@copilot-extensions/preview-sdk] Invalid payload", | ||
assert.rejects(verify(1, SIGNATURE, KEY_ID), { | ||
assert.rejects(verifyRequestByKeyId(1, SIGNATURE, KEY_ID), { | ||
name: "Error", | ||
@@ -93,3 +97,3 @@ message: "[@copilot-extensions/preview-sdk] Invalid payload", | ||
assert.rejects(verify(undefined, SIGNATURE, KEY_ID), { | ||
assert.rejects(verifyRequestByKeyId(undefined, SIGNATURE, KEY_ID), { | ||
name: "Error", | ||
@@ -99,3 +103,3 @@ message: "[@copilot-extensions/preview-sdk] Invalid payload", | ||
assert.rejects(verify(RAW_BODY, "", KEY_ID), { | ||
assert.rejects(verifyRequestByKeyId(RAW_BODY, "", KEY_ID), { | ||
name: "Error", | ||
@@ -105,3 +109,3 @@ message: "[@copilot-extensions/preview-sdk] Invalid signature", | ||
assert.rejects(verify(RAW_BODY, 1, KEY_ID), { | ||
assert.rejects(verifyRequestByKeyId(RAW_BODY, 1, KEY_ID), { | ||
name: "Error", | ||
@@ -111,3 +115,3 @@ message: "[@copilot-extensions/preview-sdk] Invalid signature", | ||
assert.rejects(verify(RAW_BODY, undefined, KEY_ID), { | ||
assert.rejects(verifyRequestByKeyId(RAW_BODY, undefined, KEY_ID), { | ||
name: "Error", | ||
@@ -117,3 +121,3 @@ message: "[@copilot-extensions/preview-sdk] Invalid signature", | ||
assert.rejects(verify(RAW_BODY, SIGNATURE, ""), { | ||
assert.rejects(verifyRequestByKeyId(RAW_BODY, SIGNATURE, ""), { | ||
name: "Error", | ||
@@ -123,3 +127,3 @@ message: "[@copilot-extensions/preview-sdk] Invalid keyId", | ||
assert.rejects(verify(RAW_BODY, SIGNATURE, 1), { | ||
assert.rejects(verifyRequestByKeyId(RAW_BODY, SIGNATURE, 1), { | ||
name: "Error", | ||
@@ -129,3 +133,3 @@ message: "[@copilot-extensions/preview-sdk] Invalid keyId", | ||
assert.rejects(verify(RAW_BODY, SIGNATURE, undefined), { | ||
assert.rejects(verifyRequestByKeyId(RAW_BODY, SIGNATURE, undefined), { | ||
name: "Error", | ||
@@ -135,1 +139,62 @@ message: "[@copilot-extensions/preview-sdk] Invalid keyId", | ||
}); | ||
test("verifyRequest() - valid", async (t) => { | ||
const result = await verifyRequest(RAW_BODY, SIGNATURE, CURRENT_PUBLIC_KEY); | ||
assert.deepEqual(result, true); | ||
}); | ||
test("verifyRequest() - invalid", async (t) => { | ||
const result = await verifyRequest(RAW_BODY, SIGNATURE, "invalid-key"); | ||
assert.deepEqual(result, false); | ||
}); | ||
test("fetchVerificationKeys()", async (t) => { | ||
const mockAgent = new MockAgent(); | ||
function fetchMock(url, opts) { | ||
opts ||= {}; | ||
opts.dispatcher = mockAgent; | ||
return fetch(url, opts); | ||
} | ||
const publicKeys = [ | ||
{ | ||
key: "<key 1>", | ||
key_identifier: "<key-id 1>", | ||
is_current: true, | ||
}, | ||
{ | ||
key: "<key 2>", | ||
key_identifier: "<key-id 2>", | ||
is_current: true, | ||
}, | ||
]; | ||
mockAgent.disableNetConnect(); | ||
const mockPool = mockAgent.get("https://api.github.com"); | ||
mockPool | ||
.intercept({ | ||
method: "get", | ||
path: `/meta/public_keys/copilot_api`, | ||
}) | ||
.reply( | ||
200, | ||
{ | ||
public_keys: publicKeys, | ||
}, | ||
{ | ||
headers: { | ||
"content-type": "application/json", | ||
"x-request-id": "<request-id>", | ||
}, | ||
} | ||
); | ||
const testRequest = defaultRequest.defaults({ | ||
request: { fetch: fetchMock }, | ||
}); | ||
const result = await fetchVerificationKeys({ | ||
request: testRequest, | ||
}); | ||
assert.deepEqual(result, publicKeys); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
28301
12
316
95
2