0
2
The import carries the web's OAuth client key
Only what this pull adds. It is built on grain runs on a Rust server modelled on knotless, without hatk.
crates/grain-import/src/lib.rs+15−1
| @@ -56,7 +56,13 @@ | ||
//! `_cursor`'s Jetstream rows (`jetstream:<url>`) are `stream_cursor`'s, | ||
//! so the stream resumes where hatk left it. | ||
//! | ||
//! Not imported: hatk's OAuth tables (web sessions sign in again), its | ||
//! Of `_oauth_keys`, `pds-client-auth-key` is `oauth_key`'s, the private | ||
//! JWK as hatk kept it: grain's OAuth client goes on signing with the key | ||
//! its JWKS has always named, so each PDS goes on knowing the client. | ||
//! | ||
//! Not imported: hatk's other OAuth keys and its sessions, web and app | ||
//! alike (every account logs in again: nothing grain-serve runs reads the | ||
//! cookies that named them), its | ||
//! search tables, classifier scores, space sync state, and records of | ||
//! collections the index does not keep. The summary names each such table | ||
//! and its rows. | ||
| @@ -398,6 +404,14 @@ pub const KEPT: &[Kept] = &[ | ||
insert: "INSERT INTO space_invite (space, member_did, author_did, created_at) | ||
VALUES (?1, ?2, ?3, ?4)", | ||
}, | ||
Kept { | ||
table: "oauth_key", | ||
source: "_oauth_keys", | ||
from: "_oauth_keys", | ||
count: "SELECT count(*) FROM _oauth_keys WHERE kid = 'pds-client-auth-key'", | ||
select: "SELECT kid, private_key FROM _oauth_keys WHERE kid = 'pds-client-auth-key'", | ||
insert: "INSERT INTO oauth_key (kid, jwk) VALUES (?1, ?2)", | ||
}, | ||
Kept { | ||
table: "space_support", | ||
source: "_space_support", | ||
crates/grain-import/src/tests.rs+24−2
| @@ -142,7 +142,8 @@ INSERT INTO _space_invites VALUES | ||
INSERT INTO _space_support VALUES | ||
('https://pds.example', 1, '[]', '2025-01-01T00:00:00.000Z'), | ||
('https://old.example', 0, '["com.atproto.space.createSpace"]', '2025-01-02T00:00:00.000Z'); | ||
INSERT INTO _oauth_keys VALUES ('k1', 'private', 'public', '2025-01-01 00:00:00'); | ||
INSERT INTO _oauth_keys VALUES ('appview-oauth-key', 'server', 'public', '2025-01-01 00:00:00'), | ||
('pds-client-auth-key', '{"key_ops":["sign"],"ext":true,"kty":"EC","x":"6wVOcgpdJpSGVwhFhUNcCtEGehv3oVbm3Vfspb8z_7w","y":"ezlUp0nPpJHIlRVbUBOb0KitkEVV7uLvtdfWFUmHgzw","crv":"P-256","d":"7YBtWZgfb9lUSihC1h2-HySNJopUjlS4GBsoVC-rZd4"}', 'public', '2025-01-01 00:00:00'); | ||
INSERT INTO _oauth_sessions VALUES ('did:plc:alice', 'https://pds.example', 'a', 'r', 'jkt'); | ||
INSERT INTO _fts_social_grain_gallery VALUES ('at://did:plc:alice/social.grain.gallery/3l3qo2vuowo2a', 'Fog'); | ||
"#; | ||
| @@ -269,6 +270,10 @@ fn hatk(dir: &Path, sql: &str) -> PathBuf { | ||
path | ||
} | ||
| ||
/// A client key as WebCrypto exports one, with the members | ||
/// it adds. | ||
const CLIENT_KEY: &str = r#"{"key_ops":["sign"],"ext":true,"kty":"EC","x":"6wVOcgpdJpSGVwhFhUNcCtEGehv3oVbm3Vfspb8z_7w","y":"ezlUp0nPpJHIlRVbUBOb0KitkEVV7uLvtdfWFUmHgzw","crv":"P-256","d":"7YBtWZgfb9lUSihC1h2-HySNJopUjlS4GBsoVC-rZd4"}"#; | ||
| ||
/// The whole of hatk's fixture database in `dir`. | ||
fn full(dir: &Path) -> PathBuf { | ||
hatk(dir, &format!("{KEPT_ROWS}{}", record_rows())) | ||
| @@ -443,15 +448,33 @@ fn every_row_of_what_the_network_cannot_rebuild_is_imported() { | ||
("push_token", 3, 3), | ||
("story_view", 2, 2), | ||
("space_invite", 2, 2), | ||
("oauth_key", 1, 1), | ||
("space_support", 2, 2), | ||
] | ||
); | ||
assert!(summary.failed.is_empty(), "{:?}", summary.failed); | ||
// The client's key, and none of the others. | ||
assert_eq!( | ||
rows("SELECT kid, jwk FROM oauth_key"), | ||
[format!("pds-client-auth-key|{CLIENT_KEY}")] | ||
); | ||
assert_eq!(summary.unrecognized, [("weird".to_string(), 1)]); | ||
| ||
// As the index reads them. | ||
drop(db); | ||
let index = Index::open(&into).unwrap(); | ||
// The web's client signs with it, as it was exported. | ||
let web = | ||
grain_serve::login::Web::new("https://grain.social", &index, "https://pds.test").unwrap(); | ||
let jwks = web.client.jwks(); | ||
let key = &jwks["keys"][0]; | ||
assert_eq!( | ||
(&key["kid"], &key["x"]), | ||
( | ||
&json!("pds-client-auth-key"), | ||
&json!("6wVOcgpdJpSGVwhFhUNcCtEGehv3oVbm3Vfspb8z_7w") | ||
) | ||
); | ||
assert_eq!(index.cursor(JETSTREAM).unwrap(), Some(1730000000000000)); | ||
assert!(index.taken_down("did:plc:bob").unwrap()); | ||
assert!(!index.taken_down("did:plc:carol").unwrap()); | ||
| @@ -609,7 +632,6 @@ fn every_record_is_imported_as_it_was_indexed() { | ||
// What is not imported, named with its rows. | ||
let left: HashMap<_, _> = summary.left.iter().cloned().collect(); | ||
let expected = [ | ||
("_oauth_keys", 1), | ||
("_oauth_sessions", 1), | ||
("app.bsky.actor.profile", 1), | ||
("app.bsky.feed.post__entities", 0), | ||