Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/workerd/api/global-scope.h
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,25 @@ class ExecutionContext: public jsg::Object {
return js.undefined();
}

// 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
// constructor, access uses a post-construction setter because the Access context is assembled by
// the host runtime after ExecutionContext construction but before handler invocation. The access
// data (audience claim, identity fetcher) originates from the Cloudflare Access integration
// pipeline and is not available during ExecutionContext construction in edgeworker.
void setAccess(jsg::Lock& js, jsg::JsRef<jsg::JsValue> value) {
access = kj::mv(value);
}

jsg::JsValue getAccess(jsg::Lock& js) {
KJ_IF_SOME(a, access) {
return a.getHandle(js);
}
return js.undefined();
}

JSG_RESOURCE_TYPE(ExecutionContext, CompatibilityFlags::Reader flags) {
JSG_METHOD(waitUntil);
JSG_METHOD(passThroughOnException);
Expand All @@ -249,6 +268,9 @@ class ExecutionContext: public jsg::Object {
if (flags.getEnableVersionApi()) {
JSG_LAZY_INSTANCE_PROPERTY(version, getVersion);
}
if (flags.getEnableCtxAccess()) {
JSG_LAZY_INSTANCE_PROPERTY(access, getAccess);
}

if (flags.getWorkerdExperimental()) {
// TODO(soon): Before making this generally available we need to:
Expand All @@ -265,6 +287,11 @@ class ExecutionContext: public jsg::Object {
}

// TODO(soon): This is getting unwieldy.
// Note: `access` is included unconditionally in all TS_OVERRIDE branches (unlike `version`
// which is gated by enableVersionApi). This is intentional — adding another conditional would
// double the branch count (from 4 to 8). Since `access` is optional (`?`), the type is
// correct regardless of whether the flag is enabled (the property will be undefined at runtime
// when the flag is off or when setAccess() hasn't been called).
if (flags.getEnableCtxExports()) {
if (flags.getEnableVersionApi()) {
JSG_TS_OVERRIDE(<Props = unknown> {
Expand All @@ -276,11 +303,13 @@ class ExecutionContext: public jsg::Object {
readonly key?: string;
readonly override?: string;
};
readonly access?: CloudflareAccessContext;
});
} else {
JSG_TS_OVERRIDE(<Props = unknown> {
readonly props: Props;
readonly exports: Cloudflare.Exports;
readonly access?: CloudflareAccessContext;
});
}
} else {
Expand All @@ -293,10 +322,12 @@ class ExecutionContext: public jsg::Object {
readonly key?: string;
readonly override?: string;
};
readonly access?: CloudflareAccessContext;
});
} else {
JSG_TS_OVERRIDE(<Props = unknown> {
readonly props: Props;
readonly access?: CloudflareAccessContext;
});
}
}
Expand All @@ -305,16 +336,19 @@ class ExecutionContext: public jsg::Object {
void visitForMemoryInfo(jsg::MemoryTracker& tracker) const {
tracker.trackField("props", props);
tracker.trackField("version", version);
tracker.trackField("access", access);
}

private:
jsg::JsRef<jsg::JsValue> exports;
jsg::JsRef<jsg::JsValue> props;
kj::Maybe<jsg::JsRef<jsg::JsValue>> version;
kj::Maybe<jsg::JsRef<jsg::JsValue>> access;

void visitForGc(jsg::GcVisitor& visitor) {
visitor.visit(props);
visitor.visit(version);
visitor.visit(access);
}
};

Expand Down
6 changes: 6 additions & 0 deletions src/workerd/api/tests/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ wd_test(
args = ["--experimental"],
)

wd_test(
src = "ctx-access-test.wd-test",
args = ["--experimental"],
data = ["ctx-access-test.js"],
)

