@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.
@@ -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>;