bhooai-nexus 0.1.2
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/README.md +184 -0
- package/apps/admin/index.html +12 -0
- package/apps/admin/package.json +25 -0
- package/apps/admin/postcss.config.js +6 -0
- package/apps/admin/public/anime-girl.svg +84 -0
- package/apps/admin/public/bhooai-nexus-logo.png +0 -0
- package/apps/admin/public/bhooai-nexus-logo.svg +135 -0
- package/apps/admin/public/favicon.ico +0 -0
- package/apps/admin/src/main.tsx +10 -0
- package/apps/admin/src/vite-env.d.ts +18 -0
- package/apps/admin/tailwind.config.js +10 -0
- package/apps/admin/tsconfig.json +17 -0
- package/apps/admin/vite.config.ts +68 -0
- package/bin/nexus.js +52 -0
- package/package.json +108 -0
- package/packages/nexus-admin/package.json +32 -0
- package/packages/nexus-admin/src/App.tsx +4878 -0
- package/packages/nexus-admin/src/alertCenter.tsx +150 -0
- package/packages/nexus-admin/src/api.ts +601 -0
- package/packages/nexus-admin/src/assets/bhooai-nexus-logo.svg +25 -0
- package/packages/nexus-admin/src/index.css +3635 -0
- package/packages/nexus-admin/src/vite-env.d.ts +19 -0
- package/packages/nexus-ads/package.json +20 -0
- package/packages/nexus-ads/src/GaqlBuilder.ts +72 -0
- package/packages/nexus-ads/src/GoogleAdsClient.ts +121 -0
- package/packages/nexus-ads/src/fetchTransport.ts +10 -0
- package/packages/nexus-ads/src/index.ts +20 -0
- package/packages/nexus-ads/src/types.ts +41 -0
- package/packages/nexus-ai-client/package.json +18 -0
- package/packages/nexus-ai-client/src/client.ts +159 -0
- package/packages/nexus-ai-client/src/errors.ts +21 -0
- package/packages/nexus-ai-client/src/index.ts +4 -0
- package/packages/nexus-ai-client/src/sse.ts +53 -0
- package/packages/nexus-ai-client/src/types.ts +174 -0
- package/packages/nexus-auth/package.json +22 -0
- package/packages/nexus-auth/src/auth.ts +116 -0
- package/packages/nexus-auth/src/bcryptjs.d.ts +10 -0
- package/packages/nexus-auth/src/cors.ts +91 -0
- package/packages/nexus-auth/src/csrf.ts +158 -0
- package/packages/nexus-auth/src/headers.ts +55 -0
- package/packages/nexus-auth/src/index.ts +11 -0
- package/packages/nexus-auth/src/jwt.ts +65 -0
- package/packages/nexus-auth/src/middleware.ts +94 -0
- package/packages/nexus-auth/src/oauth.ts +199 -0
- package/packages/nexus-auth/src/password.ts +33 -0
- package/packages/nexus-auth/src/rateLimit.ts +59 -0
- package/packages/nexus-auth/src/rbac.ts +94 -0
- package/packages/nexus-auth/src/session.ts +91 -0
- package/packages/nexus-cache/package.json +21 -0
- package/packages/nexus-cache/src/Cache.ts +155 -0
- package/packages/nexus-cache/src/PubSub.ts +98 -0
- package/packages/nexus-cache/src/RateLimiter.ts +43 -0
- package/packages/nexus-cache/src/index.ts +9 -0
- package/packages/nexus-cache/src/types.ts +47 -0
- package/packages/nexus-cli/package.json +25 -0
- package/packages/nexus-cli/src/commands/cluster.ts +133 -0
- package/packages/nexus-cli/src/commands/dev.ts +200 -0
- package/packages/nexus-cli/src/commands/doctor.ts +199 -0
- package/packages/nexus-cli/src/commands/init.ts +1009 -0
- package/packages/nexus-cli/src/commands/node.ts +106 -0
- package/packages/nexus-cli/src/commands/pysetup.ts +142 -0
- package/packages/nexus-cli/src/commands/sync.ts +116 -0
- package/packages/nexus-cli/src/commands/uninstall.ts +287 -0
- package/packages/nexus-cli/src/config-sync.ts +384 -0
- package/packages/nexus-cli/src/dotenv.ts +39 -0
- package/packages/nexus-cli/src/index.ts +94 -0
- package/packages/nexus-cli/src/supervisor.ts +394 -0
- package/packages/nexus-cli/src/util.ts +132 -0
- package/packages/nexus-cli/src/wizard.ts +149 -0
- package/packages/nexus-cli/templates/Dockerfile +60 -0
- package/packages/nexus-cli/templates/README.md +69 -0
- package/packages/nexus-cli/templates/apps/admin/index.html +12 -0
- package/packages/nexus-cli/templates/apps/admin/package.json +24 -0
- package/packages/nexus-cli/templates/apps/admin/postcss.config.js +6 -0
- package/packages/nexus-cli/templates/apps/admin/src/main.tsx +10 -0
- package/packages/nexus-cli/templates/apps/admin/src/vite-env.d.ts +18 -0
- package/packages/nexus-cli/templates/apps/admin/tailwind.config.js +9 -0
- package/packages/nexus-cli/templates/apps/admin/tsconfig.json +17 -0
- package/packages/nexus-cli/templates/apps/admin/vite.config.ts +64 -0
- package/packages/nexus-cli/templates/apps/ai-server/main.py +43 -0
- package/packages/nexus-cli/templates/apps/ai-server/providers/__init__.py +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/providers/base.py +111 -0
- package/packages/nexus-cli/templates/apps/ai-server/requirements.txt +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/__init__.py +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/chat.py +47 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/embeddings.py +30 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/lint.py +167 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/models.py +23 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/preflight.py +169 -0
- package/packages/nexus-cli/templates/apps/ai-server/settings.py +48 -0
- package/packages/nexus-cli/templates/apps/backend/package.json +34 -0
- package/packages/nexus-cli/templates/apps/backend/src/main.ts +375 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/adminRoutes.ts +732 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/clusterRoutes.ts +391 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/databaseRoutes.ts +161 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/lintProxy.ts +89 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/preflightProxy.ts +242 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/roleCatalog.ts +78 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/schemaRoutes.ts +449 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/ai/aiProxy.ts +265 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/auth/authRoutes.ts +220 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/payments/paymentRoutes.ts +100 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/payments/paymentStore.ts +172 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/requests/requestLog.ts +175 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/users/userGraph.ts +83 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/users/userModel.ts +88 -0
- package/packages/nexus-cli/templates/apps/backend/src/plugins/CronScheduler.ts +69 -0
- package/packages/nexus-cli/templates/apps/backend/src/plugins/loadPlugins.ts +107 -0
- package/packages/nexus-cli/templates/apps/backend/tsconfig.json +14 -0
- package/packages/nexus-cli/templates/apps/frontend/index.html +12 -0
- package/packages/nexus-cli/templates/apps/frontend/package.json +19 -0
- package/packages/nexus-cli/templates/apps/frontend/src/main.tsx +64 -0
- package/packages/nexus-cli/templates/apps/frontend/vite.config.ts +72 -0
- package/packages/nexus-cli/templates/bin/nexus.js +35 -0
- package/packages/nexus-cli/templates/bin/serve-all.mjs +45 -0
- package/packages/nexus-cli/templates/dockerignore +15 -0
- package/packages/nexus-cli/templates/gitignore +12 -0
- package/packages/nexus-cli/templates/nexus.config.ts +69 -0
- package/packages/nexus-cli/templates/package.json +47 -0
- package/packages/nexus-cli/templates/tsconfig.json +17 -0
- package/packages/nexus-cli/templates/uploads/.gitkeep +0 -0
- package/packages/nexus-core/package.json +38 -0
- package/packages/nexus-core/src/config/ConfigLoader.ts +161 -0
- package/packages/nexus-core/src/config/defaults.ts +155 -0
- package/packages/nexus-core/src/config/env.ts +113 -0
- package/packages/nexus-core/src/config/index.ts +6 -0
- package/packages/nexus-core/src/config/merge.ts +28 -0
- package/packages/nexus-core/src/config/schema.ts +184 -0
- package/packages/nexus-core/src/config/types.ts +352 -0
- package/packages/nexus-core/src/di/Container.ts +98 -0
- package/packages/nexus-core/src/di/index.ts +1 -0
- package/packages/nexus-core/src/errors.ts +62 -0
- package/packages/nexus-core/src/http/Router.ts +149 -0
- package/packages/nexus-core/src/http/Server.ts +145 -0
- package/packages/nexus-core/src/http/bodyParser.ts +129 -0
- package/packages/nexus-core/src/http/context.ts +112 -0
- package/packages/nexus-core/src/http/index.ts +6 -0
- package/packages/nexus-core/src/http/static.ts +85 -0
- package/packages/nexus-core/src/http/uploads.ts +94 -0
- package/packages/nexus-core/src/index.ts +4 -0
- package/packages/nexus-crypto/package.json +18 -0
- package/packages/nexus-crypto/src/csr.ts +165 -0
- package/packages/nexus-crypto/src/der.ts +262 -0
- package/packages/nexus-crypto/src/https.ts +36 -0
- package/packages/nexus-crypto/src/index.ts +5 -0
- package/packages/nexus-crypto/src/keypair.ts +59 -0
- package/packages/nexus-crypto/src/x509.ts +156 -0
- package/packages/nexus-data/package.json +27 -0
- package/packages/nexus-data/src/connection/Connection.ts +57 -0
- package/packages/nexus-data/src/connection/ConnectionManager.ts +37 -0
- package/packages/nexus-data/src/errors.ts +17 -0
- package/packages/nexus-data/src/index.ts +66 -0
- package/packages/nexus-data/src/model/Document.ts +126 -0
- package/packages/nexus-data/src/model/Model.ts +189 -0
- package/packages/nexus-data/src/model/hydrate.ts +12 -0
- package/packages/nexus-data/src/populate/populate.ts +114 -0
- package/packages/nexus-data/src/projects.ts +185 -0
- package/packages/nexus-data/src/query/Query.ts +158 -0
- package/packages/nexus-data/src/schema/Schema.ts +173 -0
- package/packages/nexus-data/src/schema/SchemaType.ts +80 -0
- package/packages/nexus-data/src/schema/validators.ts +88 -0
- package/packages/nexus-email/package.json +22 -0
- package/packages/nexus-email/src/EmailService.ts +76 -0
- package/packages/nexus-email/src/TemplateEngine.ts +62 -0
- package/packages/nexus-email/src/index.ts +37 -0
- package/packages/nexus-email/src/providers.ts +63 -0
- package/packages/nexus-email/src/queue.ts +103 -0
- package/packages/nexus-email/src/types.ts +49 -0
- package/packages/nexus-graphql/package.json +24 -0
- package/packages/nexus-graphql/src/federation/composeSupergraph.ts +153 -0
- package/packages/nexus-graphql/src/federation/createFederatedGateway.ts +36 -0
- package/packages/nexus-graphql/src/federation/executor.ts +305 -0
- package/packages/nexus-graphql/src/federation/parseMetadata.ts +87 -0
- package/packages/nexus-graphql/src/gateway/createGateway.ts +77 -0
- package/packages/nexus-graphql/src/gateway/fieldResolver.ts +45 -0
- package/packages/nexus-graphql/src/gateway/graphqlHttpHandler.ts +90 -0
- package/packages/nexus-graphql/src/gateway/publicSchema.ts +26 -0
- package/packages/nexus-graphql/src/index.ts +14 -0
- package/packages/nexus-graphql/src/subgraph/defineSubgraph.ts +115 -0
- package/packages/nexus-graphql/src/subgraph/federationDirectives.ts +47 -0
- package/packages/nexus-graphql/src/subgraph/parseKeys.ts +25 -0
- package/packages/nexus-graphql/src/subscriptions/PubSub.ts +64 -0
- package/packages/nexus-graphql/src/subscriptions/SubscriptionServer.ts +203 -0
- package/packages/nexus-graphql/src/types.ts +77 -0
- package/packages/nexus-payments/package.json +20 -0
- package/packages/nexus-payments/src/errors.ts +15 -0
- package/packages/nexus-payments/src/http.ts +53 -0
- package/packages/nexus-payments/src/index.ts +106 -0
- package/packages/nexus-payments/src/providers/PayPalProvider.ts +192 -0
- package/packages/nexus-payments/src/providers/PayUProvider.ts +164 -0
- package/packages/nexus-payments/src/providers/PayoneerProvider.ts +122 -0
- package/packages/nexus-payments/src/providers/RazorpayProvider.ts +184 -0
- package/packages/nexus-payments/src/providers/SkrillProvider.ts +117 -0
- package/packages/nexus-payments/src/signature.ts +24 -0
- package/packages/nexus-payments/src/types.ts +150 -0
- package/packages/nexus-payments/src/webhooks.ts +58 -0
- package/packages/nexus-plugins/package.json +20 -0
- package/packages/nexus-plugins/src/HookBus.ts +42 -0
- package/packages/nexus-plugins/src/admin/adminExtensions.ts +53 -0
- package/packages/nexus-plugins/src/host.ts +105 -0
- package/packages/nexus-plugins/src/index.ts +8 -0
- package/packages/nexus-plugins/src/manifest.ts +73 -0
- package/packages/nexus-plugins/src/registry.ts +123 -0
- package/packages/nexus-plugins/src/runtime/TrustedPlugin.ts +134 -0
- package/packages/nexus-plugins/src/sandbox/SandboxedPlugin.ts +279 -0
- package/packages/nexus-plugins/src/sandbox/capabilityPolicy.ts +68 -0
- package/packages/nexus-plugins/src/sandbox/workerBootstrap.mjs +211 -0
- package/packages/nexus-realtime/package.json +26 -0
- package/packages/nexus-realtime/src/adapter/PubSubAdapter.ts +42 -0
- package/packages/nexus-realtime/src/adapter/RedisPubSubAdapter.ts +59 -0
- package/packages/nexus-realtime/src/index.ts +5 -0
- package/packages/nexus-realtime/src/mediasoup/MediasoupAdapter.ts +176 -0
- package/packages/nexus-realtime/src/server/RealtimeServer.ts +252 -0
- package/packages/nexus-realtime/src/types.ts +52 -0
- package/packages/nexus-telemetry/package.json +20 -0
- package/packages/nexus-telemetry/src/index.ts +3 -0
- package/packages/nexus-telemetry/src/logger/Logger.ts +178 -0
- package/packages/nexus-telemetry/src/metrics/Metrics.ts +123 -0
- package/packages/nexus-telemetry/src/trace.ts +28 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { buildSchema, parse, validate, GraphQLSchema } from 'graphql';
|
|
2
|
+
import type { DefineSubgraphOptions, Subgraph, Resolvers, GraphQLContext, EntityRepresentation, EntityResolver } from '../types.js';
|
|
3
|
+
import { FEDERATION_DIRECTIVE_SDL, FEDERATION_LINK, FEDERATION_TYPES, entityUnionSdl } from './federationDirectives.js';
|
|
4
|
+
import { parseEntityKeys } from './parseKeys.js';
|
|
5
|
+
import { createFieldResolver } from '../gateway/fieldResolver.js';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_ENTITY_RESOLVER: EntityResolver = (rep) => rep;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Define a federation-compliant subgraph from user typeDefs + resolvers.
|
|
11
|
+
*
|
|
12
|
+
* The user SDL may use federation directives (`@key`, `@external`, `@requires`,
|
|
13
|
+
* `@provides`, `@extends`). We prepend the directive definitions + the `@link`
|
|
14
|
+
* clause and append the federation boilerplate (`_Any`, `_Entity`, `_Service`,
|
|
15
|
+
* `Query._service`, `Query._entities`) so the resulting schema is a valid
|
|
16
|
+
* subgraph: `_service.sdl` returns the subgraph SDL and `_entities` resolves
|
|
17
|
+
* representations via the provided `entityResolver` (default: pass-through).
|
|
18
|
+
*
|
|
19
|
+
* `defineSubgraph` does NOT compose — it produces a single executable subgraph.
|
|
20
|
+
* The gateway (`createGateway`) executes it in-process for the single-subgraph
|
|
21
|
+
* case; Phase 6 composes multiple subgraphs.
|
|
22
|
+
*/
|
|
23
|
+
export function defineSubgraph(options: DefineSubgraphOptions): Subgraph {
|
|
24
|
+
const { name, typeDefs, resolvers = {}, entityResolver = DEFAULT_ENTITY_RESOLVER } = options;
|
|
25
|
+
|
|
26
|
+
const entityKeys = parseEntityKeys(typeDefs);
|
|
27
|
+
const entityUnion = entityUnionSdl([...entityKeys.keys()]);
|
|
28
|
+
|
|
29
|
+
// Subgraph SDL = what _service.sdl returns: the user SDL + the @link clause.
|
|
30
|
+
// (We keep the federation directives in the published SDL — that's the point.)
|
|
31
|
+
const sdl = `${FEDERATION_LINK}\n\n${typeDefs.trim()}\n`;
|
|
32
|
+
|
|
33
|
+
// Executable schema: directive defs + @link + user SDL + federation types + _Entity union.
|
|
34
|
+
const fullSdl = [
|
|
35
|
+
FEDERATION_DIRECTIVE_SDL,
|
|
36
|
+
FEDERATION_LINK,
|
|
37
|
+
typeDefs,
|
|
38
|
+
FEDERATION_TYPES,
|
|
39
|
+
entityUnion,
|
|
40
|
+
].join('\n\n');
|
|
41
|
+
|
|
42
|
+
let schema: GraphQLSchema;
|
|
43
|
+
try {
|
|
44
|
+
schema = buildSchema(fullSdl, { assumeValid: false });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
throw new Error(`[nexus-graphql] Failed to build subgraph schema for "${name}": ${(err as Error).message}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The `_Entity` union needs a resolveType so graphql-js can pick the concrete
|
|
50
|
+
// member type for each resolved entity. Entities carry `__typename` from
|
|
51
|
+
// their representation; we also backfill it onto results that lack it.
|
|
52
|
+
const entityType = schema.getType('_Entity') as
|
|
53
|
+
| { resolveType?: (value: any) => string | null }
|
|
54
|
+
| undefined;
|
|
55
|
+
if (entityType && 'resolveType' in entityType) {
|
|
56
|
+
entityType.resolveType = (value: any) => (value && typeof value === 'object' ? (value as any).__typename ?? null : null);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Auto-wire the federation fields into the resolver map (merged, not mutated).
|
|
60
|
+
const federationResolvers: Resolvers = {
|
|
61
|
+
...resolvers,
|
|
62
|
+
_Any: {
|
|
63
|
+
// Custom scalar: pass-through serialize/parse.
|
|
64
|
+
__serialize: (value: unknown) => value,
|
|
65
|
+
__parseValue: (value: unknown) => value,
|
|
66
|
+
__parseLiteral: (value: unknown) => value,
|
|
67
|
+
} as unknown as Resolvers['_Any'],
|
|
68
|
+
Query: {
|
|
69
|
+
...(resolvers.Query ?? {}),
|
|
70
|
+
_service: () => ({ sdl }),
|
|
71
|
+
_entities: (_parent: unknown, args: Record<string, any>, ctx: GraphQLContext) =>
|
|
72
|
+
Promise.all(((args.representations as EntityRepresentation[] | undefined) ?? []).map(async (rep) => {
|
|
73
|
+
const resolved = await entityResolver(rep, ctx);
|
|
74
|
+
if (resolved && typeof resolved === 'object' && !('__typename' in resolved)) {
|
|
75
|
+
(resolved as Record<string, unknown>).__typename = rep.__typename;
|
|
76
|
+
}
|
|
77
|
+
return resolved;
|
|
78
|
+
})),
|
|
79
|
+
},
|
|
80
|
+
_Service: {
|
|
81
|
+
sdl: (parent: { sdl?: string }) => parent.sdl ?? sdl,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Validate the SDL parses + that the federation directives are used correctly
|
|
86
|
+
// (e.g. @key has a `fields` arg). buildSchema already enforces directive
|
|
87
|
+
// argument types/locations; we additionally assert the SDL parses standalone.
|
|
88
|
+
parse(sdl);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
name,
|
|
92
|
+
schema,
|
|
93
|
+
sdl,
|
|
94
|
+
entityKeys,
|
|
95
|
+
resolvers: federationResolvers,
|
|
96
|
+
entityResolver,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validate a query against a schema before execution (returns errors array).
|
|
102
|
+
* Useful for the HTTP handler to reject malformed operations early.
|
|
103
|
+
*/
|
|
104
|
+
export function validateOperation(schema: GraphQLSchema, source: string) {
|
|
105
|
+
let doc;
|
|
106
|
+
try {
|
|
107
|
+
doc = parse(source);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
return [(err as Error).message];
|
|
110
|
+
}
|
|
111
|
+
return validate(schema, doc).map((e) => e.message);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Re-export for the gateway to share the resolver wiring. */
|
|
115
|
+
export { createFieldResolver };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation directive definitions and the `@link` schema directive.
|
|
3
|
+
*
|
|
4
|
+
* These are prepended to the user's typeDefs so `buildSchema` accepts the
|
|
5
|
+
* federation directives (`@key`, `@external`, `@requires`, `@provides`,
|
|
6
|
+
* `@extends`). We target a Federation v2-style subset (the same directives
|
|
7
|
+
* Apollo uses); composition (Phase 6) is ours, but the directive *surface*
|
|
8
|
+
* matches the spec so subgraph SDL is interoperable.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: `@link` is declared so `extend schema @link(...)` parses. We do not
|
|
11
|
+
* enforce the import list — the directives are always declared and available.
|
|
12
|
+
*/
|
|
13
|
+
export const FEDERATION_DIRECTIVE_SDL = /* graphql */ `
|
|
14
|
+
directive @link(url: String!, import: [String!]) on SCHEMA
|
|
15
|
+
directive @key(fields: String!) repeatable on OBJECT | INTERFACE
|
|
16
|
+
directive @external on FIELD_DEFINITION
|
|
17
|
+
directive @requires(fields: String!) on FIELD_DEFINITION
|
|
18
|
+
directive @provides(fields: String!) on FIELD_DEFINITION
|
|
19
|
+
directive @extends on OBJECT | INTERFACE | FIELD_DEFINITION
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
/** The `extend schema @link(...)` clause advertising the federation import. */
|
|
23
|
+
export const FEDERATION_LINK = /* graphql */ `
|
|
24
|
+
extend schema @link(
|
|
25
|
+
url: "https://specs.apollo.dev/federation/v2.0"
|
|
26
|
+
import: ["@key", "@external", "@requires", "@provides", "@extends"]
|
|
27
|
+
)
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
/** Federation boilerplate types prepended/appended to make a schema subgraph-compliant. */
|
|
31
|
+
export const FEDERATION_TYPES = /* graphql */ `
|
|
32
|
+
scalar _Any
|
|
33
|
+
type _Service { sdl: String! }
|
|
34
|
+
extend type Query {
|
|
35
|
+
_service: _Service!
|
|
36
|
+
_entities(representations: [_Any!]!): [_Entity]!
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
/** Build the `_Entity` union SDL for the given entity type names (or a scalar if none). */
|
|
41
|
+
export function entityUnionSdl(entityTypeNames: string[]): string {
|
|
42
|
+
if (entityTypeNames.length === 0) return `scalar _Entity`;
|
|
43
|
+
return `union _Entity = ${entityTypeNames.join(' | ')}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Names of the federation directives we recognize (for stripping when building the public schema). */
|
|
47
|
+
export const FEDERATION_DIRECTIVE_NAMES = ['key', 'external', 'requires', 'provides', 'extends', 'link'];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse `@key(fields: "...")` directives from subgraph SDL to learn which types
|
|
3
|
+
* are entities and what their key field paths are. This is a pragmatic regex scan
|
|
4
|
+
* (not a full SDL parser) — sufficient for the v1 federation subset and for the
|
|
5
|
+
* composition step in Phase 6. Composite keys like `@key(fields: "id tenant")` are
|
|
6
|
+
* supported (space-separated, as in the spec).
|
|
7
|
+
*/
|
|
8
|
+
export function parseEntityKeys(typeDefs: string): Map<string, string[]> {
|
|
9
|
+
const keys = new Map<string, string[]>();
|
|
10
|
+
// Match `type X ... {` or `extend type X ... {` (and interfaces) up to the body.
|
|
11
|
+
const typeHeader = /(?:extend\s+)?(?:type|interface)\s+(\w+)([^{]*)\{/g;
|
|
12
|
+
let m: RegExpExecArray | null;
|
|
13
|
+
while ((m = typeHeader.exec(typeDefs)) !== null) {
|
|
14
|
+
const name = m[1]!;
|
|
15
|
+
const header = m[2]!;
|
|
16
|
+
const keyFields: string[] = [];
|
|
17
|
+
const keyRe = /@key\s*\(\s*fields:\s*"([^"]+)"\s*\)/g;
|
|
18
|
+
let k: RegExpExecArray | null;
|
|
19
|
+
while ((k = keyRe.exec(header)) !== null) {
|
|
20
|
+
keyFields.push(...k[1]!.split(/\s+/).filter(Boolean));
|
|
21
|
+
}
|
|
22
|
+
if (keyFields.length) keys.set(name, keyFields);
|
|
23
|
+
}
|
|
24
|
+
return keys;
|
|
25
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal in-process PubSub for GraphQL subscriptions. Sufficient for the
|
|
5
|
+
* single-instance default app; a Redis-backed pub/sub adapter can be plugged in
|
|
6
|
+
* later for cross-instance fanout (mirrors the realtime adapter pattern).
|
|
7
|
+
*
|
|
8
|
+
* Usage in a subscription resolver:
|
|
9
|
+
* Subscription: {
|
|
10
|
+
* messageAdded: {
|
|
11
|
+
* subscribe: () => pubsub.asyncIterator('MESSAGE_ADDED'),
|
|
12
|
+
* resolve: (payload) => payload,
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
export class PubSub {
|
|
17
|
+
private ee = new EventEmitter();
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
this.ee.setMaxListeners(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Publish a payload to a topic. */
|
|
24
|
+
publish(topic: string, payload: unknown): void {
|
|
25
|
+
this.ee.emit(topic, payload);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Return an AsyncIterable that yields payloads published to the topic(s). */
|
|
29
|
+
asyncIterator(topics: string | string[]): AsyncIterable<any> {
|
|
30
|
+
const list = Array.isArray(topics) ? topics : [topics];
|
|
31
|
+
const ee = this.ee;
|
|
32
|
+
const queue: any[] = [];
|
|
33
|
+
let pull: ((v: IteratorResult<any>) => void) | undefined;
|
|
34
|
+
|
|
35
|
+
for (const t of list) {
|
|
36
|
+
const push = (payload: any) => {
|
|
37
|
+
if (pull) {
|
|
38
|
+
const resolve = pull;
|
|
39
|
+
pull = undefined;
|
|
40
|
+
resolve({ value: payload, done: false });
|
|
41
|
+
} else {
|
|
42
|
+
queue.push(payload);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
ee.on(t, push);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
[Symbol.asyncIterator]() {
|
|
50
|
+
return {
|
|
51
|
+
next(): Promise<IteratorResult<any>> {
|
|
52
|
+
if (queue.length) return Promise.resolve({ value: queue.shift(), done: false });
|
|
53
|
+
return new Promise((resolve) => { pull = resolve; });
|
|
54
|
+
},
|
|
55
|
+
return(): Promise<IteratorResult<any>> {
|
|
56
|
+
// Unsubscribe listeners on early completion (client disconnect).
|
|
57
|
+
for (const t of list) ee.removeAllListeners(t);
|
|
58
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
|
+
import type { IncomingMessage, Server as HttpServer } from 'node:http';
|
|
3
|
+
import type { Duplex } from 'node:stream';
|
|
4
|
+
import { parse, validate } from 'graphql';
|
|
5
|
+
import type { AuthService } from '@bhooai/nexus-auth';
|
|
6
|
+
import type { Gateway, GraphQLContext } from '../types.js';
|
|
7
|
+
|
|
8
|
+
export interface SubscriptionServerOptions {
|
|
9
|
+
httpServer: HttpServer;
|
|
10
|
+
gateway: Gateway;
|
|
11
|
+
/** WS path (default '/graphql/ws'). */
|
|
12
|
+
path?: string;
|
|
13
|
+
/** Optional auth: verifies access token from connection_init payload.token or ?token=. */
|
|
14
|
+
authService?: AuthService;
|
|
15
|
+
/** Build per-connection resolver context. Default: `{ user }` from auth. */
|
|
16
|
+
context?: (init: Record<string, unknown> | undefined, user: GraphQLContext['user']) => GraphQLContext | Promise<GraphQLContext>;
|
|
17
|
+
/** Reject unauthenticated connections (default true when authService provided). */
|
|
18
|
+
requireAuth?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ClientMsg =
|
|
22
|
+
| { type: 'connection_init'; payload?: Record<string, unknown> }
|
|
23
|
+
| { type: 'subscribe'; id: string; payload: { query: string; variables?: Record<string, any>; operationName?: string } }
|
|
24
|
+
| { type: 'complete'; id: string }
|
|
25
|
+
| { type: 'ping' }
|
|
26
|
+
| { type: 'pong' };
|
|
27
|
+
|
|
28
|
+
interface ActiveSub {
|
|
29
|
+
iterator: AsyncIterator<unknown>;
|
|
30
|
+
done: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PROTOCOL = 'graphql-transport-ws';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* GraphQL subscriptions over WebSocket (the `graphql-transport-ws` protocol).
|
|
37
|
+
* Attaches to an HTTP server's 'upgrade' event. On `connection_init` we
|
|
38
|
+
* (optionally) authenticate; on `subscribe` we run the gateway's `subscribe()`
|
|
39
|
+
* and stream each `next` result back until the client sends `complete` or the
|
|
40
|
+
* socket closes.
|
|
41
|
+
*/
|
|
42
|
+
export class SubscriptionServer {
|
|
43
|
+
private wss: WebSocketServer;
|
|
44
|
+
private opts: SubscriptionServerOptions;
|
|
45
|
+
|
|
46
|
+
constructor(opts: SubscriptionServerOptions) {
|
|
47
|
+
this.opts = opts;
|
|
48
|
+
this.wss = new WebSocketServer({ noServer: true });
|
|
49
|
+
this.opts.httpServer.on('upgrade', (req, socket, head) => this.handleUpgrade(req, socket, head));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private async handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): Promise<void> {
|
|
53
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
54
|
+
if (url.pathname !== (this.opts.path ?? '/graphql/ws')) return;
|
|
55
|
+
|
|
56
|
+
const protocols = (req.headers['sec-websocket-protocol'] ?? '').toString().split(',').map((s) => s.trim());
|
|
57
|
+
if (!protocols.includes(PROTOCOL)) {
|
|
58
|
+
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
59
|
+
socket.destroy();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
64
|
+
this.handleConnection(ws, url);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private async handleConnection(ws: WebSocket, url: URL): Promise<void> {
|
|
69
|
+
let user: GraphQLContext['user'];
|
|
70
|
+
let acknowledged = false;
|
|
71
|
+
const active = new Map<string, ActiveSub>();
|
|
72
|
+
|
|
73
|
+
const send = (msg: unknown) => { if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(msg)); };
|
|
74
|
+
|
|
75
|
+
const cleanup = async () => {
|
|
76
|
+
for (const [, sub] of active) {
|
|
77
|
+
if (!sub.done) { sub.done = true; try { await sub.iterator.return?.(); } catch { /* ignore */ } }
|
|
78
|
+
}
|
|
79
|
+
active.clear();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// If auth is required and no token is supplied via ?token=, we can reject
|
|
83
|
+
// early; but the protocol sends the token in connection_init, so we wait.
|
|
84
|
+
ws.on('message', async (data) => {
|
|
85
|
+
let msg: ClientMsg;
|
|
86
|
+
try { msg = JSON.parse(data.toString('utf8')) as ClientMsg; } catch { return; }
|
|
87
|
+
|
|
88
|
+
switch (msg.type) {
|
|
89
|
+
case 'connection_init': {
|
|
90
|
+
try {
|
|
91
|
+
user = await this.authenticate(msg.payload, url);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
send({ type: 'error', id: '', payload: [{ message: (err as Error).message }] });
|
|
94
|
+
ws.close(4401, 'Unauthorized');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
acknowledged = true;
|
|
98
|
+
send({ type: 'connection_ack' });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
case 'ping':
|
|
102
|
+
send({ type: 'pong' });
|
|
103
|
+
return;
|
|
104
|
+
case 'pong':
|
|
105
|
+
return;
|
|
106
|
+
case 'subscribe': {
|
|
107
|
+
if (!acknowledged) { send({ type: 'error', id: msg.id, payload: [{ message: 'connection not acknowledged' }] }); return; }
|
|
108
|
+
await this.runSubscription(msg, ws, active, user);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
case 'complete': {
|
|
112
|
+
const sub = active.get(msg.id);
|
|
113
|
+
if (sub && !sub.done) { sub.done = true; try { await sub.iterator.return?.(); } catch { /* ignore */ } }
|
|
114
|
+
active.delete(msg.id);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
ws.on('close', () => void cleanup());
|
|
121
|
+
ws.on('error', () => void cleanup());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async authenticate(payload: Record<string, unknown> | undefined, url: URL): Promise<GraphQLContext['user']> {
|
|
125
|
+
const requireAuth = this.opts.requireAuth ?? !!this.opts.authService;
|
|
126
|
+
if (!this.opts.authService) {
|
|
127
|
+
if (requireAuth) throw new Error('Authentication required but no authService configured');
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const token = (payload?.token as string | undefined) ?? url.searchParams.get('token');
|
|
131
|
+
if (!token) {
|
|
132
|
+
if (requireAuth) throw new Error('Missing access token');
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const claims = await this.opts.authService.verifyAccessToken(token);
|
|
136
|
+
return { sub: claims.sub, roles: claims.roles ?? [], sid: claims.sid };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private async runSubscription(
|
|
140
|
+
msg: { type: 'subscribe'; id: string; payload: { query: string; variables?: Record<string, any>; operationName?: string } },
|
|
141
|
+
ws: WebSocket,
|
|
142
|
+
active: Map<string, ActiveSub>,
|
|
143
|
+
user: GraphQLContext['user'],
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
const send = (m: unknown) => { if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(m)); };
|
|
146
|
+
|
|
147
|
+
let document;
|
|
148
|
+
try {
|
|
149
|
+
document = parse(msg.payload.query);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
send({ type: 'error', id: msg.id, payload: [{ message: (err as Error).message }] });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const errors = validate(this.opts.gateway.schema, document);
|
|
156
|
+
if (errors.length) {
|
|
157
|
+
send({ type: 'error', id: msg.id, payload: errors.map((e) => ({ message: e.message })) });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const contextValue = this.opts.context
|
|
162
|
+
? await this.opts.context(undefined, user)
|
|
163
|
+
: { user };
|
|
164
|
+
|
|
165
|
+
let stream;
|
|
166
|
+
try {
|
|
167
|
+
stream = await this.opts.gateway.subscribe({
|
|
168
|
+
document,
|
|
169
|
+
variableValues: msg.payload.variables,
|
|
170
|
+
operationName: msg.payload.operationName,
|
|
171
|
+
contextValue,
|
|
172
|
+
});
|
|
173
|
+
} catch (err) {
|
|
174
|
+
send({ type: 'error', id: msg.id, payload: [{ message: (err as Error).message }] });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const sub: ActiveSub = { iterator: stream[Symbol.asyncIterator](), done: false };
|
|
179
|
+
active.set(msg.id, sub);
|
|
180
|
+
|
|
181
|
+
// Pump results asynchronously; stop when the client completes or the stream ends.
|
|
182
|
+
void (async () => {
|
|
183
|
+
try {
|
|
184
|
+
while (!sub.done) {
|
|
185
|
+
const { value, done } = await sub.iterator.next();
|
|
186
|
+
if (done) break;
|
|
187
|
+
if (ws.readyState !== ws.OPEN) break;
|
|
188
|
+
send({ type: 'next', id: msg.id, payload: value });
|
|
189
|
+
}
|
|
190
|
+
if (!sub.done && ws.readyState === ws.OPEN) send({ type: 'complete', id: msg.id });
|
|
191
|
+
} catch (err) {
|
|
192
|
+
send({ type: 'error', id: msg.id, payload: [{ message: (err as Error).message }] });
|
|
193
|
+
} finally {
|
|
194
|
+
active.delete(msg.id);
|
|
195
|
+
}
|
|
196
|
+
})();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
close(): Promise<void> {
|
|
200
|
+
this.wss.close();
|
|
201
|
+
return Promise.resolve();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { GraphQLSchema, ExecutionResult, DocumentNode } from 'graphql';
|
|
2
|
+
|
|
3
|
+
/** A field resolver: (parent, args, context, info) => value | Promise<value>. */
|
|
4
|
+
export type Resolver = (parent: any, args: Record<string, any>, context: GraphQLContext, info: any) => any | Promise<any>;
|
|
5
|
+
|
|
6
|
+
/** A subscribe resolver returning an AsyncIterable of event payloads. */
|
|
7
|
+
export type SubscribeResolver = (parent: any, args: Record<string, any>, context: GraphQLContext, info: any) => AsyncIterable<any> | Promise<AsyncIterable<any>>;
|
|
8
|
+
|
|
9
|
+
/** Resolvers for a type. Fields may have a plain resolver or `{ resolve, subscribe }` (subscriptions). */
|
|
10
|
+
export type TypeResolvers = Record<string, Resolver | { resolve?: Resolver; subscribe?: SubscribeResolver }>;
|
|
11
|
+
|
|
12
|
+
export interface Resolvers {
|
|
13
|
+
[typeName: string]: TypeResolvers;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Per-request context passed to every resolver. */
|
|
17
|
+
export interface GraphQLContext {
|
|
18
|
+
/** The raw HTTP/WS request (for auth headers, ip, etc.). */
|
|
19
|
+
request?: unknown;
|
|
20
|
+
/** Authenticated user payload (sub, roles, sid) when auth is applied. */
|
|
21
|
+
user?: { sub: string; roles: string[]; sid?: string };
|
|
22
|
+
/** Anything the host wants to inject (DB connections, services, dataloaders). */
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Entity representation: `__typename` + the key fields of an entity. */
|
|
27
|
+
export interface EntityRepresentation {
|
|
28
|
+
__typename: string;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Resolves an entity by its representation. Returns the object (with fields) or null. */
|
|
33
|
+
export type EntityResolver = (representation: EntityRepresentation, context: GraphQLContext) => Promise<any | null> | any | null;
|
|
34
|
+
|
|
35
|
+
export interface DefineSubgraphOptions {
|
|
36
|
+
/** Subgraph name (used as the `__typename` source id and in composition). */
|
|
37
|
+
name: string;
|
|
38
|
+
/** The subgraph SDL (user types + federation directives like `@key`). */
|
|
39
|
+
typeDefs: string;
|
|
40
|
+
/** Field resolvers. */
|
|
41
|
+
resolvers?: Resolvers;
|
|
42
|
+
/** Entity resolver for `_entities`. Defaults to returning the representation as-is. */
|
|
43
|
+
entityResolver?: EntityResolver;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A built subgraph, ready to be composed or executed in-process. */
|
|
47
|
+
export interface Subgraph {
|
|
48
|
+
name: string;
|
|
49
|
+
/** Executable schema including federation boilerplate (_service, _entities). */
|
|
50
|
+
schema: GraphQLSchema;
|
|
51
|
+
/** The subgraph SDL (with federation directives) — what `_service.sdl` returns. */
|
|
52
|
+
sdl: string;
|
|
53
|
+
/** Map of entity typeName -> key field paths (e.g. `["id"]` or `["id email"]`). */
|
|
54
|
+
entityKeys: Map<string, string[]>;
|
|
55
|
+
resolvers: Resolvers;
|
|
56
|
+
entityResolver: EntityResolver;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ExecuteParams {
|
|
60
|
+
schema: GraphQLSchema;
|
|
61
|
+
document: DocumentNode;
|
|
62
|
+
resolvers: Resolvers;
|
|
63
|
+
variableValues?: Record<string, any>;
|
|
64
|
+
operationName?: string;
|
|
65
|
+
contextValue?: GraphQLContext;
|
|
66
|
+
rootValue?: any;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type ExecutionResultLike = ExecutionResult;
|
|
70
|
+
|
|
71
|
+
export interface Gateway {
|
|
72
|
+
/** Public (client-facing) schema — federation internals stripped. */
|
|
73
|
+
schema: GraphQLSchema;
|
|
74
|
+
subgraphs: Subgraph[];
|
|
75
|
+
execute(params: Omit<ExecuteParams, 'schema' | 'resolvers'>): Promise<ExecutionResult>;
|
|
76
|
+
subscribe(params: Omit<ExecuteParams, 'schema' | 'resolvers'>): Promise<AsyncIterable<ExecutionResult>>;
|
|
77
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bhooai/nexus-payments",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": { "access": "public" },
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"test": "vitest run"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@bhooai/nexus-core": "^0.1.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/node": "^22.5.0",
|
|
17
|
+
"typescript": "^5.6.2",
|
|
18
|
+
"vitest": "^2.1.1"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Payment-domain error. `code` is machine-readable; `provider` names the gateway. */
|
|
2
|
+
export class PaymentError extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
readonly provider: string;
|
|
5
|
+
readonly status?: number;
|
|
6
|
+
readonly raw?: unknown;
|
|
7
|
+
constructor(message: string, opts: { code: string; provider: string; status?: number; raw?: unknown }) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'PaymentError';
|
|
10
|
+
this.code = opts.code;
|
|
11
|
+
this.provider = opts.provider;
|
|
12
|
+
this.status = opts.status;
|
|
13
|
+
this.raw = opts.raw;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { HttpTransport, HttpRequest, HttpResponse } from './types.js';
|
|
2
|
+
import { PaymentError } from './errors.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default HTTP transport built on the global `fetch` (Node >= 18). Providers
|
|
6
|
+
* accept any `HttpTransport`, so tests inject a mock and never touch the network.
|
|
7
|
+
*/
|
|
8
|
+
export const fetchTransport: HttpTransport = async (req: HttpRequest): Promise<HttpResponse> => {
|
|
9
|
+
const res = await fetch(req.url, {
|
|
10
|
+
method: req.method,
|
|
11
|
+
headers: req.headers,
|
|
12
|
+
body: req.body,
|
|
13
|
+
});
|
|
14
|
+
const text = await res.text();
|
|
15
|
+
const headers: Record<string, string> = {};
|
|
16
|
+
res.headers.forEach((v, k) => {
|
|
17
|
+
headers[k] = v;
|
|
18
|
+
});
|
|
19
|
+
return { status: res.status, body: text, headers };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Run a JSON request through `transport`, parse JSON, and throw PaymentError on non-2xx. */
|
|
23
|
+
export async function jsonRequest(
|
|
24
|
+
transport: HttpTransport,
|
|
25
|
+
provider: string,
|
|
26
|
+
req: HttpRequest,
|
|
27
|
+
): Promise<any> {
|
|
28
|
+
const res = await transport(req);
|
|
29
|
+
const parsed = safeJson(res.body);
|
|
30
|
+
if (res.status < 200 || res.status >= 300) {
|
|
31
|
+
throw new PaymentError(`${provider} request failed: HTTP ${res.status}`, {
|
|
32
|
+
code: 'HTTP_ERROR',
|
|
33
|
+
provider,
|
|
34
|
+
status: res.status,
|
|
35
|
+
raw: parsed ?? res.body,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function safeJson(body: string): any {
|
|
42
|
+
if (!body) return undefined;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(body);
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Basic-auth header value from `user:pass`. */
|
|
51
|
+
export function basicAuth(user: string, pass: string): string {
|
|
52
|
+
return 'Basic ' + Buffer.from(`${user}:${pass}`).toString('base64');
|
|
53
|
+
}
|