@enbox/api 0.6.11 → 0.6.13
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.mjs +9 -9
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/dwn-api.js +4 -1
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/typed-enbox.js +135 -20
- package/dist/esm/typed-enbox.js.map +1 -1
- package/dist/types/dwn-api.d.ts +10 -2
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/typed-enbox.d.ts +23 -0
- package/dist/types/typed-enbox.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/dwn-api.ts +11 -3
- package/src/typed-enbox.ts +149 -20
package/src/typed-enbox.ts
CHANGED
|
@@ -639,12 +639,25 @@ export class TypedEnbox<
|
|
|
639
639
|
}
|
|
640
640
|
|
|
641
641
|
// Not installed or definition has changed — configure the new version.
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
|
|
642
|
+
// In delegate mode, the wallet is responsible for installing encrypted
|
|
643
|
+
// protocols during the connect flow. The delegate cannot derive the
|
|
644
|
+
// owner's encryption keys, so configuring here would install the
|
|
645
|
+
// protocol WITHOUT $encryption keys — breaking all encrypted writes.
|
|
646
|
+
const hasEncryptedTypes = Object.values(this._definition.types)
|
|
646
647
|
.some((type) => (type as ProtocolType)?.encryptionRequired === true);
|
|
647
648
|
|
|
649
|
+
if (this._dwn.isDelegate && hasEncryptedTypes) {
|
|
650
|
+
throw new Error(
|
|
651
|
+
`TypedEnbox: Protocol '${this._definition.protocol}' requires ` +
|
|
652
|
+
`encryption but is not installed or has changed. In delegate mode, ` +
|
|
653
|
+
`encrypted protocols must be installed by the wallet during the ` +
|
|
654
|
+
`connect flow. Ensure the sync engine has completed its initial ` +
|
|
655
|
+
`sync before performing record operations.`,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const needsEncryption = !this._dwn.isDelegate && hasEncryptedTypes;
|
|
660
|
+
|
|
648
661
|
const result = await this._dwn.protocols.configure({
|
|
649
662
|
definition : this._definition,
|
|
650
663
|
encryption : options?.encryption ?? (needsEncryption || undefined),
|
|
@@ -765,12 +778,53 @@ export class TypedEnbox<
|
|
|
765
778
|
return;
|
|
766
779
|
}
|
|
767
780
|
|
|
768
|
-
// Not installed — configure it now.
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
|
|
781
|
+
// Not installed locally — configure it now.
|
|
782
|
+
//
|
|
783
|
+
// For delegates: the wallet already installed the protocol with derived
|
|
784
|
+
// `$encryption` keys on the owner's remote DWN. We fetch that remote
|
|
785
|
+
// definition and install it locally so the delegate can encrypt records
|
|
786
|
+
// using the public keys from `$encryption`. This avoids the delegate
|
|
787
|
+
// needing the owner's private X25519 key — only the already-public
|
|
788
|
+
// derived keys are used for ProtocolPath encryption.
|
|
789
|
+
//
|
|
790
|
+
// For owners: derive encryption keys locally via the KMS.
|
|
791
|
+
if (this._dwn.isDelegate) {
|
|
792
|
+
const installed = await this._autoConfigureDelegateProtocol();
|
|
793
|
+
if (installed) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// The remote definition could not be fetched. If this protocol has
|
|
798
|
+
// any types with `encryptionRequired: true`, we MUST fail — silently
|
|
799
|
+
// installing without `$encryption` keys would allow plaintext writes
|
|
800
|
+
// that the owner's remote DWN would later reject, or worse, persist
|
|
801
|
+
// unencrypted sensitive data.
|
|
802
|
+
const hasEncryptedTypes = Object.values(this._definition.types)
|
|
803
|
+
.some((type) => (type as ProtocolType)?.encryptionRequired === true);
|
|
804
|
+
|
|
805
|
+
if (hasEncryptedTypes) {
|
|
806
|
+
throw new Error(
|
|
807
|
+
`TypedEnbox: delegate cannot install protocol '${this._definition.protocol}' ` +
|
|
808
|
+
'because it contains types with encryptionRequired but the owner\'s ' +
|
|
809
|
+
'remote protocol definition (with $encryption keys) could not be fetched. ' +
|
|
810
|
+
'Ensure the owner has installed the protocol on their DWN and that the ' +
|
|
811
|
+
'delegate has network access to the owner\'s DWN endpoints.',
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// No encrypted types — safe to install the app-provided definition
|
|
816
|
+
// as-is (no encryption keys needed).
|
|
817
|
+
const result = await this._dwn.protocols.configure({
|
|
818
|
+
definition: this._definition,
|
|
819
|
+
});
|
|
820
|
+
if (result.status.code === 202) {
|
|
821
|
+
this._configured = true;
|
|
822
|
+
}
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Owner path: derive encryption keys locally when any type needs them.
|
|
827
|
+
const needsEncryption = Object.values(this._definition.types)
|
|
774
828
|
.some((type) => (type as ProtocolType)?.encryptionRequired === true);
|
|
775
829
|
|
|
776
830
|
const result = await this._dwn.protocols.configure({
|
|
@@ -783,6 +837,46 @@ export class TypedEnbox<
|
|
|
783
837
|
}
|
|
784
838
|
}
|
|
785
839
|
|
|
840
|
+
/**
|
|
841
|
+
* For delegates: fetch the owner's protocol definition (with `$encryption`
|
|
842
|
+
* keys) from the remote DWN and install it locally.
|
|
843
|
+
*
|
|
844
|
+
* Returns `true` if the remote definition was successfully installed.
|
|
845
|
+
*/
|
|
846
|
+
private async _autoConfigureDelegateProtocol(): Promise<boolean> {
|
|
847
|
+
try {
|
|
848
|
+
const { protocols: remoteProtocols } = await this._dwn.protocols.query({
|
|
849
|
+
from : this._dwn.connectedDid,
|
|
850
|
+
filter : { protocol: this._definition.protocol },
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
if (remoteProtocols.length === 0) {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// The remote definition includes `$encryption` keys injected by the
|
|
858
|
+
// owner's agent during their `protocols.configure({ encryption: true })`
|
|
859
|
+
// call. Install it as-is on the delegate's local DWN so that:
|
|
860
|
+
// 1. `encryptionRequired` is enforced locally (matching the remote)
|
|
861
|
+
// 2. `$encryption` public keys are available for record encryption
|
|
862
|
+
const remoteDefinition = remoteProtocols[0].definition;
|
|
863
|
+
|
|
864
|
+
const result = await this._dwn.protocols.configure({
|
|
865
|
+
definition: remoteDefinition,
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
if (result.status.code === 202) {
|
|
869
|
+
this._configured = true;
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return false;
|
|
874
|
+
} catch {
|
|
875
|
+
// Network error or grant issue — fall back to local-only install.
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
786
880
|
/**
|
|
787
881
|
* Protocol-scoped record operations.
|
|
788
882
|
*
|
|
@@ -869,10 +963,11 @@ export class TypedEnbox<
|
|
|
869
963
|
const typeName = lastSegment(normalizedPath);
|
|
870
964
|
const typeEntry = this._definition.types[typeName] as ProtocolType | undefined;
|
|
871
965
|
|
|
872
|
-
// Auto-enable encryption when the type requires it
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
|
|
966
|
+
// Auto-enable encryption when the type requires it.
|
|
967
|
+
// Delegates CAN encrypt because the $encryption public keys in the
|
|
968
|
+
// synced protocol definition are sufficient for ProtocolPath
|
|
969
|
+
// encryption — no private key access is needed for writes.
|
|
970
|
+
const autoEncryption = typeEntry?.encryptionRequired === true
|
|
876
971
|
? true : undefined;
|
|
877
972
|
|
|
878
973
|
const { status, record } = await this._dwn.records.write({
|
|
@@ -942,7 +1037,10 @@ export class TypedEnbox<
|
|
|
942
1037
|
const typeEntry = this._definition.types[typeName] as ProtocolType | undefined;
|
|
943
1038
|
|
|
944
1039
|
const queryFilter = mapParentContextId(request?.filter);
|
|
945
|
-
|
|
1040
|
+
// Auto-enable decryption when the type requires it. Delegates use
|
|
1041
|
+
// delivered ProtocolPath keys from the connect flow; the agent layer
|
|
1042
|
+
// resolves the correct key decrypter automatically.
|
|
1043
|
+
const autoEncryption = typeEntry?.encryptionRequired === true
|
|
946
1044
|
? true : undefined;
|
|
947
1045
|
|
|
948
1046
|
const { status, records, cursor } = await this._dwn.records.query({
|
|
@@ -998,7 +1096,9 @@ export class TypedEnbox<
|
|
|
998
1096
|
const typeEntry = this._definition.types[typeName] as ProtocolType | undefined;
|
|
999
1097
|
|
|
1000
1098
|
const readFilter = mapParentContextId(request.filter);
|
|
1001
|
-
|
|
1099
|
+
// Auto-enable decryption when the type requires it. See query()
|
|
1100
|
+
// comment for delegate decryption details.
|
|
1101
|
+
const autoEncryption = typeEntry?.encryptionRequired === true
|
|
1002
1102
|
? true : undefined;
|
|
1003
1103
|
|
|
1004
1104
|
const { status, record } = await this._dwn.records.read({
|
|
@@ -1145,14 +1245,43 @@ function mapParentContextId<T extends Record<string, unknown>>(
|
|
|
1145
1245
|
}
|
|
1146
1246
|
|
|
1147
1247
|
/**
|
|
1148
|
-
* Compares two protocol definitions for
|
|
1149
|
-
* JSON serialization.
|
|
1248
|
+
* Compares two protocol definitions for **logical** equality using
|
|
1249
|
+
* deterministic JSON serialization with `$encryption` blocks stripped.
|
|
1250
|
+
*
|
|
1251
|
+
* When a protocol is installed with `encryption: true`, the agent injects
|
|
1252
|
+
* `$encryption` blocks into the `structure` containing derived public keys.
|
|
1253
|
+
* These blocks are operational metadata — not part of the developer-authored
|
|
1254
|
+
* definition — so they must be ignored during equality checks. Otherwise,
|
|
1255
|
+
* the installed definition (with `$encryption`) would always differ from
|
|
1256
|
+
* the source definition (without `$encryption`), causing false drift
|
|
1257
|
+
* detection and spurious reconfigure warnings.
|
|
1150
1258
|
*
|
|
1151
1259
|
* Keys are sorted recursively so that semantically identical definitions
|
|
1152
1260
|
* with different key ordering are treated as equal.
|
|
1153
1261
|
*/
|
|
1154
|
-
function definitionsEqual(a: unknown, b: unknown): boolean {
|
|
1155
|
-
return stableStringify(a) === stableStringify(b);
|
|
1262
|
+
export function definitionsEqual(a: unknown, b: unknown): boolean {
|
|
1263
|
+
return stableStringify(stripEncryptionBlocks(a)) === stableStringify(stripEncryptionBlocks(b));
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Recursively removes `$encryption` keys from an object tree.
|
|
1268
|
+
* Returns a new object — the original is not mutated.
|
|
1269
|
+
*/
|
|
1270
|
+
function stripEncryptionBlocks(value: unknown): unknown {
|
|
1271
|
+
if (value === null || value === undefined || typeof value !== 'object') {
|
|
1272
|
+
return value;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (Array.isArray(value)) {
|
|
1276
|
+
return value.map((item) => stripEncryptionBlocks(item));
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const result: Record<string, unknown> = {};
|
|
1280
|
+
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
1281
|
+
if (key === '$encryption') { continue; }
|
|
1282
|
+
result[key] = stripEncryptionBlocks(val);
|
|
1283
|
+
}
|
|
1284
|
+
return result;
|
|
1156
1285
|
}
|
|
1157
1286
|
|
|
1158
1287
|
/**
|