Program -> Namespace ()

This commit is contained in:
Daniel Holmgren 2022-04-06 19:41:18 -05:00 committed by GitHub
parent 28d476eec4
commit 608752f0e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 259 additions and 268 deletions

@ -16,7 +16,7 @@ Resource name: 'bluesky'
- Full permission for account:
did:bsky:userDid|*
- Permission to write to particular program namespace:
- Permission to write to particular namespace:
did:bsky:userDid|did:bsky:microblog|*
- Permission to make only interactions in a given namespace:
did:bsky:userDid|did:bsky:microblog|interactions|*
@ -75,20 +75,20 @@ export const blueskySemantics: CapabilitySemantics<BlueskyCapability> = {
}
}
const [childDid, childProgram, childCollection, childTid] =
const [childDid, childNamespace, childCollection, childTid] =
childCap.bluesky.split('|')
const [parentDid, parentProgram, parentCollection, parentTid] =
const [parentDid, parentNamespace, parentCollection, parentTid] =
parentCap.bluesky.split('|')
if (childDid !== parentDid) {
return null
}
if (parentProgram === '*') {
if (parentNamespace === '*') {
return childCap
} else if (childProgram === '*') {
} else if (childNamespace === '*') {
return namespaceEscalation(childCap)
} else if (childProgram !== parentProgram) {
} else if (childNamespace !== parentNamespace) {
return null
}
@ -122,7 +122,7 @@ export const hasPermission = (
export const namespaceEscalation = (cap: BlueskyCapability) => {
return {
escalation: 'Bluesky program namespace esclation',
escalation: 'Bluesky namespace esclation',
capability: cap,
}
}
@ -143,13 +143,13 @@ export const tidEscalation = (cap: BlueskyCapability) => {
export function writeCap(
did: string,
program?: string,
namespace?: string,
collection?: Collection,
tid?: TID,
): BlueskyCapability {
let resource = did
if (program) {
resource += '|' + program
if (namespace) {
resource += '|' + namespace
}
if (collection) {
resource += '|' + collection

@ -12,7 +12,7 @@ const MONTH_IN_SECONDS = 60 * 60 * 24 * 30
export const delegateForPost = async (
serverDid: string,
did: string,
program: string,
namespace: string,
collection: Collection,
tid: TID,
keypair: ucan.Keypair,
@ -24,7 +24,7 @@ export const delegateForPost = async (
.withLifetimeInSeconds(30)
.delegateCapability(
blueskySemantics,
writeCap(did, program, collection, tid),
writeCap(did, namespace, collection, tid),
ucanStore,
)
.build()

@ -92,9 +92,9 @@ export const hasRelationshipsPermission =
}
export const hasPostingPermission =
(did: string, program: string, collection: Collection, tid: TID) =>
(did: string, namespace: string, collection: Collection, tid: TID) =>
(token: Chained): Error | null => {
// the capability we need for the given post
const needed = writeCap(did, program, collection, tid)
const needed = writeCap(did, namespace, collection, tid)
return hasValidCapability(did, needed)(token)
}

@ -10,7 +10,7 @@ import { Keypair } from '../common/types.js'
import * as auth from '../auth/index.js'
export class MicroblogDelegator {
programName = 'did:bsky:microblog'
namespace = 'did:bsky:microblog'
keypair: Keypair | null
ucanStore: ucan.Store | null
serverDid: string | null
@ -79,7 +79,7 @@ export class MicroblogDelegator {
return auth.delegateForPost(
serverDid,
this.did,
this.programName,
this.namespace,
collection,
tid,
this.keypair,
@ -151,7 +151,7 @@ export class MicroblogDelegator {
const params = {
tid: tid.toString(),
did: did,
program: this.programName,
namespace: this.namespace,
}
let res: AxiosResponse
try {
@ -173,7 +173,7 @@ export class MicroblogDelegator {
const tid = TID.next()
const post: Post = {
tid: tid.toString(),
program: this.programName,
namespace: this.namespace,
author: this.did,
text,
time: new Date().toISOString(),
@ -191,7 +191,7 @@ export class MicroblogDelegator {
async editPost(tid: TID, text: string): Promise<void> {
const updated: Post = {
tid: tid.toString(),
program: this.programName,
namespace: this.namespace,
author: this.did,
text,
time: new Date().toISOString(),
@ -209,7 +209,7 @@ export class MicroblogDelegator {
const data = {
tid: tid.toString(),
did: this.did,
program: this.programName,
namespace: this.namespace,
}
const token = await this.postToken('posts', tid)
try {
@ -235,7 +235,7 @@ export class MicroblogDelegator {
const did = await this.resolveDid(nameOrDid)
const params = {
did,
program: this.programName,
namespace: this.namespace,
count,
from: from?.toString(),
}
@ -318,12 +318,12 @@ export class MicroblogDelegator {
const tid = TID.next()
const like: Like = {
tid: tid.toString(),
program: this.programName,
namespace: this.namespace,
author: this.did,
time: new Date().toISOString(),
post_tid: postTid.toString(),
post_author: postAuthorDid,
post_program: this.programName,
post_namespace: this.namespace,
}
const token = await this.postToken('interactions', tid)
try {
@ -339,7 +339,7 @@ export class MicroblogDelegator {
const data = {
tid: likeTid.toString(),
did: this.did,
program: this.programName,
namespace: this.namespace,
}
const token = await this.postToken('interactions', likeTid)
try {
@ -369,7 +369,7 @@ export class MicroblogDelegator {
const params = {
did: this.did,
postAuthor: authorDid,
postProgram: this.programName,
postNamespace: this.namespace,
postTid: postTid.toString(),
}
try {
@ -396,7 +396,7 @@ export class MicroblogDelegator {
const did = await this.resolveDid(nameOrDid)
const params = {
did,
program: this.programName,
namespace: this.namespace,
count,
from: from?.toString(),
}
@ -414,7 +414,7 @@ export class MicroblogDelegator {
async likeCount(author: string, tid: TID): Promise<number> {
const params = {
author,
program: this.programName,
namespace: this.namespace,
tid: tid.toString(),
}
try {

@ -1,17 +1,17 @@
import Repo from '../repo/index.js'
import Program from '../repo/program.js'
import NamespaceImpl from '../repo/namespace-impl.js'
import { Post, Like, schema } from './types.js'
import TID from '../repo/tid.js'
export class Microblog extends Program {
export class Microblog extends NamespaceImpl {
constructor(repo: Repo) {
super('did:bsky:microblog', repo)
}
async getPost(id: TID): Promise<Post | null> {
const postCid = await this.runOnProgram(async (program) => {
return program.posts.getEntry(id)
const postCid = await this.runOnNamespace(async (namespace) => {
return namespace.posts.getEntry(id)
})
if (postCid === null) return null
const post = await this.repo.get(postCid, schema.post)
@ -22,14 +22,14 @@ export class Microblog extends Program {
const tid = TID.next()
const post: Post = {
tid: tid.toString(),
program: this.name,
namespace: this.name,
text,
author: this.repo.did,
time: new Date().toISOString(),
}
const postCid = await this.repo.put(post)
await this.runOnProgram(async (program) => {
await program.posts.addEntry(tid, postCid)
await this.runOnNamespace(async (namespace) => {
await namespace.posts.addEntry(tid, postCid)
})
return post
}
@ -37,26 +37,26 @@ export class Microblog extends Program {
async editPost(tid: TID, text: string): Promise<void> {
const post: Post = {
tid: tid.toString(),
program: this.name,
namespace: this.name,
text,
author: this.repo.did,
time: new Date().toISOString(),
}
const postCid = await this.repo.put(post)
await this.runOnProgram(async (program) => {
await program.posts.editEntry(tid, postCid)
await this.runOnNamespace(async (namespace) => {
await namespace.posts.editEntry(tid, postCid)
})
}
async deletePost(tid: TID): Promise<void> {
await this.runOnProgram(async (program) => {
await program.posts.deleteEntry(tid)
await this.runOnNamespace(async (namespace) => {
await namespace.posts.deleteEntry(tid)
})
}
async listPosts(count: number, from?: TID): Promise<Post[]> {
const entries = await this.runOnProgram(async (program) => {
return program.posts.getEntries(count, from)
const entries = await this.runOnNamespace(async (namespace) => {
return namespace.posts.getEntries(count, from)
})
const posts = await Promise.all(
entries.map((entry) => this.repo.get(entry.cid, schema.post)),
@ -68,29 +68,29 @@ export class Microblog extends Program {
const tid = TID.next()
const like: Like = {
tid: tid.toString(),
program: this.name,
namespace: this.name,
author: this.repo.did,
time: new Date().toISOString(),
post_tid: postTid.toString(),
post_author: postAuthor,
post_program: this.name,
post_namespace: this.name,
}
const likeCid = await this.repo.put(like)
await this.runOnProgram(async (program) => {
await program.interactions.addEntry(tid, likeCid)
await this.runOnNamespace(async (namespace) => {
await namespace.interactions.addEntry(tid, likeCid)
})
return tid
}
async deleteLike(tid: TID): Promise<void> {
await this.runOnProgram(async (program) => {
await program.interactions.deleteEntry(tid)
await this.runOnNamespace(async (namespace) => {
await namespace.interactions.deleteEntry(tid)
})
}
async listLikes(count: number, from?: TID): Promise<Like[]> {
const entries = await this.runOnProgram(async (program) => {
return program.interactions.getEntries(count, from)
const entries = await this.runOnNamespace(async (namespace) => {
return namespace.interactions.getEntries(count, from)
})
const likes = await Promise.all(
entries.map((entry) => this.repo.get(entry.cid, schema.like)),

@ -3,7 +3,7 @@ import { z } from 'zod'
const post = z.object({
tid: z.string(),
author: z.string(),
program: z.string(),
namespace: z.string(),
text: z.string(),
time: z.string(),
})
@ -12,11 +12,11 @@ export type Post = z.infer<typeof post>
const like = z.object({
tid: z.string(),
author: z.string(),
program: z.string(),
namespace: z.string(),
time: z.string(),
post_tid: z.string(),
post_author: z.string(),
post_program: z.string(),
post_namespace: z.string(),
})
export type Like = z.infer<typeof like>

@ -16,7 +16,7 @@ import { DID, Keypair } from '../common/types.js'
import * as check from '../common/check.js'
import IpldStore from '../blockstore/ipld-store.js'
import { streamToArray } from '../common/util.js'
import ProgramStore from './program-store.js'
import Namespace from './namespace.js'
import Relationships from './relationships.js'
import {
blueskySemantics,
@ -27,8 +27,8 @@ import {
export class Repo implements CarStreamable {
blockstore: IpldStore
ucanStore: ucan.Store
programCids: IdMapping
programs: { [name: string]: ProgramStore }
namespaceCids: IdMapping
namespaces: { [name: string]: Namespace }
relationships: Relationships
cid: CID
did: DID
@ -37,7 +37,7 @@ export class Repo implements CarStreamable {
constructor(params: {
blockstore: IpldStore
ucanStore: ucan.Store
programCids: IdMapping
namespaceCids: IdMapping
relationships: Relationships
cid: CID
did: DID
@ -45,8 +45,8 @@ export class Repo implements CarStreamable {
}) {
this.blockstore = params.blockstore
this.ucanStore = params.ucanStore
this.programCids = params.programCids
this.programs = {}
this.namespaceCids = params.namespaceCids
this.namespaces = {}
this.relationships = params.relationships
this.cid = params.cid
this.did = params.did
@ -78,7 +78,7 @@ export class Repo implements CarStreamable {
new_cids: await relationships.cids(),
auth_token: tokenCid, // @FIX THIS
relationships: relationships.cid,
programs: {},
namespaces: {},
}
const rootCid = await blockstore.put(rootObj)
@ -88,12 +88,12 @@ export class Repo implements CarStreamable {
}
const cid = await blockstore.put(commit)
const programCids: IdMapping = {}
const namespaceCids: IdMapping = {}
return new Repo({
blockstore,
ucanStore,
programCids,
namespaceCids,
relationships,
cid,
did,
@ -116,7 +116,7 @@ export class Repo implements CarStreamable {
return new Repo({
blockstore,
ucanStore: ucanStore || (await ucan.Store.fromTokens([])),
programCids: root.programs,
namespaceCids: root.namespaces,
relationships,
cid,
did: root.did,
@ -146,27 +146,27 @@ export class Repo implements CarStreamable {
}
// arrow fn to preserve scope
updateRootForProgram =
(programName: string) =>
updateRootForNamespace =
(namespaceId: string) =>
async (update: UpdateData): Promise<void> => {
const program = this.programs[programName]
if (!program) {
const namespace = this.namespaces[namespaceId]
if (!namespace) {
throw new Error(
`Tried to update program root for a program that doesnt exist: ${programName}`,
`Tried to update namespace root for a namespace that doesnt exist: ${namespaceId}`,
)
}
// if a new program, make sure we add the structural nodes
// if a new namespace, make sure we add the structural nodes
const newCids = update.newCids
if (this.programCids[programName] === undefined) {
if (this.namespaceCids[namespaceId] === undefined) {
newCids
.add(program.cid)
.add(program.posts.cid)
.add(program.interactions.cid)
.add(namespace.cid)
.add(namespace.posts.cid)
.add(namespace.interactions.cid)
}
this.programCids[programName] = program.cid
this.namespaceCids[namespaceId] = namespace.cid
await this.updateRoot({
...update,
program: programName,
namespace: namespaceId,
newCids,
})
}
@ -182,7 +182,7 @@ export class Repo implements CarStreamable {
prev: this.cid,
new_cids: newCids.toList(),
auth_token: tokenCid,
programs: this.programCids,
namespaces: this.namespaceCids,
relationships: this.relationships.cid,
}
const rootCid = await this.blockstore.put(root)
@ -203,39 +203,39 @@ export class Repo implements CarStreamable {
return this.blockstore.get(commit.root, schema.repoRoot)
}
// Program API
// Namespace API
// -----------
async createProgramStore(name: string): Promise<ProgramStore> {
if (this.programCids[name] !== undefined) {
throw new Error(`Program store already exists for program: ${name}`)
async createNamespace(id: string): Promise<Namespace> {
if (this.namespaceCids[id] !== undefined) {
throw new Error(`Namespace already exists for id: ${id}`)
}
const programStore = await ProgramStore.create(this.blockstore)
programStore.onUpdate = this.updateRootForProgram(name)
this.programs[name] = programStore
return programStore
const namespace = await Namespace.create(this.blockstore)
namespace.onUpdate = this.updateRootForNamespace(id)
this.namespaces[id] = namespace
return namespace
}
async loadOrCreateProgramStore(name: string): Promise<ProgramStore> {
if (this.programs[name]) {
return this.programs[name]
async loadOrCreateNamespace(id: string): Promise<Namespace> {
if (this.namespaces[id]) {
return this.namespaces[id]
}
const cid = this.programCids[name]
const cid = this.namespaceCids[id]
if (!cid) {
return this.createProgramStore(name)
return this.createNamespace(id)
}
const programStore = await ProgramStore.load(this.blockstore, cid)
programStore.onUpdate = this.updateRootForProgram(name)
this.programs[name] = programStore
return programStore
const namespace = await Namespace.load(this.blockstore, cid)
namespace.onUpdate = this.updateRootForNamespace(id)
this.namespaces[id] = namespace
return namespace
}
async runOnProgram<T>(
programName: string,
fn: (store: ProgramStore) => Promise<T>,
async runOnNamespace<T>(
id: string,
fn: (store: Namespace) => Promise<T>,
): Promise<T> {
const program = await this.loadOrCreateProgramStore(programName)
return fn(program)
const namespace = await this.loadOrCreateNamespace(id)
return fn(namespace)
}
// IPLD store methods
@ -258,7 +258,7 @@ export class Repo implements CarStreamable {
}
const neededCap = writeCap(
this.did,
update.program,
update.namespace,
update.collection,
update.tid,
)
@ -292,8 +292,8 @@ export class Repo implements CarStreamable {
this.cid = rootCid
const root = await this.getRoot()
this.did = root.did
this.programCids = root.programs
this.programs = {}
this.namespaceCids = root.namespaces
this.namespaces = {}
this.relationships = await Relationships.load(
this.blockstore,
root.relationships,
@ -331,9 +331,9 @@ export class Repo implements CarStreamable {
await this.blockstore.addToCar(car, commit.root)
await this.relationships.writeToCarStream(car)
await Promise.all(
Object.values(this.programCids).map(async (cid) => {
const programStore = await ProgramStore.load(this.blockstore, cid)
await programStore.writeToCarStream(car)
Object.values(this.namespaceCids).map(async (cid) => {
const namespace = await Namespace.load(this.blockstore, cid)
await namespace.writeToCarStream(car)
}),
)
}

@ -1,8 +1,8 @@
import { CID } from 'multiformats/cid'
import Repo from './index.js'
import ProgramStore from './program-store'
import Namespace from './namespace'
export class Program {
export class NamespaceImpl {
name: string
repo: Repo
@ -15,9 +15,11 @@ export class Program {
return this.repo.cid
}
async runOnProgram<T>(fn: (program: ProgramStore) => Promise<T>): Promise<T> {
return this.repo.runOnProgram(this.name, fn)
async runOnNamespace<T>(
fn: (namespace: Namespace) => Promise<T>,
): Promise<T> {
return this.repo.runOnNamespace(this.name, fn)
}
}
export default Program
export default NamespaceImpl

@ -3,11 +3,10 @@ import { BlockWriter } from '@ipld/car/lib/writer-browser'
import IpldStore from '../blockstore/ipld-store.js'
import TidCollection from './tid-collection.js'
import { Collection, ProgramRoot, schema, UpdateData } from './types.js'
import { Collection, NamespaceRoot, schema, UpdateData } from './types.js'
import CidSet from './cid-set.js'
import TID from './tid.js'
export class ProgramStore {
export class Namespace {
blockstore: IpldStore
posts: TidCollection
interactions: TidCollection
@ -37,7 +36,7 @@ export class ProgramStore {
const posts = await TidCollection.create(blockstore)
const interactions = await TidCollection.create(blockstore)
const rootObj: ProgramRoot = {
const rootObj: NamespaceRoot = {
posts: posts.cid,
interactions: interactions.cid,
profile: null,
@ -45,7 +44,7 @@ export class ProgramStore {
const cid = await blockstore.put(rootObj)
return new ProgramStore({
return new Namespace({
blockstore,
posts,
interactions,
@ -55,13 +54,13 @@ export class ProgramStore {
}
static async load(blockstore: IpldStore, cid: CID) {
const rootObj = await blockstore.get(cid, schema.programRoot)
const rootObj = await blockstore.get(cid, schema.namespaceRoot)
const posts = await TidCollection.load(blockstore, rootObj.posts)
const interactions = await TidCollection.load(
blockstore,
rootObj.interactions,
)
return new ProgramStore({
return new Namespace({
blockstore,
posts,
interactions,
@ -109,4 +108,4 @@ export class ProgramStore {
}
}
export default ProgramStore
export default Namespace

@ -42,7 +42,7 @@ export class Relationships implements CarStreamable {
async updateRoot(newCids: CidSet): Promise<void> {
this.cid = this.hamt.cid
if (this.onUpdate) {
await this.onUpdate({ program: 'relationships', newCids })
await this.onUpdate({ namespace: 'relationships', newCids })
}
}

@ -12,16 +12,16 @@ const repoRoot = z.object({
new_cids: z.array(common.cid),
auth_token: common.cid,
relationships: common.cid,
programs: z.record(common.cid),
namespaces: z.record(common.cid),
})
export type RepoRoot = z.infer<typeof repoRoot>
const programRoot = z.object({
const namespaceRoot = z.object({
posts: common.cid,
interactions: common.cid,
profile: common.cid.nullable(),
})
export type ProgramRoot = z.infer<typeof programRoot>
export type NamespaceRoot = z.infer<typeof namespaceRoot>
const commit = z.object({
root: common.cid,
@ -48,7 +48,7 @@ const follow = z.object({
export type Follow = z.infer<typeof follow>
export type UpdateData = {
program?: string
namespace?: string
collection?: Collection
tid?: TID
did?: string
@ -59,7 +59,7 @@ export const schema = {
...common,
tid,
repoRoot,
programRoot,
namespaceRoot,
commit,
idMapping,
entry,

@ -90,7 +90,7 @@ type RepoData = {
export const fillRepo = async (
repo: Repo,
programName: string,
namespaceId: string,
postsCount: number,
interCount: number,
followCount: number,
@ -100,19 +100,19 @@ export const fillRepo = async (
interactions: {},
follows: {},
}
await repo.runOnProgram(programName, async (program) => {
await repo.runOnNamespace(namespaceId, async (namespace) => {
for (let i = 0; i < postsCount; i++) {
const tid = await TID.next()
const content = randomStr(10)
const cid = await repo.put(content)
await program.posts.addEntry(tid, cid)
await namespace.posts.addEntry(tid, cid)
data.posts[tid.toString()] = content
}
for (let i = 0; i < interCount; i++) {
const tid = await TID.next()
const content = randomStr(10)
const cid = await repo.put(content)
await program.interactions.addEntry(tid, cid)
await namespace.interactions.addEntry(tid, cid)
data.interactions[tid.toString()] = content
}
})
@ -127,12 +127,12 @@ export const fillRepo = async (
export const checkRepo = async (
t: ExecutionContext<unknown>,
repo: Repo,
programName: string,
namespaceId: string,
data: RepoData,
): Promise<void> => {
await repo.runOnProgram(programName, async (program) => {
await repo.runOnNamespace(namespaceId, async (namespace) => {
for (const tid of Object.keys(data.posts)) {
const cid = await program.posts.getEntry(TID.fromStr(tid))
const cid = await namespace.posts.getEntry(TID.fromStr(tid))
const actual = cid ? await repo.get(cid, schema.string) : null
t.deepEqual(
actual,
@ -141,7 +141,7 @@ export const checkRepo = async (
)
}
for (const tid of Object.keys(data.interactions)) {
const cid = await program.interactions.getEntry(TID.fromStr(tid))
const cid = await namespace.interactions.getEntry(TID.fromStr(tid))
const actual = cid ? await repo.get(cid, schema.string) : null
t.deepEqual(
actual,

@ -13,7 +13,7 @@ type Context = {
postToken: ucan.Chained
serverDid: string
did: string
program: string
namespace: string
collection: Collection
tid: TID
}
@ -24,13 +24,13 @@ test.beforeEach(async (t) => {
const ucanStore = await ucan.Store.fromTokens([fullToken.encoded()])
const serverDid = 'did:bsky:FAKE_SERVER_DID'
const did = keypair.did()
const program = 'did:bsky:microblog'
const namespace = 'did:bsky:microblog'
const collection = 'posts'
const tid = TID.next()
const postToken = await auth.delegateForPost(
serverDid,
did,
program,
namespace,
collection,
tid,
keypair,
@ -43,7 +43,7 @@ test.beforeEach(async (t) => {
postToken,
serverDid,
did,
program,
namespace,
collection,
tid,
} as Context
@ -55,12 +55,12 @@ test('create & validate token for post', async (t) => {
await checkUcan(
ctx.postToken,
hasAudience(ctx.serverDid),
hasPostingPermission(ctx.did, ctx.program, ctx.collection, ctx.tid),
hasPostingPermission(ctx.did, ctx.namespace, ctx.collection, ctx.tid),
)
t.pass('created & validated token')
})
test('token does not work for other programs', async (t) => {
test('token does not work for other namespaces', async (t) => {
const ctx = t.context as Context
await t.throwsAsync(
checkUcan(
@ -68,13 +68,13 @@ test('token does not work for other programs', async (t) => {
hasAudience(ctx.serverDid),
hasPostingPermission(
ctx.did,
'did:bsky:otherProgram',
'did:bsky:otherNamespace',
ctx.collection,
ctx.tid,
),
),
{ instanceOf: Error },
'throw when program mismatch',
'throw when namespace mismatch',
)
t.pass('yay')
})
@ -85,7 +85,7 @@ test('token does not work for other collections', async (t) => {
checkUcan(
ctx.postToken,
hasAudience(ctx.serverDid),
hasPostingPermission(ctx.did, ctx.program, 'interactions', ctx.tid),
hasPostingPermission(ctx.did, ctx.namespace, 'interactions', ctx.tid),
),
{ instanceOf: Error },
'throw when collection mismatch',
@ -100,7 +100,7 @@ test('token does not work for other TIDs', async (t) => {
checkUcan(
ctx.postToken,
hasAudience(ctx.serverDid),
hasPostingPermission(ctx.did, ctx.program, ctx.collection, otherTid),
hasPostingPermission(ctx.did, ctx.namespace, ctx.collection, otherTid),
),
{ instanceOf: Error },
'throw when tid mismatch',

@ -1,6 +1,6 @@
import test from 'ava'
import ProgramStore from '../src/repo/program-store.js'
import Namespace from '../src/repo/namespace.js'
import IpldStore from '../src/blockstore/ipld-store.js'
import { IdMapping } from '../src/repo/types.js'
@ -8,47 +8,40 @@ import * as util from './_util.js'
type Context = {
store: IpldStore
program: ProgramStore
namespace: Namespace
}
test.beforeEach(async (t) => {
const store = IpldStore.createInMemory()
const program = await ProgramStore.create(store)
t.context = { store, program } as Context
const namespace = await Namespace.create(store)
t.context = { store, namespace } as Context
t.pass('Context setup')
})
test('loads from blockstore', async (t) => {
const { store, program } = t.context as Context
const { store, namespace } = t.context as Context
const actual = {} as IdMapping
const postTids = util.generateBulkTids(150)
for (const tid of postTids) {
const cid = await util.randomCid()
await program.posts.addEntry(tid, cid)
await namespace.posts.addEntry(tid, cid)
actual[tid.toString()] = cid
}
const interTids = util.generateBulkTids(150)
for (const tid of interTids) {
const cid = await util.randomCid()
await program.interactions.addEntry(tid, cid)
await namespace.interactions.addEntry(tid, cid)
actual[tid.toString()] = cid
}
// const relDids = util.generateBulkDids(100)
// for (const did of relDids) {
// const cid = await util.randomCid()
// await program.relationships.addEntry(did, cid)
// actual[did.toString()] = cid
// }
const profileCid = await util.randomCid()
await program.setProfile(profileCid)
await namespace.setProfile(profileCid)
actual['profile'] = profileCid
const loaded = await ProgramStore.load(store, program.cid)
const loaded = await Namespace.load(store, namespace.cid)
for (const tid of postTids) {
const got = await loaded.posts.getEntry(tid)
t.deepEqual(got, actual[tid.toString()], `Matching content for tid: ${tid}`)
@ -61,12 +54,6 @@ test('loads from blockstore', async (t) => {
}
t.pass('All interactions loaded correctly')
// for (const did of relDids) {
// const got = await loaded.relationships.getEntry(did)
// t.deepEqual(got, actual[did.toString()], `Matching content for did: ${did}`)
// }
// t.pass('All relationships loaded correctly')
const got = await loaded.profile
t.deepEqual(got, actual['profile'], 'Matching contnet for profile')
})

@ -13,8 +13,8 @@ type Context = {
ipld: IpldStore
keypair: ucan.EdKeypair
repo: Repo
programName: string
otherProgram: string
namespaceId: string
otherNamespace: string
}
test.beforeEach(async (t) => {
@ -23,9 +23,9 @@ test.beforeEach(async (t) => {
const token = await auth.claimFull(keypair.did(), keypair)
const ucanStore = await ucan.Store.fromTokens([token.encoded()])
const repo = await Repo.create(ipld, keypair.did(), keypair, ucanStore)
const programName = 'did:bsky:test'
const otherProgram = 'did:bsky:other'
t.context = { ipld, keypair, repo, programName, otherProgram } as Context
const namespaceId = 'did:bsky:test'
const otherNamespace = 'did:bsky:other'
t.context = { ipld, keypair, repo, namespaceId, otherNamespace } as Context
t.pass('Context setup')
})
@ -46,53 +46,56 @@ test('sets correct DID', async (t) => {
t.is(repo.did, keypair.did(), 'DIDs match')
})
test('runs operations on the related program', async (t) => {
const { repo, programName } = t.context as Context
test('runs operations on the related namespace', async (t) => {
const { repo, namespaceId } = t.context as Context
const tid = TID.next()
const cid = await util.randomCid()
await repo.runOnProgram(programName, async (program) => {
await program.posts.addEntry(tid, cid)
await repo.runOnNamespace(namespaceId, async (namespace) => {
await namespace.posts.addEntry(tid, cid)
})
const got = await repo.runOnProgram(programName, async (program) => {
return program.posts.getEntry(tid)
const got = await repo.runOnNamespace(namespaceId, async (namespace) => {
return namespace.posts.getEntry(tid)
})
t.deepEqual(got, cid, `Matching content for post tid: ${tid}`)
})
test('name spaces programs', async (t) => {
const { repo, programName, otherProgram } = t.context as Context
test('name spaces namespaces', async (t) => {
const { repo, namespaceId, otherNamespace } = t.context as Context
const tid = TID.next()
const cid = await util.randomCid()
await repo.runOnProgram(programName, async (program) => {
await program.posts.addEntry(tid, cid)
await repo.runOnNamespace(namespaceId, async (namespace) => {
await namespace.posts.addEntry(tid, cid)
})
const tidOther = TID.next()
const cidOther = await util.randomCid()
await repo.runOnProgram(otherProgram, async (program) => {
await program.posts.addEntry(tidOther, cidOther)
await repo.runOnNamespace(otherNamespace, async (namespace) => {
await namespace.posts.addEntry(tidOther, cidOther)
})
const got = await repo.runOnProgram(programName, async (program) => {
const got = await repo.runOnNamespace(namespaceId, async (namespace) => {
return Promise.all([
program.posts.getEntry(tid),
program.posts.getEntry(tidOther),
namespace.posts.getEntry(tid),
namespace.posts.getEntry(tidOther),
])
})
t.deepEqual(got[0], cid, 'correctly retrieves tid from program')
t.deepEqual(got[1], null, 'cannot find tid from other program')
t.deepEqual(got[0], cid, 'correctly retrieves tid from namespace')
t.deepEqual(got[1], null, 'cannot find tid from other namespace')
const gotOther = await repo.runOnProgram(otherProgram, async (program) => {
return Promise.all([
program.posts.getEntry(tid),
program.posts.getEntry(tidOther),
])
})
t.deepEqual(gotOther[0], null, 'cannot find tid from other program')
t.deepEqual(gotOther[1], cidOther, 'correctly retrieves tid from program')
const gotOther = await repo.runOnNamespace(
otherNamespace,
async (namespace) => {
return Promise.all([
namespace.posts.getEntry(tid),
namespace.posts.getEntry(tidOther),
])
},
)
t.deepEqual(gotOther[0], null, 'cannot find tid from other namespace')
t.deepEqual(gotOther[1], cidOther, 'correctly retrieves tid from namespace')
})
test('basic relationship operations', async (t) => {
@ -113,25 +116,25 @@ test('basic relationship operations', async (t) => {
})
test('loads from blockstore', async (t) => {
const { ipld, repo, programName } = t.context as Context
const { ipld, repo, namespaceId } = t.context as Context
const postTid = TID.next()
const postCid = await util.randomCid()
const interTid = TID.next()
const interCid = await util.randomCid()
const follow = util.randomFollow()
await repo.runOnProgram(programName, async (program) => {
await program.posts.addEntry(postTid, postCid)
await program.interactions.addEntry(interTid, interCid)
await repo.runOnNamespace(namespaceId, async (namespace) => {
await namespace.posts.addEntry(postTid, postCid)
await namespace.interactions.addEntry(interTid, interCid)
})
await repo.relationships.follow(follow.did, follow.username)
const loaded = await Repo.load(ipld, repo.cid)
const got = await loaded.runOnProgram(programName, async (program) => {
const got = await loaded.runOnNamespace(namespaceId, async (namespace) => {
return Promise.all([
program.posts.getEntry(postTid),
program.interactions.getEntry(interTid),
namespace.posts.getEntry(postTid),
namespace.interactions.getEntry(interTid),
])
})
const gotFollow = await loaded.relationships.getFollow(follow.did)

@ -14,7 +14,7 @@ type Context = {
repoAlice: Repo
ipldBob: IpldStore
tokenBob: ucan.Chained
programName: string
namespaceId: string
}
test.beforeEach(async (t) => {
@ -31,13 +31,13 @@ test.beforeEach(async (t) => {
const ipldBob = IpldStore.createInMemory()
const programName = 'did:bsky:test'
const namespaceId = 'did:bsky:test'
t.context = {
ipldAlice,
keypairAlice,
repoAlice,
ipldBob,
programName,
namespaceId,
} as Context
t.pass('Context setup')
})
@ -46,27 +46,27 @@ test('syncs an empty repo', async (t) => {
const { repoAlice, ipldBob } = t.context as Context
const car = await repoAlice.getFullHistory()
const repoBob = await Repo.fromCarFile(car, ipldBob)
t.deepEqual(repoBob.programCids, {}, 'loads an empty repo')
t.deepEqual(repoBob.namespaceCids, {}, 'loads an empty repo')
})
test('syncs a repo that is starting from scratch', async (t) => {
const { repoAlice, ipldBob, programName } = t.context as Context
const data = await util.fillRepo(repoAlice, programName, 150, 10, 50)
const { repoAlice, ipldBob, namespaceId } = t.context as Context
const data = await util.fillRepo(repoAlice, namespaceId, 150, 10, 50)
const car = await repoAlice.getFullHistory()
const repoBob = await Repo.fromCarFile(car, ipldBob)
await util.checkRepo(t, repoBob, programName, data)
await util.checkRepo(t, repoBob, namespaceId, data)
})
test('syncs a repo that is behind', async (t) => {
const { repoAlice, ipldBob, programName } = t.context as Context
const { repoAlice, ipldBob, namespaceId } = t.context as Context
// bring bob up to date with early version of alice's repo
const data = await util.fillRepo(repoAlice, programName, 150, 10, 50)
const data = await util.fillRepo(repoAlice, namespaceId, 150, 10, 50)
const car = await repoAlice.getFullHistory()
const repoBob = await Repo.fromCarFile(car, ipldBob)
// add more to alice's repo & have bob catch up
const data2 = await util.fillRepo(repoAlice, programName, 300, 10, 50)
const data2 = await util.fillRepo(repoAlice, namespaceId, 300, 10, 50)
const diff = await repoAlice.getDiffCar(repoBob.cid)
await repoBob.loadCar(diff)
@ -85,16 +85,16 @@ test('syncs a repo that is behind', async (t) => {
},
}
await util.checkRepo(t, repoBob, programName, allData)
await util.checkRepo(t, repoBob, namespaceId, allData)
})
test('syncs a non-historical copy of a repo', async (t) => {
const { repoAlice, programName } = t.context as Context
const data = await util.fillRepo(repoAlice, programName, 150, 20, 50)
const { repoAlice, namespaceId } = t.context as Context
const data = await util.fillRepo(repoAlice, namespaceId, 150, 20, 50)
const car = await repoAlice.getCarNoHistory()
const ipld = IpldStore.createInMemory()
const repoBob = await Repo.fromCarFile(car, ipld)
await util.checkRepo(t, repoBob, programName, data)
await util.checkRepo(t, repoBob, namespaceId, data)
})

@ -85,11 +85,11 @@ export class Database {
async getPost(
tid: string,
author: string,
program: string,
namespace: string,
): Promise<Post | null> {
const row = await this.db('posts')
.select('*')
.where({ tid, author, program })
.where({ tid, author, namespace })
if (row.length < 1) return null
return row[0]
}
@ -102,18 +102,18 @@ export class Database {
}
async updatePost(post: Post, cid: CID): Promise<void> {
const { tid, author, program, text, time } = post
const { tid, author, namespace, text, time } = post
await this.db('posts')
.where({ tid, author, program })
.where({ tid, author, namespace })
.update({ text, time, cid: cid.toString() })
}
async deletePost(
tid: string,
author: string,
program: string,
namespace: string,
): Promise<void> {
await this.db('posts').where({ tid, author, program }).del()
await this.db('posts').where({ tid, author, namespace }).del()
}
async postCount(author: string): Promise<number> {
@ -131,11 +131,11 @@ export class Database {
async getLike(
tid: string,
author: string,
program: string,
namespace: string,
): Promise<Like | null> {
const row = await this.db('likes')
.select('*')
.where({ tid, author, program })
.where({ tid, author, namespace })
if (row.length < 1) return null
return row[0]
}
@ -144,11 +144,11 @@ export class Database {
author: string,
post_tid: string,
post_author: string,
post_program: string,
post_namespace: string,
): Promise<Like | null> {
const row = await this.db('likes')
.select('*')
.where({ author, post_tid, post_author, post_program })
.where({ author, post_tid, post_author, post_namespace })
if (row.length < 1) return null
return row[0]
}
@ -163,9 +163,9 @@ export class Database {
async deleteLike(
tid: string,
author: string,
program: string,
namespace: string,
): Promise<void> {
await this.db('likes').where({ tid, author, program }).delete()
await this.db('likes').where({ tid, author, namespace }).delete()
}
// FOLLOWS
@ -250,19 +250,19 @@ export class Database {
author_name: p.username,
text: p.text,
time: p.time,
likes: await this.likeCount(p.author, p.program, p.tid),
likes: await this.likeCount(p.author, p.namespace, p.tid),
})),
)
}
async likeCount(
author: string,
program: string,
namespace: string,
tid: string,
): Promise<number> {
const res = await this.db('likes').count('*').where({
post_author: author,
post_program: program,
post_namespace: namespace,
post_tid: tid,
})
const count = (res[0] || {})['count(*)']

@ -27,10 +27,10 @@ const userDids = {
const posts = {
name: 'posts',
create: (table: Table) => {
table.unique(['tid', 'author', 'program'])
table.unique(['tid', 'author', 'namespace'])
table.string('tid')
table.string('author')
table.string('program')
table.string('namespace')
table.string('text')
table.string('time')
table.string('cid')
@ -42,22 +42,22 @@ const posts = {
const likes = {
name: 'likes',
create: (table: Table) => {
table.primary(['tid', 'author', 'program'])
table.primary(['tid', 'author', 'namespace'])
table.string('tid')
table.string('author').references('user_dids.did')
table.string('program')
table.string('namespace')
table.string('time')
table.string('cid')
table.string('post_tid')
table.string('post_author')
table.string('post_program')
table.string('post_namespace')
table.string('post_cid')
table.foreign('author').references('did').inTable('user_dids')
table.foreign('post_tid').references('tid').inTable('posts')
table.foreign('post_author').references('author').inTable('posts')
table.foreign('post_program').references('program').inTable('posts')
table.foreign('post_namespace').references('namespace').inTable('posts')
},
}

@ -14,7 +14,7 @@ const router = express.Router()
// find a like by it's TID
const byTid = z.object({
did: z.string(),
program: z.string(),
namespace: z.string(),
tid: z.string(),
})
@ -22,7 +22,7 @@ const byTid = z.object({
const byPost = z.object({
did: z.string(),
postAuthor: z.string(),
postProgram: z.string(),
postNamespace: z.string(),
postTid: z.string(),
})
@ -34,9 +34,9 @@ router.get('/', async (req, res) => {
throw new ServerError(400, 'Poorly formatted request')
}
if (check.is(req.query, byTid)) {
const { did, program, tid } = req.query
const { did, namespace, tid } = req.query
const repo = await util.loadRepo(res, did)
const interCid = await repo.runOnProgram(program, async (store) => {
const interCid = await repo.runOnNamespace(namespace, async (store) => {
return store.interactions.getEntry(TID.fromStr(tid))
})
if (interCid === null) {
@ -45,9 +45,9 @@ router.get('/', async (req, res) => {
const like = await repo.get(interCid, schema.microblog.like)
res.status(200).send(like)
} else {
const { did, postAuthor, postProgram, postTid } = req.query
const { did, postAuthor, postNamespace, postTid } = req.query
const db = util.getDB(res)
const like = await db.getLikeByPost(did, postTid, postAuthor, postProgram)
const like = await db.getLikeByPost(did, postTid, postAuthor, postNamespace)
if (like === null) {
throw new ServerError(404, 'Could not find interaction')
}
@ -60,7 +60,7 @@ router.get('/', async (req, res) => {
export const listInteractionsReq = z.object({
did: z.string(),
program: z.string(),
namespace: z.string(),
count: z.string(),
from: z.string().optional(),
})
@ -70,7 +70,7 @@ router.get('/list', async (req, res) => {
if (!check.is(req.query, listInteractionsReq)) {
throw new ServerError(400, 'Poorly formatted request')
}
const { did, program, count, from } = req.query
const { did, namespace, count, from } = req.query
const countParsed = parseInt(count)
if (isNaN(countParsed)) {
throw new ServerError(
@ -80,7 +80,7 @@ router.get('/list', async (req, res) => {
}
const fromTid = from ? TID.fromStr(from) : undefined
const repo = await util.loadRepo(res, did)
const entries = await repo.runOnProgram(program, async (store) => {
const entries = await repo.runOnNamespace(namespace, async (store) => {
return store.interactions.getEntries(countParsed, fromTid)
})
const posts = await Promise.all(
@ -105,14 +105,14 @@ router.post('/', async (req, res) => {
ucanCheck.hasAudience(SERVER_DID),
ucanCheck.hasPostingPermission(
like.author,
like.program,
like.namespace,
'interactions',
TID.fromStr(like.tid),
),
)
const repo = await util.loadRepo(res, like.author, ucanStore)
const likeCid = await repo.put(like)
await repo.runOnProgram(like.program, async (store) => {
await repo.runOnNamespace(like.namespace, async (store) => {
return store.interactions.addEntry(TID.fromStr(like.tid), likeCid)
})
const db = util.getDB(res)
@ -126,7 +126,7 @@ router.post('/', async (req, res) => {
export const deleteInteractionReq = z.object({
did: z.string(),
program: z.string(),
namespace: z.string(),
tid: z.string(),
})
export type DeleteInteractionReq = z.infer<typeof deleteInteractionReq>
@ -135,19 +135,19 @@ router.delete('/', async (req, res) => {
if (!check.is(req.body, deleteInteractionReq)) {
throw new ServerError(400, 'Poorly formatted request')
}
const { did, program, tid } = req.body
const { did, namespace, tid } = req.body
const tidObj = TID.fromStr(tid)
const ucanStore = await auth.checkReq(
req,
ucanCheck.hasAudience(SERVER_DID),
ucanCheck.hasPostingPermission(did, program, 'interactions', tidObj),
ucanCheck.hasPostingPermission(did, namespace, 'interactions', tidObj),
)
const repo = await util.loadRepo(res, did, ucanStore)
await repo.runOnProgram(program, async (store) => {
await repo.runOnNamespace(namespace, async (store) => {
return store.interactions.deleteEntry(tidObj)
})
const db = util.getDB(res)
await db.deleteLike(tidObj.toString(), did, program)
await db.deleteLike(tidObj.toString(), did, namespace)
await db.updateRepoRoot(did, repo.cid)
res.status(200).send()
})

@ -13,7 +13,7 @@ const router = express.Router()
export const getPostReq = z.object({
did: z.string(),
program: z.string(),
namespace: z.string(),
tid: z.string(),
})
export type GetPostReq = z.infer<typeof getPostReq>
@ -22,9 +22,9 @@ router.get('/', async (req, res) => {
if (!check.is(req.query, getPostReq)) {
throw new ServerError(400, 'Poorly formatted request')
}
const { did, program, tid } = req.query
const { did, namespace, tid } = req.query
const repo = await util.loadRepo(res, did)
const postCid = await repo.runOnProgram(program, async (store) => {
const postCid = await repo.runOnNamespace(namespace, async (store) => {
return store.posts.getEntry(TID.fromStr(tid))
})
if (postCid === null) {
@ -39,7 +39,7 @@ router.get('/', async (req, res) => {
export const listPostsReq = z.object({
did: z.string(),
program: z.string(),
namespace: z.string(),
count: z.string(),
from: z.string().optional(),
})
@ -49,7 +49,7 @@ router.get('/list', async (req, res) => {
if (!check.is(req.query, listPostsReq)) {
throw new ServerError(400, 'Poorly formatted request')
}
const { did, program, count, from } = req.query
const { did, namespace, count, from } = req.query
const countParsed = parseInt(count)
if (isNaN(countParsed)) {
throw new ServerError(
@ -59,7 +59,7 @@ router.get('/list', async (req, res) => {
}
const fromTid = from ? TID.fromStr(from) : undefined
const repo = await util.loadRepo(res, did)
const entries = await repo.runOnProgram(program, async (store) => {
const entries = await repo.runOnNamespace(namespace, async (store) => {
return store.posts.getEntries(countParsed, fromTid)
})
const posts = await Promise.all(
@ -84,14 +84,14 @@ router.post('/', async (req, res) => {
ucanCheck.hasAudience(SERVER_DID),
ucanCheck.hasPostingPermission(
post.author,
post.program,
post.namespace,
'posts',
TID.fromStr(post.tid),
),
)
const repo = await util.loadRepo(res, post.author, ucanStore)
const postCid = await repo.put(post)
await repo.runOnProgram(post.program, async (store) => {
await repo.runOnNamespace(post.namespace, async (store) => {
return store.posts.addEntry(TID.fromStr(post.tid), postCid)
})
const db = util.getDB(res)
@ -115,11 +115,11 @@ router.put('/', async (req, res) => {
const ucanStore = await auth.checkReq(
req,
ucanCheck.hasAudience(SERVER_DID),
ucanCheck.hasPostingPermission(post.author, post.program, 'posts', tid),
ucanCheck.hasPostingPermission(post.author, post.namespace, 'posts', tid),
)
const repo = await util.loadRepo(res, post.author, ucanStore)
const postCid = await repo.put(post)
await repo.runOnProgram(post.program, async (store) => {
await repo.runOnNamespace(post.namespace, async (store) => {
return store.posts.editEntry(tid, postCid)
})
const db = util.getDB(res)
@ -133,7 +133,7 @@ router.put('/', async (req, res) => {
export const deletePostReq = z.object({
did: z.string(),
program: z.string(),
namespace: z.string(),
tid: z.string(),
})
export type DeletePostReq = z.infer<typeof deletePostReq>
@ -142,19 +142,19 @@ router.delete('/', async (req, res) => {
if (!check.is(req.body, deletePostReq)) {
throw new ServerError(400, 'Poorly formatted request')
}
const { did, program, tid } = req.body
const { did, namespace, tid } = req.body
const tidObj = TID.fromStr(tid)
const ucanStore = await auth.checkReq(
req,
ucanCheck.hasAudience(SERVER_DID),
ucanCheck.hasPostingPermission(did, program, 'posts', tidObj),
ucanCheck.hasPostingPermission(did, namespace, 'posts', tidObj),
)
const repo = await util.loadRepo(res, did, ucanStore)
await repo.runOnProgram(program, async (store) => {
await repo.runOnNamespace(namespace, async (store) => {
return store.posts.deleteEntry(tidObj)
})
const db = util.getDB(res)
await db.deletePost(tidObj.toString(), did, program)
await db.deletePost(tidObj.toString(), did, namespace)
await db.updateRepoRoot(did, repo.cid)
res.status(200).send()
})

@ -7,7 +7,7 @@ const router = express.Router()
export const likeCountReq = z.object({
author: z.string(),
program: z.string(),
namespace: z.string(),
tid: z.string(),
})
export type LikeCountReq = z.infer<typeof likeCountReq>
@ -16,9 +16,9 @@ router.get('/likes', async (req, res) => {
if (!check.is(req.query, likeCountReq)) {
return res.status(400).send('Poorly formatted request')
}
const { author, program, tid } = req.query
const { author, namespace, tid } = req.query
const db = util.getDB(res)
const count = await db.likeCount(author, program, tid)
const count = await db.likeCount(author, namespace, tid)
res.status(200).send({ count })
})