mindoodb 0.0.64 → 0.0.65
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/dist/browser/index.mjs +11 -0
- package/dist/core/BaseMindooDB.d.ts +19 -0
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +607 -323
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/accesscontrol/tier1Evaluator.d.ts +59 -0
- package/dist/core/accesscontrol/tier1Evaluator.d.ts.map +1 -0
- package/dist/core/accesscontrol/tier1Evaluator.js +77 -0
- package/dist/core/accesscontrol/tier1Evaluator.js.map +1 -0
- package/dist/core/appendonlystores/network/IrohNetworkTransport.d.ts +58 -5
- package/dist/core/appendonlystores/network/IrohNetworkTransport.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/IrohNetworkTransport.js +77 -8
- package/dist/core/appendonlystores/network/IrohNetworkTransport.js.map +1 -1
- package/dist/core/appendonlystores/network/IrohStreamIO.d.ts +32 -10
- package/dist/core/appendonlystores/network/IrohStreamIO.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/IrohStreamIO.js +26 -10
- package/dist/core/appendonlystores/network/IrohStreamIO.js.map +1 -1
- package/dist/core/appendonlystores/network/irohLocator.d.ts +16 -0
- package/dist/core/appendonlystores/network/irohLocator.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/irohLocator.js +36 -0
- package/dist/core/appendonlystores/network/irohLocator.js.map +1 -1
- package/dist/core/builtinDbInvariants.d.ts +12 -6
- package/dist/core/builtinDbInvariants.d.ts.map +1 -1
- package/dist/core/builtinDbInvariants.js +7 -1
- package/dist/core/builtinDbInvariants.js.map +1 -1
- package/dist/core/index.d.ts +6 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +27 -11
- package/dist/core/index.js.map +1 -1
- package/dist/core/peerdevices/PeerDeviceDocument.d.ts +144 -0
- package/dist/core/peerdevices/PeerDeviceDocument.d.ts.map +1 -0
- package/dist/core/peerdevices/PeerDeviceDocument.js +209 -0
- package/dist/core/peerdevices/PeerDeviceDocument.js.map +1 -0
- package/dist/core/types.d.ts +15 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/userkeys/fingerprint.d.ts +12 -4
- package/dist/core/userkeys/fingerprint.d.ts.map +1 -1
- package/dist/core/userkeys/fingerprint.js +14 -5
- package/dist/core/userkeys/fingerprint.js.map +1 -1
- package/dist/iroh/index.d.ts +3 -2
- package/dist/iroh/index.d.ts.map +1 -1
- package/dist/iroh/index.js +8 -1
- package/dist/iroh/index.js.map +1 -1
- package/dist/node/server/TenantManager.d.ts +5 -0
- package/dist/node/server/TenantManager.d.ts.map +1 -1
- package/dist/node/server/TenantManager.js +35 -56
- package/dist/node/server/TenantManager.js.map +1 -1
- package/dist/node/server/nativeIrohStreamIO.d.ts.map +1 -1
- package/dist/node/server/nativeIrohStreamIO.js +17 -4
- package/dist/node/server/nativeIrohStreamIO.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Peer-device records: how one device advertises its Iroh endpoint id to the
|
|
3
|
+
* other members of a tenant.
|
|
4
|
+
*
|
|
5
|
+
* A device publishes one `dev_<fingerprint>` document into `userdirectory`,
|
|
6
|
+
* encrypted with the tenant `default` key. That placement is deliberate:
|
|
7
|
+
*
|
|
8
|
+
* - **`userdirectory`, not `grantaccess`.** Every member already replicates
|
|
9
|
+
* `userdirectory`, and a device can write its own record there without an
|
|
10
|
+
* administrator approving anything.
|
|
11
|
+
* - **`default`, not `$publicinfos`.** The hoster holds the `$publicinfos`
|
|
12
|
+
* key, and who talks to whom over Iroh is none of the hoster's business.
|
|
13
|
+
* `default` keeps the record readable for every tenant member and opaque to
|
|
14
|
+
* the server.
|
|
15
|
+
* - **`dev_` prefix.** That makes it a *personal document* (see
|
|
16
|
+
* {@link PERSONAL_DOC_ID_PREFIXES}), so the write invariant lets its creator
|
|
17
|
+
* — and only its creator — change and delete it.
|
|
18
|
+
*
|
|
19
|
+
* ## Why records must be verified on read
|
|
20
|
+
*
|
|
21
|
+
* Any tenant member can write a `dev_` document holding any endpoint id they
|
|
22
|
+
* like. The doc id and payload are therefore claims, not facts.
|
|
23
|
+
* {@link verifyPeerDeviceRecord} only accepts a record when all three hold:
|
|
24
|
+
*
|
|
25
|
+
* 1. the doc id is the fingerprint of the `signingPublicKey` in the payload,
|
|
26
|
+
* 2. the `doc_create` entry was signed by exactly that key, and
|
|
27
|
+
* 3. that key is an active (non-revoked) grant in the tenant.
|
|
28
|
+
*
|
|
29
|
+
* Together these mean a record can only be published by the device that owns
|
|
30
|
+
* the signing key it names, so nobody can put their own endpoint id under
|
|
31
|
+
* someone else's device.
|
|
32
|
+
*
|
|
33
|
+
* **Known v1 gap — id squatting.** A member can create `dev_<someone else's
|
|
34
|
+
* fingerprint>` before that device does. The squatted record fails step 2 and
|
|
35
|
+
* is discarded, so it cannot impersonate anyone; but the real device then
|
|
36
|
+
* cannot create its own record under that id and stays invisible to the
|
|
37
|
+
* picker. Detect it by the record being present yet unverifiable.
|
|
38
|
+
*/
|
|
39
|
+
import type { MindooDB, MindooDoc, SigningKeyPair } from "../types";
|
|
40
|
+
export declare const PEER_DEVICE_DOC_ID_PREFIX = "dev_";
|
|
41
|
+
export declare const PEER_DEVICE_FORM = "userdirectory";
|
|
42
|
+
export declare const PEER_DEVICE_TYPE = "peerdevice";
|
|
43
|
+
export declare const PEER_DEVICE_SCHEMA_VERSION = 1;
|
|
44
|
+
/** Payload of a `dev_` document. Encrypted with {@link DEFAULT_TENANT_KEY_ID}. */
|
|
45
|
+
export interface PeerDeviceDocumentPayload {
|
|
46
|
+
form: typeof PEER_DEVICE_FORM;
|
|
47
|
+
type: typeof PEER_DEVICE_TYPE;
|
|
48
|
+
schemaVersion: number;
|
|
49
|
+
/**
|
|
50
|
+
* 64-char lowercase hex Iroh endpoint id of this device. Peers dial this
|
|
51
|
+
* directly; the pkarr address lookup resolves the current relay, so the
|
|
52
|
+
* value stays valid when the device changes networks.
|
|
53
|
+
*/
|
|
54
|
+
irohEndpointId: string;
|
|
55
|
+
/**
|
|
56
|
+
* Signing public key (PEM) of the device that owns this record. The whole
|
|
57
|
+
* verification chain hangs off this field.
|
|
58
|
+
*/
|
|
59
|
+
signingPublicKey: string;
|
|
60
|
+
/** Human label shown in the peer picker, e.g. "Laptop (office)". */
|
|
61
|
+
label?: string;
|
|
62
|
+
updatedAt: number;
|
|
63
|
+
}
|
|
64
|
+
/** A record that passed {@link verifyPeerDeviceRecord}. */
|
|
65
|
+
export interface PeerDeviceRecord {
|
|
66
|
+
docId: string;
|
|
67
|
+
irohEndpointId: string;
|
|
68
|
+
signingPublicKey: string;
|
|
69
|
+
/** Fingerprint of {@link signingPublicKey}, colon-separated hex. */
|
|
70
|
+
signingKeyFingerprint: string;
|
|
71
|
+
label?: string;
|
|
72
|
+
updatedAt: number;
|
|
73
|
+
}
|
|
74
|
+
/** The directory calls a peer-device record needs to verify a claim. */
|
|
75
|
+
export interface PeerDeviceDirectory {
|
|
76
|
+
/** True when the key belongs to an active, non-revoked grant. */
|
|
77
|
+
validatePublicSigningKey(publicKey: string, opts?: {
|
|
78
|
+
forceRefresh?: boolean;
|
|
79
|
+
}): Promise<boolean>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Deterministic document id for a device, derived from its signing key
|
|
83
|
+
* fingerprint.
|
|
84
|
+
*
|
|
85
|
+
* Deterministic so a device republishing after a reload updates its record
|
|
86
|
+
* instead of accumulating a new one per session. Colons are stripped because
|
|
87
|
+
* custom ids must match `^[a-z][a-z0-9_]*$` (`CUSTOM_DOC_ID_REGEX`).
|
|
88
|
+
*/
|
|
89
|
+
export declare function peerDeviceDocumentId(signingKeyFingerprint: string): string;
|
|
90
|
+
export declare function isPeerDeviceDocId(docId: string): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Publish (or refresh) this device's record.
|
|
93
|
+
*
|
|
94
|
+
* Idempotent: the id is derived from the device's own signing key, so calling
|
|
95
|
+
* this on every unlock keeps one record per device and updates the endpoint id
|
|
96
|
+
* and label in place.
|
|
97
|
+
*
|
|
98
|
+
* Note this does *not* pass `assumeUniqueId`. The id is deterministic rather
|
|
99
|
+
* than random, so two replicas creating it concurrently must share Automerge
|
|
100
|
+
* ancestry in order to converge instead of forking. That code path also
|
|
101
|
+
* forbids `initialValues`, which is why the payload is written with a
|
|
102
|
+
* follow-up `changeDoc` — the same call that refreshes an existing record.
|
|
103
|
+
*/
|
|
104
|
+
export declare function publishPeerDeviceRecord(input: {
|
|
105
|
+
db: MindooDB;
|
|
106
|
+
/** Signing public key (PEM) of this device — must match the signing identity writing the document. */
|
|
107
|
+
signingPublicKey: string;
|
|
108
|
+
irohEndpointId: string;
|
|
109
|
+
label?: string;
|
|
110
|
+
subtle: SubtleCrypto;
|
|
111
|
+
signingKeyPair?: SigningKeyPair;
|
|
112
|
+
signingKeyPassword?: string;
|
|
113
|
+
}): Promise<MindooDoc>;
|
|
114
|
+
/** Withdraw this device's record, e.g. when signing out of the tenant. */
|
|
115
|
+
export declare function removePeerDeviceRecord(input: {
|
|
116
|
+
db: MindooDB;
|
|
117
|
+
signingPublicKey: string;
|
|
118
|
+
subtle: SubtleCrypto;
|
|
119
|
+
}): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Check one record's claim. Returns the verified record, or `null` when any
|
|
122
|
+
* step of the chain described in the module docs fails.
|
|
123
|
+
*/
|
|
124
|
+
export declare function verifyPeerDeviceRecord(input: {
|
|
125
|
+
db: MindooDB;
|
|
126
|
+
directory: PeerDeviceDirectory;
|
|
127
|
+
doc: MindooDoc;
|
|
128
|
+
subtle: SubtleCrypto;
|
|
129
|
+
}): Promise<PeerDeviceRecord | null>;
|
|
130
|
+
/**
|
|
131
|
+
* Every verified peer-device record in this `userdirectory` replica.
|
|
132
|
+
*
|
|
133
|
+
* Requires the tenant `default` key to be unlocked; records that cannot be
|
|
134
|
+
* decrypted are simply absent. Unverifiable records are skipped silently —
|
|
135
|
+
* they are the expected shape of an attack, not an error to surface.
|
|
136
|
+
*/
|
|
137
|
+
export declare function listPeerDeviceRecords(input: {
|
|
138
|
+
db: MindooDB;
|
|
139
|
+
directory: PeerDeviceDirectory;
|
|
140
|
+
subtle: SubtleCrypto;
|
|
141
|
+
/** Endpoint id of the local device, excluded from the result. */
|
|
142
|
+
excludeEndpointId?: string;
|
|
143
|
+
}): Promise<PeerDeviceRecord[]>;
|
|
144
|
+
//# sourceMappingURL=PeerDeviceDocument.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PeerDeviceDocument.d.ts","sourceRoot":"","sources":["../../../src/core/peerdevices/PeerDeviceDocument.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAGpE,eAAO,MAAM,yBAAyB,SAAS,CAAC;AAChD,eAAO,MAAM,gBAAgB,kBAAkB,CAAC;AAChD,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAC7C,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C,kFAAkF;AAClF,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,OAAO,gBAAgB,CAAC;IAC9B,IAAI,EAAE,OAAO,gBAAgB,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,2DAA2D;AAC3D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,qBAAqB,EAAE,MAAM,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAClC,iEAAiE;IACjE,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAClG;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,qBAAqB,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAAC,KAAK,EAAE;IACnD,EAAE,EAAE,QAAQ,CAAC;IACb,sGAAsG;IACtG,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;IACrB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,GAAG,OAAO,CAAC,SAAS,CAAC,CAwCrB;AAED,0EAA0E;AAC1E,wBAAsB,sBAAsB,CAAC,KAAK,EAAE;IAClD,EAAE,EAAE,QAAQ,CAAC;IACb,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,YAAY,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMhB;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,KAAK,EAAE;IAClD,EAAE,EAAE,QAAQ,CAAC;IACb,SAAS,EAAE,mBAAmB,CAAC;IAC/B,GAAG,EAAE,SAAS,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;CACtB,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAqCnC;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CAAC,KAAK,EAAE;IACjD,EAAE,EAAE,QAAQ,CAAC;IACb,SAAS,EAAE,mBAAmB,CAAC;IAC/B,MAAM,EAAE,YAAY,CAAC;IACrB,iEAAiE;IACjE,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAuB9B"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Peer-device records: how one device advertises its Iroh endpoint id to the
|
|
4
|
+
* other members of a tenant.
|
|
5
|
+
*
|
|
6
|
+
* A device publishes one `dev_<fingerprint>` document into `userdirectory`,
|
|
7
|
+
* encrypted with the tenant `default` key. That placement is deliberate:
|
|
8
|
+
*
|
|
9
|
+
* - **`userdirectory`, not `grantaccess`.** Every member already replicates
|
|
10
|
+
* `userdirectory`, and a device can write its own record there without an
|
|
11
|
+
* administrator approving anything.
|
|
12
|
+
* - **`default`, not `$publicinfos`.** The hoster holds the `$publicinfos`
|
|
13
|
+
* key, and who talks to whom over Iroh is none of the hoster's business.
|
|
14
|
+
* `default` keeps the record readable for every tenant member and opaque to
|
|
15
|
+
* the server.
|
|
16
|
+
* - **`dev_` prefix.** That makes it a *personal document* (see
|
|
17
|
+
* {@link PERSONAL_DOC_ID_PREFIXES}), so the write invariant lets its creator
|
|
18
|
+
* — and only its creator — change and delete it.
|
|
19
|
+
*
|
|
20
|
+
* ## Why records must be verified on read
|
|
21
|
+
*
|
|
22
|
+
* Any tenant member can write a `dev_` document holding any endpoint id they
|
|
23
|
+
* like. The doc id and payload are therefore claims, not facts.
|
|
24
|
+
* {@link verifyPeerDeviceRecord} only accepts a record when all three hold:
|
|
25
|
+
*
|
|
26
|
+
* 1. the doc id is the fingerprint of the `signingPublicKey` in the payload,
|
|
27
|
+
* 2. the `doc_create` entry was signed by exactly that key, and
|
|
28
|
+
* 3. that key is an active (non-revoked) grant in the tenant.
|
|
29
|
+
*
|
|
30
|
+
* Together these mean a record can only be published by the device that owns
|
|
31
|
+
* the signing key it names, so nobody can put their own endpoint id under
|
|
32
|
+
* someone else's device.
|
|
33
|
+
*
|
|
34
|
+
* **Known v1 gap — id squatting.** A member can create `dev_<someone else's
|
|
35
|
+
* fingerprint>` before that device does. The squatted record fails step 2 and
|
|
36
|
+
* is discarded, so it cannot impersonate anyone; but the real device then
|
|
37
|
+
* cannot create its own record under that id and stays invisible to the
|
|
38
|
+
* picker. Detect it by the record being present yet unverifiable.
|
|
39
|
+
*/
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.PEER_DEVICE_SCHEMA_VERSION = exports.PEER_DEVICE_TYPE = exports.PEER_DEVICE_FORM = exports.PEER_DEVICE_DOC_ID_PREFIX = void 0;
|
|
42
|
+
exports.peerDeviceDocumentId = peerDeviceDocumentId;
|
|
43
|
+
exports.isPeerDeviceDocId = isPeerDeviceDocId;
|
|
44
|
+
exports.publishPeerDeviceRecord = publishPeerDeviceRecord;
|
|
45
|
+
exports.removePeerDeviceRecord = removePeerDeviceRecord;
|
|
46
|
+
exports.verifyPeerDeviceRecord = verifyPeerDeviceRecord;
|
|
47
|
+
exports.listPeerDeviceRecords = listPeerDeviceRecords;
|
|
48
|
+
const types_1 = require("../types");
|
|
49
|
+
const fingerprint_1 = require("../userkeys/fingerprint");
|
|
50
|
+
exports.PEER_DEVICE_DOC_ID_PREFIX = "dev_";
|
|
51
|
+
exports.PEER_DEVICE_FORM = "userdirectory";
|
|
52
|
+
exports.PEER_DEVICE_TYPE = "peerdevice";
|
|
53
|
+
exports.PEER_DEVICE_SCHEMA_VERSION = 1;
|
|
54
|
+
/**
|
|
55
|
+
* Deterministic document id for a device, derived from its signing key
|
|
56
|
+
* fingerprint.
|
|
57
|
+
*
|
|
58
|
+
* Deterministic so a device republishing after a reload updates its record
|
|
59
|
+
* instead of accumulating a new one per session. Colons are stripped because
|
|
60
|
+
* custom ids must match `^[a-z][a-z0-9_]*$` (`CUSTOM_DOC_ID_REGEX`).
|
|
61
|
+
*/
|
|
62
|
+
function peerDeviceDocumentId(signingKeyFingerprint) {
|
|
63
|
+
return `${exports.PEER_DEVICE_DOC_ID_PREFIX}${signingKeyFingerprint.replace(/:/g, "").toLowerCase()}`;
|
|
64
|
+
}
|
|
65
|
+
function isPeerDeviceDocId(docId) {
|
|
66
|
+
return docId.startsWith(exports.PEER_DEVICE_DOC_ID_PREFIX);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Publish (or refresh) this device's record.
|
|
70
|
+
*
|
|
71
|
+
* Idempotent: the id is derived from the device's own signing key, so calling
|
|
72
|
+
* this on every unlock keeps one record per device and updates the endpoint id
|
|
73
|
+
* and label in place.
|
|
74
|
+
*
|
|
75
|
+
* Note this does *not* pass `assumeUniqueId`. The id is deterministic rather
|
|
76
|
+
* than random, so two replicas creating it concurrently must share Automerge
|
|
77
|
+
* ancestry in order to converge instead of forking. That code path also
|
|
78
|
+
* forbids `initialValues`, which is why the payload is written with a
|
|
79
|
+
* follow-up `changeDoc` — the same call that refreshes an existing record.
|
|
80
|
+
*/
|
|
81
|
+
async function publishPeerDeviceRecord(input) {
|
|
82
|
+
const fingerprint = await (0, fingerprint_1.fingerprintPublicKeyPem)(input.signingPublicKey, input.subtle);
|
|
83
|
+
const docId = peerDeviceDocumentId(fingerprint);
|
|
84
|
+
const doc = (await input.db.getDocument(docId).catch(() => null)) ??
|
|
85
|
+
(await input.db.createDocument({
|
|
86
|
+
id: docId,
|
|
87
|
+
decryptionKeyId: types_1.DEFAULT_TENANT_KEY_ID,
|
|
88
|
+
signingKeyPair: input.signingKeyPair,
|
|
89
|
+
signingKeyPassword: input.signingKeyPassword,
|
|
90
|
+
}));
|
|
91
|
+
const label = input.label ?? "";
|
|
92
|
+
const current = doc.getData();
|
|
93
|
+
const unchanged = current.type === exports.PEER_DEVICE_TYPE &&
|
|
94
|
+
current.schemaVersion === exports.PEER_DEVICE_SCHEMA_VERSION &&
|
|
95
|
+
current.irohEndpointId === input.irohEndpointId &&
|
|
96
|
+
current.signingPublicKey === input.signingPublicKey &&
|
|
97
|
+
(current.label ?? "") === label;
|
|
98
|
+
// Callers republish on every unlock and after every `userdirectory` sync.
|
|
99
|
+
// Rewriting `updatedAt` unconditionally would append an entry each time,
|
|
100
|
+
// and since that entry syncs and triggers the next refresh, the two halves
|
|
101
|
+
// would keep feeding each other. Only a real change is worth a write.
|
|
102
|
+
if (unchanged) {
|
|
103
|
+
return doc;
|
|
104
|
+
}
|
|
105
|
+
await input.db.changeDoc(doc, (editable) => {
|
|
106
|
+
const data = editable.getData();
|
|
107
|
+
data.form = exports.PEER_DEVICE_FORM;
|
|
108
|
+
data.type = exports.PEER_DEVICE_TYPE;
|
|
109
|
+
data.schemaVersion = exports.PEER_DEVICE_SCHEMA_VERSION;
|
|
110
|
+
data.irohEndpointId = input.irohEndpointId;
|
|
111
|
+
data.signingPublicKey = input.signingPublicKey;
|
|
112
|
+
data.label = label;
|
|
113
|
+
data.updatedAt = Date.now();
|
|
114
|
+
});
|
|
115
|
+
return doc;
|
|
116
|
+
}
|
|
117
|
+
/** Withdraw this device's record, e.g. when signing out of the tenant. */
|
|
118
|
+
async function removePeerDeviceRecord(input) {
|
|
119
|
+
const fingerprint = await (0, fingerprint_1.fingerprintPublicKeyPem)(input.signingPublicKey, input.subtle);
|
|
120
|
+
const docId = peerDeviceDocumentId(fingerprint);
|
|
121
|
+
await input.db.deleteDocument(docId).catch(() => {
|
|
122
|
+
// Already gone, or never published from this device.
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Check one record's claim. Returns the verified record, or `null` when any
|
|
127
|
+
* step of the chain described in the module docs fails.
|
|
128
|
+
*/
|
|
129
|
+
async function verifyPeerDeviceRecord(input) {
|
|
130
|
+
const docId = input.doc.getId();
|
|
131
|
+
const data = input.doc.getData();
|
|
132
|
+
const signingPublicKey = typeof data.signingPublicKey === "string" ? data.signingPublicKey : "";
|
|
133
|
+
const irohEndpointId = typeof data.irohEndpointId === "string" ? data.irohEndpointId.trim().toLowerCase() : "";
|
|
134
|
+
if (data.type !== exports.PEER_DEVICE_TYPE || !signingPublicKey || !/^[0-9a-f]{64}$/.test(irohEndpointId)) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
// 1. The id must be the fingerprint of the key the payload names, so a
|
|
138
|
+
// record cannot sit under a device id it does not belong to.
|
|
139
|
+
const fingerprint = await (0, fingerprint_1.fingerprintPublicKeyPem)(signingPublicKey, input.subtle);
|
|
140
|
+
if (peerDeviceDocumentId(fingerprint) !== docId) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
// 2. That key must be the one that actually created the document. Without
|
|
144
|
+
// this, any member could publish a record naming someone else's key.
|
|
145
|
+
const creatorKey = await resolveCreateSigner(input.db, docId);
|
|
146
|
+
if (!creatorKey || normalizePem(creatorKey) !== normalizePem(signingPublicKey)) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
// 3. And it must still be a live grant, so revoked devices drop out.
|
|
150
|
+
if (!(await input.directory.validatePublicSigningKey(signingPublicKey).catch(() => false))) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
docId,
|
|
155
|
+
irohEndpointId,
|
|
156
|
+
signingPublicKey,
|
|
157
|
+
signingKeyFingerprint: fingerprint,
|
|
158
|
+
label: typeof data.label === "string" && data.label ? data.label : undefined,
|
|
159
|
+
updatedAt: typeof data.updatedAt === "number" ? data.updatedAt : 0,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Every verified peer-device record in this `userdirectory` replica.
|
|
164
|
+
*
|
|
165
|
+
* Requires the tenant `default` key to be unlocked; records that cannot be
|
|
166
|
+
* decrypted are simply absent. Unverifiable records are skipped silently —
|
|
167
|
+
* they are the expected shape of an attack, not an error to surface.
|
|
168
|
+
*/
|
|
169
|
+
async function listPeerDeviceRecords(input) {
|
|
170
|
+
// `idPrefix` matching is boundary-aware: "dev" matches `dev_…`, not `device…`.
|
|
171
|
+
const docIds = await input.db.getAllDocumentIds({ idPrefix: "dev" });
|
|
172
|
+
const records = [];
|
|
173
|
+
for (const docId of docIds) {
|
|
174
|
+
if (!isPeerDeviceDocId(docId)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const doc = await input.db.getDocument(docId).catch(() => null);
|
|
178
|
+
if (!doc) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const record = await verifyPeerDeviceRecord({
|
|
182
|
+
db: input.db,
|
|
183
|
+
directory: input.directory,
|
|
184
|
+
doc,
|
|
185
|
+
subtle: input.subtle,
|
|
186
|
+
});
|
|
187
|
+
if (record && record.irohEndpointId !== input.excludeEndpointId) {
|
|
188
|
+
records.push(record);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return records;
|
|
192
|
+
}
|
|
193
|
+
/** Signing key that wrote the `doc_create` entry, or null when it is not here yet. */
|
|
194
|
+
async function resolveCreateSigner(db, docId) {
|
|
195
|
+
try {
|
|
196
|
+
// History is ordered causally, so `doc_create` is on the first page.
|
|
197
|
+
const page = await db.getDocumentHistoryPage(docId, { limit: 10 });
|
|
198
|
+
const create = page.entries.find((entry) => entry.entryType === "doc_create");
|
|
199
|
+
return create?.changeCreatedByPublicKey ?? null;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** PEM formatting varies by transport; compare the key material, not the wrapping. */
|
|
206
|
+
function normalizePem(pem) {
|
|
207
|
+
return pem.replace(/\s+/g, "");
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=PeerDeviceDocument.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PeerDeviceDocument.js","sourceRoot":"","sources":["../../../src/core/peerdevices/PeerDeviceDocument.ts"],"names":[],"mappings":"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"}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -4821,6 +4821,21 @@ export interface MindooDB {
|
|
|
4821
4821
|
* @return A promise that resolves when the sync is complete.
|
|
4822
4822
|
*/
|
|
4823
4823
|
syncStoreChanges(): Promise<void>;
|
|
4824
|
+
/**
|
|
4825
|
+
* Would the builtin `directory` / `userdirectory` invariant forbid this
|
|
4826
|
+
* entry? For callers that accept entries from somewhere other than a server
|
|
4827
|
+
* — a peer sync — and therefore have to apply the admin/owner rules
|
|
4828
|
+
* themselves before storing anything.
|
|
4829
|
+
*
|
|
4830
|
+
* Deliberately the same check the load path uses, so an entry admitted on
|
|
4831
|
+
* this answer is one that will also materialize. Returns false for databases
|
|
4832
|
+
* without a builtin invariant, so it can be asked unconditionally.
|
|
4833
|
+
*/
|
|
4834
|
+
violatesBuiltinWriteInvariant(entry: {
|
|
4835
|
+
entryType?: string;
|
|
4836
|
+
createdByPublicKey: string;
|
|
4837
|
+
docId?: string;
|
|
4838
|
+
}): Promise<boolean>;
|
|
4824
4839
|
/**
|
|
4825
4840
|
* Reconcile local document visibility with the current tenant KeyBag.
|
|
4826
4841
|
*
|