Skip to content

feat: add AccessContext types (replacement for #6413)#6549

Open
jamesopstad wants to merge 4 commits intomainfrom
add-access-types
Open

feat: add AccessContext types (replacement for #6413)#6549
jamesopstad wants to merge 4 commits intomainfrom
add-access-types

Conversation

@jamesopstad
Copy link
Copy Markdown
Contributor

Replacement for #6413 so that the internal build workflow can run.

@jamesopstad jamesopstad requested review from a team as code owners April 10, 2026 10:59
@jamesopstad jamesopstad requested a review from penalosa April 10, 2026 10:59
@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented Apr 10, 2026

LGTM

github run

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 10, 2026

The generated output of @cloudflare/workers-types has been changed by this PR. If this is intentional, run just generate-types to update the snapshot. Alternatively, you can download the full generated types:

Full Type Diff
diff -r types/generated-snapshot/latest/index.d.ts bazel-bin/types/definitions/latest/index.d.ts
12726,12859d12725
< type EvaluationContext = Record<string, string | number | boolean>;
< interface EvaluationDetails<T> {
<   flagKey: string;
<   value: T;
<   variant?: string | undefined;
<   reason?: string | undefined;
<   errorCode?: string | undefined;
<   errorMessage?: string | undefined;
< }
< interface FlagEvaluationError extends Error {}
< /**
<  * Feature flags binding for evaluating feature flags from a Cloudflare Workers script.
<  *
<  * @example
<  * ```typescript
<  * // Get a boolean flag value with a default
<  * const enabled = await env.FLAGS.getBooleanValue('my-feature', false);
<  *
<  * // Get a flag value with evaluation context for targeting
<  * const variant = await env.FLAGS.getStringValue('experiment', 'control', {
<  *   userId: 'user-123',
<  *   country: 'US',
<  * });
<  *
<  * // Get full evaluation details including variant and reason
<  * const details = await env.FLAGS.getBooleanDetails('my-feature', false);
<  * console.log(details.variant, details.reason);
<  * ```
<  */
< declare abstract class Flags {
<   /**
<    * Get a flag value without type checking.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Optional default value returned when evaluation fails.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   get(
<     flagKey: string,
<     defaultValue?: unknown,
<     context?: EvaluationContext,
<   ): Promise<unknown>;
<   /**
<    * Get a boolean flag value.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getBooleanValue(
<     flagKey: string,
<     defaultValue: boolean,
<     context?: EvaluationContext,
<   ): Promise<boolean>;
<   /**
<    * Get a string flag value.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getStringValue(
<     flagKey: string,
<     defaultValue: string,
<     context?: EvaluationContext,
<   ): Promise<string>;
<   /**
<    * Get a number flag value.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getNumberValue(
<     flagKey: string,
<     defaultValue: number,
<     context?: EvaluationContext,
<   ): Promise<number>;
<   /**
<    * Get an object flag value.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getObjectValue<T extends object>(
<     flagKey: string,
<     defaultValue: T,
<     context?: EvaluationContext,
<   ): Promise<T>;
<   /**
<    * Get a boolean flag value with full evaluation details.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getBooleanDetails(
<     flagKey: string,
<     defaultValue: boolean,
<     context?: EvaluationContext,
<   ): Promise<EvaluationDetails<boolean>>;
<   /**
<    * Get a string flag value with full evaluation details.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getStringDetails(
<     flagKey: string,
<     defaultValue: string,
<     context?: EvaluationContext,
<   ): Promise<EvaluationDetails<string>>;
<   /**
<    * Get a number flag value with full evaluation details.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getNumberDetails(
<     flagKey: string,
<     defaultValue: number,
<     context?: EvaluationContext,
<   ): Promise<EvaluationDetails<number>>;
<   /**
<    * Get an object flag value with full evaluation details.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getObjectDetails<T extends object>(
<     flagKey: string,
<     defaultValue: T,
<     context?: EvaluationContext,
<   ): Promise<EvaluationDetails<T>>;
< }
< /**
<  * Evaluation context for targeting rules.
<  * Keys are attribute names (e.g. "userId", "country"), values are the attribute values.
<  */
diff -r types/generated-snapshot/latest/index.ts bazel-bin/types/definitions/latest/index.ts
12743,12876d12742
< export type EvaluationContext = Record<string, string | number | boolean>;
< export interface EvaluationDetails<T> {
<   flagKey: string;
<   value: T;
<   variant?: string | undefined;
<   reason?: string | undefined;
<   errorCode?: string | undefined;
<   errorMessage?: string | undefined;
< }
< export interface FlagEvaluationError extends Error {}
< /**
<  * Feature flags binding for evaluating feature flags from a Cloudflare Workers script.
<  *
<  * @example
<  * ```typescript
<  * // Get a boolean flag value with a default
<  * const enabled = await env.FLAGS.getBooleanValue('my-feature', false);
<  *
<  * // Get a flag value with evaluation context for targeting
<  * const variant = await env.FLAGS.getStringValue('experiment', 'control', {
<  *   userId: 'user-123',
<  *   country: 'US',
<  * });
<  *
<  * // Get full evaluation details including variant and reason
<  * const details = await env.FLAGS.getBooleanDetails('my-feature', false);
<  * console.log(details.variant, details.reason);
<  * ```
<  */
< export declare abstract class Flags {
<   /**
<    * Get a flag value without type checking.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Optional default value returned when evaluation fails.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   get(
<     flagKey: string,
<     defaultValue?: unknown,
<     context?: EvaluationContext,
<   ): Promise<unknown>;
<   /**
<    * Get a boolean flag value.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getBooleanValue(
<     flagKey: string,
<     defaultValue: boolean,
<     context?: EvaluationContext,
<   ): Promise<boolean>;
<   /**
<    * Get a string flag value.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getStringValue(
<     flagKey: string,
<     defaultValue: string,
<     context?: EvaluationContext,
<   ): Promise<string>;
<   /**
<    * Get a number flag value.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getNumberValue(
<     flagKey: string,
<     defaultValue: number,
<     context?: EvaluationContext,
<   ): Promise<number>;
<   /**
<    * Get an object flag value.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getObjectValue<T extends object>(
<     flagKey: string,
<     defaultValue: T,
<     context?: EvaluationContext,
<   ): Promise<T>;
<   /**
<    * Get a boolean flag value with full evaluation details.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getBooleanDetails(
<     flagKey: string,
<     defaultValue: boolean,
<     context?: EvaluationContext,
<   ): Promise<EvaluationDetails<boolean>>;
<   /**
<    * Get a string flag value with full evaluation details.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getStringDetails(
<     flagKey: string,
<     defaultValue: string,
<     context?: EvaluationContext,
<   ): Promise<EvaluationDetails<string>>;
<   /**
<    * Get a number flag value with full evaluation details.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getNumberDetails(
<     flagKey: string,
<     defaultValue: number,
<     context?: EvaluationContext,
<   ): Promise<EvaluationDetails<number>>;
<   /**
<    * Get an object flag value with full evaluation details.
<    * @param flagKey The key of the flag to evaluate.
<    * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
<    * @param context Optional evaluation context for targeting rules.
<    */
<   getObjectDetails<T extends object>(
<     flagKey: string,
<     defaultValue: T,
<     context?: EvaluationContext,
<   ): Promise<EvaluationDetails<T>>;
< }
< /**
<  * Evaluation context for targeting rules.
<  * Keys are attribute names (e.g. "userId", "country"), values are the attribute values.
<  */

modules = [
(name = "worker", esModule = embed "ctx-access-test.js")
],
compatibilityFlags = ["nodejs_compat", "experimental", "enable_ctx_access"],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is nodejs_compat actually needed?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, but yes since the test imports node:assert

@jamesopstad jamesopstad force-pushed the add-access-types branch 2 times, most recently from efb8315 to a97d168 Compare April 10, 2026 12:08
@jamesopstad jamesopstad removed the request for review from penalosa April 10, 2026 13:34
Comment thread src/workerd/io/compatibility-date.capnp Outdated
# When enabled, creating a Blob with a resizable ArrayBuffer will throw a TypeError, matching
# expected spec behavior.

enableCtxAccess @174 :Bool
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need a compat flag?
Seems to me like we wouldn't have to?

If this is just a temporary measure then just use getWorkerdExperimental() instead

Comment thread src/workerd/api/global-scope.h Outdated
return js.undefined();
}

// Called by the host runtime (edgeworker) to set the Access context for this request.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Called by the host runtime (edgeworker) to set the Access context for this request.
// Called by the runtime to set the Access context for this request.

Comment thread src/workerd/api/global-scope.h Outdated
// Called by the host runtime (edgeworker) to set the Access context for this request.
// Must be called before the worker's handler is invoked.
//
// Unlike other ExecutionContext fields (props, version, exports) which are injected through the
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually don't think this is right, props is also per request and not per worker.
We should be able to mimic props and do it on the constructor.
That should be the correct place for it.

BSFishy added 4 commits April 17, 2026 16:55
Signed-off-by: Matt Provost <mprovost@cloudflare.com>
Signed-off-by: Matt Provost <mprovost@cloudflare.com>
Signed-off-by: Matt Provost <mprovost@cloudflare.com>
Signed-off-by: Matt Provost <mprovost@cloudflare.com>
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 17, 2026

Merging this PR will degrade performance by 28.78%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 1 regressed benchmark
✅ 71 untouched benchmarks
⏩ 129 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
simpleStringBody[Response] 19.3 µs 27.1 µs -28.78%

Comparing add-access-types (d6ef597) with main (556d2f6)

Open in CodSpeed

Footnotes

  1. 129 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

}
};

