Skip to content

Commit 71042bd

Browse files
committed
chore: copy ctx.cache design
Signed-off-by: Matt Provost <mprovost@cloudflare.com>
1 parent 33bd888 commit 71042bd

13 files changed

Lines changed: 75 additions & 141 deletions

File tree

src/workerd/api/global-scope.c++

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@ void ExecutionContext::passThroughOnException() {
6565
IoContext::current().setFailOpen();
6666
}
6767

68+
kj::StringPtr AccessContext::getAud() {
69+
JSG_FAIL_REQUIRE(Error, "Access context is not available.");
70+
}
71+
72+
jsg::Promise<jsg::JsValue> AccessContext::getIdentity(jsg::Lock& js) {
73+
JSG_FAIL_REQUIRE(Error, "Access context is not available.");
74+
}
75+
76+
jsg::Optional<jsg::Ref<AccessContext>> ExecutionContext::getAccess(jsg::Lock& js) {
77+
// Hook for the embedding application to provide an AccessContext.
78+
// The default Worker::Api implementation returns kj::none.
79+
if (IoContext::hasCurrent()) {
80+
return Worker::Isolate::from(js).getApi().getCtxAccessProperty(js);
81+
}
82+
return kj::none;
83+
}
84+
6885
void ExecutionContext::abort(jsg::Lock& js, jsg::Optional<jsg::Value> reason) {
6986
KJ_IF_SOME(r, reason) {
7087
IoContext::current().abort(js.exceptionToKj(kj::mv(r)));

src/workerd/api/global-scope.h

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,30 @@ class TestController: public jsg::Object {
191191
JSG_RESOURCE_TYPE(TestController) {}
192192
};
193193

194+
// Base class for the ctx.access object providing Cloudflare Access authentication context.
195+
// Subclass when embedding to provide an implementation.
196+
class AccessContext: public jsg::Object {
197+
public:
198+
// Returns the audience claim from the Access JWT.
199+
//
200+
// The default implementation throws — only meaningful when overridden by the embedding.
201+
virtual kj::StringPtr getAud();
202+
203+
// Fetches the full identity information for the authenticated user.
204+
//
205+
// The default implementation throws — only meaningful when overridden by the embedding.
206+
virtual jsg::Promise<jsg::JsValue> getIdentity(jsg::Lock& js);
207+
208+
JSG_RESOURCE_TYPE(AccessContext) {
209+
JSG_READONLY_INSTANCE_PROPERTY(aud, getAud);
210+
JSG_METHOD(getIdentity);
211+
JSG_TS_OVERRIDE(CloudflareAccessContext {
212+
readonly aud: string;
213+
getIdentity(): Promise<CloudflareAccessIdentity | undefined>;
214+
});
215+
}
216+
};
217+
194218
class ExecutionContext: public jsg::Object {
195219
public:
196220
ExecutionContext(jsg::Lock& js, jsg::JsValue exports)
@@ -239,24 +263,9 @@ class ExecutionContext: public jsg::Object {
239263
return js.undefined();
240264
}
241265

242-
// Called by the host runtime (edgeworker) to set the Access context for this request.
243-
// Must be called before the worker's handler is invoked.
244-
//
245-
// Unlike other ExecutionContext fields (props, version, exports) which are injected through the
246-
// constructor, access uses a post-construction setter because the Access context is assembled by
247-
// the host runtime after ExecutionContext construction but before handler invocation. The access
248-
// data (audience claim, identity fetcher) originates from the Cloudflare Access integration
249-
// pipeline and is not available during ExecutionContext construction in edgeworker.
250-
void setAccess(jsg::Lock& js, jsg::JsRef<jsg::JsValue> value) {
251-
access = kj::mv(value);
252-
}
253-
254-
jsg::JsValue getAccess(jsg::Lock& js) {
255-
KJ_IF_SOME(a, access) {
256-
return a.getHandle(js);
257-
}
258-
return js.undefined();
259-
}
266+
// Returns an AccessContext for the current request, or empty jsg::Optional otherwise.
267+
// Called by the runtime to provide Cloudflare Access authentication context.
268+
jsg::Optional<jsg::Ref<AccessContext>> getAccess(jsg::Lock& js);
260269

261270
JSG_RESOURCE_TYPE(ExecutionContext, CompatibilityFlags::Reader flags) {
262271
JSG_METHOD(waitUntil);
@@ -268,9 +277,7 @@ class ExecutionContext: public jsg::Object {
268277
if (flags.getEnableVersionApi()) {
269278
JSG_LAZY_INSTANCE_PROPERTY(version, getVersion);
270279
}
271-
if (flags.getEnableCtxAccess()) {
272-
JSG_LAZY_INSTANCE_PROPERTY(access, getAccess);
273-
}
280+
JSG_LAZY_INSTANCE_PROPERTY(access, getAccess);
274281

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

289296
// TODO(soon): This is getting unwieldy.
290-
// Note: `access` is included unconditionally in all TS_OVERRIDE branches (unlike `version`
291-
// which is gated by enableVersionApi). This is intentional — adding another conditional would
292-
// double the branch count (from 4 to 8). Since `access` is optional (`?`), the type is
293-
// correct regardless of whether the flag is enabled (the property will be undefined at runtime
294-
// when the flag is off or when setAccess() hasn't been called).
295297
if (flags.getEnableCtxExports()) {
296298
if (flags.getEnableVersionApi()) {
297299
JSG_TS_OVERRIDE(<Props = unknown> {
@@ -336,19 +338,16 @@ class ExecutionContext: public jsg::Object {
336338
void visitForMemoryInfo(jsg::MemoryTracker& tracker) const {
337339
tracker.trackField("props", props);
338340
tracker.trackField("version", version);
339-
tracker.trackField("access", access);
340341
}
341342

342343
private:
343344
jsg::JsRef<jsg::JsValue> exports;
344345
jsg::JsRef<jsg::JsValue> props;
345346
kj::Maybe<jsg::JsRef<jsg::JsValue>> version;
346-
kj::Maybe<jsg::JsRef<jsg::JsValue>> access;
347347

348348
void visitForGc(jsg::GcVisitor& visitor) {
349349
visitor.visit(props);
350350
visitor.visit(version);
351-
visitor.visit(access);
352351
}
353352
};
354353

@@ -1058,6 +1057,6 @@ class ServiceWorkerGlobalScope: public WorkerGlobalScope {
10581057
api::WorkerGlobalScope, api::ServiceWorkerGlobalScope, api::TestController, \
10591058
api::ExecutionContext, api::ExportedHandler, \
10601059
api::ServiceWorkerGlobalScope::StructuredCloneOptions, api::Navigator, \
1061-
api::AlarmInvocationInfo, api::Immediate, api::Cloudflare
1060+
api::AlarmInvocationInfo, api::Immediate, api::Cloudflare, api::AccessContext
10621061
// The list of global-scope.h types that are added to worker.c++'s JSG_DECLARE_ISOLATE_TYPE
10631062
} // namespace workerd::api

src/workerd/api/tests/BUILD.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ wd_test(
124124

125125
wd_test(
126126
src = "ctx-access-test.wd-test",
127-
args = ["--experimental"],
128127
data = ["ctx-access-test.js"],
129128
)
130129

src/workerd/api/tests/ctx-access-test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { strictEqual } from 'node:assert';
66

77
export const ctxAccessPropertyExists = {
88
test(controller, env, ctx) {
9-
// When enable_ctx_access is enabled, the property should exist on ctx
10-
// (as a lazy instance property), even though its value is undefined
11-
// because setAccess() is only called by the host runtime (edgeworker).
9+
// The access property is always present on ctx as a lazy instance property.
10+
// In standalone workerd (no embedding override), the value is undefined because
11+
// the default Worker::Api::getCtxAccessProperty() returns kj::none.
1212
strictEqual('access' in ctx, true);
1313
strictEqual(ctx.access, undefined);
1414
},

src/workerd/api/tests/ctx-access-test.wd-test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const unitTests :Workerd.Config = (
77
modules = [
88
(name = "worker", esModule = embed "ctx-access-test.js")
99
],
10-
compatibilityFlags = ["nodejs_compat", "experimental", "enable_ctx_access"],
10+
compatibilityFlags = ["nodejs_compat"],
1111
)
1212
),
1313
],

src/workerd/io/compatibility-date.capnp

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1508,13 +1508,4 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef {
15081508
$compatDisableFlag("resizable_array_buffer_in_blob");
15091509
# When enabled, creating a Blob with a resizable ArrayBuffer will throw a TypeError, matching
15101510
# expected spec behavior.
1511-
1512-
enableCtxAccess @174 :Bool
1513-
$compatEnableFlag("enable_ctx_access")
1514-
$experimental;
1515-
# Enables the ctx.access property for Cloudflare Access integration.
1516-
# When enabled, ctx.access provides Access authentication context including the
1517-
# matched application audience (AUD) and an identity-fetching function. The value
1518-
# is set by the host runtime (edgeworker) per-request; in open-source workerd the
1519-
# property will always be undefined.
15201511
}

src/workerd/io/worker.c++

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,10 @@ kj::Maybe<const Worker::Api&> Worker::Api::tryCurrent() {
520520
return kj::none;
521521
}
522522

523+
jsg::Optional<jsg::Ref<api::AccessContext>> Worker::Api::getCtxAccessProperty(jsg::Lock& js) const {
524+
return kj::none;
525+
}
526+
523527
struct Worker::Impl {
524528
kj::Maybe<jsg::JsContext<api::ServiceWorkerGlobalScope>> context;
525529

src/workerd/io/worker.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ struct CryptoAlgorithm;
4646
struct QueueExportedHandler;
4747
class WebSocket;
4848
class WebSocketRequestResponsePair;
49+
class AccessContext;
4950
class ExecutionContext;
5051
namespace pyodide {
5152
struct ArtifactBundler_State;
@@ -663,6 +664,10 @@ class Worker::Api {
663664
virtual jsg::JsObject wrapExecutionContext(
664665
jsg::Lock& lock, jsg::Ref<api::ExecutionContext> ref) const = 0;
665666

667+
// Hook for the embedding application to provide an AccessContext for the ctx.access property.
668+
// The default implementation returns kj::none (undefined).
669+
virtual jsg::Optional<jsg::Ref<api::AccessContext>> getCtxAccessProperty(jsg::Lock& js) const;
670+
666671
virtual const jsg::IsolateObserver& getObserver() const = 0;
667672
virtual void setIsolateObserver(IsolateObserver&) = 0;
668673

types/defines/access.d.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,3 @@ interface CloudflareAccessIdentity extends Record<string, unknown> {
3434
/** True if the user is authenticated via Cloudflare Gateway. */
3535
is_gateway?: boolean;
3636
}
37-
38-
/**
39-
* Cloudflare Access authentication information for the current request.
40-
*/
41-
interface CloudflareAccessContext {
42-
/**
43-
* The audience claim from the Access JWT. This identifies which Access
44-
* application the request matched.
45-
*/
46-
readonly aud: string;
47-
48-
/**
49-
* Fetches the full identity information for the authenticated user.
50-
* This makes a call to the Access identity service to retrieve extended
51-
* user information such as groups, device posture, and identity provider data.
52-
*
53-
* @returns The subject's identity, if one exists
54-
* @throws May throw if the identity service is unreachable or returns an error.
55-
*/
56-
getIdentity(): Promise<CloudflareAccessIdentity | undefined>;
57-
}

types/generated-snapshot/experimental/index.d.ts

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,10 @@ interface AlarmInvocationInfo {
586586
interface Cloudflare {
587587
readonly compatibilityFlags: Record<string, boolean>;
588588
}
589+
interface CloudflareAccessContext {
590+
readonly aud: string;
591+
getIdentity(): Promise<CloudflareAccessIdentity | undefined>;
592+
}
589593
declare abstract class ColoLocalActorNamespace {
590594
get(actorId: string): Fetcher;
591595
}
@@ -4729,25 +4733,6 @@ interface CloudflareAccessIdentity extends Record<string, unknown> {
47294733
/** True if the user is authenticated via Cloudflare Gateway. */
47304734
is_gateway?: boolean;
47314735
}
4732-
/**
4733-
* Cloudflare Access authentication information for the current request.
4734-
*/
4735-
interface CloudflareAccessContext {
4736-
/**
4737-
* The audience claim from the Access JWT. This identifies which Access
4738-
* application the request matched.
4739-
*/
4740-
readonly aud: string;
4741-
/**
4742-
* Fetches the full identity information for the authenticated user.
4743-
* This makes a call to the Access identity service to retrieve extended
4744-
* user information such as groups, device posture, and identity provider data.
4745-
*
4746-
* @returns The subject's identity, if one exists
4747-
* @throws May throw if the identity service is unreachable or returns an error.
4748-
*/
4749-
getIdentity(): Promise<CloudflareAccessIdentity | undefined>;
4750-
}
47514736
// ============ AI Search Error Interfaces ============
47524737
interface AiSearchInternalError extends Error {}
47534738
interface AiSearchNotFoundError extends Error {}

0 commit comments

Comments
 (0)