@jskit-ai/http-runtime 0.1.90 → 0.1.91

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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  "packageVersion": 1,
3
3
  "packageId": "@jskit-ai/http-runtime",
4
- "version": "0.1.90",
4
+ "version": "0.1.91",
5
5
  "kind": "runtime",
6
6
  "dependsOn": [],
7
7
  "capabilities": {
@@ -67,7 +67,7 @@ export default Object.freeze({
67
67
  "mutations": {
68
68
  "dependencies": {
69
69
  "runtime": {
70
- "@jskit-ai/kernel": "0.1.91"
70
+ "@jskit-ai/kernel": "0.1.92"
71
71
  },
72
72
  "dev": {}
73
73
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/http-runtime",
3
- "version": "0.1.90",
3
+ "version": "0.1.91",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -18,7 +18,7 @@
18
18
  "./shared/validators/operationValidation": "./src/shared/validators/operationValidation.js"
19
19
  },
20
20
  "dependencies": {
21
- "@jskit-ai/kernel": "0.1.91",
21
+ "@jskit-ai/kernel": "0.1.92",
22
22
  "json-rest-schema": "1.x.x"
23
23
  }
24
24
  }
@@ -33,6 +33,14 @@ function resolveFetch() {
33
33
  throw new Error("createHttpClient requires fetchImpl when global fetch is unavailable.");
34
34
  }
35
35
 
36
+ function normalizeResolvedRequestUrl(value, fallback = "") {
37
+ if (value === undefined || value === null || value === "") {
38
+ return fallback;
39
+ }
40
+
41
+ return String(value);
42
+ }
43
+
36
44
  function isObjectBody(value) {
37
45
  return Boolean(value) && typeof value === "object" && !(value instanceof FormData);
38
46
  }
@@ -164,6 +172,8 @@ async function readNdjsonStream(response, handlers = {}) {
164
172
 
165
173
  function createHttpClient(options = {}) {
166
174
  const configuredFetchImpl = typeof options.fetchImpl === "function" ? options.fetchImpl : null;
175
+ const configuredResolveRequestUrl =
176
+ typeof options.resolveRequestUrl === "function" ? options.resolveRequestUrl : null;
167
177
  const unsafeMethods = resolveUnsafeMethods(options.unsafeMethods);
168
178
  const hooks = options?.hooks && typeof options.hooks === "object" ? options.hooks : {};
169
179
 
@@ -189,9 +199,19 @@ function createHttpClient(options = {}) {
189
199
 
190
200
  async function fetchSessionForCsrf() {
191
201
  const activeFetch = configuredFetchImpl || resolveFetch();
202
+ const requestUrl = await resolveRequestUrl(csrf.sessionPath, {
203
+ originalUrl: csrf.sessionPath,
204
+ method: "GET",
205
+ requestOptions: {
206
+ method: "GET"
207
+ },
208
+ state: null,
209
+ stream: false,
210
+ csrfSession: true
211
+ });
192
212
  let response;
193
213
  try {
194
- response = await activeFetch(csrf.sessionPath, {
214
+ response = await activeFetch(requestUrl, {
195
215
  method: "GET",
196
216
  credentials: String(options?.credentials || "same-origin")
197
217
  });
@@ -255,6 +275,19 @@ function createHttpClient(options = {}) {
255
275
  }
256
276
  }
257
277
 
278
+ async function resolveRequestUrl(url, context = {}) {
279
+ const normalizedUrl = String(url || "").trim();
280
+ if (!configuredResolveRequestUrl) {
281
+ return normalizedUrl;
282
+ }
283
+
284
+ const resolved = await configuredResolveRequestUrl(normalizedUrl, {
285
+ ...context,
286
+ url: normalizedUrl
287
+ });
288
+ return normalizeResolvedRequestUrl(resolved, normalizedUrl);
289
+ }
290
+
258
291
  async function maybeRetry({ response, method, state, data, stream }) {
259
292
  if (!csrf.enabled) {
260
293
  return false;
@@ -321,11 +354,19 @@ function createHttpClient(options = {}) {
321
354
  ...forwardedRequestOptions
322
355
  } = requestOptions && typeof requestOptions === "object" ? requestOptions : {};
323
356
  const requestUrl = appendRequestQueryToUrl(url, requestQuery, transport);
357
+ const resolvedRequestUrl = await resolveRequestUrl(requestUrl, {
358
+ originalUrl: url,
359
+ method,
360
+ requestOptions,
361
+ state: resolvedState,
362
+ stream: Boolean(stream),
363
+ csrfSession: false
364
+ });
324
365
  const headers =
325
366
  requestOptions.headers && typeof requestOptions.headers === "object" ? { ...requestOptions.headers } : {};
326
367
 
327
368
  const decorateHeadersResult = decorateHeaders({
328
- url: requestUrl,
369
+ url: resolvedRequestUrl,
329
370
  method,
330
371
  headers,
331
372
  requestOptions,
@@ -369,7 +410,7 @@ function createHttpClient(options = {}) {
369
410
  config,
370
411
  state: resolvedState,
371
412
  transport,
372
- url: requestUrl
413
+ url: resolvedRequestUrl
373
414
  };
374
415
  }
375
416
 
@@ -56,6 +56,67 @@ test("request serializes json body and injects csrf token for unsafe methods", a
56
56
  assert.equal(calls[1][1].body, JSON.stringify({ demo: true }));
57
57
  });
58
58
 
59
+ test("request resolves request urls after query encoding and before fetch", async () => {
60
+ const calls = [];
61
+ const contexts = [];
62
+ const client = createHttpClient({
63
+ csrf: {
64
+ enabled: false
65
+ },
66
+ resolveRequestUrl(url, context = {}) {
67
+ contexts.push({ url, context });
68
+ return url.replace(/^\/api\//u, "/api/app/beepollen/");
69
+ },
70
+ fetchImpl: async (url, options) => {
71
+ calls.push([url, options]);
72
+ return mockResponse({
73
+ data: {
74
+ ok: true
75
+ }
76
+ });
77
+ }
78
+ });
79
+
80
+ const payload = await client.request("/api/vibe64/sessions", {
81
+ method: "GET",
82
+ query: {
83
+ cursor: "next page"
84
+ }
85
+ });
86
+
87
+ assert.deepEqual(payload, { ok: true });
88
+ assert.equal(calls[0][0], "/api/app/beepollen/vibe64/sessions?cursor=next+page");
89
+ assert.equal(contexts[0].url, "/api/vibe64/sessions?cursor=next+page");
90
+ assert.equal(contexts[0].context.originalUrl, "/api/vibe64/sessions");
91
+ assert.equal(contexts[0].context.method, "GET");
92
+ assert.equal(contexts[0].context.stream, false);
93
+ });
94
+
95
+ test("requestStream resolves request urls before fetch", async () => {
96
+ const calls = [];
97
+ const client = createHttpClient({
98
+ csrf: {
99
+ enabled: false
100
+ },
101
+ resolveRequestUrl(url) {
102
+ return url.replace(/^\/api\//u, "/api/app/beepollen/");
103
+ },
104
+ fetchImpl: async (url, options) => {
105
+ calls.push([url, options]);
106
+ return mockResponse({
107
+ contentType: "text/plain; charset=utf-8",
108
+ text: ""
109
+ });
110
+ }
111
+ });
112
+
113
+ await client.requestStream("/api/vibe64/events", {
114
+ method: "GET"
115
+ });
116
+
117
+ assert.equal(calls[0][0], "/api/app/beepollen/vibe64/events");
118
+ });
119
+
59
120
  test("request parses json:api responses as json payloads", async () => {
60
121
  const fetchImpl = async () =>
61
122
  mockResponse({
@@ -69,6 +69,33 @@ test("createTransientRetryHttpClient retries transient GET request failures", as
69
69
  });
70
70
  });
71
71
 
72
+ test("createTransientRetryHttpClient resolves request urls before fetch", async () => {
73
+ const calls = [];
74
+ const client = createTransientRetryHttpClient({
75
+ csrf: {
76
+ enabled: false
77
+ },
78
+ resolveRequestUrl(url) {
79
+ return url.replace(/^\/api\//u, "/api/app/beepollen/");
80
+ },
81
+ fetchImpl: async (url, options) => {
82
+ calls.push([url, options]);
83
+ return mockResponse({
84
+ data: {
85
+ ok: true
86
+ }
87
+ });
88
+ }
89
+ });
90
+
91
+ const payload = await client.request("/api/vibe64/sessions", {
92
+ method: "GET"
93
+ });
94
+
95
+ assert.deepEqual(payload, { ok: true });
96
+ assert.equal(calls[0][0], "/api/app/beepollen/vibe64/sessions");
97
+ });
98
+
72
99
  test("createTransientRetryHttpClient does not retry unsafe transient failures", async () => {
73
100
  await withImmediateTimers(async () => {
74
101
  let callCount = 0;