Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: provider + consumer + subscription #746

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions caps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Capabilities

```mermaid
erDiagram
consumer {
subscription TEXT "Subscription id"
consumer DID "Space DID"
}

subscription {
id TEXT "${order}@${provider}"
order TEXT "Unique identifier"
provider DID "Provider DID"
customer DID "Account DID"
}

subscription ||--|{ consumer : consumer
```

> Perhaps we do not need two tables here ? iIt is 1:n relationship but what is the benefit over just adding `consumer` field to the `subscription` ?
>

## `access/authorize`

When invoked we can check the `subscription` table and insert a record like

```js
{
provider: "did:web:web3.storage",
customer: "did:mailto:web.mail:alice",
order: CBOR.link({ customer })
}
```


## `consumer/add`

Delegated by the provider to the account

```json
{
"iss": "did:web:web3.storage",
"aud": "did:mailto:web.mail:alice",
"att": [{
"with": "did:web:web3.storage",
"can": "consumer/*",
"nb": {
"customer": "did:mailto:web.mail:alice",
"order": "bafy...hash"
}
}]
}
```

This capability set allows invoker:

1. Insert into `subscription` table record where
- `provider` is `with`
- `customer` is `nb.customer`
- `order` is `nb.order`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const { cid: order } = CBOR.write({ customer, consumer })
const key = `${order}@${providerDID}`

db.put(key, { provider, customer, order })


2. Insert into `consumer` table records where
- `subscription` is `${nb.order}@${with}`
- `consumer` is `*`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const { with: provider, nb: { consumer, order } } = invocation.capability

db.consumer.put(`${order}@${provider}`, { consumer, order, provider })


> Provider MAY want to enforce some constraints like limit the number of consumers but that is out of scope for now.

## `consumer/remove`

Delegated by the provider to the account

This capability allows invoker to delete records from the `consumer` table.


# Provider is in charge

Because provider is doing the delegation it can also invoke any of the capabilities and revoke capabilities it issued.


## `provider/get`

> Do we even need it ??

It is a way for to request a `consumer/*` capability delegation from the `provider`.

## `provider/add`

> Do we even need it ??

