├── .github
├── actions
│ └── test
│ │ └── action.yml
└── workflows
│ └── test.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── package-lock.json
├── package.json
├── src
├── api.js
├── api.ts
├── capabilities.js
├── client
│ ├── api.js
│ ├── api.ts
│ └── index.js
├── index.js
├── server
│ └── index.js
└── worker
│ ├── block.js
│ ├── durable-clock.js
│ ├── index.js
│ ├── middleware.js
│ ├── service.js
│ └── types.d.ts
├── test
├── helpers
│ ├── durable-objects.js
│ └── ucanto.js
└── worker
│ ├── durable-clock.test.js
│ └── service.test.js
├── tsconfig.json
└── wrangler.toml
/.github/actions/test/action.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | description: 'Setup and test'
3 |
4 | runs:
5 | using: "composite"
6 | steps:
7 | - uses: actions/setup-node@v3
8 | with:
9 | registry-url: 'https://registry.npmjs.org'
10 | node-version: 18
11 | cache: 'npm'
12 | - run: npm ci
13 | shell: bash
14 | - run: npm run lint
15 | shell: bash
16 | - run: npm test
17 | shell: bash
18 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 |
10 | jobs:
11 | test:
12 | name: Test
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v3
16 | - uses: ./.github/actions/test
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | dist
4 | .env
5 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The contents of this repository are Copyright (c) corresponding authors and
2 | contributors, licensed under the `Permissive License Stack` meaning either of:
3 |
4 | - Apache-2.0 Software License: https://www.apache.org/licenses/LICENSE-2.0
5 | ([...4tr2kfsq](https://dweb.link/ipfs/bafkreiankqxazcae4onkp436wag2lj3ccso4nawxqkkfckd6cg4tr2kfsq))
6 |
7 | - MIT Software License: https://opensource.org/licenses/MIT
8 | ([...vljevcba](https://dweb.link/ipfs/bafkreiepofszg4gfe2gzuhojmksgemsub2h4uy2gewdnr35kswvljevcba))
9 |
10 | You may not use the contents of this repository except in compliance
11 | with one of the listed Licenses. For an extended clarification of the
12 | intent behind the choice of Licensing please refer to
13 | https://protocol.ai/blog/announcing-the-permissive-license-stack/
14 |
15 | Unless required by applicable law or agreed to in writing, software
16 | distributed under the terms listed in this notice is distributed on
17 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
18 | either express or implied. See each License for the specific language
19 | governing permissions and limitations under that License.
20 |
21 |
22 |
23 | `SPDX-License-Identifier: Apache-2.0 OR MIT`
24 |
25 | Verbatim copies of both licenses are included below:
26 |
27 | Apache-2.0 Software License
28 |
29 | ```
30 | Apache License
31 | Version 2.0, January 2004
32 | http://www.apache.org/licenses/
33 |
34 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
35 |
36 | 1. Definitions.
37 |
38 | "License" shall mean the terms and conditions for use, reproduction,
39 | and distribution as defined by Sections 1 through 9 of this document.
40 |
41 | "Licensor" shall mean the copyright owner or entity authorized by
42 | the copyright owner that is granting the License.
43 |
44 | "Legal Entity" shall mean the union of the acting entity and all
45 | other entities that control, are controlled by, or are under common
46 | control with that entity. For the purposes of this definition,
47 | "control" means (i) the power, direct or indirect, to cause the
48 | direction or management of such entity, whether by contract or
49 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
50 | outstanding shares, or (iii) beneficial ownership of such entity.
51 |
52 | "You" (or "Your") shall mean an individual or Legal Entity
53 | exercising permissions granted by this License.
54 |
55 | "Source" form shall mean the preferred form for making modifications,
56 | including but not limited to software source code, documentation
57 | source, and configuration files.
58 |
59 | "Object" form shall mean any form resulting from mechanical
60 | transformation or translation of a Source form, including but
61 | not limited to compiled object code, generated documentation,
62 | and conversions to other media types.
63 |
64 | "Work" shall mean the work of authorship, whether in Source or
65 | Object form, made available under the License, as indicated by a
66 | copyright notice that is included in or attached to the work
67 | (an example is provided in the Appendix below).
68 |
69 | "Derivative Works" shall mean any work, whether in Source or Object
70 | form, that is based on (or derived from) the Work and for which the
71 | editorial revisions, annotations, elaborations, or other modifications
72 | represent, as a whole, an original work of authorship. For the purposes
73 | of this License, Derivative Works shall not include works that remain
74 | separable from, or merely link (or bind by name) to the interfaces of,
75 | the Work and Derivative Works thereof.
76 |
77 | "Contribution" shall mean any work of authorship, including
78 | the original version of the Work and any modifications or additions
79 | to that Work or Derivative Works thereof, that is intentionally
80 | submitted to Licensor for inclusion in the Work by the copyright owner
81 | or by an individual or Legal Entity authorized to submit on behalf of
82 | the copyright owner. For the purposes of this definition, "submitted"
83 | means any form of electronic, verbal, or written communication sent
84 | to the Licensor or its representatives, including but not limited to
85 | communication on electronic mailing lists, source code control systems,
86 | and issue tracking systems that are managed by, or on behalf of, the
87 | Licensor for the purpose of discussing and improving the Work, but
88 | excluding communication that is conspicuously marked or otherwise
89 | designated in writing by the copyright owner as "Not a Contribution."
90 |
91 | "Contributor" shall mean Licensor and any individual or Legal Entity
92 | on behalf of whom a Contribution has been received by Licensor and
93 | subsequently incorporated within the Work.
94 |
95 | 2. Grant of Copyright License. Subject to the terms and conditions of
96 | this License, each Contributor hereby grants to You a perpetual,
97 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
98 | copyright license to reproduce, prepare Derivative Works of,
99 | publicly display, publicly perform, sublicense, and distribute the
100 | Work and such Derivative Works in Source or Object form.
101 |
102 | 3. Grant of Patent License. Subject to the terms and conditions of
103 | this License, each Contributor hereby grants to You a perpetual,
104 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
105 | (except as stated in this section) patent license to make, have made,
106 | use, offer to sell, sell, import, and otherwise transfer the Work,
107 | where such license applies only to those patent claims licensable
108 | by such Contributor that are necessarily infringed by their
109 | Contribution(s) alone or by combination of their Contribution(s)
110 | with the Work to which such Contribution(s) was submitted. If You
111 | institute patent litigation against any entity (including a
112 | cross-claim or counterclaim in a lawsuit) alleging that the Work
113 | or a Contribution incorporated within the Work constitutes direct
114 | or contributory patent infringement, then any patent licenses
115 | granted to You under this License for that Work shall terminate
116 | as of the date such litigation is filed.
117 |
118 | 4. Redistribution. You may reproduce and distribute copies of the
119 | Work or Derivative Works thereof in any medium, with or without
120 | modifications, and in Source or Object form, provided that You
121 | meet the following conditions:
122 |
123 | (a) You must give any other recipients of the Work or
124 | Derivative Works a copy of this License; and
125 |
126 | (b) You must cause any modified files to carry prominent notices
127 | stating that You changed the files; and
128 |
129 | (c) You must retain, in the Source form of any Derivative Works
130 | that You distribute, all copyright, patent, trademark, and
131 | attribution notices from the Source form of the Work,
132 | excluding those notices that do not pertain to any part of
133 | the Derivative Works; and
134 |
135 | (d) If the Work includes a "NOTICE" text file as part of its
136 | distribution, then any Derivative Works that You distribute must
137 | include a readable copy of the attribution notices contained
138 | within such NOTICE file, excluding those notices that do not
139 | pertain to any part of the Derivative Works, in at least one
140 | of the following places: within a NOTICE text file distributed
141 | as part of the Derivative Works; within the Source form or
142 | documentation, if provided along with the Derivative Works; or,
143 | within a display generated by the Derivative Works, if and
144 | wherever such third-party notices normally appear. The contents
145 | of the NOTICE file are for informational purposes only and
146 | do not modify the License. You may add Your own attribution
147 | notices within Derivative Works that You distribute, alongside
148 | or as an addendum to the NOTICE text from the Work, provided
149 | that such additional attribution notices cannot be construed
150 | as modifying the License.
151 |
152 | You may add Your own copyright statement to Your modifications and
153 | may provide additional or different license terms and conditions
154 | for use, reproduction, or distribution of Your modifications, or
155 | for any such Derivative Works as a whole, provided Your use,
156 | reproduction, and distribution of the Work otherwise complies with
157 | the conditions stated in this License.
158 |
159 | 5. Submission of Contributions. Unless You explicitly state otherwise,
160 | any Contribution intentionally submitted for inclusion in the Work
161 | by You to the Licensor shall be under the terms and conditions of
162 | this License, without any additional terms or conditions.
163 | Notwithstanding the above, nothing herein shall supersede or modify
164 | the terms of any separate license agreement you may have executed
165 | with Licensor regarding such Contributions.
166 |
167 | 6. Trademarks. This License does not grant permission to use the trade
168 | names, trademarks, service marks, or product names of the Licensor,
169 | except as required for reasonable and customary use in describing the
170 | origin of the Work and reproducing the content of the NOTICE file.
171 |
172 | 7. Disclaimer of Warranty. Unless required by applicable law or
173 | agreed to in writing, Licensor provides the Work (and each
174 | Contributor provides its Contributions) on an "AS IS" BASIS,
175 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
176 | implied, including, without limitation, any warranties or conditions
177 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
178 | PARTICULAR PURPOSE. You are solely responsible for determining the
179 | appropriateness of using or redistributing the Work and assume any
180 | risks associated with Your exercise of permissions under this License.
181 |
182 | 8. Limitation of Liability. In no event and under no legal theory,
183 | whether in tort (including negligence), contract, or otherwise,
184 | unless required by applicable law (such as deliberate and grossly
185 | negligent acts) or agreed to in writing, shall any Contributor be
186 | liable to You for damages, including any direct, indirect, special,
187 | incidental, or consequential damages of any character arising as a
188 | result of this License or out of the use or inability to use the
189 | Work (including but not limited to damages for loss of goodwill,
190 | work stoppage, computer failure or malfunction, or any and all
191 | other commercial damages or losses), even if such Contributor
192 | has been advised of the possibility of such damages.
193 |
194 | 9. Accepting Warranty or Additional Liability. While redistributing
195 | the Work or Derivative Works thereof, You may choose to offer,
196 | and charge a fee for, acceptance of support, warranty, indemnity,
197 | or other liability obligations and/or rights consistent with this
198 | License. However, in accepting such obligations, You may act only
199 | on Your own behalf and on Your sole responsibility, not on behalf
200 | of any other Contributor, and only if You agree to indemnify,
201 | defend, and hold each Contributor harmless for any liability
202 | incurred by, or claims asserted against, such Contributor by reason
203 | of your accepting any such warranty or additional liability.
204 |
205 | END OF TERMS AND CONDITIONS
206 | ```
207 |
208 |
209 |
210 | MIT Software License
211 |
212 | ```
213 | Permission is hereby granted, free of charge, to any person obtaining a copy
214 | of this software and associated documentation files (the "Software"), to deal
215 | in the Software without restriction, including without limitation the rights
216 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
217 | copies of the Software, and to permit persons to whom the Software is
218 | furnished to do so, subject to the following conditions:
219 |
220 | The above copyright notice and this permission notice shall be included in
221 | all copies or substantial portions of the Software.
222 |
223 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
224 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
225 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
226 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
227 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
228 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
229 | THE SOFTWARE.
230 | ```
231 |
232 |
233 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
4 | w3clock
5 |
6 |
UCAN based merkle clock implementation.
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ## Background
16 |
17 | Merkle clocks are a method of recording events with partial order. This repo implements a method of managing merkle clocks with UCANs.
18 |
19 | Read the [SPEC](https://github.com/web3-storage/specs/blob/main/w3-clock.md).
20 |
21 | ### Capabilities
22 |
23 | #### `clock/advance`
24 |
25 | Advance the clock by adding a new event.
26 |
27 | #### `clock/head`
28 |
29 | Get the current clock head.
30 |
31 |
42 |
43 | ## License
44 |
45 | Dual-licensed under [MIT + Apache 2.0](LICENSE.md)
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web3-storage/clock",
3 | "version": "0.4.1",
4 | "description": "UCAN based Merkle Clock as a service.",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "npm run build:worker:debug && miniflare dist/worker.mjs --watch --debug -m",
8 | "build": "npm run build:worker && npm run build:module",
9 | "build:worker": "esbuild --bundle src/worker/index.js --format=esm --sourcemap --minify --outfile=dist/worker.mjs",
10 | "build:worker:debug": "esbuild --bundle src/worker/index.js --format=esm --sourcemap --outfile=dist/worker.mjs",
11 | "build:module": "tsc --build",
12 | "test": "mocha --experimental-vm-modules 'test/**/*.test.js'",
13 | "coverage": "c8 --reporter=text --reporter=html --exclude=node_modules --exclude=test --exclude-after-remap npm run test",
14 | "lint": "standard"
15 | },
16 | "keywords": [
17 | "merkle",
18 | "clock",
19 | "CRDT",
20 | "IPFS",
21 | "UCAN"
22 | ],
23 | "author": "Alan Shaw",
24 | "license": "Apache-2.0 OR MIT",
25 | "exports": {
26 | ".": {
27 | "types": "./dist/src/index.d.ts",
28 | "import": "./src/index.js"
29 | },
30 | "./api": {
31 | "types": "./dist/src/api.d.ts",
32 | "import": "./src/api.js"
33 | },
34 | "./client": {
35 | "types": "./dist/src/client/index.d.ts",
36 | "import": "./src/client/index.js"
37 | },
38 | "./client/api": {
39 | "types": "./dist/src/client/api.d.ts",
40 | "import": "./src/client/api.js"
41 | },
42 | "./capabilities": {
43 | "types": "./dist/src/capabilities.d.ts",
44 | "import": "./src/capabilities.js"
45 | },
46 | "./server": {
47 | "types": "./dist/src/server/index.d.ts",
48 | "import": "./src/server/index.js"
49 | }
50 | },
51 | "typesVersions": {
52 | "*": {
53 | "*": [
54 | "dist/*"
55 | ],
56 | "api": [
57 | "dist/src/api.d.ts"
58 | ],
59 | "client": [
60 | "dist/src/client/index.d.ts"
61 | ],
62 | "client/api": [
63 | "dist/src/client/api.d.ts"
64 | ],
65 | "capabilities": [
66 | "dist/src/capabilities.d.ts"
67 | ],
68 | "server": [
69 | "dist/src/server/index.d.ts"
70 | ]
71 | }
72 | },
73 | "files": [
74 | "src/*.js",
75 | "src/client",
76 | "src/server",
77 | "dist/src/*.d.ts",
78 | "dist/src/*.d.ts.map",
79 | "dist/src/client",
80 | "dist/src/server"
81 | ],
82 | "dependencies": {
83 | "@ipld/dag-cbor": "^9.0.0",
84 | "@ipld/dag-ucan": "^3.3.2",
85 | "@ucanto/client": "^9.0.1",
86 | "@ucanto/interface": "^10.0.1",
87 | "@ucanto/principal": "^9.0.1",
88 | "@ucanto/server": "^10.0.0",
89 | "@ucanto/transport": "^9.1.1",
90 | "@ucanto/validator": "^9.0.2",
91 | "@web3-storage/pail": "^0.5.0",
92 | "hashlru": "^2.3.0",
93 | "multiformats": "^13.1.0",
94 | "p-retry": "^6.2.0"
95 | },
96 | "devDependencies": {
97 | "@cloudflare/workers-types": "^4.20230115.0",
98 | "c8": "^7.13.0",
99 | "esbuild": "^0.17.10",
100 | "miniflare": "^2.12.1",
101 | "mocha": "^10.2.0",
102 | "standard": "^17.0.0",
103 | "typescript": "^5.0.2",
104 | "wrangler": "^2.12.0"
105 | },
106 | "standard": {
107 | "ignore": [
108 | "*.ts"
109 | ]
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/api.js:
--------------------------------------------------------------------------------
1 | export {} // eslint-disable-line
2 |
--------------------------------------------------------------------------------
/src/api.ts:
--------------------------------------------------------------------------------
1 | import { Failure, ServiceMethod, DID } from '@ucanto/interface'
2 | import { ClockAdvance, ClockHead } from './capabilities.js'
3 | // import { ClockFollow, ClockUnfollow, ClockFollowing } from './capabilities.js'
4 | import { EventLink } from '@web3-storage/pail/clock/api'
5 |
6 | /** DID of a merkle clock. */
7 | export type ClockDID = DID
8 | /** DID of an clock event emitter (usually an agent). */
9 | export type EmitterDID = DID
10 |
11 | export interface Service {
12 | clock: ClockService
13 | }
14 |
15 | export interface ClockService {
16 | advance: ServiceMethod[] }, Failure>
17 | head: ServiceMethod[] }, Failure>
18 | // follow: ServiceMethod
19 | // unfollow: ServiceMethod
20 | // following: ServiceMethod, Failure>
21 | }
22 |
--------------------------------------------------------------------------------
/src/capabilities.js:
--------------------------------------------------------------------------------
1 | import { capability, URI, Link, Schema } from '@ucanto/validator'
2 |
3 | /**
4 | * @typedef {import('@ucanto/interface').InferInvokedCapability} Clock
5 | * @typedef {import('@ucanto/interface').InferInvokedCapability} ClockFollow
6 | * @typedef {import('@ucanto/interface').InferInvokedCapability} ClockUnfollow
7 | * @typedef {import('@ucanto/interface').InferInvokedCapability} ClockFollowing
8 | * @typedef {import('@ucanto/interface').InferInvokedCapability} ClockAdvance
9 | * @typedef {import('@ucanto/interface').InferInvokedCapability} ClockHead
10 | */
11 |
12 | export const clock = capability({
13 | can: 'clock/*',
14 | with: URI.match({ protocol: 'did:' })
15 | })
16 |
17 | /**
18 | * Follow advances made by an agent to a clock.
19 | */
20 | export const follow = capability({
21 | can: 'clock/follow',
22 | with: URI.match({ protocol: 'did:' }),
23 | nb: Schema.struct({
24 | iss: URI.match({ protocol: 'did:' }).optional(),
25 | with: URI.match({ protocol: 'did:' }).optional()
26 | })
27 | })
28 |
29 | /**
30 | * Stop following advances made by an agent to a clock.
31 | */
32 | export const unfollow = capability({
33 | can: 'clock/unfollow',
34 | with: URI.match({ protocol: 'did:' }),
35 | nb: Schema.struct({
36 | iss: URI.match({ protocol: 'did:' }).optional(),
37 | with: URI.match({ protocol: 'did:' }).optional()
38 | })
39 | })
40 |
41 | /**
42 | * List the agents this clock is following advances from.
43 | */
44 | export const following = capability({
45 | can: 'clock/following',
46 | with: URI.match({ protocol: 'did:' })
47 | })
48 |
49 | /**
50 | * List the CIDs of the events at the head of this clock.
51 | */
52 | export const head = capability({
53 | can: 'clock/head',
54 | with: URI.match({ protocol: 'did:' })
55 | })
56 |
57 | /**
58 | * Advance the clock by adding an event.
59 | */
60 | export const advance = capability({
61 | can: 'clock/advance',
62 | with: URI.match({ protocol: 'did:' }),
63 | nb: Schema.struct({
64 | event: Link.match({ version: 1 })
65 | })
66 | })
67 |
--------------------------------------------------------------------------------
/src/client/api.js:
--------------------------------------------------------------------------------
1 | export {} // eslint-disable-line
2 |
--------------------------------------------------------------------------------
/src/client/api.ts:
--------------------------------------------------------------------------------
1 | import { Signer, Proof, DID, Principal, ConnectionView, Block } from '@ucanto/interface'
2 | import { EventView } from '@web3-storage/pail/clock/api'
3 | import { Service } from '../api'
4 |
5 | export interface InvocationConfig {
6 | /**
7 | * Signing authority that is issuing the UCAN invocation(s).
8 | */
9 | issuer: Signer
10 | /**
11 | * The principal delegated to in the current UCAN.
12 | */
13 | audience?: Principal
14 | /**
15 | * The resource the invocation applies to.
16 | */
17 | with: DID
18 | /**
19 | * Proof(s) the issuer has the capability to perform the action.
20 | */
21 | proofs: Proof[]
22 | }
23 |
24 | export interface Connectable {
25 | connection?: ConnectionView>
26 | }
27 |
28 | export interface RequestOptions extends Connectable {}
29 |
30 | export interface FollowOptions extends RequestOptions {
31 | /**
32 | * Clock event issuer.
33 | */
34 | issuer?: DID
35 | /**
36 | * Target clock.
37 | */
38 | with?: DID
39 | }
40 |
41 | export interface AdvanceOptions extends RequestOptions {
42 | /**
43 | * Event blocks that may help the service to advance the clock. This are
44 | * optional because event blocks _should_ be made available to fetch directly
45 | * from the IPFS network.
46 | */
47 | blocks?: Block>[]
48 | }
49 |
--------------------------------------------------------------------------------
/src/client/index.js:
--------------------------------------------------------------------------------
1 | import { connect as clientConnect } from '@ucanto/client'
2 | import { CAR, HTTP } from '@ucanto/transport'
3 | import * as DID from '@ipld/dag-ucan/did'
4 | import * as ClockCaps from '../capabilities.js'
5 |
6 | export const SERVICE_URL = 'https://clock.web3.storage'
7 | export const SERVICE_PRINCIPAL = 'did:web:clock.web3.storage'
8 |
9 | /**
10 | * Advance the clock by adding an event.
11 | *
12 | * @template T
13 | * @param {import('./api').InvocationConfig} conf
14 | * @param {import('@web3-storage/pail/clock/api').EventLink} event
15 | * @param {import('./api').AdvanceOptions} [options]
16 | */
17 | export async function advance ({ issuer, with: resource, proofs, audience }, event, options) {
18 | const conn = options?.connection ?? connect()
19 | const facts = options?.blocks ? [Object.fromEntries(options.blocks.map(b => [b.cid.toString(), b.cid]))] : []
20 | const invocation = ClockCaps.advance
21 | .invoke({
22 | issuer,
23 | audience: audience ?? conn.id,
24 | with: resource,
25 | nb: { event },
26 | proofs,
27 | facts
28 | })
29 |
30 | for (const block of options?.blocks ?? []) {
31 | invocation.attach(block)
32 | }
33 |
34 | return invocation.execute(conn)
35 | }
36 |
37 | /**
38 | * Retrieve the clock head.
39 | *
40 | * @template T
41 | * @param {import('./api').InvocationConfig} conf
42 | * @param {import('./api').RequestOptions} [options]
43 | */
44 | export async function head ({ issuer, with: resource, proofs, audience }, options) {
45 | const conn = options?.connection ?? connect()
46 | return await ClockCaps.head
47 | .invoke({
48 | issuer,
49 | audience: audience ?? conn.id,
50 | with: resource,
51 | proofs
52 | })
53 | .execute(conn)
54 | }
55 |
56 | // /**
57 | // * Instruct a clock to follow the issuer, or optionally a different issuer,
58 | // * contributing to a different clock.
59 | // *
60 | // * @template T
61 | // * @param {import('./api').InvocationConfig} conf
62 | // * @param {import('./api').FollowOptions} [options]
63 | // */
64 | // export async function follow ({ issuer, with: resource, proofs, audience }, options) {
65 | // const conn = options?.connection ?? connect()
66 | // const result = await ClockCaps.follow
67 | // .invoke({
68 | // issuer,
69 | // audience: audience ?? conn.id,
70 | // with: resource,
71 | // nb: {
72 | // ...(options?.issuer ? { iss: options.issuer } : {}),
73 | // ...(options?.with ? { with: options.with } : {})
74 | // },
75 | // proofs
76 | // })
77 | // .execute(conn)
78 |
79 | // if (result.error) {
80 | // throw new Error(`failed ${ClockCaps.follow.can} invocation`, { cause: result })
81 | // }
82 |
83 | // return result
84 | // }
85 |
86 | /**
87 | * @template T
88 | * @param {object} [options]
89 | * @param {import('@ucanto/interface').Principal} [options.servicePrincipal]
90 | * @param {URL} [options.serviceURL]
91 | * @returns {import('@ucanto/interface').ConnectionView>}
92 | */
93 | export function connect (options) {
94 | const url = options?.serviceURL ?? new URL(SERVICE_URL)
95 | return clientConnect({
96 | id: options?.servicePrincipal ?? DID.parse(SERVICE_PRINCIPAL),
97 | codec: CAR.outbound,
98 | channel: HTTP.open({ url, method: 'POST' })
99 | })
100 | }
101 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export * as Server from './server/index.js'
2 | export * as Client from './client/index.js'
3 | export * as Capabilities from './capabilities.js'
4 |
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | import { Verifier } from '@ucanto/principal'
2 | import * as Server from '@ucanto/server'
3 | import * as CAR from '@ucanto/transport/car'
4 | import { access, Schema, Failure } from '@ucanto/validator'
5 |
6 | /**
7 | * @template T
8 | * @param {import('@ucanto/interface').Signer} signer
9 | * @param {import('../api').Service} service
10 | */
11 | export function createServer (signer, service) {
12 | return Server.create({
13 | id: signer,
14 | codec: CAR.inbound,
15 | service,
16 | catch: err => console.error(err),
17 | // TODO: wire into revocations
18 | validateAuthorization: () => ({ ok: {} }),
19 | // @ts-expect-error
20 | authorities: [Verifier.parse('did:key:z6MkqdncRZ1wj8zxCTDUQ8CRT8NQWd63T7mZRvZUX8B7XDFi').withDID('did:web:web3.storage')]
21 | })
22 | }
23 |
24 | /**
25 | * Function that can be used to define given capability provider. It decorates
26 | * passed handler and takes care of UCAN validation and only calls the handler
27 | * when validation succeeds.
28 | *
29 | *
30 | * @template {import('@ucanto/interface').Ability} A
31 | * @template {import('@ucanto/interface').URI} R
32 | * @template {import('@ucanto/interface').Caveats} C
33 | * @template {{}} O
34 | * @template {import('@ucanto/interface').Failure} X
35 | * @template {import('@ucanto/interface').Result} Result
36 | * @param {import('@ucanto/interface').CapabilityParser>>} capability
37 | * @param {(input:import('@ucanto/server').ProviderInput>) => import('@ucanto/interface').Await} handler
38 | * @returns {import('@ucanto/interface').ServiceMethod, O & Result['ok'], X & Result['error']>}
39 | */
40 | export const provide = (capability, handler) =>
41 | /**
42 | * @param {import('@ucanto/interface').Invocation>} invocation
43 | * @param {import('@ucanto/interface').InvocationContext & { authorities?: import('@ucanto/interface').Verifier[] }} options
44 | */
45 | async (invocation, options) => {
46 | // If audience schema is not provided we expect the audience to match
47 | // the server id. Users could pass `schema.string()` if they want to accept
48 | // any audience.
49 | const audienceSchema = Schema.literal(options.id.did())
50 | const result = audienceSchema.read(invocation.audience.did())
51 | if (result.error) {
52 | return { error: new InvalidAudience({ cause: result.error }) }
53 | }
54 |
55 | for (const authority of options.authorities ?? []) {
56 | const authorization = await access(invocation, { ...options, authority, capability })
57 | if (authorization.error) continue
58 | return handler({
59 | capability: authorization.ok.capability,
60 | invocation,
61 | context: options
62 | })
63 | }
64 |
65 | const authorization = await access(invocation, {
66 | ...options,
67 | authority: options.id,
68 | capability
69 | })
70 | if (authorization.error) {
71 | return authorization
72 | }
73 | return handler({
74 | capability: authorization.ok.capability,
75 | invocation,
76 | context: options
77 | })
78 | }
79 |
80 | class InvalidAudience extends Failure {
81 | /**
82 | * @param {object} source
83 | * @param {import('@ucanto/interface').Failure} source.cause
84 | */
85 | constructor ({ cause }) {
86 | super()
87 | /** @type {'InvalidAudience'} */
88 | this.name = 'InvalidAudience'
89 | this.cause = cause
90 | }
91 |
92 | describe () {
93 | return this.cause.message
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/worker/block.js:
--------------------------------------------------------------------------------
1 | import retry from 'p-retry'
2 | import LRU from 'hashlru'
3 | import { sha256 } from 'multiformats/hashes/sha2'
4 | import { equals } from 'multiformats/bytes'
5 |
6 | export { MultiBlockFetcher } from '@web3-storage/pail/block'
7 |
8 | /**
9 | * @typedef {{ put: (block: import('multiformats').Block) => Promise }} BlockPutter
10 | */
11 |
12 | export class MemoryBlockstore {
13 | /** @param {Array} [blocks] */
14 | constructor (blocks = []) {
15 | /** @type {{ get: (k: string) => Uint8Array | undefined, set: (k: string, v: Uint8Array) => void }} */
16 | this._data = new Map(blocks.map(b => [b.cid.toString(), b.bytes]))
17 | }
18 |
19 | /** @type { import('@web3-storage/pail/api').BlockFetcher['get']} */
20 | async get (cid) {
21 | const bytes = this._data.get(cid.toString())
22 | if (!bytes) return
23 | return { cid, bytes }
24 | }
25 |
26 | /** @param {import('multiformats').Block} block */
27 | async put (block) {
28 | this._data.set(block.cid.toString(), block.bytes)
29 | }
30 | }
31 |
32 | export class LRUBlockstore extends MemoryBlockstore {
33 | /** @param {number} [max] */
34 | constructor (max = 50) {
35 | super()
36 | this._data = LRU(max)
37 | }
38 | }
39 |
40 | /**
41 | * @param {import('@web3-storage/pail/api').BlockFetcher} fetcher
42 | * @param {import('@web3-storage/pail/api').BlockFetcher & BlockPutter} cache
43 | */
44 | export function withCache (fetcher, cache) {
45 | return {
46 | /** @type { import('@web3-storage/pail/api').BlockFetcher['get']} */
47 | async get (cid) {
48 | try {
49 | const block = await cache.get(cid)
50 | if (block) return block
51 | } catch {}
52 | const block = await fetcher.get(cid)
53 | if (block) {
54 | // @ts-expect-error
55 | await cache.put(block)
56 | }
57 | return block
58 | }
59 | }
60 | }
61 |
62 | export class GatewayBlockFetcher {
63 | #url
64 |
65 | /** @param {string|URL} [url] */
66 | constructor (url) {
67 | this.#url = new URL(url ?? 'https://ipfs.io')
68 | }
69 |
70 | /** @type { import('@web3-storage/pail/api').BlockFetcher['get']} */
71 | async get (cid) {
72 | return await retry(async () => {
73 | const controller = new AbortController()
74 | const timeoutID = setTimeout(() => controller.abort(), 10000)
75 | try {
76 | const res = await fetch(new URL(`/ipfs/${cid}?format=raw`, this.#url), { signal: controller.signal })
77 | if (!res.ok) return
78 | const bytes = new Uint8Array(await res.arrayBuffer())
79 | const digest = await sha256.digest(bytes)
80 | if (!equals(digest.digest, cid.multihash.digest)) {
81 | throw new Error(`failed sha2-256 content integrity check: ${cid}`)
82 | }
83 | return { cid, bytes }
84 | } catch (err) {
85 | throw new Error(`failed to fetch block: ${cid}`, { cause: err })
86 | } finally {
87 | clearTimeout(timeoutID)
88 | }
89 | })
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/worker/durable-clock.js:
--------------------------------------------------------------------------------
1 | import * as Clock from '@web3-storage/pail/clock'
2 | import { parse } from 'multiformats/link'
3 | import * as cbor from '@ipld/dag-cbor'
4 | import { GatewayBlockFetcher, LRUBlockstore, MemoryBlockstore, MultiBlockFetcher, withCache } from './block.js'
5 |
6 | /**
7 | * @typedef {{ method: string, args: any[] }} MethodCall
8 | * @typedef {import('../api.js').EmitterDID} EmitterDID DID of an clock event emitter (usually an agent).
9 | * @typedef {import('../api.js').ClockDID} ClockDID DID of a merkle clock.
10 | * @typedef {Map>} Followings Event emitters that this clock is following (and the associated clock DID they are contributing to).
11 | * @typedef {Map>} Subscribers Clocks that want to receive advances made to this clock.
12 | * @typedef {'follow'|'unfollow'|'following'|'subscribe'|'unsubscribe'|'subscribers'|'advance'|'head'} DurableClockAPIMethod
13 | */
14 |
15 | const KEY_FOLLOWING = 'following'
16 | const KEY_SUBSCRIBERS = 'subscribers'
17 | const KEY_HEAD = 'head'
18 | /** @type {DurableClockAPIMethod[]} */
19 | const API_METHODS = ['follow', 'unfollow', 'following', 'subscribe', 'unsubscribe', 'subscribers', 'advance', 'head']
20 |
21 | /** @type {import('@cloudflare/workers-types').DurableObject} */
22 | export class DurableClock {
23 | #state
24 | #fetcher
25 | #cache
26 |
27 | /**
28 | * @param {import('@cloudflare/workers-types').DurableObjectState} state
29 | * @param {import('./types').Environment} env
30 | */
31 | constructor (state, env) {
32 | this.#state = state
33 | this.#cache = new LRUBlockstore(env.BLOCK_CACHE_SIZE ? parseInt(env.BLOCK_CACHE_SIZE) : undefined)
34 | this.#fetcher = new GatewayBlockFetcher(env.GATEWAY_URL)
35 | }
36 |
37 | /**
38 | * @param {import('@cloudflare/workers-types').Request} request
39 | * @returns {Promise}
40 | */
41 | async fetch (request) {
42 | const body = /** @type {MethodCall} */ (cbor.decode(new Uint8Array(await request.arrayBuffer())))
43 | const method = API_METHODS.find(m => m === body.method)
44 | if (!method) throw new Error(`invalid method: ${body.method}`)
45 | if (typeof this[method] !== 'function') throw new Error(`not implemented: ${method}`)
46 | const res = await this[method](...body.args)
47 | // @ts-expect-error
48 | return new Response(res && cbor.encode(res))
49 | }
50 |
51 | /**
52 | * @param {ClockDID} target Clock to follow.
53 | * @param {EmitterDID} emitter Event emitter (usually an agent) who emits the events.
54 | */
55 | async follow (target, emitter) {
56 | // TODO: maybe we could preload this into memory and update both?
57 | return await this.#state.blockConcurrencyWhile(async () => {
58 | /** @type {Followings} */
59 | const followings = (await this.#state.storage.get(KEY_FOLLOWING)) ?? new Map()
60 | const emitters = followings.get(target) ?? new Set()
61 | if (!emitters.has(emitter)) {
62 | emitters.add(emitter)
63 | followings.set(target, emitters)
64 | await this.#state.storage.put(KEY_FOLLOWING, followings)
65 | }
66 | })
67 | }
68 |
69 | /**
70 | * @param {ClockDID} target
71 | * @param {EmitterDID} emitter
72 | */
73 | async unfollow (target, emitter) {
74 | return await this.#state.blockConcurrencyWhile(async () => {
75 | /** @type {Followings} */
76 | const followings = (await this.#state.storage.get(KEY_FOLLOWING)) ?? new Map()
77 | const emitters = followings.get(target) ?? new Set()
78 | if (emitters.has(emitter)) {
79 | emitters.delete(emitter)
80 | if (emitters.size) {
81 | followings.set(target, emitters)
82 | } else {
83 | followings.delete(target)
84 | }
85 | await this.#state.storage.put(KEY_FOLLOWING, followings)
86 | }
87 | })
88 | }
89 |
90 | /** @returns {Promise>} */
91 | async following () {
92 | /** @type {Followings} */
93 | const follows = (await this.#state.storage.get(KEY_FOLLOWING)) ?? new Map()
94 | return [...follows.entries()].map(([k, v]) => [k, [...v.values()]])
95 | }
96 |
97 | /**
98 | * Subscribe to recieve advances made to this clock by the passed emitter.
99 | * @param {ClockDID} subscriber Subscriber clock.
100 | * @param {EmitterDID} emitter Event emitter.
101 | */
102 | async subscribe (subscriber, emitter) {
103 | return await this.#state.blockConcurrencyWhile(async () => {
104 | // TODO: fail if this clock is not following emitter?
105 | /** @type {Subscribers} */
106 | const subscribers = (await this.#state.storage.get(KEY_SUBSCRIBERS)) ?? new Map()
107 | const emitters = subscribers.get(subscriber) ?? new Set()
108 | if (!emitters.has(emitter)) {
109 | emitters.add(emitter)
110 | subscribers.set(subscriber, emitters)
111 | await this.#state.storage.put(KEY_SUBSCRIBERS, subscribers)
112 | }
113 | })
114 | }
115 |
116 | /**
117 | * @param {ClockDID} subscriber
118 | * @param {EmitterDID} emitter
119 | */
120 | async unsubscribe (subscriber, emitter) {
121 | return await this.#state.blockConcurrencyWhile(async () => {
122 | /** @type {Subscribers} */
123 | const subscribers = (await this.#state.storage.get(KEY_SUBSCRIBERS)) ?? new Map()
124 | const emitters = subscribers.get(subscriber) ?? new Set()
125 | if (emitters.has(emitter)) {
126 | emitters.delete(emitter)
127 | subscribers.set(subscriber, emitters)
128 | await this.#state.storage.put(KEY_SUBSCRIBERS, subscribers)
129 | }
130 | })
131 | }
132 |
133 | /** @returns {Promise>} */
134 | async subscribers () {
135 | /** @type {Subscribers} */
136 | const subscribers = (await this.#state.storage.get(KEY_SUBSCRIBERS)) ?? new Map()
137 | return [...subscribers.entries()].map(([k, v]) => [k, [...v.values()]])
138 | }
139 |
140 | /** @param {import('@web3-storage/pail/clock/api').EventLink[]} head */
141 | async #setHead (head) {
142 | await this.#state.storage.put(KEY_HEAD, head.map(h => String(h)))
143 | }
144 |
145 | /** @returns {Promise[]>} */
146 | async head () {
147 | // TODO: keep sync'd copy in memory
148 | /** @type {string[]} */
149 | const head = (await this.#state.storage.get(KEY_HEAD)) ?? []
150 | return head.map(h => parse(h))
151 | }
152 |
153 | /**
154 | * @param {import('@web3-storage/pail/clock/api').EventLink} event
155 | * @param {import('@web3-storage/pail/clock/api').EventBlockView>[]} [blocks]
156 | */
157 | async advance (event, blocks) {
158 | return await this.#state.blockConcurrencyWhile(async () => {
159 | const fetcher = withCache(
160 | blocks?.length
161 | ? new MultiBlockFetcher(new MemoryBlockstore(blocks), this.#fetcher)
162 | : this.#fetcher,
163 | this.#cache
164 | )
165 | const head = (await Clock.advance(fetcher, await this.head(), event))
166 | await this.#setHead(head)
167 | return head
168 | })
169 | }
170 | }
171 |
172 | /**
173 | * @param {import('@cloudflare/workers-types').DurableObjectNamespace} clockNamespace Durable Object API
174 | * @param {ClockDID} clock Clock that should follow events by emitter.
175 | * @param {ClockDID} target Clock targetted by events emitted by emitter.
176 | * @param {EmitterDID} emitter Agent that is emitting events that contribute to the target.
177 | */
178 | export async function follow (clockNamespace, clock, target, emitter) {
179 | const stub = clockNamespace.get(clockNamespace.idFromName(clock))
180 | const body = cbor.encode({ method: 'follow', args: [target, emitter] })
181 | await stub.fetch('http://localhost', { method: 'POST', body })
182 | if (clock !== target) {
183 | await subscribe(clockNamespace, target, clock, emitter)
184 | }
185 | }
186 |
187 | /**
188 | * @param {import('@cloudflare/workers-types').DurableObjectNamespace} clockNamespace Durable Object API
189 | * @param {ClockDID} clock Clock that should unfollow events by emitter.
190 | * @param {ClockDID} target Clock targetted by events emitted by emitter.
191 | * @param {EmitterDID} emitter Agent that is emitting events that contribute to the target.
192 | */
193 | export async function unfollow (clockNamespace, clock, target, emitter) {
194 | const stub = clockNamespace.get(clockNamespace.idFromName(clock))
195 | const body = cbor.encode({ method: 'unfollow', args: [target, emitter] })
196 | await stub.fetch('http://localhost', { method: 'POST', body })
197 | if (clock !== target) {
198 | await unsubscribe(clockNamespace, target, clock, emitter)
199 | }
200 | }
201 |
202 | /**
203 | * @param {import('@cloudflare/workers-types').DurableObjectNamespace} clockNamespace Durable Object API
204 | * @param {import('@ucanto/interface').DID} clock Clock to get following emitters for.
205 | */
206 | export async function following (clockNamespace, clock) {
207 | const stub = clockNamespace.get(clockNamespace.idFromName(clock))
208 | const body = cbor.encode({ method: 'following', args: [] })
209 | const res = await stub.fetch('http://localhost', { method: 'POST', body })
210 | const data = /** @type {Array<[ClockDID, EmitterDID[]]>} */ (cbor.decode(new Uint8Array(await res.arrayBuffer())))
211 | return new Map(data.map(([k, v]) => [k, new Set(v)]))
212 | }
213 |
214 | /**
215 | * @param {import('@cloudflare/workers-types').DurableObjectNamespace} clockNamespace Durable Object API
216 | * @param {ClockDID} clock Clock to add subscription to.
217 | * @param {ClockDID} subscriber Clock that should recieve events emitted by emitter.
218 | * @param {EmitterDID} emitter Agent that is emitting events that contribute to clock.
219 | */
220 | async function subscribe (clockNamespace, clock, subscriber, emitter) {
221 | const stub = clockNamespace.get(clockNamespace.idFromName(clock))
222 | const body = cbor.encode({ method: 'subscribe', args: [subscriber, emitter] })
223 | await stub.fetch('http://localhost', { method: 'POST', body })
224 | }
225 |
226 | /**
227 | * @param {import('@cloudflare/workers-types').DurableObjectNamespace} clockNamespace Durable Object API
228 | * @param {ClockDID} clock Clock to remove subscription from.
229 | * @param {ClockDID} subscriber Current subscription clock.
230 | * @param {EmitterDID} emitter Agent that is emitting events that contribute to clock.
231 | */
232 | async function unsubscribe (clockNamespace, clock, subscriber, emitter) {
233 | const stub = clockNamespace.get(clockNamespace.idFromName(clock))
234 | const body = cbor.encode({ method: 'unsubscribe', args: [subscriber, emitter] })
235 | await stub.fetch('http://localhost', { method: 'POST', body })
236 | }
237 |
238 | /**
239 | * @param {import('@cloudflare/workers-types').DurableObjectNamespace} clockNamespace Durable Object API
240 | * @param {ClockDID} clock Clock to get subscribers for.
241 | * @returns {Promise}
242 | */
243 | async function subscribers (clockNamespace, clock) {
244 | const stub = clockNamespace.get(clockNamespace.idFromName(clock))
245 | const body = cbor.encode({ method: 'subscribers', args: [] })
246 | const res = await stub.fetch('http://localhost', { method: 'POST', body })
247 | const data = /** @type {Array<[ClockDID, EmitterDID[]]>} */ (cbor.decode(new Uint8Array(await res.arrayBuffer())))
248 | return new Map(data.map(([k, v]) => [k, new Set(v)]))
249 | }
250 |
251 | /**
252 | * @param {import('@cloudflare/workers-types').DurableObjectNamespace} clockNamespace Durable Object API
253 | * @param {ClockDID} clock Clock we want the head of.
254 | * @returns {Promise[]>}
255 | */
256 | export async function head (clockNamespace, clock) {
257 | const stub = clockNamespace.get(clockNamespace.idFromName(clock))
258 | const body = cbor.encode({ method: 'head', args: [] })
259 | const res = await stub.fetch('http://localhost', { method: 'POST', body })
260 | return cbor.decode(new Uint8Array(await res.arrayBuffer()))
261 | }
262 |
263 | /**
264 | * Advance the clock with the passed event, created by the passed agent.
265 | *
266 | * @param {import('@cloudflare/workers-types').DurableObjectNamespace} clockNamespace Durable Object API
267 | * @param {ClockDID} clock Clock to advance.
268 | * @param {EmitterDID} emitter Agent that is emitting events that contribute to the target.
269 | * @param {import('@web3-storage/pail/clock/api').EventLink} event Event to advance the clock with.
270 | * @param {import('multiformats').Block>[]} [blocks] Provided event blocks to advance the clock with.
271 | * @returns {Promise[]>}
272 | */
273 | export async function advance (clockNamespace, clock, emitter, event, blocks) {
274 | return advanceAnyClock(clockNamespace, clock, clock, emitter, event, blocks)
275 | }
276 |
277 | /**
278 | * @param {import('@cloudflare/workers-types').DurableObjectNamespace} clockNamespace Durable Object API
279 | * @param {ClockDID} clock The clock that this event should advance.
280 | * @param {ClockDID} target The clock targetted by events emitted by emitter.
281 | * @param {EmitterDID} emitter Agent that is emitting events that contribute to the target.
282 | * @param {import('@web3-storage/pail/clock/api').EventLink} event Event to advance the clock with.
283 | * @param {import('multiformats').Block>[]} [blocks] Provided event blocks to advance the clock with.
284 | * @returns {Promise[]>}
285 | */
286 | async function advanceAnyClock (clockNamespace, clock, target, emitter, event, blocks = []) {
287 | const stub = clockNamespace.get(clockNamespace.idFromName(clock))
288 | const body = cbor.encode({
289 | method: 'advance',
290 | args: [event, blocks.map(b => ({ cid: b.cid, bytes: b.bytes }))]
291 | })
292 | const res = await stub.fetch('http://localhost', { method: 'POST', body })
293 | const data = /** @type {import('@web3-storage/pail/clock/api').EventLink[]} */ (cbor.decode(new Uint8Array(await res.arrayBuffer())))
294 |
295 | // advance subscribers of this clock
296 | if (clock === target) {
297 | const others = await subscribers(clockNamespace, clock)
298 | for (const [clock, emitters] of others) {
299 | if (emitters.has(emitter)) {
300 | await advanceAnyClock(clockNamespace, clock, target, emitter, event)
301 | }
302 | }
303 | }
304 |
305 | return data
306 | }
307 |
--------------------------------------------------------------------------------
/src/worker/index.js:
--------------------------------------------------------------------------------
1 | import { DID } from '@ucanto/core'
2 | import { Signer } from '@ucanto/principal/ed25519'
3 | import { withCORSHeaders, withErrorHandler, withCORSPreflight, withHTTPPost, composeMiddleware } from './middleware.js'
4 | import { createServer, createService } from './service.js'
5 |
6 | export default {
7 | /** @type {import('./types').Handler} */
8 | fetch (request, env, ctx) {
9 | const middleware = composeMiddleware(
10 | withCORSPreflight,
11 | withCORSHeaders,
12 | withErrorHandler,
13 | withHTTPPost
14 | )
15 | return middleware(handler)(request, env, ctx)
16 | }
17 | }
18 |
19 | /** @type {import('./types').Handler} */
20 | async function handler (request, env) {
21 | /** @type {import('@ucanto/interface').Signer} */
22 | let signer = Signer.parse(env.PRIVATE_KEY)
23 | if (env.DID) {
24 | const did = DID.parse(env.DID).did()
25 | signer = signer.withDID(did)
26 | }
27 | const service = createService({ clockNamespace: env.CLOCK })
28 | const server = createServer(signer, service)
29 |
30 | const { headers, body } = await server.request({
31 | body: new Uint8Array(await request.arrayBuffer()),
32 | headers: Object.fromEntries(request.headers)
33 | })
34 |
35 | return new Response(body, { headers })
36 | }
37 |
38 | export { DurableClock as DurableClock0 } from './durable-clock.js'
39 |
--------------------------------------------------------------------------------
/src/worker/middleware.js:
--------------------------------------------------------------------------------
1 | /** @typedef {import('./types').Context} Context */
2 |
3 | /**
4 | * Adds CORS preflight headers to the response.
5 | * @type {import('./types').Middleware}
6 | */
7 | export function withCORSPreflight (handler) {
8 | return async (request, env, ctx) => {
9 | if (request.method !== 'OPTIONS') {
10 | return handler(request, env, ctx)
11 | }
12 |
13 | const { headers } = request
14 | // Make sure the necessary headers are present for this to be a valid pre-flight request
15 | if (
16 | headers.get('Origin') != null &&
17 | headers.get('Access-Control-Request-Method') != null &&
18 | headers.get('Access-Control-Request-Headers') != null
19 | ) {
20 | /** @type {Record} */
21 | const respHeaders = {
22 | 'Content-Length': '0',
23 | 'Access-Control-Allow-Origin': headers.get('origin') || '*',
24 | 'Access-Control-Allow-Methods': 'POST,OPTIONS',
25 | 'Access-Control-Max-Age': '86400',
26 | // Allow all future content Request headers to go back to browser
27 | // such as Authorization (Bearer) or X-Client-Name-Version
28 | 'Access-Control-Allow-Headers': headers.get('Access-Control-Request-Headers') ?? ''
29 | }
30 |
31 | return new Response(undefined, { status: 204, headers: respHeaders })
32 | }
33 |
34 | return new Response('non CORS options request not allowed', { status: 405 })
35 | }
36 | }
37 |
38 | /**
39 | * Adds CORS headers to the response.
40 | * @type {import('./types').Middleware}
41 | */
42 | export function withCORSHeaders (handler) {
43 | return async (request, env, ctx) => {
44 | const response = await handler(request, env, ctx)
45 | const origin = request.headers.get('origin')
46 | if (origin) {
47 | response.headers.set('Access-Control-Allow-Origin', origin)
48 | response.headers.set('Access-Control-Allow-Methods', 'POST,OPTIONS')
49 | response.headers.set('Vary', 'Origin')
50 | } else {
51 | response.headers.set('Access-Control-Allow-Origin', '*')
52 | }
53 | return response
54 | }
55 | }
56 |
57 | /**
58 | * Catches any errors, logs them and returns a suitable response.
59 | * @type {import('./types').Middleware}
60 | */
61 | export function withErrorHandler (handler) {
62 | return async (request, env, ctx) => {
63 | try {
64 | return await handler(request, env, ctx)
65 | } catch (/** @type {any} */ err) {
66 | if (!err.status || err.status >= 500) console.error(err.stack)
67 | const msg = env.DEBUG === 'true'
68 | ? `${err.stack}${err?.cause?.stack ? `\n[cause]: ${err.cause.stack}` : ''}`
69 | : err.message
70 | return new Response(msg, { status: err.status || 500 })
71 | }
72 | }
73 | }
74 |
75 | /**
76 | * Validates the request uses a HTTP POST method.
77 | * @type {import('./types').Middleware}
78 | */
79 | export function withHTTPPost (handler) {
80 | return (request, env, ctx) => {
81 | if (request.method !== 'POST') {
82 | throw Object.assign(new Error('method not allowed'), { status: 405 })
83 | }
84 | return handler(request, env, ctx)
85 | }
86 | }
87 |
88 | /**
89 | * @param {...import('./types').Middleware} middlewares
90 | * @returns {import('./types').Middleware}
91 | */
92 | export function composeMiddleware (...middlewares) {
93 | return handler => middlewares.reduceRight((h, m) => m(h), handler)
94 | }
95 |
--------------------------------------------------------------------------------
/src/worker/service.js:
--------------------------------------------------------------------------------
1 | import { parse } from '@ipld/dag-ucan/did'
2 | import * as dagCBOR from '@ipld/dag-cbor'
3 | import * as ClockCaps from '../capabilities.js'
4 | import * as Clock from './durable-clock.js'
5 | import { provide } from '../server/index.js'
6 |
7 | export { createServer } from '../server/index.js'
8 |
9 | /**
10 | * @template T
11 | * @param {{ clockNamespace: import('@cloudflare/workers-types').DurableObjectNamespace }} conf
12 | * @returns {import('../api.js').Service}
13 | */
14 | export function createService ({ clockNamespace }) {
15 | return {
16 | clock: {
17 | // follow: provide(
18 | // ClockCaps.follow,
19 | // async ({ capability, invocation }) => {
20 | // const clock = capability.with
21 | // const target = capability.nb.with ?? capability.with
22 | // const emitter = capability.nb.iss ?? invocation.issuer.did()
23 | // // @ts-expect-error
24 | // await Clock.follow(clockNamespace, clock, target, emitter)
25 | // return {}
26 | // }
27 | // ),
28 | // unfollow: provide(
29 | // ClockCaps.unfollow,
30 | // async ({ capability, invocation }) => {
31 | // const clock = capability.with
32 | // const target = capability.nb.with ?? capability.with
33 | // const emitter = capability.nb.iss ?? invocation.issuer.did()
34 | // // @ts-expect-error
35 | // await Clock.unfollow(clockNamespace, clock, target, emitter)
36 | // return {}
37 | // }
38 | // ),
39 | // following: provide(
40 | // ClockCaps.following,
41 | // async ({ capability }) => {
42 | // // @ts-expect-error
43 | // const followings = await Clock.following(clockNamespace, capability.with)
44 | // return [...followings.entries()].map(([k, v]) => [k, [...v.values()]])
45 | // }
46 | // ),
47 | advance: provide(
48 | ClockCaps.advance,
49 | async ({ capability, invocation }) => {
50 | const event = /** @type {import('@web3-storage/pail/clock/api').EventLink} */ (capability.nb.event)
51 | const blocks = filterEventBlocks(event, [...invocation.export()])
52 | const resource = parse(capability.with).did()
53 | const head = await Clock.advance(clockNamespace, resource, invocation.issuer.did(), event, blocks)
54 | return { ok: { head } }
55 | }
56 | ),
57 | head: provide(
58 | ClockCaps.head,
59 | async ({ capability }) => {
60 | const resource = parse(capability.with).did()
61 | const head = await Clock.head(clockNamespace, resource)
62 | return { ok: { head } }
63 | }
64 | )
65 | }
66 | }
67 | }
68 |
69 | /**
70 | * @param {import('@web3-storage/pail/clock/api').EventLink} event
71 | * @param {import('@ucanto/interface').Block[]} blocks
72 | */
73 | function filterEventBlocks (event, blocks) {
74 | /** @type {import('@ucanto/interface').Block>[]} */
75 | const filteredBlocks = []
76 | const cids = [event]
77 | while (true) {
78 | const cid = cids.shift()
79 | if (!cid) break
80 | const block = blocks.find(b => b.cid.equals(cid))
81 | if (!block) continue
82 | try {
83 | /** @type {import('@web3-storage/pail/clock/api').EventView} */
84 | const value = dagCBOR.decode(block.bytes)
85 | if (!Array.isArray(value.parents)) {
86 | throw new Error(`invalid merkle clock event: ${cid}`)
87 | }
88 | cids.push(...value.parents)
89 | } catch {}
90 | filteredBlocks.push(block)
91 | }
92 | return filteredBlocks
93 | }
94 |
--------------------------------------------------------------------------------
/src/worker/types.d.ts:
--------------------------------------------------------------------------------
1 | import { DurableObjectNamespace } from '@cloudflare/workers-types'
2 |
3 | export interface Environment {
4 | DEBUG?: string
5 | /** Base64 encoded Ed25519 private key. */
6 | PRIVATE_KEY: string
7 | /** Optional DID of the service, if different from the did:key. e.g. did:web:... */
8 | DID?: string
9 | GATEWAY_URL?: string
10 | BLOCK_CACHE_SIZE?: string
11 | CLOCK: DurableObjectNamespace
12 | }
13 |
14 | export interface Context {
15 | waitUntil (promise: Promise): void
16 | }
17 |
18 | export interface Handler {
19 | (request: Request, env: E, ctx: C): Promise
20 | }
21 |
22 | /**
23 | * Middleware is a function that returns a handler with a possibly extended
24 | * context object. The first generic type is the "extended context". i.e. what
25 | * the context looks like after the middleware is run. The second generic type
26 | * is the "base context", or in other words the context _required_ by the
27 | * middleware for it to run. The third type is the environment, which should
28 | * not be modified.
29 | */
30 | export interface Middleware {
31 | (h: Handler): Handler
32 | }
33 |
--------------------------------------------------------------------------------
/test/helpers/durable-objects.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {import('@cloudflare/workers-types').DurableObjectState} DurableObjectState
3 | * @typedef {import('@cloudflare/workers-types').DurableObjectId} DurableObjectId
4 | * @typedef {import('@cloudflare/workers-types').DurableObjectStorage} DurableObjectStorage
5 | * @typedef {import('@cloudflare/workers-types').DurableObjectTransaction} DurableObjectTransaction
6 | * @typedef {import('@cloudflare/workers-types').DurableObjectNamespace} DurableObjectNamespace
7 | * @typedef {import('@cloudflare/workers-types').DurableObjectStub} DurableObjectStub
8 | */
9 |
10 | /** @implements {DurableObjectId} */
11 | export class MockId {
12 | #id
13 | #name
14 |
15 | /**
16 | * @param {string} id
17 | * @param {string} [name]
18 | */
19 | constructor (id, name) {
20 | this.#id = id
21 | this.#name = name
22 | }
23 |
24 | toString () {
25 | return this.#id
26 | }
27 |
28 | /** @param {DurableObjectId} other */
29 | equals (other) {
30 | return String(other) === this.#id
31 | }
32 |
33 | get name () {
34 | return this.#name
35 | }
36 | }
37 |
38 | class MockBasicStorage {
39 | /** @param {Map} data */
40 | constructor (data = new Map()) {
41 | this.data = data
42 | /** @type {number|null} */
43 | this.alarm = null
44 | }
45 |
46 | /**
47 | * @param {string|string[]} key
48 | * @param {import('@cloudflare/workers-types').DurableObjectGetOptions} [options]
49 | */
50 | async get (key, options) {
51 | return Array.isArray(key)
52 | ? new Map(key.map(k => [k, this.data.get(k)]))
53 | : this.data.get(key)
54 | }
55 |
56 | /**
57 | * @template T
58 | * @param {import('@cloudflare/workers-types').DurableObjectGetOptions} [options]
59 | * @returns {Promise