// Base class for the ctx.access object providing Cloudflare Access authentication context.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see you are following the pattern of CacheContext, above. CacheContext is a relatively new class (2 weeks old) which I've never seen before now. Unfortunately, I don't think CacheContext is designed properly. CacheContext seems to have invented a new way of exposing a per-context API, rather than following the common pattern.

It appears that:

  • CacheContext is declared as an API type with a virtual implementation.
  • There's a method on Worker::Api which creates instances of it.
  • In the production codebase, this method is implemented by calling a method on the production codebase's version of IoChannelFactory.

Instead, a bunch better approach would have been:

  • CacheContext should be a fully concrete class here.
  • workerd's version of IoChannelFactory should be extended with a new method to get the underlying I/O object needed by CacheContext.
  • This object may be implemented differently in workerd and production -- that's fine. But the API wrapper CacheContext is just a wrapper around this I/O object, and is implemented entirely in workerd.

AccessContext should be the same.

However, there's actually a further catch here: IoChannelFactory generally doesn't contain things that are specific to one request. Indeed, in workerd, there is only one IoChannelFactory per worker, not per-request. In the production codebase, there actually is an IoChannelFactory per-request, which is why CacheContext is able to pull what it needs off of that object. However, that's a design flaw in the production codebase which I want to fix.

For information that changes on a per-request basis, the correct thing to do is not to put it on IoChannelFactory at all, but rather to have it passed in as a parameter to newWorkerEntrypoint(), where it can then make its way onto the IoContext.

cc @tewaro who seems to have written the CacheContext stuff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants