@effected/github 0.25.0 → 0.26.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.
@@ -1,3 +1,4 @@
1
+ import { ENVIRONMENT_PUT_ROUTE_PARAMS, reservedBodyKey } from "./internal/requestOptions.js";
1
2
  import { unstubbed } from "./internal/unstubbed.js";
2
3
  import { GitHubClient } from "./GitHubClient.js";
3
4
  import { Repo } from "./Repo.js";
@@ -54,11 +55,15 @@ const make = (client) => {
54
55
  repo,
55
56
  environment: name
56
57
  });
58
+ for (const key of Object.keys(config)) {
59
+ const reason = reservedBodyKey(key, ENVIRONMENT_PUT_ROUTE_PARAMS, "an environment setting");
60
+ if (reason !== void 0) return yield* Effect.die(/* @__PURE__ */ new RangeError(`DeploymentEnvironment.upsert: "${key}" ${reason}`));
61
+ }
57
62
  yield* client.request("PUT /repos/{owner}/{repo}/environments/{environment_name}", {
58
- ...config,
59
63
  owner,
60
64
  repo,
61
- environment_name: name
65
+ environment_name: name,
66
+ data: { ...config }
62
67
  });
63
68
  }),
64
69
  list: Effect.fn("DeploymentEnvironment.list")(function* () {
package/GitHubApp.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { GitHubError } from "./GitHubError.js";
2
2
  import { GitHubGraphQLError } from "./GraphQL.js";
3
+ import { uploadChannelOf, withUploadChannel } from "./internal/uploads.js";
3
4
  import { unstubbed } from "./internal/unstubbed.js";
4
5
  import { GitHubClient, makeClientShape, resolveApiVersion } from "./GitHubClient.js";
5
6
  import { numericId } from "./internal/ids.js";
@@ -673,7 +674,11 @@ const makeRotatingClient = (mint, release, options, operation) => Effect.gen(fun
673
674
  errors: [],
674
675
  cause: error
675
676
  }))));
676
- return {
677
+ const upload = (route, params, schema) => Effect.flatMap(current, (client) => {
678
+ const channel = uploadChannelOf(client);
679
+ return channel === void 0 ? Effect.die(/* @__PURE__ */ new Error(`${operation}: the inner client carries no upload channel`)) : channel.upload(route, params, schema);
680
+ });
681
+ const shape = {
677
682
  request: (route, params) => Effect.flatMap(current, (client) => client.request(route, params)),
678
683
  requestDecoded: (route, params, schema) => Effect.flatMap(current, (client) => client.requestDecoded(route, params, schema)),
679
684
  paginate: (route, params, pageOptions) => Effect.flatMap(current, (client) => client.paginate(route, params, pageOptions)),
@@ -682,6 +687,10 @@ const makeRotatingClient = (mint, release, options, operation) => Effect.gen(fun
682
687
  rateLimit: Effect.flatMap(Ref.get(held), (state) => Option.isSome(state) ? state.value.client.rateLimit : Effect.succeed(Option.none())),
683
688
  apiVersion
684
689
  };
690
+ return withUploadChannel(shape, {
691
+ baseUrl: options.baseUrl,
692
+ upload
693
+ });
685
694
  });
686
695
 
687
696
  export { AppIdentity, BotIdentity, BotUser, GitHubApp, GitHubAppError, Installation, InstallationToken };
package/GitHubClient.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { GitHubError } from "./GitHubError.js";
2
2
  import { GitHubGraphQLError } from "./GraphQL.js";
3
+ import { DOTCOM_API_BASE_URL, originOf, uploadsBaseUrlFor, withUploadChannel } from "./internal/uploads.js";
4
+ import { rawRequestRefusal, refusedRequest } from "./internal/confinement.js";
3
5
  import { RetryPolicy } from "./Resilience.js";
4
- import { DEFAULT_API_VERSION, makeTransport } from "./internal/octokit.js";
6
+ import { DEFAULT_API_VERSION, expandOnDotcom, makeTransport } from "./internal/octokit.js";
5
7
  import { fromArray, paginate } from "./internal/paginate.js";
