@effect-gql/persisted-queries 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2025 Nick Fisher
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,106 @@
1
+ import { Config, Layer } from "effect";
2
+ import type { PersistedQueryStore } from "./store";
3
+ import type { MakeGraphQLRouterOptions } from "@effect-gql/core/server";
4
+ /**
5
+ * Operating mode for persisted queries.
6
+ *
7
+ * - `"apq"`: Automatic Persisted Queries - clients can register queries at runtime
8
+ * - `"safelist"`: Only pre-registered queries are allowed (security mode)
9
+ */
10
+ export type PersistedQueryMode = "apq" | "safelist";
11
+ /**
12
+ * Hash algorithm used for query hashing.
13
+ * Must match what clients use - Apollo clients use SHA-256.
14
+ */
15
+ export type HashAlgorithm = "sha256" | "sha512";
16
+ /**
17
+ * Configuration for the persisted queries feature.
18
+ */
19
+ export interface PersistedQueriesConfig {
20
+ /**
21
+ * Operating mode.
22
+ *
23
+ * - `"apq"`: Automatic Persisted Queries - clients can register queries at runtime.
24
+ * Unknown hashes trigger PERSISTED_QUERY_NOT_FOUND, prompting clients to retry with query.
25
+ *
26
+ * - `"safelist"`: Only pre-registered queries are allowed.
27
+ * Unknown hashes return PERSISTED_QUERY_NOT_ALLOWED error.
28
+ * Use with `makeSafelistStore()` for production security.
29
+ *
30
+ * Default: `"apq"`
31
+ */
32
+ readonly mode?: PersistedQueryMode;
33
+ /**
34
+ * Layer providing the PersistedQueryStore service.
35
+ *
36
+ * Defaults to in-memory LRU store with 1000 entries.
37
+ * Use `makeMemoryStore()` for custom size or `makeSafelistStore()` for safelist mode.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * // Custom memory store
42
+ * store: makeMemoryStore({ maxSize: 5000 })
43
+ *
44
+ * // Safelist store
45
+ * store: makeSafelistStore({ "hash1": "query {...}", "hash2": "query {...}" })
46
+ * ```
47
+ */
48
+ readonly store?: Layer.Layer<PersistedQueryStore>;
49
+ /**
50
+ * Whether to support GET requests with query parameters.
51
+ *
52
+ * When enabled, the router accepts:
53
+ * ```
54
+ * GET /graphql?extensions={"persistedQuery":{"version":1,"sha256Hash":"..."}}&variables={...}&operationName=...
55
+ * ```
56
+ *
57
+ * This enables CDN caching since the same hash always maps to the same URL.
58
+ *
59
+ * Default: `true`
60
+ */
61
+ readonly enableGet?: boolean;
62
+ /**
63
+ * Validate that the provided query matches its hash when storing.
64
+ *
65
+ * This prevents hash collision attacks where a malicious client could
66
+ * register a different query under someone else's hash.
67
+ *
68
+ * Has a slight performance overhead for computing the hash.
69
+ *
70
+ * Default: `true`
71
+ */
72
+ readonly validateHash?: boolean;
73
+ /**
74
+ * Hash algorithm to use for validation.
75
+ * Must match what clients use - Apollo clients use SHA-256.
76
+ *
77
+ * Default: `"sha256"`
78
+ */
79
+ readonly hashAlgorithm?: HashAlgorithm;
80
+ }
81
+ /**
82
+ * Options for the persisted queries router.
83
+ *
84
+ * Extends the standard GraphQL router options with persisted query configuration.
85
+ */
86
+ export interface PersistedQueriesRouterOptions extends MakeGraphQLRouterOptions, PersistedQueriesConfig {
87
+ }
88
+ /**
89
+ * Effect Config for loading persisted queries settings from environment variables.
90
+ *
91
+ * Environment variables:
92
+ * - `PERSISTED_QUERIES_MODE`: `"apq"` | `"safelist"` (default: `"apq"`)
93
+ * - `PERSISTED_QUERIES_ENABLE_GET`: boolean (default: `true`)
94
+ * - `PERSISTED_QUERIES_VALIDATE_HASH`: boolean (default: `true`)
95
+ *
96
+ * Note: The store must still be provided programmatically.
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * import { PersistedQueriesConfigFromEnv } from "@effect-gql/persisted-queries"
101
+ *
102
+ * const config = yield* Config.unwrap(PersistedQueriesConfigFromEnv)
103
+ * ```
104
+ */
105
+ export declare const PersistedQueriesConfigFromEnv: Config.Config<Omit<PersistedQueriesConfig, "store">>;
106
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AACtC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAA;AAEvE;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,UAAU,CAAA;AAEnD;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE/C;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAA;IAElC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAA;IAEjD;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAE5B;;;;;;;;;OASG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAA;IAE/B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAA;CACvC;AAED;;;;GAIG;AACH,MAAM,WAAW,6BACf,SAAQ,wBAAwB,EAAE,sBAAsB;CAAG;AAE7D;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,6BAA6B,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE,OAAO,CAAC,CAQ3F,CAAA"}
package/dist/config.js ADDED
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PersistedQueriesConfigFromEnv = void 0;
4
+ const effect_1 = require("effect");
5
+ /**
6
+ * Effect Config for loading persisted queries settings from environment variables.
7
+ *
8
+ * Environment variables:
9
+ * - `PERSISTED_QUERIES_MODE`: `"apq"` | `"safelist"` (default: `"apq"`)
10
+ * - `PERSISTED_QUERIES_ENABLE_GET`: boolean (default: `true`)
11
+ * - `PERSISTED_QUERIES_VALIDATE_HASH`: boolean (default: `true`)
12
+ *
13
+ * Note: The store must still be provided programmatically.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { PersistedQueriesConfigFromEnv } from "@effect-gql/persisted-queries"
18
+ *
19
+ * const config = yield* Config.unwrap(PersistedQueriesConfigFromEnv)
20
+ * ```
21
+ */
22
+ exports.PersistedQueriesConfigFromEnv = effect_1.Config.all({
23
+ mode: effect_1.Config.literal("apq", "safelist")("PERSISTED_QUERIES_MODE").pipe(effect_1.Config.withDefault("apq")),
24
+ enableGet: effect_1.Config.boolean("PERSISTED_QUERIES_ENABLE_GET").pipe(effect_1.Config.withDefault(true)),
25
+ validateHash: effect_1.Config.boolean("PERSISTED_QUERIES_VALIDATE_HASH").pipe(effect_1.Config.withDefault(true)),
26
+ });
27
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;AAAA,mCAAsC;AAgGtC;;;;;;;;;;;;;;;;GAgBG;AACU,QAAA,6BAA6B,GACxC,eAAM,CAAC,GAAG,CAAC;IACT,IAAI,EAAE,eAAM,CAAC,OAAO,CAClB,KAAK,EACL,UAAU,CACX,CAAC,wBAAwB,CAAC,CAAC,IAAI,CAAC,eAAM,CAAC,WAAW,CAAC,KAAc,CAAC,CAAC;IACpE,SAAS,EAAE,eAAM,CAAC,OAAO,CAAC,8BAA8B,CAAC,CAAC,IAAI,CAAC,eAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IACxF,YAAY,EAAE,eAAM,CAAC,OAAO,CAAC,iCAAiC,CAAC,CAAC,IAAI,CAAC,eAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;CAC/F,CAAC,CAAA"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * GraphQL error format for APQ responses.
3
+ * Compatible with Apollo Client's error handling.
4
+ */
5
+ export interface PersistedQueryGraphQLError {
6
+ readonly message: string;
7
+ readonly extensions: {
8
+ readonly code: string;
9
+ readonly [key: string]: unknown;
10
+ };
11
+ }
12
+ declare const PersistedQueryNotFoundError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
13
+ readonly _tag: "PersistedQueryNotFoundError";
14
+ } & Readonly<A>;
15
+ /**
16
+ * Error returned when a persisted query hash is not found in the store
17
+ * and no query body was provided.
18
+ *
19
+ * Apollo clients recognize this error and automatically retry with the full query.
20
+ * This is the expected flow for Automatic Persisted Queries (APQ).
21
+ */
22
+ export declare class PersistedQueryNotFoundError extends PersistedQueryNotFoundError_base<{
23
+ readonly hash: string;
24
+ }> {
25
+ /**
26
+ * Convert to GraphQL error format compatible with Apollo protocol.
27
+ */
28
+ toGraphQLError(): PersistedQueryGraphQLError;
29
+ }
30
+ declare const PersistedQueryVersionError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
31
+ readonly _tag: "PersistedQueryVersionError";
32
+ } & Readonly<A>;
33
+ /**
34
+ * Error returned when the persisted query protocol version is not supported.
35
+ *
36
+ * Currently only version 1 is supported, which uses SHA-256 hashing.
37
+ */
38
+ export declare class PersistedQueryVersionError extends PersistedQueryVersionError_base<{
39
+ readonly version: number;
40
+ }> {
41
+ toGraphQLError(): PersistedQueryGraphQLError;
42
+ }
43
+ declare const PersistedQueryHashMismatchError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
44
+ readonly _tag: "PersistedQueryHashMismatchError";
45
+ } & Readonly<A>;
46
+ /**
47
+ * Error returned when the provided query doesn't match its hash.
48
+ *
49
+ * This can indicate a client bug or a potential hash collision attack.
50
+ * Hash validation is enabled by default and can be disabled if needed.
51
+ */
52
+ export declare class PersistedQueryHashMismatchError extends PersistedQueryHashMismatchError_base<{
53
+ readonly providedHash: string;
54
+ readonly computedHash: string;
55
+ }> {
56
+ toGraphQLError(): PersistedQueryGraphQLError;
57
+ }
58
+ declare const PersistedQueryNotAllowedError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
59
+ readonly _tag: "PersistedQueryNotAllowedError";
60
+ } & Readonly<A>;
61
+ /**
62
+ * Error returned when trying to execute a query that is not in the safelist.
63
+ *
64
+ * In safelist mode, only pre-registered queries are allowed.
65
+ * This error is returned when a client tries to register a new query.
66
+ */
67
+ export declare class PersistedQueryNotAllowedError extends PersistedQueryNotAllowedError_base<{
68
+ readonly hash: string;
69
+ }> {
70
+ toGraphQLError(): PersistedQueryGraphQLError;
71
+ }
72
+ /**
73
+ * Union type of all APQ-related errors
74
+ */
75
+ export type PersistedQueryError = PersistedQueryNotFoundError | PersistedQueryVersionError | PersistedQueryHashMismatchError | PersistedQueryNotAllowedError;
76
+ export {};
77
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,UAAU,EAAE;QACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;QACrB,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAChC,CAAA;CACF;;;;AAED;;;;;;GAMG;AACH,qBAAa,2BAA4B,SAAQ,iCAAgD;IAC/F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB,CAAC;IACA;;OAEG;IACH,cAAc,IAAI,0BAA0B;CAQ7C;;;;AAED;;;;GAIG;AACH,qBAAa,0BAA2B,SAAQ,gCAA+C;IAC7F,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,CAAC;IACA,cAAc,IAAI,0BAA0B;CAS7C;;;;AAED;;;;;GAKG;AACH,qBAAa,+BAAgC,SAAQ,qCAEnD;IACA,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAC9B,CAAC;IACA,cAAc,IAAI,0BAA0B;CAQ7C;;;;AAED;;;;;GAKG;AACH,qBAAa,6BAA8B,SAAQ,mCAEjD;IACA,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB,CAAC;IACA,cAAc,IAAI,0BAA0B;CAQ7C;AAED;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAC3B,2BAA2B,GAC3B,0BAA0B,GAC1B,+BAA+B,GAC/B,6BAA6B,CAAA"}
package/dist/errors.js ADDED
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PersistedQueryNotAllowedError = exports.PersistedQueryHashMismatchError = exports.PersistedQueryVersionError = exports.PersistedQueryNotFoundError = void 0;
4
+ const effect_1 = require("effect");
5
+ /**
6
+ * Error returned when a persisted query hash is not found in the store
7
+ * and no query body was provided.
8
+ *
9
+ * Apollo clients recognize this error and automatically retry with the full query.
10
+ * This is the expected flow for Automatic Persisted Queries (APQ).
11
+ */
12
+ class PersistedQueryNotFoundError extends effect_1.Data.TaggedError("PersistedQueryNotFoundError") {
13
+ /**
14
+ * Convert to GraphQL error format compatible with Apollo protocol.
15
+ */
16
+ toGraphQLError() {
17
+ return {
18
+ message: "PersistedQueryNotFound",
19
+ extensions: {
20
+ code: "PERSISTED_QUERY_NOT_FOUND",
21
+ },
22
+ };
23
+ }
24
+ }
25
+ exports.PersistedQueryNotFoundError = PersistedQueryNotFoundError;
26
+ /**
27
+ * Error returned when the persisted query protocol version is not supported.
28
+ *
29
+ * Currently only version 1 is supported, which uses SHA-256 hashing.
30
+ */
31
+ class PersistedQueryVersionError extends effect_1.Data.TaggedError("PersistedQueryVersionError") {
32
+ toGraphQLError() {
33
+ return {
34
+ message: `Unsupported persisted query version: ${this.version}`,
35
+ extensions: {
36
+ code: "PERSISTED_QUERY_VERSION_NOT_SUPPORTED",
37
+ version: this.version,
38
+ },
39
+ };
40
+ }
41
+ }
42
+ exports.PersistedQueryVersionError = PersistedQueryVersionError;
43
+ /**
44
+ * Error returned when the provided query doesn't match its hash.
45
+ *
46
+ * This can indicate a client bug or a potential hash collision attack.
47
+ * Hash validation is enabled by default and can be disabled if needed.
48
+ */
49
+ class PersistedQueryHashMismatchError extends effect_1.Data.TaggedError("PersistedQueryHashMismatchError") {
50
+ toGraphQLError() {
51
+ return {
52
+ message: "Query hash does not match provided hash",
53
+ extensions: {
54
+ code: "PERSISTED_QUERY_HASH_MISMATCH",
55
+ },
56
+ };
57
+ }
58
+ }
59
+ exports.PersistedQueryHashMismatchError = PersistedQueryHashMismatchError;
60
+ /**
61
+ * Error returned when trying to execute a query that is not in the safelist.
62
+ *
63
+ * In safelist mode, only pre-registered queries are allowed.
64
+ * This error is returned when a client tries to register a new query.
65
+ */
66
+ class PersistedQueryNotAllowedError extends effect_1.Data.TaggedError("PersistedQueryNotAllowedError") {
67
+ toGraphQLError() {
68
+ return {
69
+ message: "Query not in safelist",
70
+ extensions: {
71
+ code: "PERSISTED_QUERY_NOT_ALLOWED",
72
+ },
73
+ };
74
+ }
75
+ }
76
+ exports.PersistedQueryNotAllowedError = PersistedQueryNotAllowedError;
77
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":";;;AAAA,mCAA6B;AAc7B;;;;;;GAMG;AACH,MAAa,2BAA4B,SAAQ,aAAI,CAAC,WAAW,CAAC,6BAA6B,CAE7F;IACA;;OAEG;IACH,cAAc;QACZ,OAAO;YACL,OAAO,EAAE,wBAAwB;YACjC,UAAU,EAAE;gBACV,IAAI,EAAE,2BAA2B;aAClC;SACF,CAAA;IACH,CAAC;CACF;AAdD,kEAcC;AAED;;;;GAIG;AACH,MAAa,0BAA2B,SAAQ,aAAI,CAAC,WAAW,CAAC,4BAA4B,CAE3F;IACA,cAAc;QACZ,OAAO;YACL,OAAO,EAAE,wCAAwC,IAAI,CAAC,OAAO,EAAE;YAC/D,UAAU,EAAE;gBACV,IAAI,EAAE,uCAAuC;gBAC7C,OAAO,EAAE,IAAI,CAAC,OAAO;aACtB;SACF,CAAA;IACH,CAAC;CACF;AAZD,gEAYC;AAED;;;;;GAKG;AACH,MAAa,+BAAgC,SAAQ,aAAI,CAAC,WAAW,CACnE,iCAAiC,CAIjC;IACA,cAAc;QACZ,OAAO;YACL,OAAO,EAAE,yCAAyC;YAClD,UAAU,EAAE;gBACV,IAAI,EAAE,+BAA+B;aACtC;SACF,CAAA;IACH,CAAC;CACF;AAdD,0EAcC;AAED;;;;;GAKG;AACH,MAAa,6BAA8B,SAAQ,aAAI,CAAC,WAAW,CACjE,+BAA+B,CAG/B;IACA,cAAc;QACZ,OAAO;YACL,OAAO,EAAE,uBAAuB;YAChC,UAAU,EAAE;gBACV,IAAI,EAAE,6BAA6B;aACpC;SACF,CAAA;IACH,CAAC;CACF;AAbD,sEAaC"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @effect-gql/persisted-queries
3
+ *
4
+ * Apollo Persisted Queries support for Effect GraphQL.
5
+ *
6
+ * Supports both Automatic Persisted Queries (APQ) mode for runtime registration
7
+ * and Safelist mode for pre-registered query allowlisting.
8
+ *
9
+ * ## Quick Start
10
+ *
11
+ * @example APQ Mode (runtime registration)
12
+ * ```typescript
13
+ * import { makePersistedQueriesRouter } from "@effect-gql/persisted-queries"
14
+ *
15
+ * const router = makePersistedQueriesRouter(schema, serviceLayer, {
16
+ * mode: "apq",
17
+ * enableGet: true, // Enable CDN caching
18
+ * graphiql: { path: "/graphiql" },
19
+ * })
20
+ * ```
21
+ *
22
+ * @example Safelist Mode (pre-registered queries only)
23
+ * ```typescript
24
+ * import { makePersistedQueriesRouter, makeSafelistStore } from "@effect-gql/persisted-queries"
25
+ *
26
+ * const router = makePersistedQueriesRouter(schema, serviceLayer, {
27
+ * mode: "safelist",
28
+ * store: makeSafelistStore({
29
+ * "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38": "query GetUser($id: ID!) { user(id: $id) { name email } }",
30
+ * "a1b2c3d4...": "query GetPosts { posts { title } }",
31
+ * }),
32
+ * })
33
+ * ```
34
+ *
35
+ * @example Custom Store Size
36
+ * ```typescript
37
+ * import { makePersistedQueriesRouter, makeMemoryStore } from "@effect-gql/persisted-queries"
38
+ *
39
+ * const router = makePersistedQueriesRouter(schema, serviceLayer, {
40
+ * store: makeMemoryStore({ maxSize: 5000 }),
41
+ * })
42
+ * ```
43
+ *
44
+ * @packageDocumentation
45
+ */
46
+ export { makePersistedQueriesRouter } from "./persisted-queries-router";
47
+ export { PersistedQueryStore } from "./store";
48
+ export type { PersistedQueryStore as PersistedQueryStoreInterface } from "./store";
49
+ export { makeMemoryStore, makeSafelistStore } from "./memory-store";
50
+ export type { MemoryStoreConfig } from "./memory-store";
51
+ export type { PersistedQueriesConfig, PersistedQueriesRouterOptions, PersistedQueryMode, HashAlgorithm, } from "./config";
52
+ export { PersistedQueriesConfigFromEnv } from "./config";
53
+ export { PersistedQueryNotFoundError, PersistedQueryVersionError, PersistedQueryHashMismatchError, PersistedQueryNotAllowedError, type PersistedQueryError, type PersistedQueryGraphQLError, } from "./errors";
54
+ export { computeHash, parsePersistedQueryExtension, parseGetRequestBody } from "./utils";
55
+ export type { PersistedQueryExtension, GraphQLRequestBody } from "./utils";
56
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAGH,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAA;AAGvE,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAC7C,YAAY,EAAE,mBAAmB,IAAI,4BAA4B,EAAE,MAAM,SAAS,CAAA;AAClF,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AACnE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAGvD,YAAY,EACV,sBAAsB,EACtB,6BAA6B,EAC7B,kBAAkB,EAClB,aAAa,GACd,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,6BAA6B,EAAE,MAAM,UAAU,CAAA;AAGxD,OAAO,EACL,2BAA2B,EAC3B,0BAA0B,EAC1B,+BAA+B,EAC/B,6BAA6B,EAC7B,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,GAChC,MAAM,UAAU,CAAA;AAGjB,OAAO,EAAE,WAAW,EAAE,4BAA4B,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AACxF,YAAY,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ /**
3
+ * @effect-gql/persisted-queries
4
+ *
5
+ * Apollo Persisted Queries support for Effect GraphQL.
6
+ *
7
+ * Supports both Automatic Persisted Queries (APQ) mode for runtime registration
8
+ * and Safelist mode for pre-registered query allowlisting.
9
+ *
10
+ * ## Quick Start
11
+ *
12
+ * @example APQ Mode (runtime registration)
13
+ * ```typescript
14
+ * import { makePersistedQueriesRouter } from "@effect-gql/persisted-queries"
15
+ *
16
+ * const router = makePersistedQueriesRouter(schema, serviceLayer, {
17
+ * mode: "apq",
18
+ * enableGet: true, // Enable CDN caching
19
+ * graphiql: { path: "/graphiql" },
20
+ * })
21
+ * ```
22
+ *
23
+ * @example Safelist Mode (pre-registered queries only)
24
+ * ```typescript
25
+ * import { makePersistedQueriesRouter, makeSafelistStore } from "@effect-gql/persisted-queries"
26
+ *
27
+ * const router = makePersistedQueriesRouter(schema, serviceLayer, {
28
+ * mode: "safelist",
29
+ * store: makeSafelistStore({
30
+ * "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38": "query GetUser($id: ID!) { user(id: $id) { name email } }",
31
+ * "a1b2c3d4...": "query GetPosts { posts { title } }",
32
+ * }),
33
+ * })
34
+ * ```
35
+ *
36
+ * @example Custom Store Size
37
+ * ```typescript
38
+ * import { makePersistedQueriesRouter, makeMemoryStore } from "@effect-gql/persisted-queries"
39
+ *
40
+ * const router = makePersistedQueriesRouter(schema, serviceLayer, {
41
+ * store: makeMemoryStore({ maxSize: 5000 }),
42
+ * })
43
+ * ```
44
+ *
45
+ * @packageDocumentation
46
+ */
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.parseGetRequestBody = exports.parsePersistedQueryExtension = exports.computeHash = exports.PersistedQueryNotAllowedError = exports.PersistedQueryHashMismatchError = exports.PersistedQueryVersionError = exports.PersistedQueryNotFoundError = exports.PersistedQueriesConfigFromEnv = exports.makeSafelistStore = exports.makeMemoryStore = exports.PersistedQueryStore = exports.makePersistedQueriesRouter = void 0;
49
+ // Router
50
+ var persisted_queries_router_1 = require("./persisted-queries-router");
51
+ Object.defineProperty(exports, "makePersistedQueriesRouter", { enumerable: true, get: function () { return persisted_queries_router_1.makePersistedQueriesRouter; } });
52
+ // Store interface and implementations
53
+ var store_1 = require("./store");
54
+ Object.defineProperty(exports, "PersistedQueryStore", { enumerable: true, get: function () { return store_1.PersistedQueryStore; } });
55
+ var memory_store_1 = require("./memory-store");
56
+ Object.defineProperty(exports, "makeMemoryStore", { enumerable: true, get: function () { return memory_store_1.makeMemoryStore; } });
57
+ Object.defineProperty(exports, "makeSafelistStore", { enumerable: true, get: function () { return memory_store_1.makeSafelistStore; } });
58
+ var config_1 = require("./config");
59
+ Object.defineProperty(exports, "PersistedQueriesConfigFromEnv", { enumerable: true, get: function () { return config_1.PersistedQueriesConfigFromEnv; } });
60
+ // Errors
61
+ var errors_1 = require("./errors");
62
+ Object.defineProperty(exports, "PersistedQueryNotFoundError", { enumerable: true, get: function () { return errors_1.PersistedQueryNotFoundError; } });
63
+ Object.defineProperty(exports, "PersistedQueryVersionError", { enumerable: true, get: function () { return errors_1.PersistedQueryVersionError; } });
64
+ Object.defineProperty(exports, "PersistedQueryHashMismatchError", { enumerable: true, get: function () { return errors_1.PersistedQueryHashMismatchError; } });
65
+ Object.defineProperty(exports, "PersistedQueryNotAllowedError", { enumerable: true, get: function () { return errors_1.PersistedQueryNotAllowedError; } });
66
+ // Utilities
67
+ var utils_1 = require("./utils");
68
+ Object.defineProperty(exports, "computeHash", { enumerable: true, get: function () { return utils_1.computeHash; } });
69
+ Object.defineProperty(exports, "parsePersistedQueryExtension", { enumerable: true, get: function () { return utils_1.parsePersistedQueryExtension; } });
70
+ Object.defineProperty(exports, "parseGetRequestBody", { enumerable: true, get: function () { return utils_1.parseGetRequestBody; } });
71
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;;;AAEH,SAAS;AACT,uEAAuE;AAA9D,sIAAA,0BAA0B,OAAA;AAEnC,sCAAsC;AACtC,iCAA6C;AAApC,4GAAA,mBAAmB,OAAA;AAE5B,+CAAmE;AAA1D,+GAAA,eAAe,OAAA;AAAE,iHAAA,iBAAiB,OAAA;AAU3C,mCAAwD;AAA/C,uHAAA,6BAA6B,OAAA;AAEtC,SAAS;AACT,mCAOiB;AANf,qHAAA,2BAA2B,OAAA;AAC3B,oHAAA,0BAA0B,OAAA;AAC1B,yHAAA,+BAA+B,OAAA;AAC/B,uHAAA,6BAA6B,OAAA;AAK/B,YAAY;AACZ,iCAAwF;AAA/E,oGAAA,WAAW,OAAA;AAAE,qHAAA,4BAA4B,OAAA;AAAE,4GAAA,mBAAmB,OAAA"}
@@ -0,0 +1,67 @@
1
+ import { Layer } from "effect";
2
+ import { PersistedQueryStore } from "./store";
3
+ /**
4
+ * Configuration for the in-memory LRU store
5
+ */
6
+ export interface MemoryStoreConfig {
7
+ /**
8
+ * Maximum number of queries to cache.
9
+ * When exceeded, least recently used queries are evicted.
10
+ * Default: 1000
11
+ */
12
+ readonly maxSize?: number;
13
+ }
14
+ /**
15
+ * Create an in-memory LRU (Least Recently Used) store for persisted queries.
16
+ *
17
+ * This implementation uses Map's natural insertion order for O(1) LRU operations:
18
+ * - get: O(1) - delete and re-insert to move to end (most recently used)
19
+ * - set: O(1) - insert at end, evict from front if needed
20
+ * - eviction: O(1) - delete first entry (least recently used)
21
+ *
22
+ * This is the default store implementation suitable for single-instance servers.
23
+ * For multi-instance deployments, consider using a shared store like Redis.
24
+ *
25
+ * @param config - Optional configuration for cache size
26
+ * @returns A Layer providing the PersistedQueryStore service
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * import { makeMemoryStore, makePersistedQueriesRouter } from "@effect-gql/persisted-queries"
31
+ *
32
+ * // Default store with 1000 entry limit
33
+ * const router1 = makePersistedQueriesRouter(schema, serviceLayer)
34
+ *
35
+ * // Custom store with larger cache
36
+ * const router2 = makePersistedQueriesRouter(schema, serviceLayer, {
37
+ * store: makeMemoryStore({ maxSize: 5000 })
38
+ * })
39
+ * ```
40
+ */
41
+ export declare const makeMemoryStore: (config?: MemoryStoreConfig) => Layer.Layer<PersistedQueryStore>;
42
+ /**
43
+ * Create a pre-populated safelist store.
44
+ *
45
+ * This store only allows queries that were provided at creation time.
46
+ * Any attempt to store new queries is silently ignored.
47
+ * Use this for production security where you want to allowlist specific operations.
48
+ *
49
+ * @param queries - Record mapping SHA-256 hashes to query strings
50
+ * @returns A Layer providing the PersistedQueryStore service
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * import { makeSafelistStore, makePersistedQueriesRouter } from "@effect-gql/persisted-queries"
55
+ *
56
+ * // Pre-register allowed queries
57
+ * const router = makePersistedQueriesRouter(schema, serviceLayer, {
58
+ * mode: "safelist",
59
+ * store: makeSafelistStore({
60
+ * "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38": "query GetUser($id: ID!) { user(id: $id) { name email } }",
61
+ * "a1b2c3d4...": "query GetPosts { posts { title } }",
62
+ * }),
63
+ * })
64
+ * ```
65
+ */
66
+ export declare const makeSafelistStore: (queries: Record<string, string>) => Layer.Layer<PersistedQueryStore>;
67
+ //# sourceMappingURL=memory-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../src/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,EAAU,MAAM,QAAQ,CAAA;AAC9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAE7C;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,eAAe,GAC1B,SAAQ,iBAAsB,KAC7B,KAAK,CAAC,KAAK,CAAC,mBAAmB,CAgDjC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,iBAAiB,GAC5B,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC9B,KAAK,CAAC,KAAK,CAAC,mBAAmB,CAY/B,CAAA"}
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeSafelistStore = exports.makeMemoryStore = void 0;
4
+ const effect_1 = require("effect");
5
+ const store_1 = require("./store");
6
+ /**
7
+ * Create an in-memory LRU (Least Recently Used) store for persisted queries.
8
+ *
9
+ * This implementation uses Map's natural insertion order for O(1) LRU operations:
10
+ * - get: O(1) - delete and re-insert to move to end (most recently used)
11
+ * - set: O(1) - insert at end, evict from front if needed
12
+ * - eviction: O(1) - delete first entry (least recently used)
13
+ *
14
+ * This is the default store implementation suitable for single-instance servers.
15
+ * For multi-instance deployments, consider using a shared store like Redis.
16
+ *
17
+ * @param config - Optional configuration for cache size
18
+ * @returns A Layer providing the PersistedQueryStore service
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * import { makeMemoryStore, makePersistedQueriesRouter } from "@effect-gql/persisted-queries"
23
+ *
24
+ * // Default store with 1000 entry limit
25
+ * const router1 = makePersistedQueriesRouter(schema, serviceLayer)
26
+ *
27
+ * // Custom store with larger cache
28
+ * const router2 = makePersistedQueriesRouter(schema, serviceLayer, {
29
+ * store: makeMemoryStore({ maxSize: 5000 })
30
+ * })
31
+ * ```
32
+ */
33
+ const makeMemoryStore = (config = {}) => {
34
+ const maxSize = config.maxSize ?? 1000;
35
+ // Map maintains insertion order - we use this for O(1) LRU
36
+ // First entry = least recently used, last entry = most recently used
37
+ const cache = new Map();
38
+ // Move entry to end (most recently used) by deleting and re-inserting
39
+ const touch = (hash, query) => {
40
+ cache.delete(hash);
41
+ cache.set(hash, query);
42
+ };
43
+ // Evict oldest entry (first in Map) if over capacity - O(1)
44
+ const evictIfNeeded = () => {
45
+ if (cache.size <= maxSize)
46
+ return;
47
+ // Map.keys().next() gives us the first (oldest) key in O(1)
48
+ const oldestKey = cache.keys().next().value;
49
+ if (oldestKey !== undefined) {
50
+ cache.delete(oldestKey);
51
+ }
52
+ };
53
+ return effect_1.Layer.succeed(store_1.PersistedQueryStore, store_1.PersistedQueryStore.of({
54
+ get: (hash) => effect_1.Effect.sync(() => {
55
+ const query = cache.get(hash);
56
+ if (query === undefined) {
57
+ return effect_1.Option.none();
58
+ }
59
+ // Move to end (most recently used)
60
+ touch(hash, query);
61
+ return effect_1.Option.some(query);
62
+ }),
63
+ set: (hash, query) => effect_1.Effect.sync(() => {
64
+ // If key exists, delete first to ensure it moves to end
65
+ cache.delete(hash);
66
+ cache.set(hash, query);
67
+ evictIfNeeded();
68
+ }),
69
+ has: (hash) => effect_1.Effect.sync(() => cache.has(hash)),
70
+ }));
71
+ };
72
+ exports.makeMemoryStore = makeMemoryStore;
73
+ /**
74
+ * Create a pre-populated safelist store.
75
+ *
76
+ * This store only allows queries that were provided at creation time.
77
+ * Any attempt to store new queries is silently ignored.
78
+ * Use this for production security where you want to allowlist specific operations.
79
+ *
80
+ * @param queries - Record mapping SHA-256 hashes to query strings
81
+ * @returns A Layer providing the PersistedQueryStore service
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * import { makeSafelistStore, makePersistedQueriesRouter } from "@effect-gql/persisted-queries"
86
+ *
87
+ * // Pre-register allowed queries
88
+ * const router = makePersistedQueriesRouter(schema, serviceLayer, {
89
+ * mode: "safelist",
90
+ * store: makeSafelistStore({
91
+ * "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38": "query GetUser($id: ID!) { user(id: $id) { name email } }",
92
+ * "a1b2c3d4...": "query GetPosts { posts { title } }",
93
+ * }),
94
+ * })
95
+ * ```
96
+ */
97
+ const makeSafelistStore = (queries) => effect_1.Layer.succeed(store_1.PersistedQueryStore, store_1.PersistedQueryStore.of({
98
+ get: (hash) => effect_1.Effect.succeed(queries[hash] !== undefined ? effect_1.Option.some(queries[hash]) : effect_1.Option.none()),
99
+ // No-op for safelist mode - queries cannot be added at runtime
100
+ set: () => effect_1.Effect.void,
101
+ has: (hash) => effect_1.Effect.succeed(queries[hash] !== undefined),
102
+ }));
103
+ exports.makeSafelistStore = makeSafelistStore;
104
+ //# sourceMappingURL=memory-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-store.js","sourceRoot":"","sources":["../src/memory-store.ts"],"names":[],"mappings":";;;AAAA,mCAA8C;AAC9C,mCAA6C;AAc7C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACI,MAAM,eAAe,GAAG,CAC7B,SAA4B,EAAE,EACI,EAAE;IACpC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,IAAI,CAAA;IAEtC,2DAA2D;IAC3D,qEAAqE;IACrE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAA;IAEvC,sEAAsE;IACtE,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,KAAa,EAAQ,EAAE;QAClD,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAClB,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACxB,CAAC,CAAA;IAED,4DAA4D;IAC5D,MAAM,aAAa,GAAG,GAAS,EAAE;QAC/B,IAAI,KAAK,CAAC,IAAI,IAAI,OAAO;YAAE,OAAM;QACjC,4DAA4D;QAC5D,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAA;QAC3C,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;QACzB,CAAC;IACH,CAAC,CAAA;IAED,OAAO,cAAK,CAAC,OAAO,CAClB,2BAAmB,EACnB,2BAAmB,CAAC,EAAE,CAAC;QACrB,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CACZ,eAAM,CAAC,IAAI,CAAC,GAAG,EAAE;YACf,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;YAC7B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,OAAO,eAAM,CAAC,IAAI,EAAU,CAAA;YAC9B,CAAC;YACD,mCAAmC;YACnC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;YAClB,OAAO,eAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC3B,CAAC,CAAC;QAEJ,GAAG,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CACnB,eAAM,CAAC,IAAI,CAAC,GAAG,EAAE;YACf,wDAAwD;YACxD,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;YAClB,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;YACtB,aAAa,EAAE,CAAA;QACjB,CAAC,CAAC;QAEJ,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,eAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;KAClD,CAAC,CACH,CAAA;AACH,CAAC,CAAA;AAlDY,QAAA,eAAe,mBAkD3B;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACI,MAAM,iBAAiB,GAAG,CAC/B,OAA+B,EACG,EAAE,CACpC,cAAK,CAAC,OAAO,CACX,2BAAmB,EACnB,2BAAmB,CAAC,EAAE,CAAC;IACrB,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CACZ,eAAM,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,eAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,eAAM,CAAC,IAAI,EAAE,CAAC;IAE1F,+DAA+D;IAC/D,GAAG,EAAE,GAAG,EAAE,CAAC,eAAM,CAAC,IAAI;IAEtB,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,eAAM,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;CAC3D,CAAC,CACH,CAAA;AAdU,QAAA,iBAAiB,qBAc3B"}