diff --git a/src/token-declaration-registry.ts b/src/token-declaration-registry.ts new file mode 100644 index 0000000..9ff097f --- /dev/null +++ b/src/token-declaration-registry.ts @@ -0,0 +1,39 @@ +import type { TokenDeclaration } from "./type/token-declaration.js"; + +export type TokenDeclarationRegistry = { + registerDeclaration: ( + definingOwner: string, + definingRepo: string, + name: string, + declaration: TokenDeclaration, + ) => void; + + findDeclarationForRequester: ( + requestingOwner: string, + requestingRepo: string, + reference: string, + ) => [declaration: TokenDeclaration | undefined, isRegistered: boolean]; +}; + +export function createTokenDeclarationRegistry(): TokenDeclarationRegistry { + const declarations = new Map(); + + return { + registerDeclaration(definingOwner, definingRepo, name, declaration) { + declarations.set(`${definingOwner}/${definingRepo}.${name}`, declaration); + }, + + findDeclarationForRequester(requestingOwner, requestingRepo, reference) { + const declaration = declarations.get(reference); + + if (!declaration) return [undefined, false]; + if (declaration.shared) return [declaration, true]; + + const requiredPrefix = `${requestingOwner}/${requestingRepo}.`; + + return reference.startsWith(requiredPrefix) + ? [declaration, true] + : [undefined, true]; + }, + }; +} diff --git a/src/type/consumer-config.ts b/src/type/consumer-config.ts index ddf46e8..10eaad4 100644 --- a/src/type/consumer-config.ts +++ b/src/type/consumer-config.ts @@ -1,21 +1,12 @@ -import type { InstallationPermissions } from "./github-api.js"; import type { GitHubOrganizationSecretTypes, GitHubRepositorySecretTypes, } from "./github-secret-type.js"; +import type { TokenDeclaration } from "./token-declaration.js"; export type PartialConsumerConfig = { $schema: string; - tokens: Record< - string, - { - shared: boolean; - as?: string; - owner?: string; - repositories: string[]; - permissions: InstallationPermissions; - } - >; + tokens: Record; provision: { secrets: Record< string, diff --git a/src/type/token-declaration.ts b/src/type/token-declaration.ts new file mode 100644 index 0000000..27f50d6 --- /dev/null +++ b/src/type/token-declaration.ts @@ -0,0 +1,9 @@ +import type { InstallationPermissions } from "./github-api.js"; + +export type TokenDeclaration = { + shared: boolean; + as?: string; + owner?: string; + repositories: string[]; + permissions: InstallationPermissions; +}; diff --git a/test/suite/unit/token-declaration-registry.spec.ts b/test/suite/unit/token-declaration-registry.spec.ts new file mode 100644 index 0000000..1562199 --- /dev/null +++ b/test/suite/unit/token-declaration-registry.spec.ts @@ -0,0 +1,111 @@ +import { expect, it } from "vitest"; +import { createTokenDeclarationRegistry } from "../../../src/token-declaration-registry.js"; +import type { TokenDeclaration } from "../../../src/type/token-declaration.js"; + +it("finds local token declarations", () => { + const declarationA: TokenDeclaration = { + shared: false, + repositories: ["owner-x/repo-x"], + permissions: { metadata: "read" }, + }; + const declarationB: TokenDeclaration = { + shared: false, + repositories: ["owner-y/repo-y"], + permissions: { contents: "write" }, + }; + + const registry = createTokenDeclarationRegistry(); + registry.registerDeclaration("owner-a", "repo-a", "token-a", declarationA); + registry.registerDeclaration("owner-b", "repo-b", "token-b", declarationB); + + expect( + registry.findDeclarationForRequester( + "owner-a", + "repo-a", + "owner-a/repo-a.token-a", + ), + ).toEqual([declarationA, true]); + expect( + registry.findDeclarationForRequester( + "owner-b", + "repo-b", + "owner-b/repo-b.token-b", + ), + ).toEqual([declarationB, true]); +}); + +it("finds shared token declarations", () => { + const declarationA: TokenDeclaration = { + shared: true, + repositories: ["owner-x/repo-x"], + permissions: { metadata: "read" }, + }; + const declarationB: TokenDeclaration = { + shared: true, + repositories: ["owner-y/repo-y"], + permissions: { contents: "write" }, + }; + + const registry = createTokenDeclarationRegistry(); + registry.registerDeclaration("owner-a", "repo-a", "token-a", declarationA); + registry.registerDeclaration("owner-b", "repo-b", "token-b", declarationB); + + expect( + registry.findDeclarationForRequester( + "owner-b", + "repo-b", + "owner-a/repo-a.token-a", + ), + ).toEqual([declarationA, true]); + expect( + registry.findDeclarationForRequester( + "owner-a", + "repo-a", + "owner-b/repo-b.token-b", + ), + ).toEqual([declarationB, true]); +}); + +it("doesn't find unshared tokens in other repositories", () => { + const declarationA: TokenDeclaration = { + shared: false, + repositories: ["owner-x/repo-x"], + permissions: { metadata: "read" }, + }; + const declarationB: TokenDeclaration = { + shared: false, + repositories: ["owner-y/repo-y"], + permissions: { contents: "write" }, + }; + + const registry = createTokenDeclarationRegistry(); + registry.registerDeclaration("owner-a", "repo-a", "token-a", declarationA); + registry.registerDeclaration("owner-b", "repo-b", "token-b", declarationB); + + expect( + registry.findDeclarationForRequester( + "owner-b", + "repo-b", + "owner-a/repo-a.token-a", + ), + ).toEqual([undefined, true]); + expect( + registry.findDeclarationForRequester( + "owner-a", + "repo-a", + "owner-b/repo-b.token-b", + ), + ).toEqual([undefined, true]); +}); + +it("doesn't find unregistered tokens", () => { + const registry = createTokenDeclarationRegistry(); + + expect( + registry.findDeclarationForRequester( + "owner-a", + "repo-a", + "owner-a/repo-a.token-a", + ), + ).toEqual([undefined, false]); +});