diff --git a/packages/helix-shared-tokencache/src/MemCachePlugin.js b/packages/helix-shared-tokencache/src/MemCachePlugin.js index 0e42d580..08294b15 100644 --- a/packages/helix-shared-tokencache/src/MemCachePlugin.js +++ b/packages/helix-shared-tokencache/src/MemCachePlugin.js @@ -115,6 +115,10 @@ export class MemCachePlugin { return this.base ? this.base.location : this.key; } + getLastModified() { + return this.base?.getLastModified?.() ?? null; + } + async getPluginMetadata() { const cache = this.#getOrCreateCache(); if (!cache.metadata && this.base) { diff --git a/packages/helix-shared-tokencache/src/S3CachePlugin.js b/packages/helix-shared-tokencache/src/S3CachePlugin.js index 8385ff9b..67ef770a 100644 --- a/packages/helix-shared-tokencache/src/S3CachePlugin.js +++ b/packages/helix-shared-tokencache/src/S3CachePlugin.js @@ -80,6 +80,8 @@ export class S3CachePlugin { Bucket: bucket, Key: key, })); + this.lastModified = res.LastModified; + let raw = await new Response(res.Body, {}).buffer(); if (secret) { raw = decrypt(secret, raw).toString('utf-8'); @@ -142,6 +144,8 @@ export class S3CachePlugin { Body: raw, ContentType: secret ? 'application/octet-stream' : 'text/plain', })); + // save a roundtrip to the server doing a HEAD + this.lastModified = new Date(); return true; } catch (e) { log.warn('s3: unable to serialize token cache', e); @@ -185,6 +189,10 @@ export class S3CachePlugin { get location() { return `${this.bucket}/${this.key}`; } + + getLastModified() { + return this.lastModified; + } } S3CachePlugin.encrypt = encrypt; diff --git a/packages/helix-shared-tokencache/test/MemCachePlugin.test.js b/packages/helix-shared-tokencache/test/MemCachePlugin.test.js index 0b100a78..634efbb4 100644 --- a/packages/helix-shared-tokencache/test/MemCachePlugin.test.js +++ b/packages/helix-shared-tokencache/test/MemCachePlugin.test.js @@ -208,6 +208,7 @@ describe('MemCachePlugin Test', () => { assert.strictEqual(ret, true); assert.strictEqual(ctx.token, '1234'); assert.deepStrictEqual(caches.get('foobar-key'), { data: JSON.stringify(data) }); + assert.strictEqual(p.getLastModified(), null); }); it('read the cache from base is missing', async () => { diff --git a/packages/helix-shared-tokencache/test/S3CachePlugin.test.js b/packages/helix-shared-tokencache/test/S3CachePlugin.test.js index 345e816e..9c8eb990 100644 --- a/packages/helix-shared-tokencache/test/S3CachePlugin.test.js +++ b/packages/helix-shared-tokencache/test/S3CachePlugin.test.js @@ -270,6 +270,7 @@ describe('S3CachePlugin Test', () => { key: 'myproject/auth-default/json', secret: '', }); + const lastModified = new Date('Fri, 12 Jun 2026 14:44:36 GMT'); nock('https://test-bucket.s3.us-east-1.amazonaws.com') .get('/myproject/auth-default/json?x-id=GetObject') @@ -278,13 +279,16 @@ describe('S3CachePlugin Test', () => { cachePluginMetadata: { foo: 'bar', }, - })); + }), { + 'last-modified': lastModified.toUTCString(), + }); const ctx = new MockTokenCacheContext({}); const ret = await p.beforeCacheAccess(ctx); assert.strictEqual(ret, true); assert.strictEqual(ctx.token, '1234'); assert.deepStrictEqual(await p.getPluginMetadata(), { foo: 'bar' }); + assert.strictEqual(p.getLastModified().getTime(), lastModified.getTime()); }); it('read cache data from s3 with encryption', async () => {