6
8
  import { unstubbed } from "./internal/unstubbed.js";
7
9
  import { Clock, Config, Context, Effect, Layer, Option, Schema, Stream } from "effect";
@@ -82,6 +84,34 @@ var GitHubFixtures = class {
82
84
  const resolvePolicy = (retry) => retry === void 0 ? RetryPolicy.default : retry === "off" ? RetryPolicy.none : retry;
83
85
  const perPageOf = (options) => options?.perPage ?? DEFAULT_PER_PAGE;
84
86
  const dies = unstubbed("GitHubClient.makeTest", "pass an override, or use GitHubClient.layerFixture for recorded responses.");
87
+ const DOTCOM_API_ORIGIN = originOf(DOTCOM_API_BASE_URL);
88
+ const doubleRefusal = (route, params) => rawRequestRefusal(expandOnDotcom, route, typeof params === "object" && params !== null ? params : {}, DOTCOM_API_ORIGIN);
89
+ /**
90
+ * `shape` with the live client's token-confinement refusals applied in front of
91
+ * its four raw members, so a suite on a double sees the `rejected` failure
92
+ * production would raise. A refused call never reaches the double's handler,
93
+ * stub, fixture or `requested` log, as a refused live call never reaches the
94
+ * wire.
95
+ */
96
+ const confineDouble = (shape) => ({
97
+ ...shape,
98
+ request: (route, params) => Effect.suspend(() => {
99
+ const reason = doubleRefusal(route, params);
100
+ return reason !== void 0 ? Effect.fail(refusedRequest(route, reason)) : shape.request(route, params);
101
+ }),
102
+ requestDecoded: (route, params, schema) => Effect.suspend(() => {
103
+ const reason = doubleRefusal(route, params);
104
+ return reason !== void 0 ? Effect.fail(refusedRequest(route, reason)) : shape.requestDecoded(route, params, schema);
105
+ }),
106
+ paginate: (route, params, options) => Effect.suspend(() => {
107
+ const reason = doubleRefusal(route, params);
108
+ return reason !== void 0 ? Effect.fail(refusedRequest(route, reason)) : shape.paginate(route, params, options);
109
+ }),
110
+ paginateStream: (route, params, options) => Stream.suspend(() => {
111
+ const reason = doubleRefusal(route, params);
112
+ return reason !== void 0 ? Stream.fail(refusedRequest(route, reason)) : shape.paginateStream(route, params, options);
113
+ })
114
+ });
85
115
  /**
86
116
  * The typed GitHub API client.
87
117
  *
@@ -157,10 +187,16 @@ var GitHubClient = class GitHubClient extends Context.Service()("@effected/githu
157
187
  * `rateLimit` answers `Option.none()` and `apiVersion` the live default,
158
188
  * because a real client legitimately answers both without a request.
159
189
  *
190
+ * It refuses what the live client refuses, before any override runs: a
191
+ * `baseUrl`, `url`, `request` or `method` parameter (a body field called
192
+ * `url` goes inside `data`), or a route that resolves off github.com's REST
193
+ * root, fails `kind: "rejected"` with no status. A double has no
194
+ * configured base, so an absolute-URL route must name `api.github.com`.
195
+ *
160
196
  * For recorded responses that page for real, use
161
197
  * {@link GitHubClient.layerFixture}.
162
198
  */
163
- static makeTest = (overrides = {}) => ({
199
+ static makeTest = (overrides = {}) => confineDouble({
164
200
  request: overrides.request ?? (() => dies("request")),
165
201
  requestDecoded: overrides.requestDecoded ?? (() => dies("requestDecoded")),
166
202
  paginate: overrides.paginate ?? (() => dies("paginate")),
@@ -189,8 +225,13 @@ var GitHubClient = class GitHubClient extends Context.Service()("@effected/githu
189
225
  * one fixture both records the calls and classifies a raw failure the way
190
226
  * the live client would; there is no need to classify by hand inside a
191
227
  * {@link GitHubClient.layerTest} handler.
228
+ *
229
+ * It refuses what the live client refuses, exactly as
230
+ * {@link GitHubClient.makeTest} does. A refused call fails
231
+ * `kind: "rejected"` and is **not** recorded in `requested`: it is never
232
+ * served, as a refused live call never reaches the wire.
192
233
  */
193
- static layerFixture = (fixtures) => Layer.succeed(GitHubClient, makeFixture(fixtures));
234
+ static layerFixture = (fixtures) => Layer.succeed(GitHubClient, confineDouble(makeFixture(fixtures)));
194
235
  };
195
236
  /** A calendar api-version: `YYYY-MM-DD`. */
196
237
  const API_VERSION_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
@@ -231,35 +272,43 @@ const makeClientShape = (options) => Effect.gen(function* () {
231
272
  yield* Effect.annotateCurrentSpan({ route });
232
273
  return (yield* transport.request(route, route, params)).data;
233
274
  });
275
+ const decodeWith = (route, schema) => (data) => Schema.decodeUnknownEffect(schema)(data).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(GitHubError.decode(route, "response did not match its schema", error))));
234
276
  const requestDecoded = Effect.fn("GitHubClient.requestDecoded")(function* (route, params, schema) {
235
277
  yield* Effect.annotateCurrentSpan({ route });
236
278
  const response = yield* transport.request(route, route, params);
237
- return yield* Schema.decodeUnknownEffect(schema)(response.data).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(GitHubError.decode(route, "response did not match its schema", error))));
279
+ return yield* decodeWith(route, schema)(response.data);
238
280
  });
281
+ const uploadsBaseUrl = uploadsBaseUrlFor(options.baseUrl);
282
+ const upload = (route, params, schema) => Effect.flatMap(transport.requestAt(route, route, params, uploadsBaseUrl), (response) => decodeWith(route, schema)(response.data));
239
283
  const paginateStream = (route, params, options) => paginate(() => transport.pageSource(route, route, {
240
284
  per_page: perPageOf(options),
241
285
  ...params
242
286
  }), options?.maxPages);
243
- return {
287
+ const paginateAll = Effect.fn("GitHubClient.paginate")(function* (route, params, options) {
288
+ yield* Effect.annotateCurrentSpan({
289
+ route,
290
+ perPage: perPageOf(options),
291
+ maxPages: options?.maxPages ?? -1
292
+ });
293
+ return yield* Stream.runCollect(paginateStream(route, params, options));
294
+ });
295
+ const graphql = Effect.fn("GitHubClient.graphql")(function* (document, variables) {
296
+ yield* Effect.annotateCurrentSpan({ document: document.name });
297
+ const raw = yield* transport.graphql(document.name, document.document, document.encodeVariables(variables));
298
+ return yield* document.decode(raw).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(GitHubGraphQLError.decode(document.name, "response did not match its schema", error))));
299
+ });
300
+ return withUploadChannel({
244
301
  request,
245
302
  requestDecoded,
246
- paginate: Effect.fn("GitHubClient.paginate")(function* (route, params, options) {
247
- yield* Effect.annotateCurrentSpan({
248
- route,
249
- perPage: perPageOf(options),
250
- maxPages: options?.maxPages ?? -1
251
- });
252
- return yield* Stream.runCollect(paginateStream(route, params, options));
253
- }),
303
+ paginate: paginateAll,
254
304
  paginateStream,
255
- graphql: Effect.fn("GitHubClient.graphql")(function* (document, variables) {
256
- yield* Effect.annotateCurrentSpan({ document: document.name });
257
- const raw = yield* transport.graphql(document.name, document.document, document.encodeVariables(variables));
258
- return yield* document.decode(raw).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(GitHubGraphQLError.decode(document.name, "response did not match its schema", error))));
259
- }),
305
+ graphql,
260
306
  rateLimit: transport.rateLimit,
