diff --git a/apps/client-web/.gitignore b/apps/client-web/.gitignore index a547bf3..c6d388a 100644 --- a/apps/client-web/.gitignore +++ b/apps/client-web/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +public/artifacts/* diff --git a/apps/client-web/index.html b/apps/client-web/index.html index 1975e38..1f85e1a 100644 --- a/apps/client-web/index.html +++ b/apps/client-web/index.html @@ -5,7 +5,7 @@ PARCNET Client - +
diff --git a/apps/client-web/package.json b/apps/client-web/package.json index 72fc2c0..57c8ec4 100644 --- a/apps/client-web/package.json +++ b/apps/client-web/package.json @@ -19,8 +19,7 @@ "eventemitter3": "^5.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "vite-plugin-node-polyfills": "^0.22.0", - "zod": "^3.23.8" + "vite-plugin-node-polyfills": "^0.22.0" }, "devDependencies": { "@parcnet/eslint-config": "workspace:*", @@ -33,6 +32,6 @@ "postcss": "^8.4.41", "tailwindcss": "^3.4.10", "typescript": "^5.5", - "vite": "^5.4.1" + "vite": "^5.4.4" } } diff --git a/apps/client-web/src/App.tsx b/apps/client-web/src/App.tsx index c428e2a..b92d619 100644 --- a/apps/client-web/src/App.tsx +++ b/apps/client-web/src/App.tsx @@ -1,6 +1,16 @@ import { listen } from "@parcnet/client-helpers/connection/iframe"; import { Zapp } from "@parcnet/client-rpc"; -import { Dispatch, ReactNode, useEffect, useReducer } from "react"; +import { EntriesSchema, PODSchema, proofRequest } from "@parcnet/podspec"; +import { gpcProve } from "@pcd/gpc"; +import { POD, POD_INT_MAX, POD_INT_MIN } from "@pcd/pod"; +import { + Dispatch, + Fragment, + ReactNode, + useEffect, + useReducer, + useState +} from "react"; import { ParcnetClientProcessor } from "./client/client"; import { PODCollection } from "./client/pod_collection"; import { loadPODsFromStorage, savePODsToStorage } from "./client/utils"; @@ -67,18 +77,173 @@ function App() { state.authorized && state.zapp && state.proofInProgress && ( - + )} ); } +function Reveal({ children }: { children: ReactNode }) { + const [isRevealed, setIsRevealed] = useState(false); + return ( + <> + + {isRevealed &&
{children}
} + + ); +} + +function ProvePODInfo({ + name, + schema, + pods, + selectedPOD, + onChange +}: { + name: string; + schema: PODSchema; + pods: POD[]; + selectedPOD: POD | undefined; + onChange: (pod: POD | undefined) => void; +}): ReactNode { + const revealedEntries = Object.entries(schema.entries) + .map(([name, entry]) => { + if (entry.type === "optional") { + entry = entry.innerType; + } + return [name, entry] as const; + }) + .filter(([_, entry]) => entry.isRevealed); + + const selectedPODEntries = selectedPOD?.content.asEntries(); + + const entriesWithConstraints = Object.entries(schema.entries) + .map(([name, entry]) => { + if (entry.type === "optional") { + entry = entry.innerType; + } + return [name, entry] as const; + }) + .filter( + ([_, entry]) => + !!entry.isMemberOf || + !!entry.isNotMemberOf || + !!(entry.type === "int" && entry.inRange) + ); + + return ( +
+
+
{name}
+ +
+
+ {revealedEntries.length > 0 && "Revealed entries:"} +
+
+ {revealedEntries.map(([entryName, _]) => { + return ( + +
{entryName}
+
+ {selectedPODEntries?.[entryName].value.toString() ?? "-"} +
+
+ ); + })} +
+ {entriesWithConstraints.length > 0 && ( +
+
Proven constraints:
+ {entriesWithConstraints.map(([entryName, entry]) => { + return ( +
+ {entry.isMemberOf && ( +
+ {entryName} is member + of list:{" "} + +
+ {entry.isMemberOf + .map((v) => v.value.toString()) + .join(", ")} +
+
+
+ )} + {entry.isNotMemberOf && ( +
+ {entryName} is not + member of list:{" "} + +
+ {entry.isNotMemberOf + .map((v) => v.value.toString()) + .join(", ")} +
+
+
+ )} + {entry.type === "int" && entry.inRange && ( +
+ {entryName} is +
+ {entry.inRange.min === POD_INT_MIN && + entry.inRange.max === POD_INT_MAX && + "any number"} + {entry.inRange.min !== POD_INT_MIN && + entry.inRange.max === POD_INT_MAX && + `greater than ${entry.inRange.min}`} + {entry.inRange.min === POD_INT_MIN && + entry.inRange.max !== POD_INT_MAX && + `less than ${entry.inRange.max}`} + {entry.inRange.min !== POD_INT_MIN && + entry.inRange.max !== POD_INT_MAX && + `between ${entry.inRange.min} and ${entry.inRange.max}`} +
+
+ )} +
+ ); + })} +
+ )} +
+ ); +} + function Prove({ - proveOperation + proveOperation, + dispatch }: { proveOperation: NonNullable; + dispatch: Dispatch; }): ReactNode { + const canProve = + Object.keys(proveOperation.selectedPods).length === + Object.keys(proveOperation.proofRequest.pods).length && + Object.values(proveOperation.selectedPods).every((maybePod) => !!maybePod); + /** * show exactly which fields are revealed and which are not, literally show * the whole POD and which entries are revealed @@ -86,25 +251,94 @@ function Prove({ */ return (
-
    - {Object.entries(proveOperation.pods).map(([key, pods]) => { - return ( -
    -

    {key}

    -
    - {pods.map((pod) => { - return ( -
    - {pod.contentID.toString()} - {pod.serialize()} -
    - ); - })} -
    -
    - ); - })} -
+
+

This proof will reveal the following data from your PODs:

+ {Object.entries(proveOperation.proofRequest.pods).map( + ([name, schema]) => { + return ( + { + dispatch({ + type: "set-proof-in-progress", + ...proveOperation, + selectedPods: { + ...proveOperation.selectedPods, + ...{ [name]: pod } + } + }); + }} + /> + ); + } + )} +
+ +
+
); } diff --git a/apps/client-web/src/client/gpc.ts b/apps/client-web/src/client/gpc.ts index 2c940e8..50344dc 100644 --- a/apps/client-web/src/client/gpc.ts +++ b/apps/client-web/src/client/gpc.ts @@ -1,14 +1,9 @@ import { ConnectorAdvice } from "@parcnet/client-helpers"; -import { - ParcnetGPCRPC, - ParcnetRPCSchema, - ProveResult -} from "@parcnet/client-rpc"; +import { ParcnetGPCRPC, ProveResult } from "@parcnet/client-rpc"; import { PodspecProofRequest, proofRequest } from "@parcnet/podspec"; import { Dispatch } from "react"; import { ClientAction } from "../state"; import { PODCollection } from "./pod_collection"; -import { validateInput } from "./utils"; export class ParcnetGPCProcessor implements ParcnetGPCRPC { public constructor( @@ -17,7 +12,6 @@ export class ParcnetGPCProcessor implements ParcnetGPCRPC { private readonly advice: ConnectorAdvice ) {} - @validateInput(ParcnetRPCSchema.shape.gpc.shape.canProve) public async canProve(request: PodspecProofRequest): Promise { const prs = proofRequest(request); @@ -31,7 +25,6 @@ export class ParcnetGPCProcessor implements ParcnetGPCRPC { return true; } - @validateInput(ParcnetRPCSchema.shape.gpc.shape.prove) public async prove(request: PodspecProofRequest): Promise { const prs = proofRequest(request); diff --git a/apps/client-web/src/client/pod.ts b/apps/client-web/src/client/pod.ts index f8b25d1..5d4e338 100644 --- a/apps/client-web/src/client/pod.ts +++ b/apps/client-web/src/client/pod.ts @@ -1,10 +1,8 @@ -import { ParcnetPODRPC, ParcnetRPCSchema } from "@parcnet/client-rpc"; -import * as p from "@parcnet/podspec"; +import { ParcnetPODRPC } from "@parcnet/client-rpc"; import { EntriesSchema, PODSchema } from "@parcnet/podspec"; import { POD } from "@pcd/pod"; import { PODCollection } from "./pod_collection.js"; import { QuerySubscriptions } from "./query_subscriptions.js"; -import { validateInput } from "./utils.js"; export class ParcnetPODProcessor implements ParcnetPODRPC { public constructor( @@ -12,28 +10,27 @@ export class ParcnetPODProcessor implements ParcnetPODRPC { private readonly subscriptions: QuerySubscriptions ) {} - @validateInput(ParcnetRPCSchema.shape.pod.shape.query) - public async query(query: PODSchema): Promise { - return this.pods.query(p.pod(query)).map((pod) => pod.serialize()); + public async query( + query: PODSchema + ): Promise { + return this.pods.query(query).map((pod) => pod.serialize()); } - @validateInput(ParcnetRPCSchema.shape.pod.shape.insert) public async insert(serializedPod: string): Promise { const pod = POD.deserialize(serializedPod); this.pods.insert(pod); } - @validateInput(ParcnetRPCSchema.shape.pod.shape.delete) public async delete(signature: string): Promise { this.pods.delete(signature); } - @validateInput(ParcnetRPCSchema.shape.pod.shape.subscribe) - public async subscribe(query: PODSchema): Promise { - return this.subscriptions.subscribe(p.pod(query)); + public async subscribe( + query: PODSchema + ): Promise { + return this.subscriptions.subscribe(query); } - @validateInput(ParcnetRPCSchema.shape.pod.shape.unsubscribe) public async unsubscribe(subscriptionId: string): Promise { this.subscriptions.unsubscribe(subscriptionId); } diff --git a/apps/client-web/src/client/pod_collection.ts b/apps/client-web/src/client/pod_collection.ts index 37c112d..3fa4adf 100644 --- a/apps/client-web/src/client/pod_collection.ts +++ b/apps/client-web/src/client/pod_collection.ts @@ -2,8 +2,6 @@ import * as p from "@parcnet/podspec"; import { POD } from "@pcd/pod"; import { EventEmitter } from "eventemitter3"; -type PODQuery = ReturnType; - export interface PODCollectionUpdate { type: "insert" | "delete"; affectedPOD: POD; @@ -38,9 +36,9 @@ export class PODCollection { } } - public query(query: PODQuery): POD[] { + public query(query: p.PODSchema): POD[] { console.log(query); - return query.query(this.pods).matches; + return p.pod(query).query(this.pods).matches; } public onUpdate(listener: (update: PODCollectionUpdate) => void): void { diff --git a/apps/client-web/src/client/query_subscriptions.ts b/apps/client-web/src/client/query_subscriptions.ts index a8d1728..88690e3 100644 --- a/apps/client-web/src/client/query_subscriptions.ts +++ b/apps/client-web/src/client/query_subscriptions.ts @@ -1,5 +1,6 @@ import { SubscriptionUpdateResult } from "@parcnet/client-rpc"; -import { EntriesSchema, PodSpec } from "@parcnet/podspec"; +import * as p from "@parcnet/podspec"; +import { EntriesSchema, PODSchema, PodSpec } from "@parcnet/podspec"; import { EventEmitter } from "eventemitter3"; import { PODCollection } from "./pod_collection.js"; @@ -57,9 +58,14 @@ export class QuerySubscriptions { }); } - public async subscribe(query: PodSpec): Promise { + public async subscribe( + query: PODSchema + ): Promise { const subscriptionId = (this.nextSubscriptionId++).toString(); - this.subscriptions.set(subscriptionId, { query, serial: 0 }); + this.subscriptions.set(subscriptionId, { + query: p.pod(query) as PodSpec, + serial: 0 + }); return subscriptionId; } diff --git a/apps/client-web/src/client/utils.ts b/apps/client-web/src/client/utils.ts index abaacc4..00e5729 100644 --- a/apps/client-web/src/client/utils.ts +++ b/apps/client-web/src/client/utils.ts @@ -1,24 +1,4 @@ import { POD } from "@pcd/pod"; -import { z } from "zod"; - -export function validateInput( - parser: z.ZodSchema -) { - return function actualDecorator( - originalMethod: (this: This, ...args: Args) => Return, - context: ClassMethodDecoratorContext - ): (this: This, ...args: Args) => Return { - function replacementMethod(this: This, ...args: Args): Return { - const input = parser.safeParse(args); - if (!input.success) { - throw new Error(`Invalid arguments for ${context.name.toString()}`); - } - return originalMethod.call(this, ...input.data); - } - - return replacementMethod; - }; -} export function loadPODsFromStorage(): POD[] { let pods: POD[] = []; diff --git a/apps/client-web/src/state.ts b/apps/client-web/src/state.ts index 9e7169c..0e2daaa 100644 --- a/apps/client-web/src/state.ts +++ b/apps/client-web/src/state.ts @@ -43,6 +43,9 @@ export type ClientAction = proofRequest: PodspecProofRequest; proving: boolean; resolve?: (result: ProveResult) => void; + } + | { + type: "clear-proof-in-progress"; }; export function clientReducer(state: ClientState, action: ClientAction) { @@ -66,5 +69,10 @@ export function clientReducer(state: ClientState, action: ClientAction) { resolve: action.resolve } }; + case "clear-proof-in-progress": + return { + ...state, + proofInProgress: undefined + }; } } diff --git a/apps/client-web/vite.config.ts b/apps/client-web/vite.config.ts index 87c0b82..88235f0 100644 --- a/apps/client-web/vite.config.ts +++ b/apps/client-web/vite.config.ts @@ -9,5 +9,8 @@ export default defineConfig({ nodePolyfills({ include: ["assert", "buffer"] }) - ] + ], + esbuild: { + target: "es2020" + } }); diff --git a/examples/test-app/src/apis/GPC.tsx b/examples/test-app/src/apis/GPC.tsx index 8db1d95..4dc655e 100644 --- a/examples/test-app/src/apis/GPC.tsx +++ b/examples/test-app/src/apis/GPC.tsx @@ -1,4 +1,5 @@ import { PodspecProofRequest } from "@parcnet/podspec"; +import JSONBig from "json-bigint"; import { ReactNode, useState } from "react"; import { ProveResult } from "../../../../packages/client-rpc/src"; import { TryIt } from "../components/TryIt"; @@ -8,7 +9,11 @@ const request: PodspecProofRequest = { pods: { pod1: { entries: { - wis: { type: "int", inRange: { min: BigInt(5), max: BigInt(1000) } }, + wis: { + type: "int", + inRange: { min: BigInt(5), max: BigInt(1000) }, + isRevealed: true + }, str: { type: "int", inRange: { min: BigInt(5), max: BigInt(1000) } } } }, @@ -16,7 +21,8 @@ const request: PodspecProofRequest = { entries: { test: { type: "string", - isMemberOf: [{ type: "string", value: "secret" }] + isMemberOf: [{ type: "string", value: "secret" }], + isRevealed: true } } } @@ -41,7 +47,11 @@ const request: PodspecProofRequest = { pods: { pod1: { entries: { - wis: { type: "int", inRange: { min: BigInt(5), max: BigInt(1000) } }, + wis: { + type: "int", + inRange: { min: BigInt(5), max: BigInt(1000) }, + isRevealed: true + }, str: { type: "int", inRange: { min: BigInt(5), max: BigInt(1000) } } } }, @@ -49,15 +59,17 @@ const request: PodspecProofRequest = { entries: { test: { type: "string", - isMemberOf: [{ type: "string", value: "secret" }] + isMemberOf: [{ type: "string", value: "secret" }], + isRevealed: true } } } } }; - + +const gpcProof = await z.gpc.prove(request); + `} - const gpcProof = await z.gpc.prove(request);

{proof && (
-              {JSON.stringify(proof, null, 2)}
+              {JSONBig.stringify(proof, null, 2)}
             
)} diff --git a/packages/app-connector/src/api_wrapper.ts b/packages/app-connector/src/api_wrapper.ts index 95ebb4f..6763069 100644 --- a/packages/app-connector/src/api_wrapper.ts +++ b/packages/app-connector/src/api_wrapper.ts @@ -98,15 +98,17 @@ class ParcnetGPCWrapper { // In a world with POD2, we would use new POD2 types rather than GPCPCD. // The existing args system and GPC wrapper works well, so we can use that. - async prove(args: PodspecProofRequest): Promise { + async prove

>( + args: PodspecProofRequest

+ ): Promise { const result = await this.#api.gpc.prove(args); return result; } - async verify( + async verify

>( proof: GPCProof, revealedClaims: GPCRevealedClaims, - proofRequest: PodspecProofRequest + proofRequest: PodspecProofRequest

): Promise { return this.#api.gpc.verify(proof, revealedClaims, proofRequest); } diff --git a/packages/client-helpers/src/connection/iframe.ts b/packages/client-helpers/src/connection/iframe.ts index f55b07c..f711f7f 100644 --- a/packages/client-helpers/src/connection/iframe.ts +++ b/packages/client-helpers/src/connection/iframe.ts @@ -3,6 +3,8 @@ import { InitializationMessageSchema, InitializationMessageType, ParcnetRPC, + ParcnetRPCMethodName, + ParcnetRPCSchema, RPCMessage, RPCMessageSchema, RPCMessageType, @@ -58,6 +60,32 @@ export class AdviceChannel implements ConnectorAdvice { } } +function getSchema(method: ParcnetRPCMethodName) { + switch (method) { + case "gpc.canProve": + return ParcnetRPCSchema.shape.gpc.shape.canProve; + case "gpc.prove": + return ParcnetRPCSchema.shape.gpc.shape.prove; + case "gpc.verify": + return ParcnetRPCSchema.shape.gpc.shape.verify; + case "identity.getSemaphoreV3Commitment": + return ParcnetRPCSchema.shape.identity.shape.getSemaphoreV3Commitment; + case "pod.query": + return ParcnetRPCSchema.shape.pod.shape.query; + case "pod.insert": + return ParcnetRPCSchema.shape.pod.shape.insert; + case "pod.delete": + return ParcnetRPCSchema.shape.pod.shape.delete; + case "pod.subscribe": + return ParcnetRPCSchema.shape.pod.shape.subscribe; + case "pod.unsubscribe": + return ParcnetRPCSchema.shape.pod.shape.unsubscribe; + default: + const unknownMethod: never = method; + throw new Error(`Unknown method: ${unknownMethod as string}`); + } +} + async function handleMessage( rpc: ParcnetRPC, port: MessagePort, @@ -71,11 +99,16 @@ async function handleMessage( } const object = deepGet(rpc, path); const functionToInvoke = (object as Record)[functionName]; + try { if (functionToInvoke && typeof functionToInvoke === "function") { + const schema = getSchema(message.fn); + const parsedArgs = schema.parameters().parse(message.args); try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const result = await functionToInvoke.apply(object, message.args); + const result = await schema + .returnType() + .parse(functionToInvoke.apply(object, parsedArgs)); + port.postMessage({ type: RPCMessageType.PARCNET_CLIENT_INVOKE_RESULT, result, diff --git a/packages/podspec/src/gpc/proofRequest.ts b/packages/podspec/src/gpc/proof_request.ts similarity index 81% rename from packages/podspec/src/gpc/proofRequest.ts rename to packages/podspec/src/gpc/proof_request.ts index 376d76c..2975e21 100644 --- a/packages/podspec/src/gpc/proofRequest.ts +++ b/packages/podspec/src/gpc/proof_request.ts @@ -11,6 +11,8 @@ import { PodSpec } from "../parse/pod.js"; import { EntriesSchema } from "../schemas/entries.js"; import { PODSchema } from "../schemas/pod.js"; +type Pods = Record; + /** * A ProofRequest contains the data necessary to verify that a given GPC proof * matches our expectations of it. @@ -26,8 +28,19 @@ export type ProofRequest = { * A PodspecProofRequest allows us to generate a {@link ProofRequest} from a * set of Podspecs defining the allowable PODs. */ -export interface PodspecProofRequest { - pods: Record>; +export interface PodspecProofRequest< + P extends Record = Record> +> { + pods: Readonly<{ + [K in keyof P]: P[K] extends PODSchema + ? P[K] & PODSchema + : never; + }>; + inputPods?: Readonly<{ + [K in keyof P]: P[K] extends PODSchema + ? P[K] & PODSchema + : never; + }>; externalNullifier?: PODValue; watermark?: PODValue; } @@ -36,21 +49,24 @@ export interface PodspecProofRequest { * A ProofRequestSpec allows us to generate a {@link ProofRequest} from a * set of Podspecs defining the allowable PODs. */ -export class ProofRequestSpec

{ +export class ProofRequestSpec< + P extends PodspecProofRequest, + T extends Pods +> { /** * Private constructor, see {@link create}. * @param schema The schema of the PODs that are allowed in this proof. */ - private constructor(public readonly schema: P) {} + private constructor(public readonly schema: PodspecProofRequest) {} /** * Create a new ProofRequestSpec. * @param schema The schema of the PODs that are allowed in this proof. * @returns A new ProofRequestSpec. */ - public static create

( - schema: P - ): ProofRequestSpec

{ + public static create

, T extends Pods>( + schema: PodspecProofRequest + ): ProofRequestSpec { return new ProofRequestSpec(schema); } @@ -76,7 +92,12 @@ export class ProofRequestSpec

{ */ public queryForInputs(pods: POD[]): Record { const result: Record = {}; - for (const [podName, podSchema] of Object.entries(this.schema.pods)) { + for (const [podName, podSchema] of Object.entries( + (this.schema.inputPods ?? this.schema.pods) as Record< + string, + PODSchema + > + )) { result[podName] = PodSpec.create(podSchema).query(pods).matches; } return result as Record; @@ -86,7 +107,7 @@ export class ProofRequestSpec

{ /** * Export for convenience. */ -export const proofRequest =

(schema: P) => +export const proofRequest =

(schema: PodspecProofRequest

) => ProofRequestSpec.create(schema); /** @@ -95,12 +116,16 @@ export const proofRequest =

(schema: P) => * @param request The PodspecProofRequest to derive the ProofRequest from. * @returns A ProofRequest. */ -function makeProofRequest(request: PodspecProofRequest): ProofRequest { +function makeProofRequest

( + request: PodspecProofRequest

+): ProofRequest { const pods: Record = {}; const membershipLists: PODMembershipLists = {}; const tuples: Record = {}; - for (const [podName, podSchema] of Object.entries(request.pods)) { + for (const [podName, podSchema] of Object.entries( + request.pods as Record> + )) { const podConfig: GPCProofObjectConfig = { entries: {} }; for (const [entryName, schema] of Object.entries(podSchema.entries)) { diff --git a/packages/podspec/src/index.ts b/packages/podspec/src/index.ts index ccd63e9..7d98886 100644 --- a/packages/podspec/src/index.ts +++ b/packages/podspec/src/index.ts @@ -3,13 +3,13 @@ import { ProofRequest, proofRequest, ProofRequestSpec -} from "./gpc/proofRequest.js"; +} from "./gpc/proof_request.js"; import { entries, EntriesOutputType, EntriesSpec } from "./parse/entries.js"; import { pod, PodSpec } from "./parse/pod.js"; import { EntriesSchema } from "./schemas/entries.js"; import { PODSchema } from "./schemas/pod.js"; -import { InferEntriesType, InferPodType } from "./type_inference.js"; -export * from "./utils.js"; +import { InferPodType } from "./type_inference.js"; +export * from "./pod_value_utils.js"; export { entries, @@ -18,7 +18,6 @@ export { type EntriesOutputType, type EntriesSchema, type EntriesSpec, - type InferEntriesType, type InferPodType, type PODSchema, type PodSpec, diff --git a/packages/podspec/src/parse/entries.ts b/packages/podspec/src/parse/entries.ts index dec758a..a390aca 100644 --- a/packages/podspec/src/parse/entries.ts +++ b/packages/podspec/src/parse/entries.ts @@ -16,7 +16,11 @@ import { checkPODEdDSAPublicKeyValue, eddsaPublicKeyCoercer } from "../schemas/eddsa_pubkey.js"; -import { EntriesSchema, EntriesTupleSchema } from "../schemas/entries.js"; +import { + EntriesSchema, + EntriesSchemaLiteral, + EntriesTupleSchema +} from "../schemas/entries.js"; import { DefinedEntrySchema, EntrySchema, @@ -24,6 +28,7 @@ import { } from "../schemas/entry.js"; import { checkPODIntValue, intCoercer } from "../schemas/int.js"; import { checkPODStringValue, stringCoercer } from "../schemas/string.js"; +import { deepFreeze } from "../utils.js"; import { parseEntry } from "./entry.js"; import { FAILURE, @@ -31,7 +36,7 @@ import { PODValueNativeTypes, safeCheckTuple, SUCCESS -} from "./parseUtils.js"; +} from "./parse_utils.js"; const COERCERS: Record unknown> = { string: stringCoercer, @@ -92,12 +97,13 @@ function isValidEntryType(type: string): type is EntrySchema["type"] { * A specification for a set of entries. */ export class EntriesSpec { + public readonly schema: EntriesSchemaLiteral; /** * The constructor is private - see {@link create} for public construction. * * @param schema The schema to use for this set of entries. */ - private constructor(public readonly schema: E) { + private constructor(schema: E) { for (const [name, entry] of Object.entries(schema)) { const entryType = entry.type === "optional" ? entry.innerType.type : entry.type; @@ -107,6 +113,7 @@ export class EntriesSpec { ); } } + this.schema = deepFreeze(schema); } /** @@ -121,7 +128,7 @@ export class EntriesSpec { input: Record, options: EntriesParseOptions = DEFAULT_ENTRIES_PARSE_OPTIONS, path: string[] = [] - ): ParseResult> { + ): ParseResult>> { return safeParseEntries(this.schema, input, options, path); } @@ -132,7 +139,7 @@ export class EntriesSpec { input: Record, options: EntriesParseOptions = DEFAULT_ENTRIES_PARSE_OPTIONS, path: string[] = [] - ): EntriesOutputType { + ): EntriesOutputType> { const result = this.safeParse(input, options, path); if (result.isValid) { return result.value; diff --git a/packages/podspec/src/parse/entry.ts b/packages/podspec/src/parse/entry.ts index 5f10aa3..83f329c 100644 --- a/packages/podspec/src/parse/entry.ts +++ b/packages/podspec/src/parse/entry.ts @@ -17,7 +17,7 @@ import { safeCheckPODValue, safeMembershipChecks, SUCCESS -} from "./parseUtils.js"; +} from "./parse_utils.js"; /** * Options controlling how parsing of a single entry is performed. diff --git a/packages/podspec/src/parse/parseUtils.ts b/packages/podspec/src/parse/parse_utils.ts similarity index 100% rename from packages/podspec/src/parse/parseUtils.ts rename to packages/podspec/src/parse/parse_utils.ts diff --git a/packages/podspec/src/parse/pod.ts b/packages/podspec/src/parse/pod.ts index 10967b7..04dd958 100644 --- a/packages/podspec/src/parse/pod.ts +++ b/packages/podspec/src/parse/pod.ts @@ -7,7 +7,7 @@ import { PodspecSignerExcludedByListIssue, PodspecSignerNotInListIssue } from "../error.js"; -import { EntriesSchema } from "../schemas/entries.js"; +import { EntriesSchema, EntriesSchemaLiteral } from "../schemas/entries.js"; import { PODSchema } from "../schemas/pod.js"; import { DEFAULT_ENTRIES_PARSE_OPTIONS, @@ -15,7 +15,12 @@ import { EntriesParseOptions, safeParseEntries } from "./entries.js"; -import { FAILURE, ParseResult, safeCheckTuple, SUCCESS } from "./parseUtils.js"; +import { + FAILURE, + ParseResult, + safeCheckTuple, + SUCCESS +} from "./parse_utils.js"; /** * "Strong" PODContent is an extension of PODContent which extends the @@ -37,13 +42,21 @@ export interface StrongPOD extends POD { * additional constraints. */ export class PodSpec { + public readonly schema: PODSchema>; + + public entries(): EntriesSchemaLiteral { + return this.schema.entries; + } + /** * Create a new PodSpec. The constructor is private, see {@link create} for * public creation. * * @param schema The schema for the POD. */ - private constructor(public readonly schema: PODSchema) {} + private constructor(schema: PODSchema) { + this.schema = Object.freeze(schema); + } /** * Parse a POD according to this PodSpec. @@ -58,7 +71,7 @@ export class PodSpec { input: POD, options: EntriesParseOptions = DEFAULT_ENTRIES_PARSE_OPTIONS, path: string[] = [] - ): ParseResult>> { + ): ParseResult>>> { return safeParsePod(this.schema, input, options, path); } @@ -75,7 +88,7 @@ export class PodSpec { input: POD, options: EntriesParseOptions = DEFAULT_ENTRIES_PARSE_OPTIONS, path: string[] = [] - ): StrongPOD> { + ): StrongPOD>> { const result = this.safeParse(input, options, path); if (result.isValid) { return result.value; @@ -95,9 +108,14 @@ export class PodSpec { public query(input: POD[]): { matches: POD[]; matchingIndexes: number[] } { const matchingIndexes: number[] = []; const matches: POD[] = []; + const signatures = new Set(); for (const [index, pod] of input.entries()) { const result = this.safeParse(pod, { exitEarly: true }); if (result.isValid) { + if (signatures.has(pod.signature)) { + continue; + } + signatures.add(pod.signature); matchingIndexes.push(index); matches.push(pod); } @@ -108,6 +126,20 @@ export class PodSpec { }; } + public extend>>( + updater: ( + schema: PODSchema> + ) => R extends PODSchema> ? R : never + ) { + const clone = structuredClone(this.schema); + const newSchema: R = updater(clone); + return PodSpec.create(newSchema) as PodSpec; + } + + public cloneSchema(): PODSchema { + return structuredClone(this.schema); + } + /** * Creates a new PodSpec instance. * diff --git a/packages/podspec/src/pod_value_utils.ts b/packages/podspec/src/pod_value_utils.ts new file mode 100644 index 0000000..1ee9e72 --- /dev/null +++ b/packages/podspec/src/pod_value_utils.ts @@ -0,0 +1,58 @@ +import { + PODCryptographicValue, + PODEdDSAPublicKeyValue, + PODIntValue, + PODStringValue +} from "@pcd/pod"; + +// Some terse utility functions for converting native JavaScript values, or +// arrays of values, to their POD equivalents. +// Mostly used to cut down noise in tests, but could be useful in general? + +export function $s(value: string): PODStringValue; +export function $s(value: string[]): PODStringValue[]; +export function $s( + value: string | string[] +): PODStringValue | PODStringValue[] { + if (typeof value === "string") { + return { type: "string", value }; + } else { + return value.map((s) => ({ type: "string", value: s })); + } +} + +export function $e(value: string): PODEdDSAPublicKeyValue; +export function $e(value: string[]): PODEdDSAPublicKeyValue[]; +export function $e( + value: string | string[] +): PODEdDSAPublicKeyValue | PODEdDSAPublicKeyValue[] { + if (typeof value === "string") { + return { type: "eddsa_pubkey", value }; + } else { + return value.map((s) => ({ type: "eddsa_pubkey", value: s })); + } +} + +export function $i(value: bigint | number): PODIntValue; +export function $i(value: bigint[] | number[]): PODIntValue[]; +export function $i( + value: bigint | bigint[] | number | number[] +): PODIntValue | PODIntValue[] { + if (typeof value === "number" || typeof value === "bigint") { + return { type: "int", value: BigInt(value) }; + } else { + return value.map((s) => ({ type: "int", value: BigInt(s) })); + } +} + +export function $c(value: bigint | number): PODCryptographicValue; +export function $c(value: bigint[] | number[]): PODCryptographicValue[]; +export function $c( + value: bigint | bigint[] | number | number[] +): PODCryptographicValue | PODCryptographicValue[] { + if (typeof value === "number" || typeof value === "bigint") { + return { type: "cryptographic", value: BigInt(value) }; + } else { + return value.map((s) => ({ type: "cryptographic", value: BigInt(s) })); + } +} diff --git a/packages/podspec/src/schemas/cryptographic.ts b/packages/podspec/src/schemas/cryptographic.ts index c278cae..1ea59cf 100644 --- a/packages/podspec/src/schemas/cryptographic.ts +++ b/packages/podspec/src/schemas/cryptographic.ts @@ -9,7 +9,7 @@ import { FAILURE, ParseResult, safeCheckBigintBounds -} from "../parse/parseUtils.js"; +} from "../parse/parse_utils.js"; /** * Schema for a cryptographic value. diff --git a/packages/podspec/src/schemas/eddsa_pubkey.ts b/packages/podspec/src/schemas/eddsa_pubkey.ts index b4af30e..4753af4 100644 --- a/packages/podspec/src/schemas/eddsa_pubkey.ts +++ b/packages/podspec/src/schemas/eddsa_pubkey.ts @@ -4,7 +4,7 @@ import { FAILURE, ParseResult, safeCheckPublicKeyFormat -} from "../parse/parseUtils.js"; +} from "../parse/parse_utils.js"; /** * Schema for an EdDSA public key. diff --git a/packages/podspec/src/schemas/entries.ts b/packages/podspec/src/schemas/entries.ts index 3d47413..a67b84e 100644 --- a/packages/podspec/src/schemas/entries.ts +++ b/packages/podspec/src/schemas/entries.ts @@ -6,6 +6,13 @@ import { EntrySchema } from "./entry.js"; */ export type EntriesSchema = Record; +type EntriesSchemaLiteralEntries = { + [K in keyof T]: T[K] & EntrySchema; +}; + +export type EntriesSchemaLiteral = + EntriesSchemaLiteralEntries & EntriesSchema; + /** * Schema for a tuple of entries. */ diff --git a/packages/podspec/src/schemas/int.ts b/packages/podspec/src/schemas/int.ts index 0faaa37..aa9e63d 100644 --- a/packages/podspec/src/schemas/int.ts +++ b/packages/podspec/src/schemas/int.ts @@ -4,7 +4,7 @@ import { FAILURE, ParseResult, safeCheckBigintBounds -} from "../parse/parseUtils.js"; +} from "../parse/parse_utils.js"; /** * Schema for a PODIntValue. diff --git a/packages/podspec/src/schemas/pod.ts b/packages/podspec/src/schemas/pod.ts index 7d98541..09fa6c6 100644 --- a/packages/podspec/src/schemas/pod.ts +++ b/packages/podspec/src/schemas/pod.ts @@ -1,5 +1,5 @@ import { PODValue } from "@pcd/pod"; -import { EntriesSchema } from "./entries.js"; +import { EntriesSchema, EntriesSchemaLiteral } from "./entries.js"; /** * Schema for a tuple of entries. @@ -14,8 +14,8 @@ export type PODTupleSchema = { * Schema for validating a POD. */ export type PODSchema = { - entries: E; - tuples?: PODTupleSchema[]; + entries: EntriesSchemaLiteral; + tuples?: PODTupleSchema>[]; signerPublicKey?: { isMemberOf?: string[]; isNotMemberOf?: string[]; diff --git a/packages/podspec/src/schemas/string.ts b/packages/podspec/src/schemas/string.ts index c3e677e..8c7771d 100644 --- a/packages/podspec/src/schemas/string.ts +++ b/packages/podspec/src/schemas/string.ts @@ -1,6 +1,6 @@ import { PODName, PODStringValue } from "@pcd/pod"; import { IssueCode, PodspecInvalidTypeIssue } from "../error.js"; -import { FAILURE, ParseResult, SUCCESS } from "../parse/parseUtils.js"; +import { FAILURE, ParseResult, SUCCESS } from "../parse/parse_utils.js"; /** * Schema for a PODStringValue. diff --git a/packages/podspec/src/type_inference.ts b/packages/podspec/src/type_inference.ts index 4255116..1025ce0 100644 --- a/packages/podspec/src/type_inference.ts +++ b/packages/podspec/src/type_inference.ts @@ -1,14 +1,6 @@ import { EntriesOutputType } from "./parse/entries.js"; import { PodSpec, StrongPOD } from "./parse/pod.js"; -/** - * Infer a typed version of PODEntries, specific to a set of entries defined - * by a Podspec. - */ -export type InferEntriesType = T extends { [K in keyof T]: infer U } - ? U - : never; - /** * Infer a typed version of a POD from a given Podspec. */ diff --git a/packages/podspec/src/utils.ts b/packages/podspec/src/utils.ts index 1ee9e72..e8efaea 100644 --- a/packages/podspec/src/utils.ts +++ b/packages/podspec/src/utils.ts @@ -1,58 +1,42 @@ -import { - PODCryptographicValue, - PODEdDSAPublicKeyValue, - PODIntValue, - PODStringValue -} from "@pcd/pod"; +export function deepFreeze(obj: T): T { + if (typeof obj !== "object" || obj === null) { + return obj; + } -// Some terse utility functions for converting native JavaScript values, or -// arrays of values, to their POD equivalents. -// Mostly used to cut down noise in tests, but could be useful in general? + Object.freeze(obj); -export function $s(value: string): PODStringValue; -export function $s(value: string[]): PODStringValue[]; -export function $s( - value: string | string[] -): PODStringValue | PODStringValue[] { - if (typeof value === "string") { - return { type: "string", value }; - } else { - return value.map((s) => ({ type: "string", value: s })); - } -} + Object.values(obj).forEach((value) => { + deepFreeze(value); + }); -export function $e(value: string): PODEdDSAPublicKeyValue; -export function $e(value: string[]): PODEdDSAPublicKeyValue[]; -export function $e( - value: string | string[] -): PODEdDSAPublicKeyValue | PODEdDSAPublicKeyValue[] { - if (typeof value === "string") { - return { type: "eddsa_pubkey", value }; - } else { - return value.map((s) => ({ type: "eddsa_pubkey", value: s })); - } + return obj; } -export function $i(value: bigint | number): PODIntValue; -export function $i(value: bigint[] | number[]): PODIntValue[]; -export function $i( - value: bigint | bigint[] | number | number[] -): PODIntValue | PODIntValue[] { - if (typeof value === "number" || typeof value === "bigint") { - return { type: "int", value: BigInt(value) }; - } else { - return value.map((s) => ({ type: "int", value: BigInt(s) })); - } +export function extend( + thing: T, + updater: (thing: T) => R extends T ? R : never +): R { + const clone = structuredClone(thing); + return updater(clone); } -export function $c(value: bigint | number): PODCryptographicValue; -export function $c(value: bigint[] | number[]): PODCryptographicValue[]; -export function $c( - value: bigint | bigint[] | number | number[] -): PODCryptographicValue | PODCryptographicValue[] { - if (typeof value === "number" || typeof value === "bigint") { - return { type: "cryptographic", value: BigInt(value) }; - } else { - return value.map((s) => ({ type: "cryptographic", value: BigInt(s) })); - } +type DeepMerge = T extends object + ? U extends object + ? { + [K in keyof T | keyof U]: K extends keyof T + ? K extends keyof U + ? DeepMerge + : T[K] + : K extends keyof U + ? U[K] + : never; + } + : T + : U; + +export function typedMerge>( + target: T, + source: U +): DeepMerge { + return { ...target, ...source } as DeepMerge; } diff --git a/packages/podspec/test/podspec.spec.ts b/packages/podspec/test/podspec.spec.ts index 00f0329..3fb5789 100644 --- a/packages/podspec/test/podspec.spec.ts +++ b/packages/podspec/test/podspec.spec.ts @@ -18,8 +18,8 @@ import { PodspecNotInTupleListIssue } from "../src/error.js"; import * as p from "../src/index.js"; +import { $i, $s } from "../src/pod_value_utils.js"; import { EntriesTupleSchema } from "../src/schemas/entries.js"; -import { $i, $s } from "../src/utils.js"; export const GPC_NPM_ARTIFACTS_PATH = path.join( __dirname, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9fdaec..3404ec0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,10 +131,7 @@ importers: version: 18.3.1(react@18.3.1) vite-plugin-node-polyfills: specifier: ^0.22.0 - version: 0.22.0(rollup@4.21.2)(vite@5.4.2(@types/node@22.5.4)) - zod: - specifier: ^3.23.8 - version: 3.23.8 + version: 0.22.0(rollup@4.21.2)(vite@5.4.4(@types/node@22.5.4)) devDependencies: '@parcnet/eslint-config': specifier: workspace:* @@ -147,7 +144,7 @@ importers: version: 18.3.0 '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.3.1(vite@5.4.2(@types/node@22.5.4)) + version: 4.3.1(vite@5.4.4(@types/node@22.5.4)) autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.44) @@ -167,8 +164,8 @@ importers: specifier: ^5.5 version: 5.5.4 vite: - specifier: ^5.4.1 - version: 5.4.2(@types/node@22.5.4) + specifier: ^5.4.4 + version: 5.4.4(@types/node@22.5.4) examples/test-app: dependencies: @@ -3639,8 +3636,8 @@ packages: peerDependencies: vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 - vite@5.4.2: - resolution: {integrity: sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==} + vite@5.4.4: + resolution: {integrity: sha512-RHFCkULitycHVTtelJ6jQLd+KSAAzOgEYorV32R2q++M6COBjKJR6BxqClwp5sf0XaBDjVMuJ9wnNfyAJwjMkA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -4653,14 +4650,14 @@ snapshots: '@typescript-eslint/types': 8.4.0 eslint-visitor-keys: 3.4.3 - '@vitejs/plugin-react@4.3.1(vite@5.4.2(@types/node@22.5.4))': + '@vitejs/plugin-react@4.3.1(vite@5.4.4(@types/node@22.5.4))': dependencies: '@babel/core': 7.25.2 '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.2) '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.25.2) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.2(@types/node@22.5.4) + vite: 5.4.4(@types/node@22.5.4) transitivePeerDependencies: - supports-color @@ -7324,7 +7321,7 @@ snapshots: debug: 4.3.6 pathe: 1.1.2 tinyrainbow: 1.2.0 - vite: 5.4.2(@types/node@22.5.4) + vite: 5.4.4(@types/node@22.5.4) transitivePeerDependencies: - '@types/node' - less @@ -7336,15 +7333,15 @@ snapshots: - supports-color - terser - vite-plugin-node-polyfills@0.22.0(rollup@4.21.2)(vite@5.4.2(@types/node@22.5.4)): + vite-plugin-node-polyfills@0.22.0(rollup@4.21.2)(vite@5.4.4(@types/node@22.5.4)): dependencies: '@rollup/plugin-inject': 5.0.5(rollup@4.21.2) node-stdlib-browser: 1.2.0 - vite: 5.4.2(@types/node@22.5.4) + vite: 5.4.4(@types/node@22.5.4) transitivePeerDependencies: - rollup - vite@5.4.2(@types/node@22.5.4): + vite@5.4.4(@types/node@22.5.4): dependencies: esbuild: 0.21.5 postcss: 8.4.44 @@ -7371,7 +7368,7 @@ snapshots: tinybench: 2.9.0 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.4.2(@types/node@22.5.4) + vite: 5.4.4(@types/node@22.5.4) vite-node: 2.0.5(@types/node@22.5.4) why-is-node-running: 2.3.0 optionalDependencies: