diff --git a/src/workerd/api/global-scope.c++ b/src/workerd/api/global-scope.c++ index 2f705fe7bbb..760464a0eb0 100644 --- a/src/workerd/api/global-scope.c++ +++ b/src/workerd/api/global-scope.c++ @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -96,6 +97,31 @@ jsg::Optional> ExecutionContext::getTracing(jsg::Lock& js) { return js.alloc(); } +kj::StringPtr AccessContext::getAud() { + return info->getAudience(); +} + +jsg::Promise> AccessContext::getIdentity(jsg::Lock& js) { + auto& ioctx = IoContext::current(); + return ioctx.awaitIo(js, info->getIdentity(), + [](jsg::Lock& js, kj::Maybe json) -> jsg::Optional { + KJ_IF_SOME(j, json) { + return jsg::JsValue(js.parseJson(j).getHandle(js)); + } + return kj::none; + }); +} + +jsg::Optional> ExecutionContext::getAccess(jsg::Lock& js) { + // Pull the per-request AccessInfo (if any) off the current IncomingRequest. Standalone workerd + // never supplies one; production embedders construct one before calling newWorkerEntrypoint(). + if (!IoContext::hasCurrent()) return kj::none; + auto& ioctx = IoContext::current(); + KJ_IF_SOME(info, ioctx.getAccessInfo()) { + return js.alloc(ioctx.addObject(kj::addRef(info))); + } + return kj::none; +} void ExecutionContext::abort(jsg::Lock& js, jsg::Optional reason) { KJ_IF_SOME(r, reason) { IoContext::current().abort(js.exceptionToKj(kj::mv(r))); diff --git a/src/workerd/api/global-scope.h b/src/workerd/api/global-scope.h index 3befcebbe8e..0f679da3456 100644 --- a/src/workerd/api/global-scope.h +++ b/src/workerd/api/global-scope.h @@ -25,6 +25,10 @@ namespace workerd::jsg { class DOMException; } // namespace workerd::jsg +namespace workerd { +class AccessInfo; +} // namespace workerd + namespace workerd::api { class Tracing; @@ -240,6 +244,37 @@ class CacheContext: public jsg::Object { } }; +// Concrete wrapper exposing per-request Cloudflare Access authentication info to JavaScript +// as `ctx.access`. The actual auth data is supplied by the embedding application via +// `workerd::AccessInfo`, which is plumbed through `newWorkerEntrypoint()` onto +// `IoContext::IncomingRequest`. +// +// Standalone workerd never constructs one of these (no `AccessInfo` is supplied), so +// `ctx.access` is `undefined`. Embedders construct a concrete `AccessInfo` subclass and pass it +// through the entrypoint; `ExecutionContext::getAccess()` lazily wraps it in this class. +class AccessContext: public jsg::Object { + public: + explicit AccessContext(IoOwn info): info(kj::mv(info)) {} + + // Returns the audience claim from the Access JWT. + kj::StringPtr getAud(); + + // Fetches the full identity information for the authenticated user. Resolves to `undefined` + // if no identity is associated with the request (e.g. service-token authentication). + jsg::Promise> getIdentity(jsg::Lock& js); + + JSG_RESOURCE_TYPE(AccessContext) { + JSG_READONLY_INSTANCE_PROPERTY(aud, getAud); + JSG_METHOD(getIdentity); + JSG_TS_OVERRIDE(CloudflareAccessContext { + readonly aud: string; + getIdentity(): Promise; + }); + } + + private: + IoOwn info; +}; class ExecutionContext: public jsg::Object { public: ExecutionContext(jsg::Lock& js, jsg::JsValue exports) @@ -295,6 +330,10 @@ class ExecutionContext: public jsg::Object { jsg::Optional> getTracing(jsg::Lock& js); + // Returns an AccessContext for the current request, or empty jsg::Optional otherwise. + // Called by the runtime to provide Cloudflare Access authentication context. + jsg::Optional> getAccess(jsg::Lock& js); + JSG_RESOURCE_TYPE(ExecutionContext, CompatibilityFlags::Reader flags) { JSG_METHOD(waitUntil); JSG_METHOD(passThroughOnException); @@ -306,6 +345,7 @@ class ExecutionContext: public jsg::Object { if (flags.getEnableVersionApi()) { JSG_LAZY_INSTANCE_PROPERTY(version, getVersion); } + JSG_LAZY_INSTANCE_PROPERTY(access, getAccess); // ctx.tracing - user tracing API. The *type* is always visible (so the generated // `Tracing` / `Span` types exist in every compat-date snapshot, not only the @@ -340,11 +380,13 @@ class ExecutionContext: public jsg::Object { readonly key?: string; readonly override?: string; }; + readonly access?: CloudflareAccessContext; }); } else { JSG_TS_OVERRIDE( { readonly props: Props; readonly exports: Cloudflare.Exports; + readonly access?: CloudflareAccessContext; }); } } else { @@ -357,10 +399,12 @@ class ExecutionContext: public jsg::Object { readonly key?: string; readonly override?: string; }; + readonly access?: CloudflareAccessContext; }); } else { JSG_TS_OVERRIDE( { readonly props: Props; + readonly access?: CloudflareAccessContext; }); } } @@ -1089,6 +1133,6 @@ class ServiceWorkerGlobalScope: public WorkerGlobalScope { api::ExecutionContext, api::ExportedHandler, \ api::ServiceWorkerGlobalScope::StructuredCloneOptions, api::Navigator, \ api::AlarmInvocationInfo, api::Immediate, api::Cloudflare, api::CachePurgeError, \ - api::CachePurgeResult, api::CachePurgeOptions, api::CacheContext + api::CachePurgeResult, api::CachePurgeOptions, api::CacheContext, api::AccessContext // The list of global-scope.h types that are added to worker.c++'s JSG_DECLARE_ISOLATE_TYPE } // namespace workerd::api diff --git a/src/workerd/api/tests/BUILD.bazel b/src/workerd/api/tests/BUILD.bazel index 69f3177f816..1d0b6d91abd 100644 --- a/src/workerd/api/tests/BUILD.bazel +++ b/src/workerd/api/tests/BUILD.bazel @@ -137,6 +137,11 @@ wd_test( args = ["--experimental"], ) +wd_test( + src = "ctx-access-test.wd-test", + data = ["ctx-access-test.js"], +) + wd_test( src = "cache-test.wd-test", args = ["--experimental"], diff --git a/src/workerd/api/tests/ctx-access-test.js b/src/workerd/api/tests/ctx-access-test.js new file mode 100644 index 00000000000..a1e8c8ed7bb --- /dev/null +++ b/src/workerd/api/tests/ctx-access-test.js @@ -0,0 +1,16 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +import { strictEqual } from 'node:assert'; + +export const ctxAccessPropertyExists = { + test(controller, env, ctx) { + // The access property is always present on ctx as a lazy instance property. + // In standalone workerd no AccessInfo is supplied to newWorkerEntrypoint(), so the + // current IncomingRequest has no AccessInfo and getAccess() returns kj::none, which + // surfaces as `undefined` to JS. + strictEqual('access' in ctx, true); + strictEqual(ctx.access, undefined); + }, +}; diff --git a/src/workerd/api/tests/ctx-access-test.wd-test b/src/workerd/api/tests/ctx-access-test.wd-test new file mode 100644 index 00000000000..1c85d9902fe --- /dev/null +++ b/src/workerd/api/tests/ctx-access-test.wd-test @@ -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"], + ) + ), + ], +); diff --git a/src/workerd/io/BUILD.bazel b/src/workerd/io/BUILD.bazel index 43ca28d5262..21273397149 100644 --- a/src/workerd/io/BUILD.bazel +++ b/src/workerd/io/BUILD.bazel @@ -47,6 +47,7 @@ wd_cc_library( "worker-fs.c++", ] + ["//src/workerd/api:srcs"], hdrs = [ + "access-info.h", "compatibility-date.h", "external-pusher.h", "hibernation-manager.h", diff --git a/src/workerd/io/access-info.h b/src/workerd/io/access-info.h new file mode 100644 index 00000000000..57d1223dc83 --- /dev/null +++ b/src/workerd/io/access-info.h @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#pragma once + +#include +#include +#include + +namespace workerd { + +// Per-request Cloudflare Access authentication information. +// +// This is the I/O-side carrier for Access auth data. It is created by the embedding application +// (e.g. the production runtime) before invoking the worker, plumbed through `newWorkerEntrypoint()` +// into the `IoContext::IncomingRequest`, and surfaced to JavaScript by the concrete +// `api::AccessContext` wrapper as `ctx.access`. +// +// In standalone workerd this is never constructed; `ctx.access` evaluates to `undefined`. +// +// This type intentionally lives in `io/` rather than `api/` because: +// - It is the polymorphism boundary between embedders (workerd vs. production), not the +// JS-facing type. +// - It carries per-request data that flows through `newWorkerEntrypoint` → `IncomingRequest`, +// not through `Worker::Api` (which is per-isolate) or `IoChannelFactory`. +class AccessInfo: public kj::Refcounted { + public: + virtual ~AccessInfo() noexcept(false) = default; + + // The audience claim from the Access JWT. Stable for the lifetime of the request. + virtual kj::StringPtr getAudience() = 0; + + // Fetches the full identity information for the authenticated user, equivalent to calling + // /cdn-cgi/access/get-identity. The returned string is a JSON document; `kj::none` indicates + // no identity is available (e.g. service-token authentication). + virtual kj::Promise> getIdentity() = 0; +}; + +} // namespace workerd diff --git a/src/workerd/io/io-context.c++ b/src/workerd/io/io-context.c++ index ac35d319934..75cb1deca6c 100644 --- a/src/workerd/io/io-context.c++ +++ b/src/workerd/io/io-context.c++ @@ -4,6 +4,7 @@ #include "io-context.h" +#include #include #include #include @@ -218,11 +219,13 @@ IoContext::IncomingRequest::IoContext_IncomingRequest(kj::Own context kj::Own ioChannelFactoryParam, kj::Own metricsParam, kj::Maybe> workerTracer, - kj::Maybe maybeTriggerInvocationSpan) + kj::Maybe maybeTriggerInvocationSpan, + kj::Maybe> accessInfo) : context(kj::mv(contextParam)), metrics(kj::mv(metricsParam)), workerTracer(kj::mv(workerTracer)), ioChannelFactory(kj::mv(ioChannelFactoryParam)), + accessInfo(kj::mv(accessInfo)), maybeTriggerInvocationSpan(kj::mv(maybeTriggerInvocationSpan)) {} tracing::InvocationSpanContext& IoContext::IncomingRequest::getInvocationSpanContext() { diff --git a/src/workerd/io/io-context.h b/src/workerd/io/io-context.h index 34300da17b9..df522a74baf 100644 --- a/src/workerd/io/io-context.h +++ b/src/workerd/io/io-context.h @@ -8,6 +8,7 @@ #include "worker.h" #include +#include #include #include #include @@ -71,7 +72,8 @@ class IoContext_IncomingRequest final { kj::Own ioChannelFactory, kj::Own metrics, kj::Maybe> workerTracer, - kj::Maybe maybeTriggerInvocationSpan); + kj::Maybe maybeTriggerInvocationSpan, + kj::Maybe> accessInfo = kj::none); KJ_DISALLOW_COPY_AND_MOVE(IoContext_IncomingRequest); ~IoContext_IncomingRequest() noexcept(false); @@ -131,6 +133,12 @@ class IoContext_IncomingRequest final { return rootUserTraceSpan.addRef(); } + // The Cloudflare Access auth info for this request, if any was provided by the embedder. Used + // to populate `ctx.access` in JavaScript. + kj::Maybe getAccessInfo() { + return accessInfo.map([](kj::Own& p) -> AccessInfo& { return *p; }); + } + // The invocation span context is a unique identifier for a specific // worker invocation. tracing::InvocationSpanContext& getInvocationSpanContext(); @@ -140,6 +148,7 @@ class IoContext_IncomingRequest final { kj::Own metrics; kj::Maybe> workerTracer; kj::Own ioChannelFactory; + kj::Maybe> accessInfo; // Root user trace span for this request. Populated during delivered() via // BaseTracer::makeUserRequestSpan(); otherwise a null SpanParent. The tracer it references @@ -240,6 +249,13 @@ class IoContext final: public kj::Refcounted, private kj::TaskSet::ErrorHandler return getCurrentIncomingRequest().getRootUserTraceSpan(); } + // The Cloudflare Access auth info for the current incoming request, if any was provided by + // the embedder. Used to populate `ctx.access` in JavaScript. + kj::Maybe getAccessInfo() { + if (incomingRequests.empty()) return kj::none; + return getCurrentIncomingRequest().getAccessInfo(); + } + LimitEnforcer& getLimitEnforcer() { return *limitEnforcer; } diff --git a/src/workerd/io/worker-entrypoint.c++ b/src/workerd/io/worker-entrypoint.c++ index 8afb03fda5f..5898800005e 100644 --- a/src/workerd/io/worker-entrypoint.c++ +++ b/src/workerd/io/worker-entrypoint.c++ @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -61,7 +62,8 @@ class WorkerEntrypoint final: public WorkerInterface { kj::Maybe cfBlobJson, kj::Maybe versionInfo, kj::Maybe maybeTriggerInvocationSpan, - bool isDynamicDispatch); + bool isDynamicDispatch, + kj::Maybe> accessInfo); kj::Promise request(kj::HttpMethod method, kj::StringPtr url, @@ -110,7 +112,8 @@ class WorkerEntrypoint final: public WorkerInterface { kj::Own ioChannelFactory, kj::Own metrics, kj::Maybe> workerTracer, - kj::Maybe maybeTriggerInvocationSpan); + kj::Maybe maybeTriggerInvocationSpan, + kj::Maybe> accessInfo); template kj::Promise maybeAddGcPassForTest(IoContext& context, kj::Promise promise); @@ -184,7 +187,8 @@ kj::Own WorkerEntrypoint::construct(ThreadContext& threadContex kj::Maybe cfBlobJson, kj::Maybe versionInfo, kj::Maybe maybeTriggerInvocationSpan, - bool isDynamicDispatch) { + bool isDynamicDispatch, + kj::Maybe> accessInfo) { TRACE_EVENT("workerd", "WorkerEntrypoint::construct()"); auto obj = kj::heap(kj::Badge(), threadContext, @@ -192,7 +196,7 @@ kj::Own WorkerEntrypoint::construct(ThreadContext& threadContex kj::mv(cfBlobJson), kj::mv(versionInfo)); obj->init(kj::mv(worker), kj::mv(actor), kj::mv(limitEnforcer), kj::mv(ioContextDependency), kj::mv(ioChannelFactory), kj::addRef(*metrics), kj::mv(workerTracer), - kj::mv(maybeTriggerInvocationSpan)); + kj::mv(maybeTriggerInvocationSpan), kj::mv(accessInfo)); auto& wrapper = metrics->wrapWorkerInterface(*obj); return kj::attachRef(wrapper, kj::mv(obj), kj::mv(metrics)); } @@ -222,7 +226,8 @@ void WorkerEntrypoint::init(kj::Own worker, kj::Own ioChannelFactory, kj::Own metrics, kj::Maybe> workerTracer, - kj::Maybe maybeTriggerInvocationSpan) { + kj::Maybe maybeTriggerInvocationSpan, + kj::Maybe> accessInfo) { TRACE_EVENT("workerd", "WorkerEntrypoint::init()"); // We need to construct the IoContext -- unless this is an actor and it already has a // IoContext, in which case we reuse it. @@ -252,7 +257,7 @@ void WorkerEntrypoint::init(kj::Own worker, } incomingRequest = kj::heap(kj::mv(context), kj::mv(ioChannelFactory), - kj::mv(metrics), kj::mv(workerTracer), kj::mv(maybeTriggerInvocationSpan)) + kj::mv(metrics), kj::mv(workerTracer), kj::mv(maybeTriggerInvocationSpan), kj::mv(accessInfo)) .attach(kj::mv(actor)); } @@ -1008,12 +1013,13 @@ kj::Own newWorkerEntrypoint(ThreadContext& threadContext, kj::Maybe cfBlobJson, kj::Maybe versionInfo, kj::Maybe maybeTriggerInvocationSpan, - bool isDynamicDispatch) { + bool isDynamicDispatch, + kj::Maybe> accessInfo) { return WorkerEntrypoint::construct(threadContext, kj::mv(worker), kj::mv(entrypointName), kj::mv(props), kj::mv(actor), kj::mv(limitEnforcer), kj::mv(ioContextDependency), kj::mv(ioChannelFactory), kj::mv(metrics), waitUntilTasks, tunnelExceptions, kj::mv(workerTracer), kj::mv(cfBlobJson), kj::mv(versionInfo), - kj::mv(maybeTriggerInvocationSpan), isDynamicDispatch); + kj::mv(maybeTriggerInvocationSpan), isDynamicDispatch, kj::mv(accessInfo)); } } // namespace workerd diff --git a/src/workerd/io/worker-entrypoint.h b/src/workerd/io/worker-entrypoint.h index e7ed1c1dfa8..f0cfd1b6498 100644 --- a/src/workerd/io/worker-entrypoint.h +++ b/src/workerd/io/worker-entrypoint.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include @@ -47,6 +48,9 @@ kj::Own newWorkerEntrypoint(ThreadContext& threadContext, // subtask of another request. If it is kj::none, then this invocation is a top-level // invocation. kj::Maybe maybeTriggerInvocationSpan = kj::none, - bool isDynamicDispatch = false); + bool isDynamicDispatch = false, + // Per-request Cloudflare Access info. Supplied by the embedding application; standalone + // workerd passes kj::none, which causes `ctx.access` to be `undefined` in JS. + kj::Maybe> accessInfo = kj::none); } // namespace workerd diff --git a/types/defines/access.d.ts b/types/defines/access.d.ts new file mode 100644 index 00000000000..db2b1a5307e --- /dev/null +++ b/types/defines/access.d.ts @@ -0,0 +1,36 @@ +/** + * 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 { + /** 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; + /** True if the user connected via Cloudflare WARP. */ + is_warp?: boolean; + /** True if the user is authenticated via Cloudflare Gateway. */ + is_gateway?: boolean; +} diff --git a/types/generated-snapshot/experimental/index.d.ts b/types/generated-snapshot/experimental/index.d.ts index 08e93fe3474..a3368baeaf2 100755 --- a/types/generated-snapshot/experimental/index.d.ts +++ b/types/generated-snapshot/experimental/index.d.ts @@ -501,6 +501,7 @@ interface ExecutionContext { readonly key?: string; readonly override?: string; }; + readonly access?: CloudflareAccessContext; tracing?: Tracing; abort(reason?: any): void; } @@ -604,6 +605,10 @@ interface CachePurgeOptions { interface CacheContext { purge(options: CachePurgeOptions): Promise; } +interface CloudflareAccessContext { + readonly aud: string; + getIdentity(): Promise; +} declare abstract class ColoLocalActorNamespace { get(actorId: string): Fetcher; } @@ -4734,6 +4739,51 @@ declare abstract class Span { get isTraced(): boolean; setAttribute(key: string, value?: boolean | number | string): void; } +/** + * 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 { + /** 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; + /** True if the user connected via Cloudflare WARP. */ + is_warp?: boolean; + /** True if the user is authenticated via Cloudflare Gateway. */ + is_gateway?: boolean; +} // ============ AI Search Error Interfaces ============ interface AiSearchInternalError extends Error {} interface AiSearchNotFoundError extends Error {} diff --git a/types/generated-snapshot/experimental/index.ts b/types/generated-snapshot/experimental/index.ts index 6e9a53463b3..b4cf2da77dd 100755 --- a/types/generated-snapshot/experimental/index.ts +++ b/types/generated-snapshot/experimental/index.ts @@ -503,6 +503,7 @@ export interface ExecutionContext { readonly key?: string; readonly override?: string; }; + readonly access?: CloudflareAccessContext; tracing?: Tracing; abort(reason?: any): void; } @@ -606,6 +607,10 @@ export interface CachePurgeOptions { export interface CacheContext { purge(options: CachePurgeOptions): Promise; } +export interface CloudflareAccessContext { + readonly aud: string; + getIdentity(): Promise; +} export declare abstract class ColoLocalActorNamespace { get(actorId: string): Fetcher; } @@ -4740,6 +4745,51 @@ export declare abstract class Span { get isTraced(): boolean; setAttribute(key: string, value?: boolean | number | string): void; } +/** + * 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 { + /** 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; + /** True if the user connected via Cloudflare WARP. */ + is_warp?: boolean; + /** True if the user is authenticated via Cloudflare Gateway. */ + is_gateway?: boolean; +} // ============ AI Search Error Interfaces ============ export interface AiSearchInternalError extends Error {} export interface AiSearchNotFoundError extends Error {} diff --git a/types/generated-snapshot/latest/index.d.ts b/types/generated-snapshot/latest/index.d.ts index 20c39772a75..a06cd84cac9 100755 --- a/types/generated-snapshot/latest/index.d.ts +++ b/types/generated-snapshot/latest/index.d.ts @@ -480,6 +480,7 @@ interface ExecutionContext { readonly exports: Cloudflare.Exports; readonly props: Props; cache?: CacheContext; + readonly access?: CloudflareAccessContext; tracing?: Tracing; } type ExportedHandlerFetchHandler< @@ -581,6 +582,10 @@ interface CachePurgeOptions { interface CacheContext { purge(options: CachePurgeOptions): Promise; } +interface CloudflareAccessContext { + readonly aud: string; + getIdentity(): Promise; +} declare abstract class ColoLocalActorNamespace { get(actorId: string): Fetcher; } @@ -4066,6 +4071,51 @@ declare abstract class Span { get isTraced(): boolean; setAttribute(key: string, value?: boolean | number | string): void; } +/** + * 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 { + /** 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; + /** True if the user connected via Cloudflare WARP. */ + is_warp?: boolean; + /** True if the user is authenticated via Cloudflare Gateway. */ + is_gateway?: boolean; +} // ============ AI Search Error Interfaces ============ interface AiSearchInternalError extends Error {} interface AiSearchNotFoundError extends Error {} diff --git a/types/generated-snapshot/latest/index.ts b/types/generated-snapshot/latest/index.ts index 9821fd88ad4..a421ce780ce 100755 --- a/types/generated-snapshot/latest/index.ts +++ b/types/generated-snapshot/latest/index.ts @@ -482,6 +482,7 @@ export interface ExecutionContext { readonly exports: Cloudflare.Exports; readonly props: Props; cache?: CacheContext; + readonly access?: CloudflareAccessContext; tracing?: Tracing; } export type ExportedHandlerFetchHandler< @@ -583,6 +584,10 @@ export interface CachePurgeOptions { export interface CacheContext { purge(options: CachePurgeOptions): Promise; } +export interface CloudflareAccessContext { + readonly aud: string; + getIdentity(): Promise; +} export declare abstract class ColoLocalActorNamespace { get(actorId: string): Fetcher; } @@ -4072,6 +4077,51 @@ export declare abstract class Span { get isTraced(): boolean; setAttribute(key: string, value?: boolean | number | string): void; } +/** + * 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 { + /** 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; + /** True if the user connected via Cloudflare WARP. */ + is_warp?: boolean; + /** True if the user is authenticated via Cloudflare Gateway. */ + is_gateway?: boolean; +} // ============ AI Search Error Interfaces ============ export interface AiSearchInternalError extends Error {} export interface AiSearchNotFoundError extends Error {}