261
307
  apiVersion
262
- };
308
+ }, {
309
+ baseUrl: options.baseUrl,
310
+ upload
311
+ });
263
312
  });
264
313
  const makeFixture = (fixtures) => {
265
314
  const requested = fixtures.requested;
package/GitHubError.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { headerNumber, retryAfterMillisFrom } from "./internal/headers.js";
2
+ import { scrubThrowable } from "./internal/scrub.js";
2
3
  import { Schema } from "effect";
3
4
 
4
5
  // src/GitHubError.ts
@@ -106,7 +107,7 @@ var GitHubError = class GitHubError extends Schema.TaggedError()("GitHubError",
106
107
  * 422s ("Update is not a fast forward") are prose only.
107
108
  */
108
109
  validation: Schema.optionalKey(Schema.Array(GitHubValidationEntry)),
109
- /** The underlying throwable, when one exists. */
110
+ /** The underlying throwable, when one exists; an octokit throwable is kept as a credential-free copy. */
110
111
  cause: Schema.optionalKey(Schema.Defect())
111
112
  }) {
112
113
  /** `"GitBranch.upsert failed (422): Reference already exists"`. */
@@ -173,9 +174,19 @@ var GitHubError = class GitHubError extends Schema.TaggedError()("GitHubError",
173
174
  * epoch second, and turning it into a delay needs a "now" the caller controls.
174
175
  * The client supplies `Clock.currentTimeMillis`, which is the `TestClock`
175
176
  * under test.
177
+ *
178
+ * The kept `cause` is a credential-free copy of the throwable, not the
179
+ * throwable itself. octokit redacts only the `authorization` credential and
180
+ * two query values; the copy also drops the request body (a webhook secret,
181
+ * GraphQL variables), the per-request options and the response body beyond
182
+ * its top-level `message` and `documentation_url`, drops `set-cookie`, and
183
+ * reads `REDACTED` for every other credential-bearing header. It keeps the
184
+ * prototype (so `instanceof` still answers), `name`, `message`, `status`,
185
+ * the method, the redacted url and the remaining headers. A throwable with
186
+ * no `request`, `response` or `headers` field is kept as it is.
176
187
  */
177
188
  static fromOctokit(operation, error, nowMillis) {
178
- return fromFacts(operation, readThrowable(error), nowMillis, error);
189
+ return fromFacts(operation, readThrowable(error), nowMillis, scrubThrowable(error));
179
190
  }
180
191
  /**
181
192
  * Classify a raw HTTP response: a status, its headers and its JSON body.
package/GitHubRelease.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { GitHubError } from "./GitHubError.js";
2
+ import { originOf, uploadChannelOf, uploadsBaseUrlFor } from "./internal/uploads.js";
2
3
  import { unstubbed } from "./internal/unstubbed.js";
3
4
  import { GitHubClient } from "./GitHubClient.js";
4
5
  import { Repo } from "./Repo.js";
@@ -19,7 +20,15 @@ var ReleaseInfo = class extends Schema.Class("ReleaseInfo")({
19
20
  prerelease: Schema.Boolean,
20
21
  /** The web URL. */
21
22
  url: Schema.String,
22
- /** The templated upload endpoint GitHub hands back for assets. */
23
+ /**
24
+ * The templated upload endpoint GitHub hands back for assets.
25
+ *
26
+ * @remarks
27
+ * {@link GitHubReleaseShape.uploadAsset} never routes by this value: it is
28
+ * response data. It only cross-checks that its origin is the upload host
29
+ * derived from the client's configured base URL, and refuses the upload
30
+ * when it is not.
31
+ */
23
32
  uploadUrl: Schema.String
24
33
  }) {};
