@jskit-ai/http-runtime 0.1.144 → 0.1.145

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.144",
4
+ "version": "0.1.145",
5
5
  "kind": "runtime",
6
6
  "dependsOn": [],
7
7
  "capabilities": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/http-runtime",
3
- "version": "0.1.144",
3
+ "version": "0.1.145",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -288,8 +288,8 @@ function createHttpClient(options = {}) {
288
288
  return normalizeResolvedRequestUrl(resolved, normalizedUrl);
289
289
  }
290
290
 
291
- async function maybeRetry({ response, method, state, data, stream }) {
292
- if (!csrf.enabled) {
291
+ async function maybeRetry({ response, method, state, data, stream, csrfEnabled = csrf.enabled }) {
292
+ if (!csrfEnabled) {
293
293
  return false;
294
294
  }
295
295
 
@@ -351,8 +351,10 @@ function createHttpClient(options = {}) {
351
351
  const {
352
352
  transport: _transport,
353
353
  query: requestQuery,
354
+ csrf: requestCsrf,
354
355
  ...forwardedRequestOptions
355
356
  } = requestOptions && typeof requestOptions === "object" ? requestOptions : {};
357
+ const csrfEnabled = csrf.enabled && requestCsrf !== false;
356
358
  const requestUrl = appendRequestQueryToUrl(url, requestQuery, transport);
357
359
  const resolvedRequestUrl = await resolveRequestUrl(requestUrl, {
358
360
  originalUrl: url,
@@ -398,7 +400,7 @@ function createHttpClient(options = {}) {
398
400
  config.body = JSON.stringify(config.body);
399
401
  }
400
402
 
401
- if (csrf.enabled && unsafeMethods.has(method) && !hasHeader(headers, csrf.headerName)) {
403
+ if (csrfEnabled && unsafeMethods.has(method) && !hasHeader(headers, csrf.headerName)) {
402
404
  const token = await ensureCsrfToken();
403
405
  if (token) {
404
406
  setHeaderIfMissing(headers, csrf.headerName, token);
@@ -409,6 +411,7 @@ function createHttpClient(options = {}) {
409
411
  method,
410
412
  config,
411
413
  state: resolvedState,
414
+ csrfEnabled,
412
415
  transport,
413
416
  url: resolvedRequestUrl
414
417
  };
@@ -445,6 +448,7 @@ function createHttpClient(options = {}) {
445
448
  contentType,
446
449
  isJson,
447
450
  stream,
451
+ csrfEnabled,
448
452
  retryRequest
449
453
  }) {
450
454
  if (
@@ -453,7 +457,8 @@ function createHttpClient(options = {}) {
453
457
  method,
454
458
  state,
455
459
  data,
456
- stream
460
+ stream,
461
+ csrfEnabled
457
462
  })
458
463
  ) {
459
464
  return retryRequest();
@@ -525,6 +530,7 @@ function createHttpClient(options = {}) {
525
530
  contentType: result.contentType,
526
531
  isJson: result.isJson,
527
532
  stream,
533
+ csrfEnabled: requestContext.csrfEnabled,
528
534
  retryRequest() {
529
535
  return retryRequest(resolvedState);
530
536
  }
@@ -56,6 +56,57 @@ 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-level csrf false skips session preflight and is not forwarded to fetch", async (t) => {
60
+ for (const method of ["POST", "PATCH", "DELETE"]) {
61
+ await t.test(method, async () => {
62
+ const calls = [];
63
+ const client = createHttpClient({
64
+ fetchImpl: async (url, options) => {
65
+ calls.push([url, options]);
66
+ return mockResponse({ data: { ok: true } });
67
+ }
68
+ });
69
+
70
+ await client.request("/api/books/1", {
71
+ method,
72
+ csrf: false,
73
+ body: method === "DELETE" ? undefined : { title: "Book" }
74
+ });
75
+
76
+ assert.equal(calls.length, 1);
77
+ assert.equal(calls[0][0], "/api/books/1");
78
+ assert.equal(Object.hasOwn(calls[0][1], "csrf"), false);
79
+ assert.equal(Object.hasOwn(calls[0][1].headers, "csrf-token"), false);
80
+ });
81
+ }
82
+ });
83
+
84
+ test("request-level csrf false does not retry a csrf-shaped failure", async () => {
85
+ const calls = [];
86
+ const client = createHttpClient({
87
+ fetchImpl: async (url, options) => {
88
+ calls.push([url, options]);
89
+ return mockResponse({
90
+ status: 403,
91
+ data: {
92
+ error: {
93
+ code: "CSRF_TOKEN_INVALID"
94
+ }
95
+ }
96
+ });
97
+ }
98
+ });
99
+
100
+ await assert.rejects(
101
+ client.request("/api/books/1", {
102
+ method: "DELETE",
103
+ csrf: false
104
+ })
105
+ );
106
+ assert.equal(calls.length, 1);
107
+ assert.equal(calls[0][0], "/api/books/1");
108
+ });
109
+
59
110
  test("request resolves request urls after query encoding and before fetch", async () => {
60
111
  const calls = [];
61
112
  const contexts = [];