houdini-core 2.0.0-next.32 → 2.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "houdini-core",
3
- "version": "2.0.0-next.32",
3
+ "version": "2.0.1",
4
4
  "description": "The core GraphQL client for Houdini",
5
5
  "keywords": [
6
6
  "graphql",
@@ -21,13 +21,13 @@
21
21
  "minimatch": "^10.2.5"
22
22
  },
23
23
  "optionalDependencies": {
24
- "houdini-core-darwin-x64": "2.0.0-next.32",
25
- "houdini-core-darwin-arm64": "2.0.0-next.32",
26
- "houdini-core-linux-x64": "2.0.0-next.32",
27
- "houdini-core-linux-arm64": "2.0.0-next.32",
28
- "houdini-core-win32-x64": "2.0.0-next.32",
29
- "houdini-core-win32-arm64": "2.0.0-next.32",
30
- "houdini-core-wasm": "2.0.0-next.32"
24
+ "houdini-core-darwin-x64": "2.0.1",
25
+ "houdini-core-darwin-arm64": "2.0.1",
26
+ "houdini-core-linux-x64": "2.0.1",
27
+ "houdini-core-linux-arm64": "2.0.1",
28
+ "houdini-core-win32-x64": "2.0.1",
29
+ "houdini-core-win32-arm64": "2.0.1",
30
+ "houdini-core-wasm": "2.0.1"
31
31
  },
32
32
  "files": [
33
33
  "bin",
package/postInstall.js CHANGED
@@ -5,7 +5,7 @@ const https = require('https')
5
5
  const child_process = require('child_process')
6
6
 
7
7
  // Adjust the version you want to install. You can also make this dynamic.
8
- const BINARY_DISTRIBUTION_VERSION = '2.0.0-next.32'
8
+ const BINARY_DISTRIBUTION_VERSION = '2.0.1'
9
9
 
10
10
  // Windows binaries end with .exe so we need to special case them.
11
11
  const binaryName = process.platform === 'win32' ? 'houdini-core.exe' : 'houdini-core'
package/runtime/client.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  throwOnError as throwOnErrorPlugin,
23
23
  optimisticKeys,
24
24
  cachePolicy,
25
+ sessionRelay,
25
26
  } from './plugins/index.js'
26
27
  import pluginsFromPlugins from './plugins/injectedPlugins.js'
27
28
 
@@ -30,7 +31,6 @@ export { fetch, mutation, query, subscription } from './plugins/index.js'
30
31
  export { DocumentStore, type ClientPlugin, type SendParams } from 'houdini/runtime/documentStore'
31
32
 
