feat(sync): round trip cloud snapshots

This commit is contained in:
2026-05-16 00:01:28 -04:00
parent 06e947f43e
commit 40ab6b4874
22 changed files with 694 additions and 69 deletions
+1
View File
@@ -1,5 +1,6 @@
export interface ElyKvNamespace {
get(key: string): Promise<string | null>;
put(key: string, value: string): Promise<void>;
delete(key: string): Promise<void>;
}
+19
View File
@@ -1,4 +1,5 @@
import type { AuthContext } from "./auth.js";
import { authSessionCacheKvKey } from "./auth.js";
import type { Env } from "./bindings.js";
import {
type DeviceApprovalDocument,
@@ -227,6 +228,7 @@ export async function registerDeviceDocument(
throw new DevicePersistenceError("device_registration_missing");
}
await bindSessionDeviceContext(env, context, registration.deviceId, nowSeconds);
await refreshSessionDeviceCache(env, context, registration.deviceId);
return {
version: 1,
@@ -246,6 +248,23 @@ async function bindSessionDeviceContext(
.run();
}
async function refreshSessionDeviceCache(
env: Env,
context: AuthContext,
deviceId: string,
): Promise<void> {
await env.ELY_KV.put(
authSessionCacheKvKey(env.ELY_ENVIRONMENT, context.tokenHash),
JSON.stringify({
version: 1,
user_id: context.userId,
session_id: context.sessionId,
device_id: deviceId,
expires_at: context.expiresAt,
}),
);
}
export async function approveDeviceDocument(
request: Request,
env: Env,
+4
View File
@@ -319,6 +319,10 @@ function testEnv(options: TestEnvOptions = {}): Env {
options.kvReads?.push(key);
return Promise.resolve(values.get(key) ?? null);
},
put(key: string, value: string): Promise<void> {
values.set(key, value);
return Promise.resolve();
},
delete(key: string): Promise<void> {
values.delete(key);
return Promise.resolve();
@@ -47,6 +47,9 @@ function testEnv(sentEmails: ElyEmailMessageBuilder[] | null): Env {
get() {
return Promise.resolve(null);
},
put() {
return Promise.resolve();
},
delete() {
return Promise.resolve();
},
+44 -1
View File
@@ -157,6 +157,8 @@ describe("device routes", () => {
it("registers the current device as a pending idempotent D1 write", async () => {
const tokenHash = await authTokenHash(ACCESS_TOKEN);
const sessionCacheKey = authSessionCacheKvKey("local", tokenHash);
const kvPuts: [string, string][] = [];
const d1 = testD1Database([
{
device_id: "device-01",
@@ -182,7 +184,8 @@ describe("device routes", () => {
}),
testEnv({
d1,
kvEntries: [[authSessionCacheKvKey("local", tokenHash), sessionDocument()]],
kvEntries: [[sessionCacheKey, sessionDocument()]],
kvPuts,
}),
);
@@ -220,6 +223,46 @@ describe("device routes", () => {
assert.equal(d1.binds[0]?.[7], IDEMPOTENCY_KEY);
assert.deepEqual(d1.binds[1], ["user-01", IDEMPOTENCY_KEY]);
assert.deepEqual(d1.binds[2]?.slice(0, 3), ["session-01", "user-01", "device-01"]);
assert.deepEqual(kvPuts, [[sessionCacheKey, sessionDocument("device-01")]]);
});
it("registers and caches device context for sessions without a current device", async () => {
const tokenHash = await authTokenHash(ACCESS_TOKEN);
const sessionCacheKey = authSessionCacheKvKey("local", tokenHash);
const kvPuts: [string, string][] = [];
const d1 = testD1Database([
{
device_id: "device-01",
public_key: PUBLIC_KEY,
device_name: "MacBook Pro",
platform: "macOS",
approval_status: "pending",
created_at: 1_780_000_100,
approved_at: null,
last_active_at: 1_780_000_100,
revoked_at: null,
},
]);
const response = await handleRequest(
new Request("https://elydora.test/api/devices/register", {
method: "POST",
headers: {
authorization: `Bearer ${ACCESS_TOKEN}`,
"content-type": "application/json",
},
body: JSON.stringify(deviceRegistrationBody()),
}),
testEnv({
d1,
kvEntries: [[sessionCacheKey, sessionDocument(null)]],
kvPuts,
}),
);
assert.equal(response.status, 201);
assert.deepEqual(d1.binds[2]?.slice(0, 3), ["session-01", "user-01", "device-01"]);
assert.deepEqual(kvPuts, [[sessionCacheKey, sessionDocument("device-01")]]);
});
it("rejects invalid device registration payloads before D1 writes", async () => {
+8 -2
View File
@@ -15,6 +15,7 @@ export interface TestEnvOptions {
d1?: RecordedD1Database;
kvEntries?: [string, string][];
kvDeletes?: string[];
kvPuts?: [string, string][];
kvReads?: string[];
r2Deletes?: string[];
r2Gets?: string[];
@@ -51,6 +52,11 @@ export function testEnv(options: TestEnvOptions): Env {
options.kvReads?.push(key);
return Promise.resolve(values.get(key) ?? null);
},
put(key: string, value: string): Promise<void> {
options.kvPuts?.push([key, value]);
values.set(key, value);
return Promise.resolve();
},
delete(key: string): Promise<void> {
options.kvDeletes?.push(key);
values.delete(key);
@@ -110,12 +116,12 @@ export function testD1Database(rows: unknown[] | TestD1DatabaseOptions): Recorde
};
}
export function sessionDocument(deviceId = "device-01"): string {
export function sessionDocument(deviceId: string | null = "device-01"): string {
return JSON.stringify({
version: 1,
user_id: "user-01",
session_id: "session-01",
device_id: deviceId,
...(deviceId === null ? {} : { device_id: deviceId }),
expires_at: "2099-01-01T00:00:00.000Z",
});
}
+4
View File
@@ -359,6 +359,10 @@ function testEnv(
get(key: string): Promise<string | null> {
return Promise.resolve(values.get(key) ?? null);
},
put(key: string, value: string): Promise<void> {
values.set(key, value);
return Promise.resolve();
},
delete(key: string): Promise<void> {
values.delete(key);
return Promise.resolve();