feat: panproto v0.39.0 + per-file Merkle schema trees + granular OAuth scopes Pin panproto to v0.39.0 (panproto/panproto#49) so each commit's project schema is stored as a SchemaTree of FileSchemaObject leaves, deduplicated by git blob OID. Cospan's import flow shrinks from ~9 GB to ~260 MiB projected for the full 215-commit history. - Replace every Object::Schema match with vcs::resolve_commit_schema or native walk_tree iteration. get_project_schema and get_file_schema now read per-file leaves directly instead of parsing vertex-id prefixes. - Round-trip integration test exercises FileSchema → SchemaTree → Commit → ref → walk; verifies content-addressed dedup holds (re-putting an unchanged leaf returns the same ObjectId). - Granular OAuth scopes: dev.cospan.auth.{reader,contributor,maintainer, owner,push}Access permission-set lexicons drive ATProto consent screens. RequiredScope/RequiredRole extractors gate XRPC handlers; collaborator. add and createPushToken now demand session + role. - Frontend lib/auth/scopes.ts mirrors the Rust intents and surfaces the granted scope through locals.user.scope. - Push-token JWT signature verification: cospan-node now resolves the appview JWKS by `kid` and rejects forged or expired tokens. - Refresh stale test_handlers assertions to match the updated wire format (listRefs `name`/`target`, getHead flat branch/detached, negotiate contract); fix three pre-existing CI clippy errors.
Author: Aaron Steven White
Commit
0ea2d76b1f38f2c3ff9cae5e533b5772e3307f23Parent: aedf5c8b48
Structural diff unavailable
These commits were pushed via plain git push, so no pre-parsed
schemas are available. Install git-remote-cospan and re-push via panproto:// to
see scope-level changes, breaking change detection, and semantic diffs.
brew install panproto/tap/git-remote-cospan71 files changed +3053 -1104
@@ -610,9 +610,12 @@ dependencies = [
610610 "blake3", 611611 "chrono", 612612 "dirs", 613+ "elliptic-curve", 613614 "git2", 614615 "jsonwebtoken", 615616 "mockall", 617+ "moka", 618+ "p256", 616619 "panproto-check", 617620 "panproto-core", 618621 "panproto-git",
@@ -2492,8 +2495,8 @@ dependencies = [
24922495 24932496 [[package]] 24942497 name = "panproto-check" 2495-version = "0.34.0" 2496-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2498+version = "0.39.0" 2499+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 24972500 dependencies = [ 24982501 "memchr", 24992502 "panproto-gat",
@@ -2508,8 +2511,8 @@ dependencies = [
25082511 25092512 [[package]] 25102513 name = "panproto-core" 2511-version = "0.34.0" 2512-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2514+version = "0.39.0" 2515+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 25132516 dependencies = [ 25142517 "panproto-check", 25152518 "panproto-gat",
@@ -2524,8 +2527,8 @@ dependencies = [
25242527 25252528 [[package]] 25262529 name = "panproto-expr" 2527-version = "0.34.0" 2528-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2530+version = "0.39.0" 2531+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 25292532 dependencies = [ 25302533 "rustc-hash", 25312534 "serde",
@@ -2534,8 +2537,8 @@ dependencies = [
25342537 25352538 [[package]] 25362539 name = "panproto-expr-parser" 2537-version = "0.34.0" 2538-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2540+version = "0.39.0" 2541+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 25392542 dependencies = [ 25402543 "chumsky", 25412544 "logos 0.16.1",
@@ -2544,9 +2547,10 @@ dependencies = [
25442547 25452548 [[package]] 25462549 name = "panproto-gat" 2547-version = "0.34.0" 2548-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2550+version = "0.39.0" 2551+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 25492552 dependencies = [ 2553+ "miette", 25502554 "panproto-expr", 25512555 "rustc-hash", 25522556 "serde",
@@ -2555,8 +2559,8 @@ dependencies = [
25552559 25562560 [[package]] 25572561 name = "panproto-git" 2558-version = "0.34.0" 2559-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2562+version = "0.39.0" 2563+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 25602564 dependencies = [ 25612565 "git2", 25622566 "miette",
@@ -2573,8 +2577,8 @@ dependencies = [
25732577 25742578 [[package]] 25752579 name = "panproto-grammars" 2576-version = "0.34.0" 2577-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2580+version = "0.39.0" 2581+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 25782582 dependencies = [ 25792583 "cc", 25802584 "serde",
@@ -2585,8 +2589,8 @@ dependencies = [
25852589 25862590 [[package]] 25872591 name = "panproto-inst" 2588-version = "0.34.0" 2589-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2592+version = "0.39.0" 2593+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 25902594 dependencies = [ 25912595 "bumpalo", 25922596 "panproto-expr",
@@ -2601,8 +2605,8 @@ dependencies = [
26012605 26022606 [[package]] 26032607 name = "panproto-io" 2604-version = "0.34.0" 2605-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2608+version = "0.39.0" 2609+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 26062610 dependencies = [ 26072611 "bumpalo", 26082612 "memchr",
@@ -2624,8 +2628,8 @@ dependencies = [
26242628 26252629 [[package]] 26262630 name = "panproto-lens" 2627-version = "0.34.0" 2628-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2631+version = "0.39.0" 2632+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 26292633 dependencies = [ 26302634 "panproto-expr", 26312635 "panproto-gat",
@@ -2641,8 +2645,8 @@ dependencies = [
26412645 26422646 [[package]] 26432647 name = "panproto-lens-dsl" 2644-version = "0.34.0" 2645-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2648+version = "0.39.0" 2649+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 26462650 dependencies = [ 26472651 "miette", 26482652 "nickel-lang",
@@ -2660,9 +2664,10 @@ dependencies = [
26602664 26612665 [[package]] 26622666 name = "panproto-mig" 2663-version = "0.34.0" 2664-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2667+version = "0.39.0" 2668+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 26652669 dependencies = [ 2670+ "blake3", 26662671 "panproto-expr", 26672672 "panproto-gat", 26682673 "panproto-inst",
@@ -2674,8 +2679,8 @@ dependencies = [
26742679 26752680 [[package]] 26762681 name = "panproto-parse" 2677-version = "0.34.0" 2678-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2682+version = "0.39.0" 2683+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 26792684 dependencies = [ 26802685 "memchr", 26812686 "miette",
@@ -2694,8 +2699,8 @@ dependencies = [
26942699 26952700 [[package]] 26962701 name = "panproto-project" 2697-version = "0.34.0" 2698-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2702+version = "0.39.0" 2703+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 26992704 dependencies = [ 27002705 "blake3", 27012706 "globset",
@@ -2704,6 +2709,7 @@ dependencies = [
27042709 "panproto-parse", 27052710 "panproto-protocols", 27062711 "panproto-schema", 2712+ "panproto-vcs", 27072713 "rustc-hash", 27082714 "serde", 27092715 "serde_json",
@@ -2714,8 +2720,8 @@ dependencies = [
27142720 27152721 [[package]] 27162722 name = "panproto-protocols" 2717-version = "0.34.0" 2718-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2723+version = "0.39.0" 2724+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 27192725 dependencies = [ 27202726 "blake3", 27212727 "panproto-gat",
@@ -2729,8 +2735,8 @@ dependencies = [
27292735 27302736 [[package]] 27312737 name = "panproto-schema" 2732-version = "0.34.0" 2733-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2738+version = "0.39.0" 2739+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 27342740 dependencies = [ 27352741 "panproto-expr", 27362742 "panproto-gat",
@@ -2742,8 +2748,8 @@ dependencies = [
27422748 27432749 [[package]] 27442750 name = "panproto-vcs" 2745-version = "0.34.0" 2746-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2751+version = "0.39.0" 2752+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 27472753 dependencies = [ 27482754 "blake3", 27492755 "panproto-check",
@@ -2763,8 +2769,8 @@ dependencies = [
27632769 27642770 [[package]] 27652771 name = "panproto-xrpc" 2766-version = "0.34.0" 2767-source = "git+https://github.com/panproto/panproto.git?tag=v0.34.0#c3880731588e50c142dd693d8108f0f16456c929" 2772+version = "0.39.0" 2773+source = "git+https://github.com/panproto/panproto.git?tag=v0.39.0#02158abb80252378a21bb1a9bee839d053a21795" 27682774 dependencies = [ 27692775 "hex", 27702776 "miette",
@@ -13,24 +13,24 @@ license = "AGPL-3.0-or-later"
1313 repository = "https://github.com/cospan-dev/cospan" 1414 1515 [workspace.dependencies] 16-# panproto v0.34.0 17-panproto-core = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 18-panproto-vcs = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 19-panproto-schema = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 20-panproto-check = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 21-panproto-lens = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 22-panproto-lens-dsl = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 23-panproto-protocols = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 24-panproto-io = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 25-panproto-inst = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 26-panproto-gat = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 27-panproto-mig = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 28-panproto-expr = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 29-panproto-expr-parser = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 30-panproto-xrpc = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 31-panproto-parse = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0", features = ["group-all"] } 32-panproto-git = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 33-panproto-project = { git = "https://github.com/panproto/panproto.git", tag = "v0.34.0" } 16+# panproto v0.39.0 (Merkle file-schema trees, https://github.com/panproto/panproto/issues/49) 17+panproto-core = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 18+panproto-vcs = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 19+panproto-schema = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 20+panproto-check = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 21+panproto-lens = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 22+panproto-lens-dsl = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 23+panproto-protocols = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 24+panproto-io = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 25+panproto-inst = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 26+panproto-gat = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 27+panproto-mig = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 28+panproto-expr = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 29+panproto-expr-parser = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 30+panproto-xrpc = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 31+panproto-parse = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0", features = ["group-all"] } 32+panproto-git = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 33+panproto-project = { git = "https://github.com/panproto/panproto.git", tag = "v0.39.0" } 3434 git2 = "0.20" 3535 3636 # Web framework
@@ -7,6 +7,9 @@ declare global {
77 authenticated: boolean; 88 did: string; 99 handle: string; 10+ avatar?: string; 11+ /** Raw granted scope string from the PDS (space-separated). */ 12+ scope?: string; 1013 }; 1114 } 1215
@@ -18,6 +18,7 @@ export const handle: Handle = async ({ event, resolve }) => {
1818 did: session.did, 1919 handle: session.handle, 2020 avatar: avatarCookie ? decodeURIComponent(avatarCookie) : undefined, 21+ scope: session.scope, 2122 }; 2223 } 2324 } catch {
@@ -0,0 +1,81 @@
1+// ATProto OAuth granular scopes — frontend helpers. 2+// 3+// Mirrors the Rust `auth::scope` module in the appview. Supports the 4+// four Cospan intents and checks whether the session's granted scope 5+// string covers a required operation. Used to gate UI actions before 6+// hitting the backend, and to compute the `?intent=...` value for 7+// login-upgrade redirects. 8+ 9+export type AuthIntent = 'browse' | 'contribute' | 'maintain' | 'own'; 10+ 11+const PERMISSION_SET: Record<AuthIntent, string> = { 12+ browse: 'dev.cospan.auth.readerAccess', 13+ contribute: 'dev.cospan.auth.contributorAccess', 14+ maintain: 'dev.cospan.auth.maintainerAccess', 15+ own: 'dev.cospan.auth.ownerAccess', 16+}; 17+ 18+const INTENT_ORDER: AuthIntent[] = ['browse', 'contribute', 'maintain', 'own']; 19+ 20+// Which permission-sets each intent implies (via `includes:` hierarchy). 21+// Must match apps/web/../lexicons/dev/cospan/auth/*.json. 22+const INCLUDES_CHAIN: Record<AuthIntent, AuthIntent[]> = { 23+ browse: ['browse'], 24+ contribute: ['browse', 'contribute'], 25+ maintain: ['browse', 'contribute', 'maintain'], 26+ own: ['browse', 'contribute', 'maintain', 'own'], 27+}; 28+ 29+/** Build the scope string to request at OAuth login for a given intent. */ 30+export function buildScopeString(intent: AuthIntent, appviewDid: string | null): string { 31+ const aud = appviewDid ? encodeURIComponent(appviewDid) : '*'; 32+ return `atproto include:${PERMISSION_SET[intent]}?aud=${aud}`; 33+} 34+ 35+/** 36+ * Return the highest intent tier fully covered by the given granted scope 37+ * string, or `null` if only `atproto` (or nothing) was granted. 38+ */ 39+export function grantedIntent(granted: string | null | undefined): AuthIntent | null { 40+ if (!granted) return null; 41+ const tokens = granted.split(/\s+/).filter(Boolean); 42+ const includes = new Set<string>(); 43+ for (const tok of tokens) { 44+ if (tok.startsWith('include:')) { 45+ const head = tok.slice('include:'.length).split('?')[0]; 46+ includes.add(head); 47+ } 48+ } 49+ for (const intent of [...INTENT_ORDER].reverse()) { 50+ if (includes.has(PERMISSION_SET[intent])) { 51+ return intent; 52+ } 53+ } 54+ return null; 55+} 56+ 57+/** Does the granted scope string include at least `required`'s tier? */ 58+export function hasIntent( 59+ granted: string | null | undefined, 60+ required: AuthIntent, 61+): boolean { 62+ const g = grantedIntent(granted); 63+ if (!g) return false; 64+ // Higher intents include lower ones. 65+ return INCLUDES_CHAIN[g].includes(required); 66+} 67+ 68+/** Build a login-upgrade URL that re-triggers OAuth with a higher intent. */ 69+export function buildUpgradeUrl( 70+ appviewUrl: string, 71+ handle: string, 72+ intent: AuthIntent, 73+ returnTo?: string, 74+): string { 75+ const params = new URLSearchParams({ handle, intent }); 76+ if (returnTo) params.set('return', returnTo); 77+ return `${appviewUrl}/oauth/login?${params.toString()}`; 78+} 79+ 80+export const ALL_INTENTS = INTENT_ORDER; 81+export const PERMISSION_SET_NSID = PERMISSION_SET;