32
33
  export type HoudiniClientConstructorArgs = {
33
- url?: string
34
34
  fetchParams?: FetchParamFn
35
35
  plugins?: NestedList<ClientPlugin>
36
36
  pipeline?: NestedList<ClientPlugin>
@@ -49,14 +49,22 @@ export class HoudiniClient extends BaseClient {
49
49
  // store throwOnError operations for access by stores
50
50
  throwOnError_operations: string[] = []
51
51
 
52
- constructor({
53
- url,
54
- fetchParams,
55
- plugins,
56
- pipeline,
57
- throwOnError,
58
- cache = cacheRef,
59
- }: HoudiniClientConstructorArgs = {}) {
52
+ constructor(args: HoudiniClientConstructorArgs = {}) {
53
+ // Houdini 2.0 migration guard: the API url is no longer passed to the client. Pre-2.0 apps
54
+ // did `new HoudiniClient({ url })` (or `new HoudiniClient(url)`); that value would now be
55
+ // silently ignored, which is one of the widest blast radiuses in the migration — so fail
56
+ // loudly with the new home for it.
57
+ const legacyUrl =
58
+ typeof (args as unknown) === 'string' ? args : (args as { url?: unknown }).url
59
+ if (legacyUrl != null) {
60
+ throw new Error(
61
+ 'HoudiniClient no longer accepts a `url`. Set `url` in houdini.config.js for a remote ' +
62
+ 'API, or `endpoint` in src/server/+config to change the local API path.'
63
+ )
64
+ }
65
+
66
+ const { fetchParams, plugins, pipeline, throwOnError, cache = cacheRef } = args
67
+
60
68
  // if we were given plugins and pipeline there's an error
61
69
  if (plugins && pipeline) {
62
70
  throw new Error(
@@ -67,12 +75,16 @@ export class HoudiniClient extends BaseClient {
67
75
  const serverPort =
68
76
  globalThis.process?.env?.HOUDINI_PORT ?? globalThis.process?.env?.PORT ?? '5173'
69
77
 
78
+ // resolve the endpoint from config (no `url` arg anymore). An absolute endpoint (a remote
79
+ // api) is used as-is; a relative one (the local mount) is prefixed with the origin on SSR.
80
+ const endpoint = localApiEndpoint()
81
+ const resolvedUrl = /^https?:\/\//.test(endpoint)
82
+ ? endpoint
83
+ : (globalThis.window ? '' : `http://localhost:${serverPort}`) + endpoint
84
+
70
85
  super({
71
86
  config: getCurrentConfig,
72
- url:
73
- url ??
74
- (globalThis.window ? '' : `http://localhost:${serverPort}`) +
75
- localApiEndpoint(getCurrentConfig()),
87
+ url: resolvedUrl,
76
88
  plugins: flatten(
77
89
  ([] as NestedList<ClientPlugin>).concat(
78
90
  // if they specified a throw behavior
@@ -85,6 +97,8 @@ export class HoudiniClient extends BaseClient {
85
97
  (
86
98
  [
87
99
  optimisticKeys(cache ?? cacheRef),
100
+ // relay an @session mutation's mint token to the auth endpoint (sets the cookie)
101
+ sessionRelay(),
88
102
  // make sure that documents always work
89
103
  queryPlugin(cache ?? cacheRef),
90
104
  mutationPlugin(cache ?? cacheRef),
package/runtime/config.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ConfigFile } from 'houdini'
2
+ import { resolveApiEndpoint } from 'houdini/runtime'
2
3
 
3
4
  import config from './imports/config.js'
4
5
  import pluginConfigs from './imports/pluginConfig.js'
@@ -49,8 +50,10 @@ export function computeID(configFile: ConfigFile, type: string, data: any): stri
49
50
  // only compute the config file once
50
51
  let _configFile: ConfigFile | null = null
51
52
 
52
- export function localApiEndpoint(configFile: ConfigFile) {
53
- return configFile.router?.apiEndpoint ?? '/_api'
53
+ // the GraphQL endpoint comes straight from the public config bundle (no injection): the remote
54
+ // `url`, the local mount `apiURL`, or the default — see resolveApiEndpoint.
55
+ export function localApiEndpoint() {
56
+ return resolveApiEndpoint(getCurrentConfig())
54
57
  }
55
58
 
56
59
  export function getCurrentConfig(): ConfigFile {
@@ -1,7 +1,10 @@
1
+ import { getAuthUrl } from 'houdini/runtime'
1
2
  import type { ClientPlugin, ClientPluginContext } from 'houdini/runtime/documentStore'
2
3
  import { ArtifactKind, DataSource } from 'houdini/runtime/types'
3
4
  import type { RequestPayload, FetchContext } from 'houdini/runtime/types'
4
5
 
6
+ import { getCurrentConfig } from '../config.js'
7
+
5
8
  export const fetch = (target?: RequestHandler | string): ClientPlugin => {
6
9
  return () => {
7
10
  return {
@@ -38,6 +41,28 @@ export const fetch = (target?: RequestHandler | string): ClientPlugin => {
38
41
  }
39
42
  }
40
43
 
44
+ // a @session mutation against a REMOTE api has to go through Houdini's same-origin
45
+ // proxy so the server can sit in the request path and write the session cookie
46
+ // server-authoritatively. The app is remote exactly when `url` is set in the public
47
+ // config (with a local schema the mutation hits the local Yoga and the mint plugin
48
+ // signs inline). The proxy lives under the session endpoint. We override the URL but
49
+ // never a user-supplied custom fetch function (it owns transport).
50
+ const artifact = ctx.artifact as typeof ctx.artifact & { sessionPath?: string }
51
+ if (
52
+ getCurrentConfig().url &&
53
+ artifact.kind === ArtifactKind.Mutation &&
54
+ artifact.sessionPath &&
55
+ typeof target !== 'function'
56
+ ) {
57
+ // tell the proxy which operation this is via a header so it never has to parse the
58
+ // (possibly multipart) body to find the session path. Must stay in sync with
59
+ // HOUDINI_OPERATION_HEADER in router/server.ts.
60
+ fetchFn = defaultFetch(getAuthUrl() + '/proxy', {
61
+ ...ctx.fetchParams,
62
+ headers: { ...ctx.fetchParams?.headers, 'x-houdini-operation': ctx.name },
63
+ })
64
+ }
65
+
41
66
  const result = await fetchFn({
42
67
  // wrap the user's fetch function so we can identify SSR by checking
43
68
  // the response.url
@@ -61,6 +86,9 @@ export const fetch = (target?: RequestHandler | string): ClientPlugin => {
61
86
  variables: ctx.variables ?? {},
62
87
  data: result.data,
63
88
  errors: !result.errors || result.errors.length === 0 ? null : result.errors,
89
+ // surface response-level extensions (e.g. the @session mint
90
+ // token) so the runtime/hooks can read them off the result
91
+ extensions: result.extensions,
64
92
  partial: false,
65
93
  stale: false,
66
94
  source: DataSource.Network,
@@ -90,6 +118,11 @@ const defaultFetch = (
90
118
  headers: {
91
119
  Accept: 'application/graphql+json, application/json',
92
120
  'Content-Type': 'application/json',
121
+ // a header a cross-origin <form>/simple request cannot set. The server
122
+ // requires it for CORS-simple POSTs to the graphql endpoint (uploads use
123
+ // multipart, which bypasses preflight) so it can't be a CSRF channel. Must
124
+ // stay in sync with HOUDINI_REQUEST_HEADER in router/server.ts.
125
+ 'x-houdini-request': 'true',
93
126
  ...params?.headers,
94
127
  },
95
128
  })
@@ -6,4 +6,5 @@ export * from './mutation.js'
6
6
  export * from './subscription.js'
7
7
  export * from './throwOnError.js'
8
8
  export * from './fetchParams.js'
9
+ export * from './sessionRelay.js'
9
10
  export { optimisticKeys } from './optimisticKeys.js'
@@ -0,0 +1,60 @@
1
+ import { getAuthUrl, HOUDINI_SESSION_EVENT, valueAtPath } from 'houdini/runtime'
2
+ import type { ClientPlugin } from 'houdini/runtime/documentStore'
3
+ import { ArtifactKind } from 'houdini/runtime/types'
4
+
5
+ // sessionRelay is the client half of @session. When a session mutation executes, the server
6
+ // signs the resolver's session subtree into a token in the response extensions
7
+ // (extensions.houdiniSession). This plugin relays that token to the auth endpoint, which
8
+ // verifies it and sets the httpOnly cookie — JS can't set httpOnly cookies itself — and then
9
+ // mirrors the same write into local state (a window event the router listens for) so
10
+ // useSession() updates without a refresh.
11
+ //
12
+ // It runs in the document pipeline, so it fires for ANY @session mutation execution
13
+ // (useMutation, useMutationForm, a raw send), not just form submits: session-by-mutation
14
+ // isn't tied to forms. The no-JS form path never reaches here (the server writes the cookie
15
+ // directly and marks its internal request so no token is minted).
16
+ export const sessionRelay = (): ClientPlugin => () => ({
17
+ async end(ctx, { value, resolve }) {
18
+ const artifact = ctx.artifact as typeof ctx.artifact & {
19
+ sessionPath?: string
20
+ sessionMerge?: boolean
21
+ }
22
+ const result = value as { data?: any; extensions?: Record<string, any> } | null
23
+ // the local-Yoga path mints a token here for the client to relay; the remote-api proxy
24
+ // instead writes the cookie itself and signals it with houdiniSessionApplied. Either way the
25
+ // session changed and useSession() must mirror it.
26
+ const token = result?.extensions?.houdiniSession
27
+ const proxyApplied = result?.extensions?.houdiniSessionApplied === true
28
+ const isSessionMutation = artifact.kind === ArtifactKind.Mutation && !!artifact.sessionPath
29
+ if (isSessionMutation && typeof window !== 'undefined' && (token || proxyApplied)) {
30
+ // relay the minted token to the auth endpoint to set the cookie. The proxy path already
31
+ // set the cookie server-side (proxyApplied, no token), so there's nothing to relay there.
32
+ if (token) {
33
+ try {
34
+ await fetch(getAuthUrl(), {
35
+ method: 'POST',
36
+ body: JSON.stringify({ token }),
37
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
38
+ })
39
+ } catch {
40
+ // best-effort — if the relay fails the cookie just isn't updated this time
41
+ }
42
+ }
43
+
44
+ // mirror the write into local state so useSession() updates without a refresh. A null
45
+ // subtree clears (replace with {}); otherwise merge upserts and the default replaces. The
46
+ // cookie (set by the relay or the proxy) stays the source of truth; this just reflects it.
47
+ const next = valueAtPath(result?.data, artifact.sessionPath!.split('.'))
48
+ window.dispatchEvent(
49
+ new CustomEvent(HOUDINI_SESSION_EVENT, {
50
+ bubbles: true,
51
+ detail: {
52
+ session: next ?? {},
53
+ merge: next != null && !!artifact.sessionMerge,
54
+ },
55
+ })
56
+ )
57
+ }
58
+ resolve(ctx)
59
+ },
60
+ })