25
34
  /**
@@ -172,16 +181,26 @@ const make = (client) => {
172
181
  release: release.id,
173
182
  asset: asset.name
174
183
  });
175
- const raw = yield* client.requestDecoded(asset.label === void 0 ? "POST /repos/{owner}/{repo}/releases/{release_id}/assets{?name}" : "POST /repos/{owner}/{repo}/releases/{release_id}/assets{?name,label}", {
184
+ const channel = uploadChannelOf(client);
185
+ const uploadsBaseUrl = uploadsBaseUrlFor(channel?.baseUrl);
186
+ const expected = originOf(uploadsBaseUrl);
187
+ const offered = originOf(release.uploadUrl);
188
+ if (expected === void 0 || offered !== expected) return yield* Effect.fail(new GitHubError({
189
+ kind: "rejected",
190
+ operation: "GitHubRelease.uploadAsset",
191
+ reason: offered === void 0 ? `release ${release.id} has no absolute upload URL; refusing to upload` : `release ${release.id}'s upload URL is on ${offered}, not this client's upload host ${expected ?? uploadsBaseUrl}; refusing to send the token there`
192
+ }));
193
+ const route = asset.label === void 0 ? "POST /repos/{owner}/{repo}/releases/{release_id}/assets{?name}" : "POST /repos/{owner}/{repo}/releases/{release_id}/assets{?name,label}";
194
+ const params = {
176
195
  owner,
177
196
  repo,
178
197
  release_id: release.id,
179
198
  name: asset.name,
180
199
  ...asset.label === void 0 ? {} : { label: asset.label },
181
200
  data: asset.data,
182
- baseUrl: UPLOADS_BASE_URL,
183
201
  headers: { "content-type": asset.contentType }
184
- }, AssetResponse);
202
+ };
203
+ const raw = channel === void 0 ? yield* client.requestDecoded(route, params, AssetResponse) : yield* channel.upload(route, params, AssetResponse);
185
204
  return assetOf(raw);
186
205
  }),
187
206
  listAssets: Effect.fn("GitHubRelease.listAssets")(function* (id, options) {
@@ -199,7 +218,5 @@ const make = (client) => {
199
218
  })
200
219
  };
201
220
  };
202
- /** Release assets do not go to the API host. */
203
- const UPLOADS_BASE_URL = "https://uploads.github.com";
204
221
 
