Add Cloudflare signing key contract
This commit is contained in:
Generated
+1593
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@elydora/cloudflare-worker",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"check": "tsc --noEmit -p tsconfig.json",
|
||||
"test": "tsc -p tsconfig.test.json --outDir dist && node --test dist/tests/*.test.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "4.20260508.1",
|
||||
"@types/node": "24.12.3",
|
||||
"typescript": "6.0.3",
|
||||
"wrangler": "4.90.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface Env {
|
||||
ELY_KV: KVNamespace;
|
||||
ELY_ENVIRONMENT: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Env } from "./bindings.js";
|
||||
|
||||
export default {
|
||||
fetch(_request: Request, _env: Env): Response {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
const KEY_ID_PATTERN = /^[a-z0-9._-]{3,128}$/;
|
||||
const PUBLIC_KEY_PATTERN = /^[a-f0-9]{64}$/;
|
||||
const KV_NAMESPACE_PREFIX = "ely";
|
||||
const PUBLIC_SIGNING_KEYS_NAMESPACE = "public_signing_keys";
|
||||
|
||||
export interface PublicSigningKey {
|
||||
key_id: string;
|
||||
public_key: string;
|
||||
}
|
||||
|
||||
export interface PublicSigningKeysDocument {
|
||||
version: 1;
|
||||
keys: PublicSigningKey[];
|
||||
}
|
||||
|
||||
export class SigningKeysSchemaError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "SigningKeysSchemaError";
|
||||
}
|
||||
}
|
||||
|
||||
export function publicSigningKeysKvKey(environment: string): string {
|
||||
const normalizedEnvironment = normalizedEnvironmentName(environment);
|
||||
return `${KV_NAMESPACE_PREFIX}:${normalizedEnvironment}:${PUBLIC_SIGNING_KEYS_NAMESPACE}`;
|
||||
}
|
||||
|
||||
export function parsePublicSigningKeysDocument(value: string): PublicSigningKeysDocument {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch {
|
||||
throw new SigningKeysSchemaError("public signing keys document must be valid JSON");
|
||||
}
|
||||
|
||||
if (!isRecord(parsed)) {
|
||||
throw new SigningKeysSchemaError("public signing keys document must be an object");
|
||||
}
|
||||
|
||||
if (parsed.version !== 1) {
|
||||
throw new SigningKeysSchemaError("public signing keys document version must be 1");
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed.keys) || parsed.keys.length === 0) {
|
||||
throw new SigningKeysSchemaError("public signing keys document must contain keys");
|
||||
}
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
keys: parsePublicSigningKeys(parsed.keys),
|
||||
};
|
||||
}
|
||||
|
||||
function parsePublicSigningKeys(values: unknown[]): PublicSigningKey[] {
|
||||
const keys: PublicSigningKey[] = [];
|
||||
const seenKeyIds = new Set<string>();
|
||||
|
||||
for (const value of values) {
|
||||
const key = parsePublicSigningKey(value);
|
||||
if (seenKeyIds.has(key.key_id)) {
|
||||
throw new SigningKeysSchemaError(`duplicate public signing key id: ${key.key_id}`);
|
||||
}
|
||||
seenKeyIds.add(key.key_id);
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
function parsePublicSigningKey(value: unknown): PublicSigningKey {
|
||||
if (!isRecord(value)) {
|
||||
throw new SigningKeysSchemaError("public signing key must be an object");
|
||||
}
|
||||
|
||||
const keyId = stringField(value, "key_id");
|
||||
const publicKey = stringField(value, "public_key").toLowerCase();
|
||||
|
||||
if (!KEY_ID_PATTERN.test(keyId)) {
|
||||
throw new SigningKeysSchemaError(`invalid public signing key id: ${keyId}`);
|
||||
}
|
||||
if (!PUBLIC_KEY_PATTERN.test(publicKey)) {
|
||||
throw new SigningKeysSchemaError(`invalid public signing key value for ${keyId}`);
|
||||
}
|
||||
|
||||
return { key_id: keyId, public_key: publicKey };
|
||||
}
|
||||
|
||||
function normalizedEnvironmentName(value: string): string {
|
||||
const environment = value.trim();
|
||||
if (!/^[a-z0-9._-]{3,64}$/.test(environment)) {
|
||||
throw new SigningKeysSchemaError(`invalid environment name: ${value}`);
|
||||
}
|
||||
return environment;
|
||||
}
|
||||
|
||||
function stringField(value: Record<string, unknown>, field: string): string {
|
||||
const fieldValue = value[field];
|
||||
if (typeof fieldValue !== "string" || fieldValue.trim() === "") {
|
||||
throw new SigningKeysSchemaError(`${field} must be a non-empty string`);
|
||||
}
|
||||
return fieldValue.trim();
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import {
|
||||
SigningKeysSchemaError,
|
||||
parsePublicSigningKeysDocument,
|
||||
publicSigningKeysKvKey,
|
||||
} from "../src/signing_keys.js";
|
||||
|
||||
const PUBLIC_KEY = "a".repeat(64);
|
||||
|
||||
describe("public signing keys", () => {
|
||||
it("builds an environment-prefixed KV key", () => {
|
||||
assert.equal(publicSigningKeysKvKey("production"), "ely:production:public_signing_keys");
|
||||
});
|
||||
|
||||
it("parses a valid key document", () => {
|
||||
const document = parsePublicSigningKeysDocument(
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
keys: [{ key_id: "elydora-alpha-plugins", public_key: PUBLIC_KEY.toUpperCase() }],
|
||||
}),
|
||||
);
|
||||
|
||||
assert.deepEqual(document, {
|
||||
version: 1,
|
||||
keys: [{ key_id: "elydora-alpha-plugins", public_key: PUBLIC_KEY }],
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects duplicate key ids", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
parsePublicSigningKeysDocument(
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
keys: [
|
||||
{ key_id: "elydora-alpha-plugins", public_key: PUBLIC_KEY },
|
||||
{ key_id: "elydora-alpha-plugins", public_key: "b".repeat(64) },
|
||||
],
|
||||
}),
|
||||
),
|
||||
SigningKeysSchemaError,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects malformed public keys", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
parsePublicSigningKeysDocument(
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
keys: [{ key_id: "elydora-alpha-plugins", public_key: "abcd" }],
|
||||
}),
|
||||
),
|
||||
SigningKeysSchemaError,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["@cloudflare/workers-types"],
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": false,
|
||||
"verbatimModuleSyntax": true,
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": false,
|
||||
"verbatimModuleSyntax": true,
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["src/signing_keys.ts", "tests/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
name = "ely-browser-cloud"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2026-05-08"
|
||||
|
||||
[vars]
|
||||
ELY_ENVIRONMENT = "local"
|
||||
Reference in New Issue
Block a user