mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-10-03 22:33:11 +00:00
55 lines
2 KiB
TypeScript
55 lines
2 KiB
TypeScript
|
|
/**
|
||
|
|
* Reads the group-sync freshness marker the pre-token Lambda fails closed on.
|
||
|
|
*
|
||
|
|
* The group-sync Lambda writes `lastSuccessfulSyncMs` (epoch ms) to a single
|
||
|
|
* item in the sync-state table on every successful Google→Cognito sync. If that
|
||
|
|
* marker is missing or older than {@link MAX_SYNC_AGE_MS}, group membership may
|
||
|
|
* be stale (a revoked user could still appear entitled), so the pre-token Lambda
|
||
|
|
* drops to base scopes (design.md §2.3 fail-closed; agentforce-plan §0.1 CR-5).
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
||
|
|
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';
|
||
|
|
|
||
|
|
/** Stale threshold: 30 min. A 5-min sync cadence means >6 missed runs is a fault. */
|
||
|
|
export const MAX_SYNC_AGE_MS = 30 * 60 * 1000;
|
||
|
|
|
||
|
|
/** Fixed partition key of the singleton sync-state item. */
|
||
|
|
export const SYNC_STATE_PK = 'group-sync';
|
||
|
|
|
||
|
|
let cached: DynamoDBDocumentClient | undefined;
|
||
|
|
|
||
|
|
function doc(): DynamoDBDocumentClient {
|
||
|
|
cached ??= DynamoDBDocumentClient.from(new DynamoDBClient({}));
|
||
|
|
return cached;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fetch the last successful sync timestamp (epoch ms), or `null` if the marker
|
||
|
|
* is absent or unreadable. A read failure resolves to `null` (treated as stale)
|
||
|
|
* so an outage of the sync-state table fails CLOSED, never open.
|
||
|
|
*/
|
||
|
|
export async function readLastSuccessfulSyncMs(
|
||
|
|
tableName: string,
|
||
|
|
client: DynamoDBDocumentClient = doc(),
|
||
|
|
): Promise<number | null> {
|
||
|
|
try {
|
||
|
|
const res = await client.send(
|
||
|
|
new GetCommand({ TableName: tableName, Key: { pk: SYNC_STATE_PK } }),
|
||
|
|
);
|
||
|
|
const ts = res.Item?.['lastSuccessfulSyncMs'];
|
||
|
|
return typeof ts === 'number' && Number.isFinite(ts) ? ts : null;
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** True iff `lastSyncMs` is present and within {@link MAX_SYNC_AGE_MS} of `nowMs`. */
|
||
|
|
export function isSyncFresh(
|
||
|
|
lastSyncMs: number | null,
|
||
|
|
nowMs: number,
|
||
|
|
maxAgeMs: number = MAX_SYNC_AGE_MS,
|
||
|
|
): boolean {
|
||
|
|
return lastSyncMs !== null && nowMs - lastSyncMs <= maxAgeMs && lastSyncMs <= nowMs;
|
||
|
|
}
|