205
222
  export { GitHubRelease, ReleaseAsset, ReleaseInfo };
@@ -1,4 +1,5 @@
1
1
  import { GraphQLDocument } from "./GraphQL.js";
2
+ import { reservedSettingKey } from "./internal/requestOptions.js";
2
3
  import { unstubbed, unstubbedRead } from "./internal/unstubbed.js";
3
4
  import { GitHubClient } from "./GitHubClient.js";
4
5
  import { Repo } from "./Repo.js";
@@ -8,7 +9,7 @@ import { Context, Effect, Layer, Schema } from "effect";
8
9
  // src/GitHubRepository.ts
9
10
  /**
10
11
  * A {@link RepositoryPatch} from a draft, dropping every explicitly-`undefined`
11
- * field.
12
+ * field, plus any settings octokit's types do not know yet.
12
13
  *
13
14
  * @remarks
14
15
  * The supported spelling for "apply only what was configured" — the natural
@@ -18,6 +19,13 @@ import { Context, Effect, Layer, Schema } from "effect";
18
19
  * `PATCH` treats an absent field as "leave it alone", while an explicit `null`
19
20
  * or `undefined` is a value.
20
21
  *
22
+ * `options.extra` is the typed escape hatch for a setting GitHub accepts
23
+ * before octokit's generated types declare it: the draft stays checked, and
24
+ * only the extras ride unchecked. The result is still typed `RepositoryPatch`,
25
+ * and `GitHubRepository.updateSettings` sends every key it carries. A known key
26
+ * passed through `extra` throws a `RangeError` rather than skipping the draft's
27
+ * type check (see {@link RepositoryPatchOptions.extra}).
28
+ *
21
29
  * A key-by-key loop still defeats TypeScript's correlation between two indexed
22
30
  * accesses (`draft[key] = source[key]` over a union `key`), which no helper can
23
31
  * fix — build the draft as a literal where you can.
@@ -27,23 +35,60 @@ import { Context, Effect, Layer, Schema } from "effect";
27
35
  * import { repositoryPatch } from "@effected/github";
28
36
  *
29
37
  * // `config.has_issues` is `boolean | undefined`; absent fields drop out.
30
- * const patch = repositoryPatch({
31
- * has_issues: config.has_issues,
32
- * has_wiki: config.has_wiki,
33
- * description: config.description,
34
- * });
38
+ * const patch = repositoryPatch(
39
+ * {
40
+ * has_issues: config.has_issues,
41
+ * has_wiki: config.has_wiki,
42
+ * description: config.description,
43
+ * },
44
+ * // A setting GitHub accepts before octokit's types declare it.
45
+ * { extra: { has_new_feature: true } },
46
+ * );
35
47
  * ```
36
48
  *
37
49
  * @param draft - The fields to apply, any of which may be `undefined`.
50
+ * @param options - `extra`: settings the generated type does not declare.
38
51
  * @returns A patch carrying only the fields that were actually set.
39
52
  *
40
53
  * @public
41
54
  */