It is a way to request a `consumer/add` without having to do `provider/get` first. Because `provider` can invoke `consumer/add` this just short circuits.
8 changes: 0 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,10 @@
"docusaurus-plugin-typedoc": "^0.18.0",
"lint-staged": "^13.1.0",
"prettier": "2.8.3",
"simple-git-hooks": "^2.8.1",
"typedoc-plugin-markdown": "^3.14.0",
"typescript": "4.9.5",
"wrangler": "^2.8.0"
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
},
"lint-staged": {
"*.{js,ts,yml,json}": "prettier --write",
"*.js": "eslint --fix"
},
"prettier": {
"trailingComma": "es5",
"tabWidth": 2,
Expand Down
74 changes: 74 additions & 0 deletions packages/access-api/migrations/0007_add_provider_contracts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
-- Migration number: 0007 2023-03-10T14:14:00.000Z

-- goal: add tables to keep track of the subscriptions accounts have with
-- providers and a table to keep track of consumers of the subscriptions.

-- Table is used to keep track of the accounts subscribed to a provider(s).
-- Insertion here are caused by `customer/add` capability invocation.
-- Records here are idempotent meaning that invoking `customer/add` for the
-- same (order, provider, customer) triple will have no effect.
CREATE TABLE
IF NOT EXISTS subscriptions (
-- CID of the `customer/*` delegation from provider to the customer.
provision TEXT NOT NULL,
-- CID of the Task that created this subscription, usually this would be
-- `customer/add` invocation.
cause TEXT NOT NULL,
-- Unique identifier for this subscription
order TEXT NOT NULL,
-- DID of the provider e.g. a storage provider
provider TEXT NOT NULL,
-- DID of the customer
customer TEXT NOT NULL,
-- metadata
inserted_at TEXT NOT NULL DEFAULT (strftime ('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime ('%Y-%m-%dT%H:%M:%fZ', 'now')),

-- Operation is idempotent, so we'll have a CID of the task that created
-- this subscription. All subsequent invocations will be NOOPs.
CONSTRAINT task_cid UNIQUE (cause),
-- Subscription ID is derived from (order, provider) and is unique.
-- Note that `customer` is not part of the primary key because we want to
-- allow provider to choose how to enforce uniqueness constraint using
-- the `order` field.
PRIMARY KEY (order, provider)
)

-- Table is used to keep track of the consumers of a subscription. Insertion
-- is caused by `customer/add` capability invocation typically by the account
-- that has been delegated `customer/*` capability when subscription was
-- created.
-- Note that while this table has a superset of the columns of `subscription`
-- table wi still need both because consumers may be added and removed without
-- canceling the subscription.
CREATE TABLE
IF NOT EXISTS consumers (
-- CID of the invocation that created this subscription
cause TEXT NOT NULL,

-- Below fields are used only to derive subscription ID.
-- Unique identifier for this subscription
order TEXT NOT NULL,
-- DID of the provider e.g. a storage provider
provider TEXT NOT NULL,

-- subscription ID is derived from (order, provider). This is a virtual
-- column which is not stored in the database but could be used in queries.
subscription TEXT GENERATED ALWAYS AS (format("%s@%s", order, provider)) VIRTUAL,

-- consumer DID
consumer TEXT NOT NULL,
-- metadata
inserted_at TEXT NOT NULL DEFAULT (strftime ('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime ('%Y-%m-%dT%H:%M:%fZ', 'now')),

-- Operation that caused insertion of this record.
CONSTRAINT task_cid UNIQUE (cause),
-- We use (order, provider, consumer) as a composite primary key to enforce
-- uniqueness constraint from the provider side. This allows provider to
-- decide how to generate the order ID to enforce whatever constraint they
-- want. E.g. web3.storage will enforce one provider per space by generating
-- order by account DID, while nft.storage may choose to not have such
-- limitation and generate a unique order based on other factors.
PRIMARY KEY (order, provider, consumer)
);
2 changes: 2 additions & 0 deletions packages/access-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@
},
"rules": {
"unicorn/prefer-number-properties": "off",
"@typescript-eslint/ban-types": "off",
"unicorn/prefer-export-from": "off",
"jsdoc/no-undefined-types": [
"error",
{
Expand Down
32 changes: 21 additions & 11 deletions packages/access-api/src/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import type {
} from '@web3-storage/access/types'
import type { Handler as _Handler } from '@web3-storage/worker-utils/router'
import { Spaces } from './models/spaces.js'
import { Validations } from './models/validations.js'
import { loadConfig } from './config.js'
import type { DID } from '@ucanto/interface'
import { ConnectionView, Signer as EdSigner } from '@ucanto/principal/ed25519'
import { Accounts } from './models/accounts.js'
import { DelegationsStorage as Delegations } from './types/delegations.js'
import { ProvisionsStorage } from './types/provisions.js'
import {
DelegationStore,
ProvisionStore,
ConsumerStore,
ValidationStore,
AccountStore,
} from './types/index.js'

export {}

Expand All @@ -26,8 +30,12 @@ export interface AnalyticsEngineEvent {
}

export interface Email {
sendValidation: ({ to: string, url: string }) => Promise<void>
send: ({ to: string, textBody: string, subject: string }) => Promise<void>
sendValidation: (input: { to: string; url: string }) => Promise<void>
send: (input: {
to: string
textBody: string
subject: string
}) => Promise<void>
}

export interface Env {
Expand Down Expand Up @@ -59,16 +67,18 @@ export interface Env {

export interface RouteContext {
log: Logging
signer: EdSigner.Signer
signer: EdSigner.Signer<DID<'web'>>
config: ReturnType<typeof loadConfig>
url: URL
email: Email
models: {
accounts: Accounts
delegations: Delegations
accounts: AccountStore
delegations: DelegationStore
spaces: Spaces
provisions: ProvisionsStorage
validations: Validations
provisions: ProvisionStore
validations: ValidationStore
consumers: ConsumerStore
subscriptions: SubscriptionStore
}
uploadApi: ConnectionView
}
Expand Down
11 changes: 8 additions & 3 deletions packages/access-api/src/models/accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import * as Ucanto from '@ucanto/interface'
import { Kysely } from 'kysely'
import { D1Dialect } from 'kysely-d1'
import { GenericPlugin } from '../utils/d1.js'
import * as API from '../types/index.js'

/**
* @typedef {import('@web3-storage/access/src/types.js').DelegationRecord} DelegationRecord
*/

/**
* Accounts
* @implements {API.AccountStore}
*/
export class Accounts {
/**
Expand All @@ -33,7 +35,7 @@ export class Accounts {
}

/**
* @param {Ucanto.URI<"did:">} did
* @param {Ucanto.DID} did
*/
async create(did) {
const result = await this.d1
Expand All @@ -44,17 +46,20 @@ export class Accounts {
.onConflict((oc) => oc.column('did').doNothing())
.returning('accounts.did')
.execute()

return { data: result }
}

/**
* @param {Ucanto.URI<"did:">} did
* @param {Ucanto.DID} did
*/
async get(did) {
return await this.d1
const out = await this.d1
.selectFrom('accounts')
.selectAll()
.where('accounts.did', '=', did)
.executeTakeFirst()

return out
}
}
Loading