@pdsjs/spaces 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +40 -0
- package/src/authority.d.ts +47 -0
- package/src/authority.js +148 -0
- package/src/blake3.d.ts +14 -0
- package/src/blake3.js +272 -0
- package/src/car.d.ts +41 -0
- package/src/car.js +98 -0
- package/src/commit.d.ts +109 -0
- package/src/commit.js +152 -0
- package/src/handlers/auth.d.ts +67 -0
- package/src/handlers/auth.js +436 -0
- package/src/handlers/manage.d.ts +20 -0
- package/src/handlers/manage.js +508 -0
- package/src/handlers/read.d.ts +18 -0
- package/src/handlers/read.js +394 -0
- package/src/handlers/write.d.ts +18 -0
- package/src/handlers/write.js +462 -0
- package/src/index.d.ts +15 -0
- package/src/index.js +56 -0
- package/src/lthash.d.ts +24 -0
- package/src/lthash.js +86 -0
- package/src/mac.d.ts +50 -0
- package/src/mac.js +107 -0
- package/src/memory-storage.d.ts +4 -0
- package/src/memory-storage.js +244 -0
- package/src/path.d.ts +22 -0
- package/src/path.js +33 -0
- package/src/routes.d.ts +23 -0
- package/src/routes.js +42 -0
- package/src/service-auth.d.ts +42 -0
- package/src/service-auth.js +153 -0
- package/src/space-row.d.ts +15 -0
- package/src/space-row.js +33 -0
- package/src/token.d.ts +95 -0
- package/src/token.js +223 -0
- package/src/uri.d.ts +32 -0
- package/src/uri.js +89 -0
- package/src/verifier.d.ts +15 -0
- package/src/verifier.js +74 -0
- package/src/writer.d.ts +77 -0
- package/src/writer.js +195 -0
package/src/commit.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { LtHash } from './lthash.js';
|
|
2
|
+
export type SignedCommit = {
|
|
3
|
+
ver: number;
|
|
4
|
+
hash: Uint8Array;
|
|
5
|
+
ikm: Uint8Array;
|
|
6
|
+
sig: Uint8Array;
|
|
7
|
+
mac: Uint8Array;
|
|
8
|
+
rev: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} SignedCommit
|
|
12
|
+
* @property {number} ver
|
|
13
|
+
* @property {Uint8Array} hash
|
|
14
|
+
* @property {Uint8Array} ikm
|
|
15
|
+
* @property {Uint8Array} sig
|
|
16
|
+
* @property {Uint8Array} mac
|
|
17
|
+
* @property {string} rev
|
|
18
|
+
*/
|
|
19
|
+
export declare class RepoCommit {
|
|
20
|
+
setHash: LtHash;
|
|
21
|
+
/** @param {LtHash} [setHash] */
|
|
22
|
+
constructor(setHash?: LtHash);
|
|
23
|
+
/** @param {Uint8Array|null|undefined} state */
|
|
24
|
+
static fromState(state: Uint8Array | null | undefined): RepoCommit;
|
|
25
|
+
/** @param {Iterable<{collection: string, rkey: string, cid: string}>} records */
|
|
26
|
+
static fromRecords(records: Iterable<{
|
|
27
|
+
collection: string;
|
|
28
|
+
rkey: string;
|
|
29
|
+
cid: string;
|
|
30
|
+
}>): RepoCommit;
|
|
31
|
+
/**
|
|
32
|
+
* Fold in every record an index describes, to compare against a commit.
|
|
33
|
+
* @param {Record<string, string>} index - "{collection}/{rkey}" to CID
|
|
34
|
+
*/
|
|
35
|
+
static fromIndex(index: Record<string, string>): RepoCommit;
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} collection
|
|
38
|
+
* @param {string} rkey
|
|
39
|
+
* @param {string} cid
|
|
40
|
+
* @returns {this}
|
|
41
|
+
*/
|
|
42
|
+
add(collection: string, rkey: string, cid: string): this;
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} collection
|
|
45
|
+
* @param {string} rkey
|
|
46
|
+
* @param {string} cid
|
|
47
|
+
* @returns {this}
|
|
48
|
+
*/
|
|
49
|
+
remove(collection: string, rkey: string, cid: string): this;
|
|
50
|
+
/** @param {{collection: string, rkey: string, cid: string|null, prev: string|null}} op */
|
|
51
|
+
applyOp(op: {
|
|
52
|
+
collection: string;
|
|
53
|
+
rkey: string;
|
|
54
|
+
cid: string | null;
|
|
55
|
+
prev: string | null;
|
|
56
|
+
}): this;
|
|
57
|
+
/**
|
|
58
|
+
* @param {Iterable<{collection: string, rkey: string, cid: string|null, prev: string|null}>} ops
|
|
59
|
+
* @returns {this}
|
|
60
|
+
*/
|
|
61
|
+
applyOps(ops: Iterable<{
|
|
62
|
+
collection: string;
|
|
63
|
+
rkey: string;
|
|
64
|
+
cid: string | null;
|
|
65
|
+
prev: string | null;
|
|
66
|
+
}>): this;
|
|
67
|
+
state(): Uint8Array<ArrayBufferLike>;
|
|
68
|
+
/**
|
|
69
|
+
* Whether this repo's contents match a signed commit. Verify the commit first —
|
|
70
|
+
* on its own this says nothing about authenticity.
|
|
71
|
+
* @param {SignedCommit} commit
|
|
72
|
+
*/
|
|
73
|
+
matches(commit: SignedCommit): Promise<boolean>;
|
|
74
|
+
/**
|
|
75
|
+
* Sign a commit over the current contents.
|
|
76
|
+
*
|
|
77
|
+
* The signature covers only the ctx, never the digest, so a leaked commit
|
|
78
|
+
* proves nothing about what the author wrote. The digest is bound to the ctx
|
|
79
|
+
* by a symmetric MAC instead: readers get integrity, third parties get
|
|
80
|
+
* nothing. A fresh ikm per commit means each reader receives a distinct one.
|
|
81
|
+
*
|
|
82
|
+
* @param {{space: string, author: string, rev: string}} ctx
|
|
83
|
+
* @param {{sign: (bytes: Uint8Array) => Promise<Uint8Array>}} signer
|
|
84
|
+
* @returns {Promise<SignedCommit>}
|
|
85
|
+
*/
|
|
86
|
+
sign(ctx: {
|
|
87
|
+
space: string;
|
|
88
|
+
author: string;
|
|
89
|
+
rev: string;
|
|
90
|
+
}, signer: {
|
|
91
|
+
sign: (bytes: Uint8Array) => Promise<Uint8Array>;
|
|
92
|
+
}): Promise<SignedCommit>;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Verify a commit's signature (authenticity) and MAC (integrity). Once this
|
|
96
|
+
* passes, `hash` is trusted as the author's claim about their repo, which is
|
|
97
|
+
* what makes RepoCommit#matches meaningful.
|
|
98
|
+
*
|
|
99
|
+
* @param {SignedCommit} commit
|
|
100
|
+
* @param {{space: string, author: string, rev: string}} ctx
|
|
101
|
+
* @param {string} didKey
|
|
102
|
+
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} verifier
|
|
103
|
+
* @returns {Promise<boolean>}
|
|
104
|
+
*/
|
|
105
|
+
export declare function verifyCommit(commit: SignedCommit, ctx: {
|
|
106
|
+
space: string;
|
|
107
|
+
author: string;
|
|
108
|
+
rev: string;
|
|
109
|
+
}, didKey: string, verifier: import('@pdsjs/core/ports').SignatureVerifierPort): Promise<boolean>;
|
package/src/commit.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// @pdsjs/spaces/commit - permissioned repo commits with deniable signatures.
|
|
2
|
+
|
|
3
|
+
import { LtHash } from './lthash.js';
|
|
4
|
+
import {
|
|
5
|
+
bytesEqual,
|
|
6
|
+
COMMIT_VERSION,
|
|
7
|
+
computeMac,
|
|
8
|
+
encodeCommitCtx,
|
|
9
|
+
} from './mac.js';
|
|
10
|
+
import { formatSetHashElement } from './path.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} SignedCommit
|
|
14
|
+
* @property {number} ver
|
|
15
|
+
* @property {Uint8Array} hash
|
|
16
|
+
* @property {Uint8Array} ikm
|
|
17
|
+
* @property {Uint8Array} sig
|
|
18
|
+
* @property {Uint8Array} mac
|
|
19
|
+
* @property {string} rev
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export class RepoCommit {
|
|
23
|
+
/** @param {LtHash} [setHash] */
|
|
24
|
+
constructor(setHash = new LtHash()) {
|
|
25
|
+
this.setHash = setHash;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @param {Uint8Array|null|undefined} state */
|
|
29
|
+
static fromState(state) {
|
|
30
|
+
return new RepoCommit(new LtHash(state ?? null));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @param {Iterable<{collection: string, rkey: string, cid: string}>} records */
|
|
34
|
+
static fromRecords(records) {
|
|
35
|
+
const commit = new RepoCommit();
|
|
36
|
+
for (const { collection, rkey, cid } of records) {
|
|
37
|
+
commit.add(collection, rkey, cid);
|
|
38
|
+
}
|
|
39
|
+
return commit;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fold in every record an index describes, to compare against a commit.
|
|
44
|
+
* @param {Record<string, string>} index - "{collection}/{rkey}" to CID
|
|
45
|
+
*/
|
|
46
|
+
static fromIndex(index) {
|
|
47
|
+
const commit = new RepoCommit();
|
|
48
|
+
for (const [path, cid] of Object.entries(index)) {
|
|
49
|
+
commit.setHash.add(`${path}/${cid}`);
|
|
50
|
+
}
|
|
51
|
+
return commit;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} collection
|
|
56
|
+
* @param {string} rkey
|
|
57
|
+
* @param {string} cid
|
|
58
|
+
* @returns {this}
|
|
59
|
+
*/
|
|
60
|
+
add(collection, rkey, cid) {
|
|
61
|
+
this.setHash.add(formatSetHashElement(collection, rkey, cid));
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string} collection
|
|
67
|
+
* @param {string} rkey
|
|
68
|
+
* @param {string} cid
|
|
69
|
+
* @returns {this}
|
|
70
|
+
*/
|
|
71
|
+
remove(collection, rkey, cid) {
|
|
72
|
+
this.setHash.remove(formatSetHashElement(collection, rkey, cid));
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @param {{collection: string, rkey: string, cid: string|null, prev: string|null}} op */
|
|
77
|
+
applyOp(op) {
|
|
78
|
+
if (op.prev) this.remove(op.collection, op.rkey, op.prev);
|
|
79
|
+
if (op.cid) this.add(op.collection, op.rkey, op.cid);
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {Iterable<{collection: string, rkey: string, cid: string|null, prev: string|null}>} ops
|
|
85
|
+
* @returns {this}
|
|
86
|
+
*/
|
|
87
|
+
applyOps(ops) {
|
|
88
|
+
for (const op of ops) this.applyOp(op);
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
state() {
|
|
93
|
+
return this.setHash.state();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Whether this repo's contents match a signed commit. Verify the commit first —
|
|
98
|
+
* on its own this says nothing about authenticity.
|
|
99
|
+
* @param {SignedCommit} commit
|
|
100
|
+
*/
|
|
101
|
+
async matches(commit) {
|
|
102
|
+
return bytesEqual(await this.setHash.digest(), commit.hash);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Sign a commit over the current contents.
|
|
107
|
+
*
|
|
108
|
+
* The signature covers only the ctx, never the digest, so a leaked commit
|
|
109
|
+
* proves nothing about what the author wrote. The digest is bound to the ctx
|
|
110
|
+
* by a symmetric MAC instead: readers get integrity, third parties get
|
|
111
|
+
* nothing. A fresh ikm per commit means each reader receives a distinct one.
|
|
112
|
+
*
|
|
113
|
+
* @param {{space: string, author: string, rev: string}} ctx
|
|
114
|
+
* @param {{sign: (bytes: Uint8Array) => Promise<Uint8Array>}} signer
|
|
115
|
+
* @returns {Promise<SignedCommit>}
|
|
116
|
+
*/
|
|
117
|
+
async sign(ctx, signer) {
|
|
118
|
+
const hash = await this.setHash.digest();
|
|
119
|
+
const ikm = crypto.getRandomValues(new Uint8Array(32));
|
|
120
|
+
const ctxBytes = encodeCommitCtx(ctx, ikm);
|
|
121
|
+
return {
|
|
122
|
+
ver: COMMIT_VERSION,
|
|
123
|
+
hash,
|
|
124
|
+
ikm,
|
|
125
|
+
mac: await computeMac(ikm, ctxBytes, hash),
|
|
126
|
+
sig: await signer.sign(ctxBytes),
|
|
127
|
+
rev: ctx.rev,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Verify a commit's signature (authenticity) and MAC (integrity). Once this
|
|
134
|
+
* passes, `hash` is trusted as the author's claim about their repo, which is
|
|
135
|
+
* what makes RepoCommit#matches meaningful.
|
|
136
|
+
*
|
|
137
|
+
* @param {SignedCommit} commit
|
|
138
|
+
* @param {{space: string, author: string, rev: string}} ctx
|
|
139
|
+
* @param {string} didKey
|
|
140
|
+
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} verifier
|
|
141
|
+
* @returns {Promise<boolean>}
|
|
142
|
+
*/
|
|
143
|
+
export async function verifyCommit(commit, ctx, didKey, verifier) {
|
|
144
|
+
if (commit.ver !== COMMIT_VERSION) return false;
|
|
145
|
+
if (commit.rev !== ctx.rev) return false;
|
|
146
|
+
|
|
147
|
+
const ctxBytes = encodeCommitCtx(ctx, commit.ikm);
|
|
148
|
+
const mac = await computeMac(commit.ikm, ctxBytes, commit.hash);
|
|
149
|
+
if (!bytesEqual(mac, commit.mac)) return false;
|
|
150
|
+
|
|
151
|
+
return verifier.verify(didKey, ctxBytes, commit.sig);
|
|
152
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Object} ctx
|
|
3
|
+
* @param {import('@pdsjs/core/ports').SpaceStoragePort} ctx.spaceStorage
|
|
4
|
+
* @param {() => Promise<string|null>} ctx.getDid
|
|
5
|
+
* @param {() => Promise<{sign: (bytes: Uint8Array) => Promise<Uint8Array>}>} ctx.getSigner
|
|
6
|
+
* @param {(did: string) => Promise<any>} ctx.resolveDid
|
|
7
|
+
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} ctx.verifier
|
|
8
|
+
* @param {typeof fetch} [ctx.fetch]
|
|
9
|
+
* @returns {import('@pdsjs/core/pds').Routes}
|
|
10
|
+
*/
|
|
11
|
+
export declare function createAuthRoutes(ctx: {
|
|
12
|
+
spaceStorage: import('@pdsjs/core/ports').SpaceStoragePort;
|
|
13
|
+
getDid: () => Promise<string | null>;
|
|
14
|
+
getSigner: () => Promise<{
|
|
15
|
+
sign: (bytes: Uint8Array) => Promise<Uint8Array>;
|
|
16
|
+
}>;
|
|
17
|
+
resolveDid: (did: string) => Promise<any>;
|
|
18
|
+
verifier: import('@pdsjs/core/ports').SignatureVerifierPort;
|
|
19
|
+
fetch?: typeof fetch;
|
|
20
|
+
}): import('@pdsjs/core/pds').Routes;
|
|
21
|
+
/**
|
|
22
|
+
* Verify a space credential presented to a repo host.
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} opts
|
|
25
|
+
* @param {string} opts.credential - the raw JWT
|
|
26
|
+
* @param {string} opts.space - the space the request targets
|
|
27
|
+
* @param {(did: string) => Promise<any>} opts.resolveDid
|
|
28
|
+
* @param {import('@pdsjs/core/ports').SignatureVerifierPort} opts.verifier
|
|
29
|
+
* @returns {Promise<{iss: string}>}
|
|
30
|
+
*/
|
|
31
|
+
export declare function verifySpaceCredential({ credential, space, resolveDid, verifier, }: {
|
|
32
|
+
credential: string;
|
|
33
|
+
space: string;
|
|
34
|
+
resolveDid: (did: string) => Promise<any>;
|
|
35
|
+
verifier: import('@pdsjs/core/ports').SignatureVerifierPort;
|
|
36
|
+
}): Promise<{
|
|
37
|
+
iss: string;
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Ask a space's managing app whether to authorize a user.
|
|
41
|
+
*
|
|
42
|
+
* Fails closed: anything short of an explicit `authorized: true` is a refusal,
|
|
43
|
+
* so an app being down denies access rather than granting it.
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} opts
|
|
46
|
+
* @param {string|null} opts.managingApp - a `did#serviceId` identifier
|
|
47
|
+
* @param {string} opts.authorityDid
|
|
48
|
+
* @param {string} opts.space
|
|
49
|
+
* @param {string} opts.userDid
|
|
50
|
+
* @param {string|undefined} opts.clientId
|
|
51
|
+
* @param {(did: string) => Promise<any>} opts.resolveDid
|
|
52
|
+
* @param {() => Promise<{sign: (bytes: Uint8Array) => Promise<Uint8Array>}>} opts.getSigner
|
|
53
|
+
* @param {typeof fetch} [opts.fetch]
|
|
54
|
+
* @returns {Promise<boolean>}
|
|
55
|
+
*/
|
|
56
|
+
export declare function checkUserAccess({ managingApp, authorityDid, space, userDid, clientId, resolveDid, getSigner, fetch: doFetch, }: {
|
|
57
|
+
managingApp: string | null;
|
|
58
|
+
authorityDid: string;
|
|
59
|
+
space: string;
|
|
60
|
+
userDid: string;
|
|
61
|
+
clientId: string | undefined;
|
|
62
|
+
resolveDid: (did: string) => Promise<any>;
|
|
63
|
+
getSigner: () => Promise<{
|
|
64
|
+
sign: (bytes: Uint8Array) => Promise<Uint8Array>;
|
|
65
|
+
}>;
|
|
66
|
+
fetch?: typeof fetch;
|
|
67
|
+
}): Promise<boolean>;
|