@vercel/flags-core 1.10.1 → 1.10.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @vercel/flags-core
2
2
 
3
+ ## 1.10.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#578](https://github.com/vercel/flags/pull/578) [`63d13be`](https://github.com/vercel/flags/commit/63d13be0193102a4f0cbcbf48e7f67efad96a462) Thanks [@luismeyer](https://github.com/luismeyer)! - Fail fast on authentication errors. When the stream gets a 401 or no OIDC token is available, the client no longer falls back to polling, which would fail the same way, and goes straight to revalidating over HTTP on reads. When polling is the primary source, its first auth failure stops it, and a missing OIDC token is no longer retried. A failed OIDC token refresh is still retried. A client without credentials no longer keeps scripts and test files running.
8
+
9
+ ## 1.10.2
10
+
11
+ ### Patch Changes
12
+
13
+ - [#567](https://github.com/vercel/flags/pull/567) [`5f738f0`](https://github.com/vercel/flags/commit/5f738f08f182838d0efb13fd3efb89751d48cb0a) Thanks [@vincent-derks](https://github.com/vincent-derks)! - Improve guidance for authorization errors.
14
+
15
+ - [#573](https://github.com/vercel/flags/pull/573) [`e2a1a39`](https://github.com/vercel/flags/commit/e2a1a394e3de4b271c8305bae6cbcf053814ab70) Thanks [@luismeyer](https://github.com/luismeyer)! - Read embedded flag definitions when the client is created, using the SDK key or `VERCEL_PROJECT_ID` in OIDC mode.
16
+
3
17
  ## 1.10.1
4
18
 
5
19
  ### Patch Changes
@@ -576,7 +576,7 @@ function bulkEvaluate(flags, shared) {
576
576
  }
577
577
 
578
578
  // package.json
579
- var version = "1.10.1";
579
+ var version = "1.10.3";
580
580
 
581
581
  // src/lib/report-value.ts
582
582
  function internalReportValue(key, value, data) {
@@ -1104,19 +1104,28 @@ function unauthorizedMessage(sourceProjectId) {
1104
1104
  if (!sourceProjectId) {
1105
1105
  return "unauthorized (401)";
1106
1106
  }
1107
- return `unauthorized (401): this deployment is not allowed to read the flags of project "${sourceProjectId}". Check that the project is connected.`;
1107
+ return `unauthorized (401): this deployment is not allowed to read the flags of project "${sourceProjectId}". Check that the project is connected. See https://vercel.com/docs/flags/vercel-flags/project-connections`;
1108
1108
  }
1109
+ var oidcTokenMessage = [
1110
+ "@vercel/flags-core: Failed to get OIDC token.",
1111
+ "Are you running in a Vercel Environment where OIDC tokens are available?",
1112
+ "Did you mean to use an SDK Key instead? Use the environment variable FLAGS or pass it directly to the client."
1113
+ ].join(" ");
1114
+ var missingCredentials = /`vc link|vercel login|^The 'x-vercel-oidc-token' header is missing from the request\.$/;
1115
+ var OidcTokenError = class extends Error {
1116
+ constructor(cause) {
1117
+ super(oidcTokenMessage, { cause });
1118
+ this.name = "OidcTokenError";
1119
+ }
1120
+ };
1109
1121
  async function getOidcToken() {
1110
1122
  try {
1111
1123
  return await getVercelOidcToken();
1112
- } catch {
1113
- throw new Error(
1114
- [
1115
- "@vercel/flags-core: Failed to get OIDC token.",
1116
- "Are you running in a Vercel Environment where OIDC tokens are available?",
1117
- "Did you mean to use an SDK Key instead? Use the environment variable FLAGS or pass it directly to the client."
1118
- ].join(" ")
1119
- );
1124
+ } catch (error) {
1125
+ if (error instanceof Error && missingCredentials.test(error.message)) {
1126
+ throw new OidcTokenError(error);
1127
+ }
1128
+ throw new Error(oidcTokenMessage, { cause: error });
1120
1129
  }
1121
1130
  }
1122
1131
  function getProjectIdFromOidcToken(oidcToken) {
@@ -1159,13 +1168,24 @@ var Authentication = class {
1159
1168
  }
1160
1169
  return await getOidcToken();
1161
1170
  }
1162
- async resolveBundledDefinitionsLookup() {
1171
+ getStaticBundledDefinitionsLookup() {
1163
1172
  if (this.sdkKey) {
1164
1173
  return { type: "sdk-key", sdkKey: this.sdkKey };
1165
1174
  }
1166
1175
  if (this.sourceProjectId) {
1167
1176
  return { type: "project-id", projectId: this.sourceProjectId };
1168
1177
  }
1178
+ const projectId = process.env.VERCEL_PROJECT_ID;
1179
+ if (projectId) {
1180
+ return { type: "project-id", projectId };
1181
+ }
1182
+ return void 0;
1183
+ }
1184
+ async resolveBundledDefinitionsLookup() {
1185
+ const lookup = this.getStaticBundledDefinitionsLookup();
1186
+ if (lookup) {
1187
+ return lookup;
1188
+ }
1169
1189
  const oidcToken = await this.resolveToken();
1170
1190
  return {
1171
1191
  type: "project-id",
@@ -1675,6 +1695,13 @@ var BundledSource = class {
1675
1695
  }
1676
1696
  return void 0;
1677
1697
  }
1698
+ /**
1699
+ * Starts reading bundled definitions without waiting for the result.
1700
+ */
1701
+ preload() {
1702
+ this.getResult().catch(() => {
1703
+ });
1704
+ }
1678
1705
  getResult() {
1679
1706
  if (!this.promise) {
1680
1707
  this.promise = this.options.readBundledDefinitions(this.options.auth);
@@ -2142,7 +2169,8 @@ async function fetchDatafile(options) {
2142
2169
  return await Promise.race([fetchAttempt(attempt + 1), aborted]);
2143
2170
  } catch (error) {
2144
2171
  signal.throwIfAborted();
2145
- if (attempt === maxAttempts - 1 || error instanceof DatafileHttpError && !error.retryable) {
2172
+ if (attempt === maxAttempts - 1 || error instanceof DatafileHttpError && !error.retryable || // Retrying will not produce a token that is not there.
2173
+ error instanceof OidcTokenError) {
2146
2174
  throw error instanceof Error ? error : new Error("Unknown fetch error");
2147
2175
  }
2148
2176
  debug(options.clientName, "datafile.fetch.retry", () => ({
@@ -2560,7 +2588,7 @@ async function connectStream(config, callbacks) {
2560
2588
  rejectInit(error);
2561
2589
  }
2562
2590
  onDisconnect?.();
2563
- abortController.abort();
2591
+ abortController.abort(error);
2564
2592
  break;
2565
2593
  }
2566
2594
  if (!response.ok) {
@@ -2662,7 +2690,7 @@ async function connectStream(config, callbacks) {
2662
2690
  }
2663
2691
  if (error instanceof TokenResolutionError && !initialDataReceived) {
2664
2692
  rejectInit(error.cause);
2665
- abortController.abort();
2693
+ abortController.abort(error.cause);
2666
2694
  break;
2667
2695
  }
2668
2696
  if (connectionAbort.signal.reason !== PING_TIMEOUT) {
@@ -2736,7 +2764,7 @@ var StreamSource = class extends TypedEmitter {
2736
2764
  if (this.abortController === abortController) {
2737
2765
  this.promise = void 0;
2738
2766
  this.abortController = void 0;
2739
- this.emit("exhausted");
2767
+ this.emit("exhausted", abortController.signal.reason);
2740
2768
  }
2741
2769
  },
2742
2770
  { once: true }
@@ -2797,7 +2825,7 @@ var UNKNOWN_FRESHNESS = {
2797
2825
  assess: () => ({ status: "unknown" })
2798
2826
  };
2799
2827
  function isUnauthorizedError(error) {
2800
- if (error instanceof UnauthorizedError) {
2828
+ if (error instanceof UnauthorizedError || error instanceof OidcTokenError) {
2801
2829
  return true;
2802
2830
  }
2803
2831
  if (!(error instanceof Error)) {
@@ -2832,8 +2860,9 @@ var Controller = class {
2832
2860
  // Build-step deduplication
2833
2861
  buildDataPromise = null;
2834
2862
  buildReadTracked = false;
2835
- // Suppresses usage tracking while the credential is rejected. Set on a 401
2836
- // from any source, cleared as soon as a source delivers data again.
2863
+ // Suppresses usage tracking while the credential is rejected or no OIDC
2864
+ // token is available. Set on an auth failure from any source, cleared as
2865
+ // soon as a source delivers data again.
2837
2866
  unauthorized = false;
2838
2867
  constructor(options) {
2839
2868
  this.options = normalizeOptions(options);
@@ -2870,6 +2899,9 @@ var Controller = class {
2870
2899
  auth: this.options.auth,
2871
2900
  readBundledDefinitions
2872
2901
  });
2902
+ if (!this.options.datafile && this.options.auth.getStaticBundledDefinitionsLookup()) {
2903
+ this.bundledSource.preload();
2904
+ }
2873
2905
  this.wireSourceEvents();
2874
2906
  if (this.options.datafile) {
2875
2907
  this.cache.seed(tagData({ ...this.options.datafile }, "provided"));
@@ -2926,10 +2958,16 @@ var Controller = class {
2926
2958
  this.transition("degraded", "stream-disconnected");
2927
2959
  }
2928
2960
  };
2929
- onStreamExhausted = () => {
2961
+ onStreamExhausted = (reason) => {
2930
2962
  if (this.state !== "streaming" && this.state !== "degraded") {
2931
2963
  return;
2932
2964
  }
2965
+ if (isUnauthorizedError(reason)) {
2966
+ if (this.state === "streaming") {
2967
+ this.transition("degraded", "stream-auth-failed");
2968
+ }
2969
+ return;
2970
+ }
2933
2971
  void this.activateFallbackSource().catch(() => {
2934
2972
  });
2935
2973
  };
@@ -2941,7 +2979,7 @@ var Controller = class {
2941
2979
  this.onSourceError(error);
2942
2980
  if (this.state === "polling" && isUnauthorizedError(error)) {
2943
2981
  this.pollingSource.stop();
2944
- this.transition("degraded", "polling-unauthorized");
2982
+ this.transition("degraded", "polling-auth-failed");
2945
2983
  }
2946
2984
  };
2947
2985
  // ---------------------------------------------------------------------------
@@ -3351,6 +3389,10 @@ var Controller = class {
3351
3389
  this.transition("degraded", "stream-still-connecting");
3352
3390
  return;
3353
3391
  }
3392
+ if (outcome === "auth-failed") {
3393
+ this.transition("degraded", "stream-auth-failed");
3394
+ return;
3395
+ }
3354
3396
  }
3355
3397
  if (this.options.polling.enabled) {
3356
3398
  if (this.state !== "polling") {
@@ -3383,7 +3425,7 @@ var Controller = class {
3383
3425
  return "connected";
3384
3426
  } catch (error) {
3385
3427
  this.noteUnauthorized(error);
3386
- return "failed";
3428
+ return isUnauthorizedError(error) ? "auth-failed" : "failed";
3387
3429
  }
3388
3430
  }
3389
3431
  let timeoutId;
@@ -3417,7 +3459,7 @@ var Controller = class {
3417
3459
  } catch (error) {
3418
3460
  clearTimeout(timeoutId);
3419
3461
  this.noteUnauthorized(error);
3420
- return "failed";
3462
+ return isUnauthorizedError(error) ? "auth-failed" : "failed";
3421
3463
  }
3422
3464
  }
3423
3465
  // ---------------------------------------------------------------------------
@@ -3745,4 +3787,4 @@ export {
3745
3787
  resetDefaultFlagsClient,
3746
3788
  createClient
3747
3789
  };
3748
- //# sourceMappingURL=chunk-CAVQRHGX.js.map
3790
+ //# sourceMappingURL=chunk-2P2UTT7O.js.map