42
- const repositoryPatch = (draft) => {
55
+ const repositoryPatch = (draft, options) => {
43
56
  const out = {};
44
- for (const [key, value] of Object.entries(draft)) if (value !== void 0) out[key] = value;
57
+ for (const [key, value] of Object.entries(draft)) {
58
+ refuseReserved(key);
59
+ if (value !== void 0) out[key] = value;
60
+ }
61
+ for (const [key, value] of Object.entries(options?.extra ?? {})) {
62
+ refuseReserved(key);
63
+ if (Object.hasOwn(GRAPHQL_ONLY_SETTINGS, key)) throw new RangeError(`repositoryPatch: "${key}" can only be set through GraphQL; use GitHubRepository.applySettings, not a REST patch`);
64
+ if (REPOSITORY_REST_SETTING_KEYS.has(key)) throw new RangeError(`repositoryPatch: "${key}" is a declared repository setting; pass it in the draft, where its type is checked, not in extra`);
65
+ if (value !== void 0) out[key] = value;
66
+ }
45
67
  return out;
46
68
  };
69
+ /** A `RangeError` for a key octokit would read as something other than a setting. */
70
+ const refuseReserved = (key) => {
71
+ const reason = reservedSettingKey(key);
72
+ if (reason !== void 0) throw new RangeError(`repositoryPatch: "${key}" ${reason}; it is never sent`);
73
+ };
74
+ /**
75
+ * The parameters of a repository PATCH, with the settings as octokit's `data`.
76
+ *
77
+ * @remarks
78
+ * `data` is octokit's explicit body: `@octokit/endpoint` sends it verbatim and
79
+ * reads no key inside it as a request option or a path parameter. Spreading
80
+ * the settings beside `owner` and `repo` instead lets an open map choose the
81
+ * request's host, headers, fetch implementation or path, which is how an
82
+ * authenticated PATCH reaches another host with the token attached. So the
83
+ * confinement holds by construction; the reserved-name refusals are defence in
84
+ * depth. The generated parameter type has no `data`, so the cast is here, on
85
+ * the one shape both write paths send.
86
+ */
87
+ const patchParams = (owner, repo, body) => ({
88
+ owner,
89
+ repo,
90
+ data: body
91
+ });
47
92
  /**
48
93
  * Fields in the user-facing `security_and_analysis` block that GitHub accepts
49
94
  * as `{ status: "enabled" | "disabled" }`.
@@ -268,26 +313,29 @@ const make = (client) => {
268
313
  repo,
269
314
  fields: Object.keys(patch).length
270
315
  });
271
- return yield* client.request("PATCH /repos/{owner}/{repo}", {
272
- ...preparePatch(patch),
273
- owner,
274
- repo
275
- });
316
+ return yield* client.request("PATCH /repos/{owner}/{repo}", patchParams(owner, repo, preparePatch(patch)));
276
317
  }),
277
318
  defaultBranch: Effect.map(settings, (repository) => repository.default_branch),
278
319
  nodeId: Effect.map(settings, (repository) => repository.node_id),
279
320
  ownerType,
280
- applySettings: Effect.fn("GitHubRepository.applySettings")(function* (input) {
321
+ applySettings: Effect.fn("GitHubRepository.applySettings")(function* (input, options) {
281
322
  const { owner, repo } = yield* Repo;
323
+ const sendUnknown = options?.unknownKeys === "send";
282
324
  yield* Effect.annotateCurrentSpan({
283
325
  owner,
284
- repo
326
+ repo,
327
+ unknownKeys: sendUnknown ? "send" : "ignore"
285
328
  });
286
329
  const rest = {};
287
330
  const graphql = {};
288
331
  const graphqlKeys = [];
289
332
  const ignored = [];
333
+ const unverified = [];
290
334
  for (const [key, value] of Object.entries(input)) {
335
+ if (reservedSettingKey(key) !== void 0) {
336
+ ignored.push(key);
337
+ continue;
338
+ }
291
339
  const graphqlField = Object.hasOwn(GRAPHQL_ONLY_SETTINGS, key) ? GRAPHQL_ONLY_SETTINGS[key] : void 0;
292
340
  if (graphqlField !== void 0) {
293
341
  graphql[graphqlField] = value;
@@ -295,19 +343,22 @@ const make = (client) => {
295
343
  continue;
296
344
  }
297
345
  const removedIn = Object.hasOwn(REMOVED_BY_PINNED_VERSION, key) ? REMOVED_BY_PINNED_VERSION[key] : void 0;
298
- if (!REPOSITORY_REST_SETTING_KEYS.has(key) || removedIn !== void 0 && client.apiVersion >= removedIn) {
346
+ if (removedIn !== void 0 && client.apiVersion >= removedIn) {
299
347
  ignored.push(key);
300
348
  continue;
301
349
  }
350
+ if (!REPOSITORY_REST_SETTING_KEYS.has(key)) {
351
+ if (!sendUnknown) {
352
+ ignored.push(key);
353
+ continue;
354
+ }
355
+ unverified.push(key);
356
+ }
302
357
  rest[key] = value;
303
358
  }
304
359
  const prepared = preparePatch(rest);
305
360
  const restKeys = Object.keys(prepared);
306
- if (restKeys.length > 0) yield* client.request("PATCH /repos/{owner}/{repo}", {
307
- ...prepared,
308
- owner,
309
- repo
310
- });
361
+ if (restKeys.length > 0) yield* client.request("PATCH /repos/{owner}/{repo}", patchParams(owner, repo, prepared));
311
362
  if (Object.keys(graphql).length > 0) {
312
363
  const repository = yield* client.request("GET /repos/{owner}/{repo}", {
313
364
  owner,
@@ -321,7 +372,8 @@ const make = (client) => {
321
372
  return {
322
373
  rest: restKeys,
323
374
  graphql: graphqlKeys,
324
- ignored
375
+ ignored,
376
+ unverified: unverified.filter((key) => Object.hasOwn(prepared, key))
325
377
  };
326
378
  })
327
379
  };
package/GraphQL.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { retryAfterMillisFrom } from "./internal/headers.js";
2
+ import { scrubThrowable } from "./internal/scrub.js";
2
3
  import { Schema } from "effect";
3
4
 
4
5
  // src/GraphQL.ts
@@ -78,6 +79,11 @@ var GitHubGraphQLError = class GitHubGraphQLError extends Schema.TaggedError()("
78
79
  * failure with a `status`. Both arrive as throwables and both are read
79
80
  * structurally, for the same reason the REST classifier does it — the error
80
81
  * classes live in packages this one does not declare.
82
+ *
83
+ * The kept `cause` is the same credential-free copy
84
+ * {@link GitHubError.fromOctokit} keeps: the GraphQL document, its variables
85
+ * and any partial `data` are dropped with the rest of the request and response
86
+ * bodies.
81
87
  */
82
88
  static fromThrowable(operation, error, nowMillis) {
83
89
  const record = typeof error === "object" && error !== null ? error : void 0;
@@ -92,7 +98,7 @@ var GitHubGraphQLError = class GitHubGraphQLError extends Schema.TaggedError()("
92
98
  reason,
93
99
  errors: entries,
94
100
  ...retryAfterMillis !== void 0 ? { retryAfterMillis } : {},
95
- cause: error
101
+ cause: scrubThrowable(error)
96
102
  });
97
103
  }
98
104
  };