wd_test(
src = "cache-test.wd-test",
args = ["--experimental"],
Expand Down
11 changes: 11 additions & 0 deletions src/workerd/api/tests/ctx-access-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { strictEqual } from 'node:assert';

export const ctxAccessPropertyExists = {
test(controller, env, ctx) {
// When enable_ctx_access is enabled, the property should exist on ctx
// (as a lazy instance property), even though its value is undefined
// because setAccess() is only called by the host runtime (edgeworker).
strictEqual('access' in ctx, true);
strictEqual(ctx.access, undefined);
},
};
14 changes: 14 additions & 0 deletions src/workerd/api/tests/ctx-access-test.wd-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Workerd = import "/workerd/workerd.capnp";

const unitTests :Workerd.Config = (
services = [
( name = "ctx-access-test",
worker = (
modules = [
(name = "worker", esModule = embed "ctx-access-test.js")
],
compatibilityFlags = ["nodejs_compat", "experimental", "enable_ctx_access"],
)
),
],
);
9 changes: 9 additions & 0 deletions src/workerd/io/compatibility-date.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -1508,4 +1508,13 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef {
$compatDisableFlag("resizable_array_buffer_in_blob");
# When enabled, creating a Blob with a resizable ArrayBuffer will throw a TypeError, matching
# expected spec behavior.

enableCtxAccess @174 :Bool
$compatEnableFlag("enable_ctx_access")
$experimental;
# Enables the ctx.access property for Cloudflare Access integration.
# When enabled, ctx.access provides Access authentication context including the
# matched application audience (AUD) and an identity-fetching function. The value
# is set by the host runtime (edgeworker) per-request; in open-source workerd the
# property will always be undefined.
}
57 changes: 57 additions & 0 deletions types/defines/access.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Represents the identity of a user authenticated via Cloudflare Access.
* This matches the result of calling /cdn-cgi/access/get-identity.
*
* The exact structure of the returned object depends on the identity provider
* configuration for the Access application. The fields below represent commonly
* available properties, but additional provider-specific fields may be present.
*/
interface CloudflareAccessIdentity extends Record<string, unknown> {
/** The user's email address, if available from the identity provider. */
email?: string;
/** The user's display name. */
name?: string;
/** The user's unique identifier. */
user_uuid?: string;
/** The Cloudflare account ID. */
account_id?: string;
/** Login timestamp (Unix epoch seconds). */
iat?: number;
/** The user's IP address at authentication time. */
ip?: string;
/** Authentication methods used (e.g., "pwd"). */
amr?: string[];
/** Identity provider information. */
idp?: { id: string; type: string };
/** Geographic information about where the user authenticated. */
geo?: { country: string };
/** Group memberships from the identity provider. */
groups?: Array<{ id: string; name: string; email?: string }>;
/** Device posture check results, keyed by check ID. */
devicePosture?: Record<string, unknown>;
/** True if the user connected via Cloudflare WARP. */
is_warp?: boolean;
/** True if the user is authenticated via Cloudflare Gateway. */
is_gateway?: boolean;
}

/**
* Cloudflare Access authentication information for the current request.
*/
interface CloudflareAccessContext {
/**
* The audience claim from the Access JWT. This identifies which Access
* application the request matched.
*/
readonly aud: string;

/**
* Fetches the full identity information for the authenticated user.
* This makes a call to the Access identity service to retrieve extended
* user information such as groups, device posture, and identity provider data.
*
* @returns The subject's identity, if one exists
* @throws May throw if the identity service is unreachable or returns an error.
*/
getIdentity(): Promise<CloudflareAccessIdentity | undefined>;
}
65 changes: 65 additions & 0 deletions types/generated-snapshot/experimental/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ interface ExecutionContext<Props = unknown> {
readonly key?: string;
readonly override?: string;
};
readonly access?: CloudflareAccessContext;
abort(reason?: any): void;
}
type ExportedHandlerFetchHandler<
Expand Down Expand Up @@ -4683,6 +4684,70 @@ interface EventCounts {
): void;
[Symbol.iterator](): IterableIterator<string[]>;
}
/**
* Represents the identity of a user authenticated via Cloudflare Access.
* This matches the result of calling /cdn-cgi/access/get-identity.
*
* The exact structure of the returned object depends on the identity provider
* configuration for the Access application. The fields below represent commonly
* available properties, but additional provider-specific fields may be present.
*/
interface CloudflareAccessIdentity extends Record<string, unknown> {
/** The user's email address, if available from the identity provider. */
email?: string;
/** The user's display name. */
name?: string;
/** The user's unique identifier. */
user_uuid?: string;
/** The Cloudflare account ID. */
account_id?: string;
/** Login timestamp (Unix epoch seconds). */
iat?: number;
/** The user's IP address at authentication time. */
ip?: string;
/** Authentication methods used (e.g., "pwd"). */
amr?: string[];
/** Identity provider information. */
idp?: {
id: string;
type: string;
};
/** Geographic information about where the user authenticated. */
geo?: {
country: string;
};
/** Group memberships from the identity provider. */
groups?: Array<{
id: string;
name: string;
email?: string;
}>;
/** Device posture check results, keyed by check ID. */
devicePosture?: Record<string, unknown>;
/** True if the user connected via Cloudflare WARP. */
is_warp?: boolean;
/** True if the user is authenticated via Cloudflare Gateway. */
is_gateway?: boolean;
}
/**
* Cloudflare Access authentication information for the current request.
*/
interface CloudflareAccessContext {
/**
* The audience claim from the Access JWT. This identifies which Access
* application the request matched.
*/
readonly aud: string;
/**
* Fetches the full identity information for the authenticated user.
* This makes a call to the Access identity service to retrieve extended
* user information such as groups, device posture, and identity provider data.
*
* @returns The subject's identity, if one exists
* @throws May throw if the identity service is unreachable or returns an error.
*/
getIdentity(): Promise<CloudflareAccessIdentity | undefined>;
}
// ============ AI Search Error Interfaces ============
interface AiSearchInternalError extends Error {}
interface AiSearchNotFoundError extends Error {}
Expand Down
65 changes: 65 additions & 0 deletions types/generated-snapshot/experimental/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ export interface ExecutionContext<Props = unknown> {
readonly key?: string;
readonly override?: string;
};
readonly access?: CloudflareAccessContext;
abort(reason?: any): void;
}
export type ExportedHandlerFetchHandler<
Expand Down Expand Up @@ -4689,6 +4690,70 @@ export interface EventCounts {
): void;
[Symbol.iterator](): IterableIterator<string[]>;
}
/**
* Represents the identity of a user authenticated via Cloudflare Access.
* This matches the result of calling /cdn-cgi/access/get-identity.
*
* The exact structure of the returned object depends on the identity provider
* configuration for the Access application. The fields below represent commonly
* available properties, but additional provider-specific fields may be present.
*/
export interface CloudflareAccessIdentity extends Record<string, unknown> {
/** The user's email address, if available from the identity provider. */
email?: string;
/** The user's display name. */
name?: string;
/** The user's unique identifier. */
user_uuid?: string;
/** The Cloudflare account ID. */
account_id?: string;
/** Login timestamp (Unix epoch seconds). */
iat?: number;
/** The user's IP address at authentication time. */
ip?: string;
/** Authentication methods used (e.g., "pwd"). */
amr?: string[];
/** Identity provider information. */
idp?: {
id: string;
type: string;
};
/** Geographic information about where the user authenticated. */
geo?: {
country: string;
};
/** Group memberships from the identity provider. */
groups?: Array<{
id: string;
name: string;
email?: string;
}>;
/** Device posture check results, keyed by check ID. */
devicePosture?: Record<string, unknown>;
/** True if the user connected via Cloudflare WARP. */
is_warp?: boolean;
/** True if the user is authenticated via Cloudflare Gateway. */
is_gateway?: boolean;
}
/**
* Cloudflare Access authentication information for the current request.
*/
export interface CloudflareAccessContext {
/**
* The audience claim from the Access JWT. This identifies which Access
* application the request matched.
*/
readonly aud: string;
/**
* Fetches the full identity information for the authenticated user.
* This makes a call to the Access identity service to retrieve extended
* user information such as groups, device posture, and identity provider data.
*
* @returns The subject's identity, if one exists
* @throws May throw if the identity service is unreachable or returns an error.
*/
getIdentity(): Promise<CloudflareAccessIdentity | undefined>;
}
// ============ AI Search Error Interfaces ============
export interface AiSearchInternalError extends Error {}
export interface AiSearchNotFoundError extends Error {}
Expand Down
Loading
Loading