@weaveprotocol/core 0.1.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/README.md +1106 -0
- package/dist/elements/auth-styles.d.ts +18 -0
- package/dist/elements/auth-styles.d.ts.map +1 -0
- package/dist/elements/auth-styles.js +150 -0
- package/dist/elements/auth-styles.js.map +1 -0
- package/dist/elements/dom.d.ts +28 -0
- package/dist/elements/dom.d.ts.map +1 -0
- package/dist/elements/dom.js +74 -0
- package/dist/elements/dom.js.map +1 -0
- package/dist/elements/index.d.ts +13 -0
- package/dist/elements/index.d.ts.map +1 -0
- package/dist/elements/index.js +11 -0
- package/dist/elements/index.js.map +1 -0
- package/dist/elements/weave-auth.d.ts +52 -0
- package/dist/elements/weave-auth.d.ts.map +1 -0
- package/dist/elements/weave-auth.js +427 -0
- package/dist/elements/weave-auth.js.map +1 -0
- package/dist/identity/account-store.d.ts +113 -0
- package/dist/identity/account-store.d.ts.map +1 -0
- package/dist/identity/account-store.js +305 -0
- package/dist/identity/account-store.js.map +1 -0
- package/dist/identity/account-vault.d.ts +199 -0
- package/dist/identity/account-vault.d.ts.map +1 -0
- package/dist/identity/account-vault.js +251 -0
- package/dist/identity/account-vault.js.map +1 -0
- package/dist/identity/agent-note.d.ts +19 -0
- package/dist/identity/agent-note.d.ts.map +1 -0
- package/dist/identity/agent-note.js +29 -0
- package/dist/identity/agent-note.js.map +1 -0
- package/dist/identity/crypto-p256.d.ts +7 -0
- package/dist/identity/crypto-p256.d.ts.map +1 -0
- package/dist/identity/crypto-p256.js +84 -0
- package/dist/identity/crypto-p256.js.map +1 -0
- package/dist/identity/device-key.d.ts +60 -0
- package/dist/identity/device-key.d.ts.map +1 -0
- package/dist/identity/device-key.js +103 -0
- package/dist/identity/device-key.js.map +1 -0
- package/dist/identity/did.d.ts +22 -0
- package/dist/identity/did.d.ts.map +1 -0
- package/dist/identity/did.js +37 -0
- package/dist/identity/did.js.map +1 -0
- package/dist/identity/folder-account.d.ts +65 -0
- package/dist/identity/folder-account.d.ts.map +1 -0
- package/dist/identity/folder-account.js +115 -0
- package/dist/identity/folder-account.js.map +1 -0
- package/dist/identity/identity-manager.d.ts +36 -0
- package/dist/identity/identity-manager.d.ts.map +1 -0
- package/dist/identity/identity-manager.js +115 -0
- package/dist/identity/identity-manager.js.map +1 -0
- package/dist/identity/index.d.ts +10 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +10 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/identity/keys.d.ts +24 -0
- package/dist/identity/keys.d.ts.map +1 -0
- package/dist/identity/keys.js +38 -0
- package/dist/identity/keys.js.map +1 -0
- package/dist/identity/pairing.d.ts +83 -0
- package/dist/identity/pairing.d.ts.map +1 -0
- package/dist/identity/pairing.js +120 -0
- package/dist/identity/pairing.js.map +1 -0
- package/dist/identity/passkey-diagnostics.d.ts +54 -0
- package/dist/identity/passkey-diagnostics.d.ts.map +1 -0
- package/dist/identity/passkey-diagnostics.js +178 -0
- package/dist/identity/passkey-diagnostics.js.map +1 -0
- package/dist/identity/recovery-code.d.ts +50 -0
- package/dist/identity/recovery-code.d.ts.map +1 -0
- package/dist/identity/recovery-code.js +116 -0
- package/dist/identity/recovery-code.js.map +1 -0
- package/dist/identity/root-signer.d.ts +59 -0
- package/dist/identity/root-signer.d.ts.map +1 -0
- package/dist/identity/root-signer.js +44 -0
- package/dist/identity/root-signer.js.map +1 -0
- package/dist/identity/ucan.d.ts +164 -0
- package/dist/identity/ucan.d.ts.map +1 -0
- package/dist/identity/ucan.js +273 -0
- package/dist/identity/ucan.js.map +1 -0
- package/dist/identity/webauthn.d.ts +87 -0
- package/dist/identity/webauthn.d.ts.map +1 -0
- package/dist/identity/webauthn.js +155 -0
- package/dist/identity/webauthn.js.map +1 -0
- package/dist/index.d.ts +116 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +85 -0
- package/dist/index.js.map +1 -0
- package/dist/network/index.d.ts +17 -0
- package/dist/network/index.d.ts.map +1 -0
- package/dist/network/index.js +10 -0
- package/dist/network/index.js.map +1 -0
- package/dist/network/introductions.d.ts +85 -0
- package/dist/network/introductions.d.ts.map +1 -0
- package/dist/network/introductions.js +94 -0
- package/dist/network/introductions.js.map +1 -0
- package/dist/network/local-transport.d.ts +22 -0
- package/dist/network/local-transport.d.ts.map +1 -0
- package/dist/network/local-transport.js +55 -0
- package/dist/network/local-transport.js.map +1 -0
- package/dist/network/mesh.d.ts +31 -0
- package/dist/network/mesh.d.ts.map +1 -0
- package/dist/network/mesh.js +332 -0
- package/dist/network/mesh.js.map +1 -0
- package/dist/network/multi-signaling.d.ts +29 -0
- package/dist/network/multi-signaling.d.ts.map +1 -0
- package/dist/network/multi-signaling.js +125 -0
- package/dist/network/multi-signaling.js.map +1 -0
- package/dist/network/network-manager.d.ts +37 -0
- package/dist/network/network-manager.d.ts.map +1 -0
- package/dist/network/network-manager.js +64 -0
- package/dist/network/network-manager.js.map +1 -0
- package/dist/network/peer-auth.d.ts +98 -0
- package/dist/network/peer-auth.d.ts.map +1 -0
- package/dist/network/peer-auth.js +86 -0
- package/dist/network/peer-auth.js.map +1 -0
- package/dist/network/rtc-transport.d.ts +18 -0
- package/dist/network/rtc-transport.d.ts.map +1 -0
- package/dist/network/rtc-transport.js +149 -0
- package/dist/network/rtc-transport.js.map +1 -0
- package/dist/network/signaling.d.ts +40 -0
- package/dist/network/signaling.d.ts.map +1 -0
- package/dist/network/signaling.js +124 -0
- package/dist/network/signaling.js.map +1 -0
- package/dist/network/transport.d.ts +50 -0
- package/dist/network/transport.d.ts.map +1 -0
- package/dist/network/transport.js +13 -0
- package/dist/network/transport.js.map +1 -0
- package/dist/network/ws-transport.d.ts +42 -0
- package/dist/network/ws-transport.d.ts.map +1 -0
- package/dist/network/ws-transport.js +194 -0
- package/dist/network/ws-transport.js.map +1 -0
- package/dist/node/actions.d.ts +62 -0
- package/dist/node/actions.d.ts.map +1 -0
- package/dist/node/actions.js +500 -0
- package/dist/node/actions.js.map +1 -0
- package/dist/node/carrier.d.ts +85 -0
- package/dist/node/carrier.d.ts.map +1 -0
- package/dist/node/carrier.js +192 -0
- package/dist/node/carrier.js.map +1 -0
- package/dist/node/copy.d.ts +38 -0
- package/dist/node/copy.d.ts.map +1 -0
- package/dist/node/copy.js +53 -0
- package/dist/node/copy.js.map +1 -0
- package/dist/node/index.d.ts +16 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +11 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/node.d.ts +12 -0
- package/dist/node/node.d.ts.map +1 -0
- package/dist/node/node.js +872 -0
- package/dist/node/node.js.map +1 -0
- package/dist/node/space-runtime.d.ts +144 -0
- package/dist/node/space-runtime.d.ts.map +1 -0
- package/dist/node/space-runtime.js +1221 -0
- package/dist/node/space-runtime.js.map +1 -0
- package/dist/node/stores.d.ts +45 -0
- package/dist/node/stores.d.ts.map +1 -0
- package/dist/node/stores.js +31 -0
- package/dist/node/stores.js.map +1 -0
- package/dist/node/types.d.ts +508 -0
- package/dist/node/types.d.ts.map +1 -0
- package/dist/node/types.js +2 -0
- package/dist/node/types.js.map +1 -0
- package/dist/privacy/index.d.ts +4 -0
- package/dist/privacy/index.d.ts.map +1 -0
- package/dist/privacy/index.js +4 -0
- package/dist/privacy/index.js.map +1 -0
- package/dist/privacy/key-distribution.d.ts +42 -0
- package/dist/privacy/key-distribution.d.ts.map +1 -0
- package/dist/privacy/key-distribution.js +50 -0
- package/dist/privacy/key-distribution.js.map +1 -0
- package/dist/privacy/privacy-guard.d.ts +24 -0
- package/dist/privacy/privacy-guard.d.ts.map +1 -0
- package/dist/privacy/privacy-guard.js +87 -0
- package/dist/privacy/privacy-guard.js.map +1 -0
- package/dist/privacy/space-encryption.d.ts +48 -0
- package/dist/privacy/space-encryption.d.ts.map +1 -0
- package/dist/privacy/space-encryption.js +61 -0
- package/dist/privacy/space-encryption.js.map +1 -0
- package/dist/query/engine.d.ts +27 -0
- package/dist/query/engine.d.ts.map +1 -0
- package/dist/query/engine.js +84 -0
- package/dist/query/engine.js.map +1 -0
- package/dist/query/filter.d.ts +15 -0
- package/dist/query/filter.d.ts.map +1 -0
- package/dist/query/filter.js +207 -0
- package/dist/query/filter.js.map +1 -0
- package/dist/query/types.d.ts +144 -0
- package/dist/query/types.d.ts.map +1 -0
- package/dist/query/types.js +22 -0
- package/dist/query/types.js.map +1 -0
- package/dist/react/context.d.ts +81 -0
- package/dist/react/context.d.ts.map +1 -0
- package/dist/react/context.js +95 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/index.d.ts +40 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +36 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/use-live.d.ts +16 -0
- package/dist/react/use-live.d.ts.map +1 -0
- package/dist/react/use-live.js +55 -0
- package/dist/react/use-live.js.map +1 -0
- package/dist/react/use-query.d.ts +17 -0
- package/dist/react/use-query.d.ts.map +1 -0
- package/dist/react/use-query.js +23 -0
- package/dist/react/use-query.js.map +1 -0
- package/dist/react/use-space.d.ts +29 -0
- package/dist/react/use-space.d.ts.map +1 -0
- package/dist/react/use-space.js +48 -0
- package/dist/react/use-space.js.map +1 -0
- package/dist/react/use-spaces.d.ts +19 -0
- package/dist/react/use-spaces.d.ts.map +1 -0
- package/dist/react/use-spaces.js +55 -0
- package/dist/react/use-spaces.js.map +1 -0
- package/dist/react/use-weave-auth.d.ts +10 -0
- package/dist/react/use-weave-auth.d.ts.map +1 -0
- package/dist/react/use-weave-auth.js +15 -0
- package/dist/react/use-weave-auth.js.map +1 -0
- package/dist/react/weave-auth.d.ts +17 -0
- package/dist/react/weave-auth.d.ts.map +1 -0
- package/dist/react/weave-auth.js +38 -0
- package/dist/react/weave-auth.js.map +1 -0
- package/dist/records/describe.d.ts +38 -0
- package/dist/records/describe.d.ts.map +1 -0
- package/dist/records/describe.js +101 -0
- package/dist/records/describe.js.map +1 -0
- package/dist/records/links.d.ts +14 -0
- package/dist/records/links.d.ts.map +1 -0
- package/dist/records/links.js +25 -0
- package/dist/records/links.js.map +1 -0
- package/dist/records/rules.d.ts +59 -0
- package/dist/records/rules.d.ts.map +1 -0
- package/dist/records/rules.js +118 -0
- package/dist/records/rules.js.map +1 -0
- package/dist/records/version.d.ts +54 -0
- package/dist/records/version.d.ts.map +1 -0
- package/dist/records/version.js +70 -0
- package/dist/records/version.js.map +1 -0
- package/dist/schema/collection-def.d.ts +96 -0
- package/dist/schema/collection-def.d.ts.map +1 -0
- package/dist/schema/collection-def.js +272 -0
- package/dist/schema/collection-def.js.map +1 -0
- package/dist/schema/expression.d.ts +69 -0
- package/dist/schema/expression.d.ts.map +1 -0
- package/dist/schema/expression.js +92 -0
- package/dist/schema/expression.js.map +1 -0
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +4 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/schema-engine.d.ts +42 -0
- package/dist/schema/schema-engine.d.ts.map +1 -0
- package/dist/schema/schema-engine.js +36 -0
- package/dist/schema/schema-engine.js.map +1 -0
- package/dist/schema/signer.d.ts +27 -0
- package/dist/schema/signer.d.ts.map +1 -0
- package/dist/schema/signer.js +36 -0
- package/dist/schema/signer.js.map +1 -0
- package/dist/schemas/apps.d.ts +88 -0
- package/dist/schemas/apps.d.ts.map +1 -0
- package/dist/schemas/apps.js +168 -0
- package/dist/schemas/apps.js.map +1 -0
- package/dist/schemas/index.d.ts +410 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +208 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/screens.d.ts +67 -0
- package/dist/schemas/screens.d.ts.map +1 -0
- package/dist/schemas/screens.js +242 -0
- package/dist/schemas/screens.js.map +1 -0
- package/dist/session/agent-link.d.ts +79 -0
- package/dist/session/agent-link.d.ts.map +1 -0
- package/dist/session/agent-link.js +251 -0
- package/dist/session/agent-link.js.map +1 -0
- package/dist/session/auth.d.ts +233 -0
- package/dist/session/auth.d.ts.map +1 -0
- package/dist/session/auth.js +783 -0
- package/dist/session/auth.js.map +1 -0
- package/dist/session/connect.d.ts +211 -0
- package/dist/session/connect.d.ts.map +1 -0
- package/dist/session/connect.js +355 -0
- package/dist/session/connect.js.map +1 -0
- package/dist/session/connection.d.ts +70 -0
- package/dist/session/connection.d.ts.map +1 -0
- package/dist/session/connection.js +120 -0
- package/dist/session/connection.js.map +1 -0
- package/dist/session/credentials.d.ts +44 -0
- package/dist/session/credentials.d.ts.map +1 -0
- package/dist/session/credentials.js +60 -0
- package/dist/session/credentials.js.map +1 -0
- package/dist/session/index.d.ts +25 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +18 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/pairing.d.ts +59 -0
- package/dist/session/pairing.d.ts.map +1 -0
- package/dist/session/pairing.js +144 -0
- package/dist/session/pairing.js.map +1 -0
- package/dist/session/places.d.ts +62 -0
- package/dist/session/places.d.ts.map +1 -0
- package/dist/session/places.js +101 -0
- package/dist/session/places.js.map +1 -0
- package/dist/session/stay-signed-in.d.ts +37 -0
- package/dist/session/stay-signed-in.d.ts.map +1 -0
- package/dist/session/stay-signed-in.js +124 -0
- package/dist/session/stay-signed-in.js.map +1 -0
- package/dist/space/account-registry.d.ts +64 -0
- package/dist/space/account-registry.d.ts.map +1 -0
- package/dist/space/account-registry.js +66 -0
- package/dist/space/account-registry.js.map +1 -0
- package/dist/space/index.d.ts +3 -0
- package/dist/space/index.d.ts.map +1 -0
- package/dist/space/index.js +3 -0
- package/dist/space/index.js.map +1 -0
- package/dist/space/pass.d.ts +63 -0
- package/dist/space/pass.d.ts.map +1 -0
- package/dist/space/pass.js +54 -0
- package/dist/space/pass.js.map +1 -0
- package/dist/space/presets.d.ts +31 -0
- package/dist/space/presets.d.ts.map +1 -0
- package/dist/space/presets.js +24 -0
- package/dist/space/presets.js.map +1 -0
- package/dist/space/roles.d.ts +186 -0
- package/dist/space/roles.d.ts.map +1 -0
- package/dist/space/roles.js +500 -0
- package/dist/space/roles.js.map +1 -0
- package/dist/space/space-access.d.ts +80 -0
- package/dist/space/space-access.d.ts.map +1 -0
- package/dist/space/space-access.js +135 -0
- package/dist/space/space-access.js.map +1 -0
- package/dist/space/space-manager.d.ts +87 -0
- package/dist/space/space-manager.d.ts.map +1 -0
- package/dist/space/space-manager.js +187 -0
- package/dist/space/space-manager.js.map +1 -0
- package/dist/storage/directory-access.d.ts +78 -0
- package/dist/storage/directory-access.d.ts.map +1 -0
- package/dist/storage/directory-access.js +164 -0
- package/dist/storage/directory-access.js.map +1 -0
- package/dist/storage/encrypted-adapter.d.ts +49 -0
- package/dist/storage/encrypted-adapter.d.ts.map +1 -0
- package/dist/storage/encrypted-adapter.js +106 -0
- package/dist/storage/encrypted-adapter.js.map +1 -0
- package/dist/storage/folder-adapter.d.ts +105 -0
- package/dist/storage/folder-adapter.d.ts.map +1 -0
- package/dist/storage/folder-adapter.js +280 -0
- package/dist/storage/folder-adapter.js.map +1 -0
- package/dist/storage/folder-reconcile.d.ts +53 -0
- package/dist/storage/folder-reconcile.d.ts.map +1 -0
- package/dist/storage/folder-reconcile.js +65 -0
- package/dist/storage/folder-reconcile.js.map +1 -0
- package/dist/storage/index.d.ts +10 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +9 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/indexeddb-adapter.d.ts +12 -0
- package/dist/storage/indexeddb-adapter.d.ts.map +1 -0
- package/dist/storage/indexeddb-adapter.js +190 -0
- package/dist/storage/indexeddb-adapter.js.map +1 -0
- package/dist/storage/mst.d.ts +121 -0
- package/dist/storage/mst.d.ts.map +1 -0
- package/dist/storage/mst.js +402 -0
- package/dist/storage/mst.js.map +1 -0
- package/dist/storage/storage-provider.d.ts +85 -0
- package/dist/storage/storage-provider.d.ts.map +1 -0
- package/dist/storage/storage-provider.js +220 -0
- package/dist/storage/storage-provider.js.map +1 -0
- package/dist/sync/anti-entropy.d.ts +49 -0
- package/dist/sync/anti-entropy.d.ts.map +1 -0
- package/dist/sync/anti-entropy.js +62 -0
- package/dist/sync/anti-entropy.js.map +1 -0
- package/dist/sync/index.d.ts +4 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +4 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/sync-engine.d.ts +71 -0
- package/dist/sync/sync-engine.d.ts.map +1 -0
- package/dist/sync/sync-engine.js +309 -0
- package/dist/sync/sync-engine.js.map +1 -0
- package/dist/sync/sync-messages.d.ts +75 -0
- package/dist/sync/sync-messages.d.ts.map +1 -0
- package/dist/sync/sync-messages.js +12 -0
- package/dist/sync/sync-messages.js.map +1 -0
- package/dist/types.d.ts +243 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/encoding.d.ts +63 -0
- package/dist/utils/encoding.d.ts.map +1 -0
- package/dist/utils/encoding.js +130 -0
- package/dist/utils/encoding.js.map +1 -0
- package/dist/utils/errors.d.ts +43 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +28 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/events.d.ts +19 -0
- package/dist/utils/events.d.ts.map +1 -0
- package/dist/utils/events.js +27 -0
- package/dist/utils/events.js.map +1 -0
- package/dist/utils/hash.d.ts +23 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +49 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/index.d.ts +9 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/validation/capability-gate.d.ts +50 -0
- package/dist/validation/capability-gate.d.ts.map +1 -0
- package/dist/validation/capability-gate.js +71 -0
- package/dist/validation/capability-gate.js.map +1 -0
- package/dist/validation/crypto-gate.d.ts +16 -0
- package/dist/validation/crypto-gate.d.ts.map +1 -0
- package/dist/validation/crypto-gate.js +37 -0
- package/dist/validation/crypto-gate.js.map +1 -0
- package/dist/validation/index.d.ts +6 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +6 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/stateful-gate.d.ts +15 -0
- package/dist/validation/stateful-gate.d.ts.map +1 -0
- package/dist/validation/stateful-gate.js +50 -0
- package/dist/validation/stateful-gate.js.map +1 -0
- package/dist/validation/structural-gate.d.ts +18 -0
- package/dist/validation/structural-gate.d.ts.map +1 -0
- package/dist/validation/structural-gate.js +54 -0
- package/dist/validation/structural-gate.js.map +1 -0
- package/dist/validation/validation-engine.d.ts +28 -0
- package/dist/validation/validation-engine.d.ts.map +1 -0
- package/dist/validation/validation-engine.js +38 -0
- package/dist/validation/validation-engine.js.map +1 -0
- package/package.json +107 -0
|
@@ -0,0 +1,1221 @@
|
|
|
1
|
+
import { createSchemaEngine } from '../schema/schema-engine.js';
|
|
2
|
+
import { parseUCAN, resolveDelegationRoot } from '../identity/ucan.js';
|
|
3
|
+
import { isAgentNote } from '../identity/agent-note.js';
|
|
4
|
+
import { didToPublicKey } from '../identity/did.js';
|
|
5
|
+
import { createExpression } from '../schema/expression.js';
|
|
6
|
+
import { createStorageProvider } from '../storage/storage-provider.js';
|
|
7
|
+
import { newRecordKey, nextVersion, RECORD_KEY_PATTERN } from '../records/version.js';
|
|
8
|
+
import { checkLinks } from '../records/links.js';
|
|
9
|
+
import { allows, changedFixedField, describeWho, onePerKey, permissionName } from '../records/rules.js';
|
|
10
|
+
import { reconcileFolder } from '../storage/folder-reconcile.js';
|
|
11
|
+
import { createCryptoGate } from '../validation/crypto-gate.js';
|
|
12
|
+
import { createStructuralGate } from '../validation/structural-gate.js';
|
|
13
|
+
import { createStatefulGate } from '../validation/stateful-gate.js';
|
|
14
|
+
import { createCapabilityGate } from '../validation/capability-gate.js';
|
|
15
|
+
import { createValidationEngine } from '../validation/validation-engine.js';
|
|
16
|
+
import { encryptExpression, decryptExpression } from '../privacy/space-encryption.js';
|
|
17
|
+
import { createNetworkManager } from '../network/network-manager.js';
|
|
18
|
+
import { createMesh } from '../network/mesh.js';
|
|
19
|
+
import { createWebSocketTransport } from '../network/ws-transport.js';
|
|
20
|
+
import { createClientAuth, createMeshAuth } from '../network/peer-auth.js';
|
|
21
|
+
import { deriveInviteKey, deriveReadKey, generateInviteSecret, inviteKey as inviteRecordKey, memberKey, revokeKey, roleKey, signInvite, verifyInvite, } from '../space/space-access.js';
|
|
22
|
+
import { ACCESS_COLLECTIONS, DEFINITION_COLLECTION, INVITE_COLLECTION, MEMBER_COLLECTION, REVOKE_COLLECTION, ROLE_COLLECTION, checkRole, replayAccess, roleHolds, standing, } from '../space/roles.js';
|
|
23
|
+
import { createSyncEngine } from '../sync/sync-engine.js';
|
|
24
|
+
import { CATALOG_COLLECTION, checkStoredCollection, toJsonSchema, validateJsonSchema, } from '../schema/collection-def.js';
|
|
25
|
+
import { CARRIER_COLLECTION, MEMBERSHIP_COLLECTION, PROFILE_COLLECTION } from '../space/account-registry.js';
|
|
26
|
+
import { PASS_COLLECTION } from '../space/pass.js';
|
|
27
|
+
import { base32Encode, cidFromBytes, sha256 } from '../utils/hash.js';
|
|
28
|
+
import { utf8Encode } from '../utils/encoding.js';
|
|
29
|
+
/** Collections the node writes itself, through their own calls — never through `put` */
|
|
30
|
+
const MANAGED = new Set([MEMBERSHIP_COLLECTION, PROFILE_COLLECTION, CARRIER_COLLECTION, PASS_COLLECTION, ...ACCESS_COLLECTIONS]);
|
|
31
|
+
/**
|
|
32
|
+
* Access records a peer without the space key must still be able to judge:
|
|
33
|
+
* who holds which role decides every write. Kept in the clear, even in a
|
|
34
|
+
* private space. Collection definitions stay sealed — a peer that cannot
|
|
35
|
+
* read the records has no use for their rules.
|
|
36
|
+
*/
|
|
37
|
+
const IN_THE_CLEAR = new Set([ROLE_COLLECTION, MEMBER_COLLECTION, INVITE_COLLECTION, REVOKE_COLLECTION]);
|
|
38
|
+
/** Which collection each access record key belongs to */
|
|
39
|
+
const ACCESS_KEYS = [
|
|
40
|
+
['role:', ROLE_COLLECTION],
|
|
41
|
+
['member:', MEMBER_COLLECTION],
|
|
42
|
+
['invite:', INVITE_COLLECTION],
|
|
43
|
+
['revoke:', REVOKE_COLLECTION],
|
|
44
|
+
['collection:', DEFINITION_COLLECTION],
|
|
45
|
+
];
|
|
46
|
+
/** How many records a keep list may name */
|
|
47
|
+
const MAX_KEEP = 10_000;
|
|
48
|
+
/**
|
|
49
|
+
* The key of a person's profile record in a space: one per identity, named by
|
|
50
|
+
* a hash of it (record keys are lower case; a did:key is not).
|
|
51
|
+
*/
|
|
52
|
+
export async function profileKey(did) {
|
|
53
|
+
const digest = await sha256(new TextEncoder().encode(did));
|
|
54
|
+
return `profile:${Array.from(digest.subarray(0, 20), (b) => b.toString(16).padStart(2, '0')).join('')}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The room a space's peers meet in on a relay. A hash of the space's id, so a
|
|
58
|
+
* relay learns which connections belong together but not which space they are
|
|
59
|
+
* — it cannot match the room to an invite, or to the same space elsewhere.
|
|
60
|
+
*/
|
|
61
|
+
export async function relayRoom(spaceId) {
|
|
62
|
+
return base32Encode((await sha256(new TextEncoder().encode(`weave-room/v1|${spaceId}`))).subarray(0, 20));
|
|
63
|
+
}
|
|
64
|
+
/** The one mesh a node's spaces share, when it has relays to meet on */
|
|
65
|
+
export function meshFor(network, did) {
|
|
66
|
+
if (!network?.relays?.length)
|
|
67
|
+
return undefined;
|
|
68
|
+
return createMesh({ did, relays: network.relays, ...(network.iceServers ? { iceServers: network.iceServers } : {}) });
|
|
69
|
+
}
|
|
70
|
+
/** The capability a record in a space requires */
|
|
71
|
+
export const writeCapability = (spaceId) => ({
|
|
72
|
+
with: `space:${spaceId}`,
|
|
73
|
+
can: 'expression/write',
|
|
74
|
+
});
|
|
75
|
+
/** The CID a note is known by — what a revoke names */
|
|
76
|
+
export const noteCid = (encoded) => cidFromBytes(utf8Encode(encoded));
|
|
77
|
+
const STANDS = { ok: true };
|
|
78
|
+
function looksEncrypted(body) {
|
|
79
|
+
const envelope = body;
|
|
80
|
+
return typeof envelope?.ciphertext === 'string' && typeof envelope?.iv === 'string';
|
|
81
|
+
}
|
|
82
|
+
function isFolderAdapter(adapter) {
|
|
83
|
+
return typeof adapter.reload === 'function';
|
|
84
|
+
}
|
|
85
|
+
const isStringList = (value) => Array.isArray(value) && value.every((v) => typeof v === 'string');
|
|
86
|
+
export async function openSpaceRuntime(deps) {
|
|
87
|
+
const { record, provider, signer, schemas, session, emit } = deps;
|
|
88
|
+
const { space, key } = record;
|
|
89
|
+
/** An invite waiting to be used — forgotten here once it is, whatever the stored record still says */
|
|
90
|
+
let waitingInvite = record.invite !== null;
|
|
91
|
+
const adapter = await deps.stores(`spaces/${space.id}`);
|
|
92
|
+
// Tabs sharing this browser's store are covered by compaction's grace period.
|
|
93
|
+
// A folder's other writers — a sync service bringing another device's tree
|
|
94
|
+
// back hours later — are not, so a folder keeps its old nodes.
|
|
95
|
+
const shared = isFolderAdapter(adapter);
|
|
96
|
+
const storage = createStorageProvider(adapter, shared ? {} : { compactEvery: 256 });
|
|
97
|
+
if (!shared)
|
|
98
|
+
void storage.compact().catch(() => { });
|
|
99
|
+
const resolvePublicKey = async (did) => provider.importPublicKey(didToPublicKey(did).publicKeyBytes);
|
|
100
|
+
const validation = createValidationEngine({
|
|
101
|
+
cryptoGate: createCryptoGate(provider),
|
|
102
|
+
// Shape is not a reason to refuse a record on arrival. Whether it fits can
|
|
103
|
+
// depend on which definition, or which app's schema, a node happens to have;
|
|
104
|
+
// refusing would leave nodes that disagree forever. Shape is checked when a
|
|
105
|
+
// record is written here, and reported as `conforms` when it is read.
|
|
106
|
+
structuralGate: createStructuralGate(createSchemaEngine(), { allowUnknownCollections: true }),
|
|
107
|
+
statefulGate: createStatefulGate(),
|
|
108
|
+
// The note behind a key must cover this space. Whether its account may
|
|
109
|
+
// write here is the access history's question, asked below.
|
|
110
|
+
capabilityGate: createCapabilityGate({ provider, requiredCapability: () => writeCapability(space.id) }),
|
|
111
|
+
resolvePublicKey,
|
|
112
|
+
getExpression: (id) => storage.getExpression(id),
|
|
113
|
+
});
|
|
114
|
+
// A signature never changes, so a verdict on one holds forever. Keyed on the
|
|
115
|
+
// id *and* the signature: the id does not cover it, so a copy with a broken
|
|
116
|
+
// signature shares the genuine one's id. Only passes are kept — a failure
|
|
117
|
+
// remembered by id would let anyone who sends a mangled copy first get the
|
|
118
|
+
// real record refused; and failures are what a stranger can mint for free.
|
|
119
|
+
const verdicts = new Map();
|
|
120
|
+
const verdictKey = (e) => `${e.id}|${e.signature}`;
|
|
121
|
+
/** Whether a version is signed, and by a key whose note covers this space — and which account that is */
|
|
122
|
+
async function judge(expression) {
|
|
123
|
+
const cached = verdicts.get(verdictKey(expression));
|
|
124
|
+
if (cached)
|
|
125
|
+
return cached;
|
|
126
|
+
const result = await validation.validate(expression);
|
|
127
|
+
let verdict;
|
|
128
|
+
if (!result.valid) {
|
|
129
|
+
const failed = result.gates.find((gate) => !gate.passed);
|
|
130
|
+
verdict = { verified: false, root: null, ...(failed?.reason ? { reason: failed.reason } : {}) };
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const at = Math.floor(Date.parse(expression.createdAt) / 1000);
|
|
134
|
+
const chain = expression.proof ? await resolveDelegationRoot(expression.proof, () => null, provider, { at }) : null;
|
|
135
|
+
verdict = { verified: true, root: chain?.valid ? chain.rootDid : expression.author };
|
|
136
|
+
verdicts.set(verdictKey(expression), verdict);
|
|
137
|
+
}
|
|
138
|
+
return verdict;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* A version's content: its body and its links. In a private space both are
|
|
142
|
+
* sealed together, so a relay learns neither what a record says nor what it
|
|
143
|
+
* points at.
|
|
144
|
+
*/
|
|
145
|
+
async function openBody(expression) {
|
|
146
|
+
if (!looksEncrypted(expression.body))
|
|
147
|
+
return { body: expression.body, links: expression.links ?? [], encrypted: false };
|
|
148
|
+
if (!key)
|
|
149
|
+
return { body: null, links: [], encrypted: true };
|
|
150
|
+
try {
|
|
151
|
+
const opened = (await decryptExpression(expression, key)).body;
|
|
152
|
+
const links = checkLinks(opened?.links ?? []) === null ? (opened?.links ?? []) : [];
|
|
153
|
+
return { body: opened?.body ?? null, links, encrypted: true };
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return { body: null, links: [], encrypted: true };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Whether `first` can be the first version of the record `version` belongs
|
|
161
|
+
* to. A later version names its first version itself, and a check that took
|
|
162
|
+
* that on trust would let anyone point at a record with no rules — or in
|
|
163
|
+
* another collection — and be judged by that instead.
|
|
164
|
+
*/
|
|
165
|
+
const firstOf = (version, first) => !!first && first.seq === 0 && first.key === version.key && first.collection === version.collection;
|
|
166
|
+
/** A record's first version: the version itself at seq 0, else the one kept apart. */
|
|
167
|
+
async function genesisOf(version) {
|
|
168
|
+
return version.seq === 0 ? version : storage.getGenesis(version.key);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Whether a version is one this node should show. A version whose collection
|
|
172
|
+
* differs from its record's first version is someone reusing a key: it is
|
|
173
|
+
* ignored on read, the same way on every node once they hold the same data.
|
|
174
|
+
*/
|
|
175
|
+
async function consistent(version) {
|
|
176
|
+
if (version.seq === 0)
|
|
177
|
+
return true;
|
|
178
|
+
const genesis = await storage.getGenesis(version.key);
|
|
179
|
+
return !genesis || genesis.collection === version.collection;
|
|
180
|
+
}
|
|
181
|
+
// ─── The access history ────────────────────────────────────────────
|
|
182
|
+
//
|
|
183
|
+
// Every version in the access collections, kept whole (`retain`), turned
|
|
184
|
+
// into plain events and replayed. Rebuilt when records change; everything
|
|
185
|
+
// about who may do what is read from the replay.
|
|
186
|
+
const accessGenesis = {
|
|
187
|
+
id: space.id,
|
|
188
|
+
creator: space.creator,
|
|
189
|
+
roles: space.roles,
|
|
190
|
+
creatorRole: space.creatorRole,
|
|
191
|
+
};
|
|
192
|
+
/** Events, by version id — building one checks signatures and invites, so they are kept */
|
|
193
|
+
const events = new Map();
|
|
194
|
+
async function toEvent(version) {
|
|
195
|
+
const cacheKey = `${version.id}|${version.signature}`;
|
|
196
|
+
if (events.has(cacheKey))
|
|
197
|
+
return events.get(cacheKey);
|
|
198
|
+
const event = await buildEvent(version);
|
|
199
|
+
// A failed signature is what a stranger can mint for free; only settled answers from a valid one are kept.
|
|
200
|
+
if (event || (await judge(version)).verified)
|
|
201
|
+
events.set(cacheKey, event);
|
|
202
|
+
return event;
|
|
203
|
+
}
|
|
204
|
+
async function buildEvent(version) {
|
|
205
|
+
if (!ACCESS_COLLECTIONS.has(version.collection) || !version.retain)
|
|
206
|
+
return null;
|
|
207
|
+
// An agent never changes the space's collections or who may do what: a person does.
|
|
208
|
+
if (isAgentNote(version.proof))
|
|
209
|
+
return null;
|
|
210
|
+
const verdict = await judge(version);
|
|
211
|
+
if (!verdict.verified || !verdict.root)
|
|
212
|
+
return null;
|
|
213
|
+
const seen = version.seen ?? [];
|
|
214
|
+
const base = { id: version.id, key: version.key, root: verdict.root, seen };
|
|
215
|
+
if (version.collection === DEFINITION_COLLECTION) {
|
|
216
|
+
if (!version.key.startsWith('collection:'))
|
|
217
|
+
return null;
|
|
218
|
+
return { ...base, keep: [], kind: 'definition', name: version.key.slice('collection:'.length), deleted: !!version.deleted };
|
|
219
|
+
}
|
|
220
|
+
// Everything else in the history is taken away by a change, never deleted: a delete has no body to carry a keep list.
|
|
221
|
+
if (version.deleted || looksEncrypted(version.body))
|
|
222
|
+
return null;
|
|
223
|
+
const body = version.body;
|
|
224
|
+
if (!body || typeof body !== 'object')
|
|
225
|
+
return null;
|
|
226
|
+
const keep = isStringList(body.keep) ? body.keep.slice(0, MAX_KEEP) : [];
|
|
227
|
+
switch (version.collection) {
|
|
228
|
+
case ROLE_COLLECTION: {
|
|
229
|
+
const name = typeof body.name === 'string' ? body.name : '';
|
|
230
|
+
if (version.key !== roleKey(name))
|
|
231
|
+
return null;
|
|
232
|
+
if (body.removed === true)
|
|
233
|
+
return { ...base, keep, kind: 'role', name, role: null };
|
|
234
|
+
const role = {
|
|
235
|
+
name,
|
|
236
|
+
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
237
|
+
rank: body.rank,
|
|
238
|
+
permissions: body.permissions,
|
|
239
|
+
};
|
|
240
|
+
return checkRole(role) ? null : { ...base, keep, kind: 'role', name, role };
|
|
241
|
+
}
|
|
242
|
+
case MEMBER_COLLECTION: {
|
|
243
|
+
const did = body.did;
|
|
244
|
+
const role = body.role;
|
|
245
|
+
if (typeof did !== 'string' || !(role === null || typeof role === 'string'))
|
|
246
|
+
return null;
|
|
247
|
+
if (version.key !== (await memberKey(did)))
|
|
248
|
+
return null;
|
|
249
|
+
const invite = body.invite;
|
|
250
|
+
let viaInvite;
|
|
251
|
+
if (invite !== undefined) {
|
|
252
|
+
if (typeof invite?.key !== 'string' || typeof invite.signature !== 'string')
|
|
253
|
+
return null;
|
|
254
|
+
if (!(await verifyInvite(space.id, did, invite.key, invite.signature, provider)))
|
|
255
|
+
return null;
|
|
256
|
+
viaInvite = invite.key;
|
|
257
|
+
}
|
|
258
|
+
return { ...base, keep, kind: 'member', did, role, ...(viaInvite !== undefined ? { viaInvite } : {}) };
|
|
259
|
+
}
|
|
260
|
+
case INVITE_COLLECTION: {
|
|
261
|
+
const inviteDid = body.key;
|
|
262
|
+
if (typeof inviteDid !== 'string' || typeof body.role !== 'string' || typeof body.open !== 'boolean')
|
|
263
|
+
return null;
|
|
264
|
+
if (version.key !== (await inviteRecordKey(inviteDid)))
|
|
265
|
+
return null;
|
|
266
|
+
return { ...base, keep, kind: 'invite', inviteKey: inviteDid, role: body.role, open: body.open };
|
|
267
|
+
}
|
|
268
|
+
case REVOKE_COLLECTION: {
|
|
269
|
+
if (typeof body.note !== 'string')
|
|
270
|
+
return null;
|
|
271
|
+
let issuer;
|
|
272
|
+
try {
|
|
273
|
+
issuer = parseUCAN(body.note).payload.iss;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const note = await noteCid(body.note);
|
|
279
|
+
if (version.key !== (await revokeKey(note)))
|
|
280
|
+
return null;
|
|
281
|
+
return { ...base, keep, kind: 'revoke', note, issuer };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
let accessCache = null;
|
|
287
|
+
const access = () => (accessCache ??= loadAccess());
|
|
288
|
+
async function loadAccess() {
|
|
289
|
+
const found = [];
|
|
290
|
+
// By key, not by the current version's collection: whatever sits on top
|
|
291
|
+
// may be anyone's, and every version of the history counts.
|
|
292
|
+
for (const [prefix, collection] of ACCESS_KEYS) {
|
|
293
|
+
for (const versions of (await storage.histories(prefix)).values()) {
|
|
294
|
+
for (const held of versions) {
|
|
295
|
+
if (held.collection !== collection)
|
|
296
|
+
continue;
|
|
297
|
+
const event = await toEvent(held);
|
|
298
|
+
if (event)
|
|
299
|
+
found.push(event);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const history = replayAccess(accessGenesis, found);
|
|
304
|
+
reportRole(history.current);
|
|
305
|
+
return { history, events: found };
|
|
306
|
+
}
|
|
307
|
+
let reportedRole;
|
|
308
|
+
function reportRole(state) {
|
|
309
|
+
const role = standing(state, deps.rootDid)?.name ?? null;
|
|
310
|
+
if (role === reportedRole)
|
|
311
|
+
return;
|
|
312
|
+
reportedRole = role;
|
|
313
|
+
deps.onRole?.(role);
|
|
314
|
+
}
|
|
315
|
+
/** The note a version was written under, when it was delegated */
|
|
316
|
+
const noteOf = async (expression) => (expression.proof ? noteCid(expression.proof) : undefined);
|
|
317
|
+
const definitions = new Map();
|
|
318
|
+
/** The definition held in one version of `sys.collection` */
|
|
319
|
+
function definitionIn(id) {
|
|
320
|
+
let found = definitions.get(id);
|
|
321
|
+
if (!found) {
|
|
322
|
+
found = (async () => {
|
|
323
|
+
const version = await storage.getExpression(id);
|
|
324
|
+
if (!version)
|
|
325
|
+
return 'missing';
|
|
326
|
+
if (version.collection !== CATALOG_COLLECTION || version.deleted)
|
|
327
|
+
return 'invalid';
|
|
328
|
+
const opened = await openBody(version);
|
|
329
|
+
if (opened.body === null)
|
|
330
|
+
return 'unreadable';
|
|
331
|
+
const definition = opened.body;
|
|
332
|
+
if (checkStoredCollection(definition) !== null || version.key !== `collection:${definition.name}`)
|
|
333
|
+
return 'invalid';
|
|
334
|
+
return { definition };
|
|
335
|
+
})();
|
|
336
|
+
definitions.set(id, found);
|
|
337
|
+
// Something missing may turn up; only settled answers are worth keeping.
|
|
338
|
+
void found.then((answer) => answer === 'missing' && definitions.delete(id));
|
|
339
|
+
}
|
|
340
|
+
return found;
|
|
341
|
+
}
|
|
342
|
+
/** The rules in force for a collection in one state of the history — null when there are none to judge by */
|
|
343
|
+
async function rulesAt(state, collection) {
|
|
344
|
+
if (collection.startsWith('sys.'))
|
|
345
|
+
return null;
|
|
346
|
+
const entry = state.definitions.get(collection);
|
|
347
|
+
if (!entry)
|
|
348
|
+
return null;
|
|
349
|
+
const found = await definitionIn(entry.event);
|
|
350
|
+
return typeof found === 'string' ? null : { rules: found.definition.rules ?? {} };
|
|
351
|
+
}
|
|
352
|
+
const standings = new Map();
|
|
353
|
+
/**
|
|
354
|
+
* Whether a version stands: signed, in this space, consistent with its first
|
|
355
|
+
* version, and — by the access history — written by someone allowed to.
|
|
356
|
+
* The same verdict for a version from a peer, from a folder, or written
|
|
357
|
+
* here, and for one read back later.
|
|
358
|
+
*/
|
|
359
|
+
function standingOf(expression) {
|
|
360
|
+
const cacheKey = verdictKey(expression);
|
|
361
|
+
let found = standings.get(cacheKey);
|
|
362
|
+
if (!found) {
|
|
363
|
+
found = judgeStanding(expression);
|
|
364
|
+
standings.set(cacheKey, found);
|
|
365
|
+
}
|
|
366
|
+
return found;
|
|
367
|
+
}
|
|
368
|
+
async function judgeStanding(expression) {
|
|
369
|
+
// A version may only claim the space it actually sits in.
|
|
370
|
+
if (expression.space !== space.id)
|
|
371
|
+
return { ok: false, reason: 'It belongs to a different space' };
|
|
372
|
+
const verdict = await judge(expression);
|
|
373
|
+
if (!verdict.verified || !verdict.root)
|
|
374
|
+
return { ok: false, reason: verdict.reason ?? 'Its signature does not check out' };
|
|
375
|
+
if (deps.peopleOnly && isAgentNote(expression.proof))
|
|
376
|
+
return { ok: false, reason: 'Only the account itself writes here, not an agent' };
|
|
377
|
+
const { history } = await access();
|
|
378
|
+
if (ACCESS_COLLECTIONS.has(expression.collection)) {
|
|
379
|
+
const event = await toEvent(expression);
|
|
380
|
+
if (!event)
|
|
381
|
+
return { ok: false, reason: 'It is not a well-formed change to who may do what' };
|
|
382
|
+
const status = history.status(event.id);
|
|
383
|
+
if (!status || status.status === 'waiting')
|
|
384
|
+
return { ok: false, reason: 'Access changes it depends on have not arrived yet', later: true };
|
|
385
|
+
return status.status === 'applied' ? STANDS : { ok: false, reason: status.reason };
|
|
386
|
+
}
|
|
387
|
+
const first = expression.seq === 0 ? expression : expression.genesis ? await storage.getExpression(expression.genesis) : null;
|
|
388
|
+
if (!first)
|
|
389
|
+
return { ok: false, reason: 'Its first version has not arrived yet', later: true };
|
|
390
|
+
if (!firstOf(expression, first))
|
|
391
|
+
return { ok: false, reason: 'The first version it names is not this record\'s' };
|
|
392
|
+
const seen = expression.seen ?? [];
|
|
393
|
+
const state = history.at(seen);
|
|
394
|
+
if (!state)
|
|
395
|
+
return { ok: false, reason: 'Access changes it depends on have not arrived yet', later: true };
|
|
396
|
+
const root = verdict.root;
|
|
397
|
+
const found = await rulesAt(state, expression.collection);
|
|
398
|
+
const rules = found?.rules;
|
|
399
|
+
const action = expression.seq === 0 ? 'create' : expression.deleted ? 'delete' : 'edit';
|
|
400
|
+
const who = !rules ? undefined : action === 'create' ? rules.create : action === 'delete' ? (rules.delete ?? rules.edit) : rules.edit;
|
|
401
|
+
const creator = action !== 'create' && (await judge(first)).root === root;
|
|
402
|
+
const needs = (role) => role !== null &&
|
|
403
|
+
(!rules || allows(who, { member: true, creator, can: (permission) => roleHolds(role, permissionName(expression.collection, permission)) }));
|
|
404
|
+
// The two refusals worth telling apart: not a member at all, and not allowed by the rules.
|
|
405
|
+
const role = standing(state, root);
|
|
406
|
+
if (!role)
|
|
407
|
+
return { ok: false, reason: 'Its author was not a member of this space, as of what it had seen' };
|
|
408
|
+
if (!needs(role)) {
|
|
409
|
+
return {
|
|
410
|
+
ok: false,
|
|
411
|
+
reason: action === 'create'
|
|
412
|
+
? `Only ${describeWho(who)} can create ${expression.collection} records`
|
|
413
|
+
: `Only ${describeWho(who)} can ${action} this ${expression.collection} record`,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const judged = history.judge({ id: expression.id, root, seen, note: await noteOf(expression) }, needs);
|
|
417
|
+
if (!judged.ok)
|
|
418
|
+
return judged;
|
|
419
|
+
if (!rules || expression.deleted)
|
|
420
|
+
return STANDS;
|
|
421
|
+
if (expression.seq === 0 && rules.onePer) {
|
|
422
|
+
const opened = await openBody(expression);
|
|
423
|
+
if (opened.body === null && opened.encrypted)
|
|
424
|
+
return STANDS;
|
|
425
|
+
const expected = await onePerKey(expression.collection, rules.onePer, { root, links: opened.links, body: opened.body });
|
|
426
|
+
if (expected !== expression.key) {
|
|
427
|
+
return { ok: false, reason: `${expression.collection} allows one per ${rules.onePer.join(' + ')} — its key must be derived from them` };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (expression.seq > 0 && rules.fixed?.length) {
|
|
431
|
+
const [now, then] = await Promise.all([openBody(expression), openBody(first)]);
|
|
432
|
+
if (now.body !== null && then.body !== null) {
|
|
433
|
+
const field = changedFixedField(rules.fixed, then.body, now.body);
|
|
434
|
+
if (field)
|
|
435
|
+
return { ok: false, reason: `"${field}" is fixed once a ${expression.collection} record is created` };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return STANDS;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Whether a change to the access history may be stored. Not whether it
|
|
442
|
+
* counts — that can change as other changes arrive, so every well-formed one
|
|
443
|
+
* is kept and the replay decides. Only what never changes is asked: that
|
|
444
|
+
* what it saw is here, and that its author is someone the history has heard
|
|
445
|
+
* of, or holds an invite it has.
|
|
446
|
+
*/
|
|
447
|
+
async function admissible(expression) {
|
|
448
|
+
if (expression.space !== space.id)
|
|
449
|
+
return { ok: false, reason: 'It belongs to a different space' };
|
|
450
|
+
if (!expression.retain)
|
|
451
|
+
return { ok: false, reason: 'A change to who may do what must be kept' };
|
|
452
|
+
const event = await toEvent(expression);
|
|
453
|
+
if (!event)
|
|
454
|
+
return { ok: false, reason: (await judge(expression)).reason ?? 'It is not a well-formed change to who may do what' };
|
|
455
|
+
const { history } = await access();
|
|
456
|
+
if (!history.at(event.seen))
|
|
457
|
+
return { ok: false, reason: 'Access changes it depends on have not arrived yet', later: true };
|
|
458
|
+
const known = history.named(event.root) ||
|
|
459
|
+
(event.kind === 'member' && event.viaInvite !== undefined && history.knownInvite(event.viaInvite)) ||
|
|
460
|
+
(event.kind === 'revoke' && history.named(event.issuer));
|
|
461
|
+
return known ? STANDS : { ok: false, reason: 'Its author has never been a member of this space' };
|
|
462
|
+
}
|
|
463
|
+
/** Whether a version from outside — a peer, a folder — may be stored */
|
|
464
|
+
const admit = (expression) => (ACCESS_COLLECTIONS.has(expression.collection) ? admissible(expression) : standingOf(expression));
|
|
465
|
+
/**
|
|
466
|
+
* The version of a record that counts: the current one, unless it no longer
|
|
467
|
+
* stands — then the newest one before it that does, if the store kept one.
|
|
468
|
+
*/
|
|
469
|
+
async function currentOf(recordKey) {
|
|
470
|
+
const current = await storage.getCurrent(recordKey);
|
|
471
|
+
if (!current || !(await consistent(current)))
|
|
472
|
+
return null;
|
|
473
|
+
if ((await standingOf(current)).ok)
|
|
474
|
+
return current;
|
|
475
|
+
for (const version of await storage.history(recordKey)) {
|
|
476
|
+
if (version.id === current.id || !(await consistent(version)))
|
|
477
|
+
continue;
|
|
478
|
+
if ((await standingOf(version)).ok)
|
|
479
|
+
return version;
|
|
480
|
+
}
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
/** Every record that counts, current version each — deletes included */
|
|
484
|
+
async function everyCurrent(collection) {
|
|
485
|
+
const current = collection ? await storage.queryExpressions(collection) : await storage.listCurrent();
|
|
486
|
+
const shown = [];
|
|
487
|
+
for (const version of current) {
|
|
488
|
+
const counted = (await standingOf(version)).ok && (await consistent(version)) ? version : await currentOf(version.key);
|
|
489
|
+
if (counted && (!collection || counted.collection === collection))
|
|
490
|
+
shown.push(counted);
|
|
491
|
+
}
|
|
492
|
+
return shown;
|
|
493
|
+
}
|
|
494
|
+
async function view(expression) {
|
|
495
|
+
const [{ body, links, encrypted }, verdict, genesis, stands] = await Promise.all([
|
|
496
|
+
openBody(expression),
|
|
497
|
+
judge(expression),
|
|
498
|
+
genesisOf(expression),
|
|
499
|
+
standingOf(expression),
|
|
500
|
+
]);
|
|
501
|
+
const creator = genesis ? await judge(genesis) : null;
|
|
502
|
+
const issues = body === null || expression.deleted ? null : await contentIssues(expression.collection, body, links);
|
|
503
|
+
const reason = !verdict.verified ? verdict.reason : !stands.ok ? stands.reason : undefined;
|
|
504
|
+
return Object.freeze({
|
|
505
|
+
key: expression.key,
|
|
506
|
+
version: expression.id,
|
|
507
|
+
seq: expression.seq,
|
|
508
|
+
space: space.id,
|
|
509
|
+
collection: expression.collection,
|
|
510
|
+
author: expression.author,
|
|
511
|
+
root: verdict.root,
|
|
512
|
+
createdBy: creator?.verified ? creator.root : null,
|
|
513
|
+
createdAt: genesis?.createdAt ?? expression.createdAt,
|
|
514
|
+
updatedAt: expression.createdAt,
|
|
515
|
+
body: body,
|
|
516
|
+
links,
|
|
517
|
+
encrypted,
|
|
518
|
+
verified: verdict.verified && stands.ok,
|
|
519
|
+
...(isAgentNote(expression.proof) ? { viaAgent: true } : {}),
|
|
520
|
+
...(reason ? { reason } : {}),
|
|
521
|
+
...(expression.deleted ? { deleted: true } : {}),
|
|
522
|
+
conforms: issues === null ? null : issues.length === 0,
|
|
523
|
+
...(issues?.length ? { issues } : {}),
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
let catalogCache = null;
|
|
527
|
+
const catalog = () => (catalogCache ??= loadCatalog());
|
|
528
|
+
async function loadCatalog() {
|
|
529
|
+
const result = new Map();
|
|
530
|
+
const { history } = await access();
|
|
531
|
+
for (const [name, entry] of history.current.definitions) {
|
|
532
|
+
const found = await definitionIn(entry.event);
|
|
533
|
+
if (typeof found === 'string')
|
|
534
|
+
continue;
|
|
535
|
+
if (found.definition.name !== name)
|
|
536
|
+
continue;
|
|
537
|
+
result.set(name, { definition: found.definition, definedBy: entry.definedBy, version: entry.event });
|
|
538
|
+
}
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
541
|
+
/** A collection's definition: what the space says. The protocol knows no kinds of record of its own. */
|
|
542
|
+
async function definitionOf(collection) {
|
|
543
|
+
return (await catalog()).get(collection)?.definition ?? null;
|
|
544
|
+
}
|
|
545
|
+
/** Issues with a body against its collection's schema; null when there is no schema to check against. */
|
|
546
|
+
async function shapeIssues(collection, body) {
|
|
547
|
+
// The protocol's own bookkeeping — definitions, profiles, roles — has no user-facing schema.
|
|
548
|
+
if (collection.startsWith('sys.'))
|
|
549
|
+
return null;
|
|
550
|
+
const described = await definitionOf(collection);
|
|
551
|
+
if (described)
|
|
552
|
+
return validateJsonSchema(described.schema, body);
|
|
553
|
+
if (schemas.getCollection(collection)) {
|
|
554
|
+
const checked = await schemas.validate(collection, body);
|
|
555
|
+
return (checked.issues ?? []).map((issue) => ({ path: '/', message: issue.message }));
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Issues with a record's links against its collection's declaration. Only
|
|
561
|
+
* what can be judged from what is held: a link to a record not here yet is
|
|
562
|
+
* fine — you routinely hold a reaction before its post.
|
|
563
|
+
*/
|
|
564
|
+
async function linkIssues(collection, links) {
|
|
565
|
+
const malformed = checkLinks(links);
|
|
566
|
+
if (malformed)
|
|
567
|
+
return [{ path: '/links', message: malformed }];
|
|
568
|
+
if (links.length === 0)
|
|
569
|
+
return [];
|
|
570
|
+
const declared = (await definitionOf(collection))?.links;
|
|
571
|
+
// An undescribed collection says nothing about its links, so nothing is wrong.
|
|
572
|
+
if (!declared)
|
|
573
|
+
return (await definitionOf(collection)) ? [{ path: '/links', message: `${collection} declares no links` }] : [];
|
|
574
|
+
const issues = [];
|
|
575
|
+
const perRole = new Map();
|
|
576
|
+
for (const [index, link] of links.entries()) {
|
|
577
|
+
const declaration = declared[link.rel];
|
|
578
|
+
if (!declaration) {
|
|
579
|
+
issues.push({ path: `/links/${index}`, message: `${collection} has no "${link.rel}" link (it has: ${Object.keys(declared).join(', ')})` });
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
perRole.set(link.rel, (perRole.get(link.rel) ?? 0) + 1);
|
|
583
|
+
if (declaration.to === '*')
|
|
584
|
+
continue;
|
|
585
|
+
const target = await currentOf(link.to);
|
|
586
|
+
if (target && !target.deleted && !declaration.to.includes(target.collection)) {
|
|
587
|
+
issues.push({ path: `/links/${index}`, message: `"${link.rel}" must point at ${declaration.to.join(' or ')}, not ${target.collection}` });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
for (const [rel, count] of perRole) {
|
|
591
|
+
if (declared[rel]?.cardinality === 'one' && count > 1)
|
|
592
|
+
issues.push({ path: '/links', message: `At most one "${rel}" link` });
|
|
593
|
+
}
|
|
594
|
+
return issues;
|
|
595
|
+
}
|
|
596
|
+
/** Shape and link issues together; null when neither has anything to check against. */
|
|
597
|
+
async function contentIssues(collection, body, links) {
|
|
598
|
+
const shape = await shapeIssues(collection, body);
|
|
599
|
+
const linked = await linkIssues(collection, links);
|
|
600
|
+
if (shape === null && linked.length === 0 && !(await definitionOf(collection)))
|
|
601
|
+
return null;
|
|
602
|
+
return [...(shape ?? []), ...linked];
|
|
603
|
+
}
|
|
604
|
+
function describe(name, entry, records) {
|
|
605
|
+
const definition = entry?.definition;
|
|
606
|
+
return Object.freeze({
|
|
607
|
+
name,
|
|
608
|
+
...(definition?.title !== undefined ? { title: definition.title } : {}),
|
|
609
|
+
...(definition?.description !== undefined ? { description: definition.description } : {}),
|
|
610
|
+
schema: definition?.schema ?? null,
|
|
611
|
+
version: definition?.version ?? null,
|
|
612
|
+
history: definition?.history ?? 'latest',
|
|
613
|
+
links: definition?.links ?? {},
|
|
614
|
+
definedBy: entry?.definedBy ?? null,
|
|
615
|
+
permissions: definition?.permissions ?? [],
|
|
616
|
+
rules: definition?.rules ?? {},
|
|
617
|
+
...(definition?.screen !== undefined ? { screen: definition.screen } : {}),
|
|
618
|
+
records,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
// ─── What points where ─────────────────────────────────────────────
|
|
622
|
+
//
|
|
623
|
+
// Derived from the records held, rebuilt when they change, never synced: any
|
|
624
|
+
// node holding the records can build it, and derived data travelling between
|
|
625
|
+
// peers would only be one more thing to disagree about.
|
|
626
|
+
let linkIndexCache = null;
|
|
627
|
+
const linkIndex = () => (linkIndexCache ??= buildLinkIndex());
|
|
628
|
+
async function buildLinkIndex() {
|
|
629
|
+
const index = new Map();
|
|
630
|
+
for (const version of await everyCurrent()) {
|
|
631
|
+
if (version.deleted)
|
|
632
|
+
continue;
|
|
633
|
+
const { links } = await openBody(version);
|
|
634
|
+
for (const link of links) {
|
|
635
|
+
const list = index.get(link.to) ?? [];
|
|
636
|
+
list.push({ rel: link.rel, from: version.key });
|
|
637
|
+
index.set(link.to, list);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return index;
|
|
641
|
+
}
|
|
642
|
+
// ─── Profiles ──────────────────────────────────────────────────────
|
|
643
|
+
//
|
|
644
|
+
// Each person says who they are in a space with one record, keyed by their
|
|
645
|
+
// identity (`profileKey`), whose versions are always retained. The fold
|
|
646
|
+
// takes the newest version signed by the identity the key names, so nobody
|
|
647
|
+
// can rename anyone else: a version written by someone else under your key
|
|
648
|
+
// is simply never the answer, and it cannot push yours out, because yours
|
|
649
|
+
// are kept.
|
|
650
|
+
let profilesCache = null;
|
|
651
|
+
const profileMap = () => (profilesCache ??= loadProfiles());
|
|
652
|
+
async function loadProfiles() {
|
|
653
|
+
const result = new Map();
|
|
654
|
+
// By key: the current version may be anyone's.
|
|
655
|
+
for (const [key, versions] of await storage.histories('profile:')) {
|
|
656
|
+
for (const version of versions) {
|
|
657
|
+
if (version.collection !== PROFILE_COLLECTION)
|
|
658
|
+
continue;
|
|
659
|
+
const verdict = await judge(version);
|
|
660
|
+
if (!verdict.verified || !verdict.root || (await profileKey(verdict.root)) !== key)
|
|
661
|
+
continue;
|
|
662
|
+
if (!(await standingOf(version)).ok)
|
|
663
|
+
continue;
|
|
664
|
+
if (version.deleted)
|
|
665
|
+
break; // they took it down
|
|
666
|
+
const name = (await openBody(version)).body?.name;
|
|
667
|
+
if (typeof name === 'string' && name.trim()) {
|
|
668
|
+
result.set(verdict.root, { did: verdict.root, name: name.trim().slice(0, 64), updatedAt: version.createdAt });
|
|
669
|
+
}
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return result;
|
|
674
|
+
}
|
|
675
|
+
/** Records changed: everything derived from them may have too. */
|
|
676
|
+
const recordsChanged = () => {
|
|
677
|
+
linkIndexCache = null;
|
|
678
|
+
catalogCache = null;
|
|
679
|
+
profilesCache = null;
|
|
680
|
+
accessCache = null;
|
|
681
|
+
standings.clear();
|
|
682
|
+
emit({ type: 'records', space: space.id });
|
|
683
|
+
};
|
|
684
|
+
// ─── Peers ─────────────────────────────────────────────────────────
|
|
685
|
+
let connection = 'offline';
|
|
686
|
+
let rejected = 0;
|
|
687
|
+
const networks = [];
|
|
688
|
+
/** Which network a peer was met on, so replies go back the same way */
|
|
689
|
+
const routes = new Map();
|
|
690
|
+
const sync = createSyncEngine({
|
|
691
|
+
storageProvider: storage,
|
|
692
|
+
sendToPeer: (peerId, message) => {
|
|
693
|
+
routes.get(peerId)?.send(peerId, { type: 'sync', from: session.did, payload: message });
|
|
694
|
+
},
|
|
695
|
+
validate: async (expression) => {
|
|
696
|
+
const verdict = await admit(expression);
|
|
697
|
+
return verdict.ok ? { valid: true } : { valid: false, reason: verdict.reason, ...(verdict.later ? { later: true } : {}) };
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
/**
|
|
701
|
+
* Tells every connected peer this node's root, soon after it took in
|
|
702
|
+
* something new — from a peer or from a folder. Without it, a record passed
|
|
703
|
+
* along waits for the next heartbeat at every hop: a carrier would sit on a
|
|
704
|
+
* friend's write for half a minute before the pod, or your phone, heard of
|
|
705
|
+
* it. A peer already level answers and nothing moves.
|
|
706
|
+
*/
|
|
707
|
+
let announceTimer = null;
|
|
708
|
+
const announceSoon = () => {
|
|
709
|
+
if (announceTimer)
|
|
710
|
+
return;
|
|
711
|
+
announceTimer = setTimeout(() => {
|
|
712
|
+
announceTimer = null;
|
|
713
|
+
sync.notifyPeers(connectedPeers());
|
|
714
|
+
}, 100);
|
|
715
|
+
};
|
|
716
|
+
sync.on('expression-received', () => {
|
|
717
|
+
recordsChanged();
|
|
718
|
+
announceSoon();
|
|
719
|
+
});
|
|
720
|
+
sync.on('rejected', (peer, _expression, reason) => {
|
|
721
|
+
rejected += 1;
|
|
722
|
+
emit({ type: 'rejected', space: space.id, peer, reason });
|
|
723
|
+
});
|
|
724
|
+
const net = deps.network;
|
|
725
|
+
if (net) {
|
|
726
|
+
const room = encodeURIComponent(space.id);
|
|
727
|
+
// A carrier holds the read key pair without the key it comes from: it may be served the space, and cannot open it.
|
|
728
|
+
const readKey = space.visibility === 'private' ? (key ? await deriveReadKey(key, provider) : (record.read ?? null)) : null;
|
|
729
|
+
if (deps.mesh) {
|
|
730
|
+
// Every peer met through a relay proves who it is, and in a private space that it may read.
|
|
731
|
+
const auth = createMeshAuth(space.id, session, space.visibility === 'private' ? { key: readKey, publicDid: space.readKey ?? '' } : null, provider);
|
|
732
|
+
networks.push(deps.mesh.join(await relayRoom(space.id), auth));
|
|
733
|
+
}
|
|
734
|
+
// Both sides of a socket to a node prove who they are; in a private space the client also proves it may read.
|
|
735
|
+
const authenticator = net.nodes?.length ? createClientAuth(space.id, session, readKey, provider) : null;
|
|
736
|
+
for (const node of net.nodes ?? []) {
|
|
737
|
+
const url = `${node}${node.includes('?') ? '&' : '?'}space=${room}`;
|
|
738
|
+
networks.push(createNetworkManager({
|
|
739
|
+
did: session.did,
|
|
740
|
+
createTransport: () => createWebSocketTransport({ url, did: session.did, authenticator }),
|
|
741
|
+
}));
|
|
742
|
+
}
|
|
743
|
+
for (const transport of net.transports?.(space.id, session.did) ?? []) {
|
|
744
|
+
networks.push(createNetworkManager({ did: session.did, createTransport: () => transport }));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
const connectedPeers = () => [...new Set(networks.flatMap((network) => network.getPeers().map((peer) => peer.did)))];
|
|
748
|
+
for (const network of networks) {
|
|
749
|
+
network.on('message', (message) => {
|
|
750
|
+
if (message.type === 'sync')
|
|
751
|
+
void sync.handleMessage(message.from, message.payload);
|
|
752
|
+
});
|
|
753
|
+
network.on('peer-connected', (info) => {
|
|
754
|
+
routes.set(info.did, network);
|
|
755
|
+
sync.addPeer(info.did);
|
|
756
|
+
// Reconcile at once rather than waiting for the heartbeat.
|
|
757
|
+
sync.notifyPeers([info.did]);
|
|
758
|
+
emit({ type: 'status', space: space.id });
|
|
759
|
+
});
|
|
760
|
+
network.on('peer-disconnected', (info) => {
|
|
761
|
+
if (routes.get(info.did) === network) {
|
|
762
|
+
routes.delete(info.did);
|
|
763
|
+
sync.removePeer(info.did);
|
|
764
|
+
}
|
|
765
|
+
emit({ type: 'status', space: space.id });
|
|
766
|
+
});
|
|
767
|
+
network.on('error', () => {
|
|
768
|
+
if (!networks.some((n) => n.isConnected()))
|
|
769
|
+
connection = 'error';
|
|
770
|
+
emit({ type: 'status', space: space.id });
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
if (networks.length > 0) {
|
|
774
|
+
connection = 'connecting';
|
|
775
|
+
void Promise.allSettled(networks.map((network) => network.connect())).then((results) => {
|
|
776
|
+
connection = results.some((result) => result.status === 'fulfilled') ? 'connected' : 'error';
|
|
777
|
+
emit({ type: 'status', space: space.id });
|
|
778
|
+
});
|
|
779
|
+
sync.start();
|
|
780
|
+
}
|
|
781
|
+
// ─── Writers outside this process ──────────────────────────────────
|
|
782
|
+
//
|
|
783
|
+
// A folder can be written by another origin, or by another device behind
|
|
784
|
+
// whatever syncs the folder. Nothing announces it, so it is looked for.
|
|
785
|
+
let watchTimer = null;
|
|
786
|
+
if (deps.watchIntervalMs > 0 && isFolderAdapter(adapter)) {
|
|
787
|
+
let running = false;
|
|
788
|
+
watchTimer = setInterval(() => {
|
|
789
|
+
if (running)
|
|
790
|
+
return;
|
|
791
|
+
running = true;
|
|
792
|
+
// The same verdict as for a version from a peer.
|
|
793
|
+
reconcileFolder(storage, adapter, async (expression) => (await admit(expression)).ok)
|
|
794
|
+
.then((result) => {
|
|
795
|
+
if (!result.changed)
|
|
796
|
+
return;
|
|
797
|
+
recordsChanged();
|
|
798
|
+
announceSoon();
|
|
799
|
+
})
|
|
800
|
+
.catch(() => {
|
|
801
|
+
// A revoked permission or a folder that went away; the next pass will tell.
|
|
802
|
+
})
|
|
803
|
+
.finally(() => {
|
|
804
|
+
running = false;
|
|
805
|
+
});
|
|
806
|
+
}, deps.watchIntervalMs);
|
|
807
|
+
}
|
|
808
|
+
// Tabs of one browser share one IndexedDB but not memory, so a write in one
|
|
809
|
+
// is invisible to the other until it is told. A nudge is enough: the store
|
|
810
|
+
// already holds the data.
|
|
811
|
+
const channel = typeof globalThis.BroadcastChannel === 'function'
|
|
812
|
+
? new globalThis.BroadcastChannel(`weave-node:${deps.rootDid}:${space.id}`)
|
|
813
|
+
: null;
|
|
814
|
+
if (channel) {
|
|
815
|
+
channel.onmessage = () => recordsChanged();
|
|
816
|
+
// In Node a channel holds the process open; it must never be the only thing doing so.
|
|
817
|
+
channel.unref?.();
|
|
818
|
+
}
|
|
819
|
+
/** Why this account cannot write here at all, or null when it can */
|
|
820
|
+
async function cannotWrite() {
|
|
821
|
+
const { history } = await access();
|
|
822
|
+
if (standing(history.current, deps.rootDid))
|
|
823
|
+
return null;
|
|
824
|
+
return waitingInvite
|
|
825
|
+
? `You've joined "${space.name}", but its invite hasn't reached this device yet — connect to someone in the space first`
|
|
826
|
+
: `"${space.name}" was shared with you to view — you can't change it`;
|
|
827
|
+
}
|
|
828
|
+
/** Signs and stores one version. A delete carries no body. */
|
|
829
|
+
async function write(collection, body, version, deleted = false, links = [], options = {}) {
|
|
830
|
+
const writer = options.as ?? session;
|
|
831
|
+
if (deps.peopleOnly && isAgentNote(writer.proof()))
|
|
832
|
+
throw new Error('An agent can\'t change the account itself. Ask the person to do it.');
|
|
833
|
+
// Every peer would ignore it (see buildEvent); say why here instead.
|
|
834
|
+
if (ACCESS_COLLECTIONS.has(collection) && isAgentNote(writer.proof())) {
|
|
835
|
+
throw new Error(collection === CATALOG_COLLECTION
|
|
836
|
+
? 'An agent can\'t add or change collections. Propose an app instead (apps_propose), and a person in the space adds it.'
|
|
837
|
+
: 'An agent can\'t change who may do what in a space. Ask the person to do it.');
|
|
838
|
+
}
|
|
839
|
+
// Every other copy would refuse it, so refuse it here rather than show a
|
|
840
|
+
// change that exists on this device alone.
|
|
841
|
+
if (!options.joining) {
|
|
842
|
+
const problem = await cannotWrite();
|
|
843
|
+
if (problem)
|
|
844
|
+
throw new Error(problem);
|
|
845
|
+
}
|
|
846
|
+
let payload = null;
|
|
847
|
+
if (!deleted) {
|
|
848
|
+
// Refused here, where the writer can fix it. On arrival a misfit is kept
|
|
849
|
+
// and flagged instead — see `conforms`.
|
|
850
|
+
const issues = await contentIssues(collection, body, links);
|
|
851
|
+
if (issues?.length) {
|
|
852
|
+
throw new Error(`Not a valid ${collection}: ${issues.map((i) => (i.path === '/' ? i.message : `${i.path} ${i.message}`)).join('; ')}`);
|
|
853
|
+
}
|
|
854
|
+
// Encrypt *before* signing: peers without the key still verify the
|
|
855
|
+
// signature and relay the record, they just cannot read it.
|
|
856
|
+
payload = body;
|
|
857
|
+
if (space.visibility === 'private' && !IN_THE_CLEAR.has(collection)) {
|
|
858
|
+
if (!key)
|
|
859
|
+
throw new Error('This private space has no key on this node');
|
|
860
|
+
// Body and links sealed together: a relay learns neither.
|
|
861
|
+
const content = links.length ? { body, links } : { body };
|
|
862
|
+
const sealed = await encryptExpression({ id: '', author: '', collection, createdAt: '', body: content, signature: '', key: version.key, seq: version.seq }, key);
|
|
863
|
+
payload = sealed.body;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
// Whether superseded versions are kept is the writer's decision, carried
|
|
867
|
+
// on the version — never each reader's, or nodes that had seen different
|
|
868
|
+
// definitions would store different things and never converge. The access
|
|
869
|
+
// history keeps everything: it is replayed whole.
|
|
870
|
+
const retain = ACCESS_COLLECTIONS.has(collection) ||
|
|
871
|
+
collection === PROFILE_COLLECTION ||
|
|
872
|
+
(await catalog()).get(collection)?.definition.history === 'all';
|
|
873
|
+
const { history, events: held } = await access();
|
|
874
|
+
const signed = await signer.sign(createExpression({
|
|
875
|
+
author: writer.did,
|
|
876
|
+
collection,
|
|
877
|
+
space: space.id,
|
|
878
|
+
body: payload,
|
|
879
|
+
proof: writer.proof(),
|
|
880
|
+
version,
|
|
881
|
+
retain,
|
|
882
|
+
deleted,
|
|
883
|
+
seen: history.heads(),
|
|
884
|
+
// In the clear only where the body is: a private space sealed them above.
|
|
885
|
+
...(space.visibility === 'public' && !deleted && links.length ? { links } : {}),
|
|
886
|
+
}), writer.key);
|
|
887
|
+
// The same verdict every other peer will reach: refused here, with the reason, rather than there.
|
|
888
|
+
const judged = await judge(signed);
|
|
889
|
+
if (!judged.verified)
|
|
890
|
+
throw new Error(judged.reason ?? 'This key may not write here.');
|
|
891
|
+
if (ACCESS_COLLECTIONS.has(collection)) {
|
|
892
|
+
const event = await toEvent(signed);
|
|
893
|
+
if (!event)
|
|
894
|
+
throw new Error('That is not a well-formed change to who may do what');
|
|
895
|
+
const status = replayAccess(accessGenesis, [...held, event]).status(event.id);
|
|
896
|
+
if (status?.status !== 'applied')
|
|
897
|
+
throw new Error(status?.status === 'dropped' ? status.reason : 'That change could not be made');
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
const stands = await standingOf(signed);
|
|
901
|
+
if (!stands.ok)
|
|
902
|
+
throw new Error(stands.reason);
|
|
903
|
+
}
|
|
904
|
+
await storage.addExpression(signed);
|
|
905
|
+
channel?.postMessage('changed');
|
|
906
|
+
sync.onLocalChange(signed);
|
|
907
|
+
recordsChanged();
|
|
908
|
+
return signed;
|
|
909
|
+
}
|
|
910
|
+
async function get(recordKey) {
|
|
911
|
+
const current = await currentOf(recordKey);
|
|
912
|
+
return current && !current.deleted ? view(current) : null;
|
|
913
|
+
}
|
|
914
|
+
/** The live current version of a record, or a clear error. */
|
|
915
|
+
async function requireLive(recordKey) {
|
|
916
|
+
const current = await currentOf(recordKey);
|
|
917
|
+
if (!current || current.deleted)
|
|
918
|
+
throw new Error(`No record ${recordKey} in this space`);
|
|
919
|
+
return current;
|
|
920
|
+
}
|
|
921
|
+
/** The fields for writing a key again: the version after whatever the store holds, counted or not */
|
|
922
|
+
async function after(recordKey) {
|
|
923
|
+
const held = await storage.getCurrent(recordKey);
|
|
924
|
+
return held ? nextVersion(held) : { key: recordKey, seq: 0 };
|
|
925
|
+
}
|
|
926
|
+
async function writeFirst(collection, body, recordKey, links, as) {
|
|
927
|
+
const current = await currentOf(recordKey);
|
|
928
|
+
if (current && !current.deleted)
|
|
929
|
+
throw new Error(`A record ${recordKey} already exists — update it instead`);
|
|
930
|
+
// Writing a key that was deleted brings it back: the next version after the delete.
|
|
931
|
+
return write(collection, body, await after(recordKey), false, links, as ? { as } : {});
|
|
932
|
+
}
|
|
933
|
+
async function upsert(collection, recordKey, body, options = {}) {
|
|
934
|
+
return view(await write(collection, body, await after(recordKey), false, [], options));
|
|
935
|
+
}
|
|
936
|
+
async function removeKey(recordKey, as) {
|
|
937
|
+
const current = await requireLive(recordKey);
|
|
938
|
+
await write(current.collection, null, await after(current.key), true, [], as ? { as } : {});
|
|
939
|
+
}
|
|
940
|
+
const guard = (collection) => {
|
|
941
|
+
if (MANAGED.has(collection))
|
|
942
|
+
throw new Error(`${collection} is written by the node itself`);
|
|
943
|
+
};
|
|
944
|
+
/** Display order: when a record was created, then its key. It decides nothing. */
|
|
945
|
+
const createdAtOf = async (version) => (await genesisOf(version))?.createdAt ?? version.createdAt;
|
|
946
|
+
/**
|
|
947
|
+
* What a change taking power from these people keeps: every record of
|
|
948
|
+
* theirs that counts now — what this node has seen them write.
|
|
949
|
+
*/
|
|
950
|
+
async function keepFrom(dids) {
|
|
951
|
+
const keep = [];
|
|
952
|
+
for (const version of await everyCurrent()) {
|
|
953
|
+
if (ACCESS_COLLECTIONS.has(version.collection))
|
|
954
|
+
continue;
|
|
955
|
+
const root = (await judge(version)).root;
|
|
956
|
+
if (root && dids.has(root))
|
|
957
|
+
keep.push(version.id);
|
|
958
|
+
}
|
|
959
|
+
return keep.slice(0, MAX_KEEP);
|
|
960
|
+
}
|
|
961
|
+
/** The rules and standing that decide whether this account may do something now */
|
|
962
|
+
async function mayNow(collection, action, first) {
|
|
963
|
+
const { history } = await access();
|
|
964
|
+
const role = standing(history.current, deps.rootDid);
|
|
965
|
+
if (!role)
|
|
966
|
+
return false;
|
|
967
|
+
const found = await rulesAt(history.current, collection);
|
|
968
|
+
if (!found)
|
|
969
|
+
return true;
|
|
970
|
+
const who = action === 'create' ? found.rules.create : action === 'delete' ? (found.rules.delete ?? found.rules.edit) : found.rules.edit;
|
|
971
|
+
const creator = !!first && (await judge(first)).root === deps.rootDid;
|
|
972
|
+
return allows(who, { member: true, creator, can: (permission) => roleHolds(role, permissionName(collection, permission)) });
|
|
973
|
+
}
|
|
974
|
+
return Object.freeze({
|
|
975
|
+
async list(options = {}) {
|
|
976
|
+
const current = (await everyCurrent(options.collection)).filter((e) => options.collection || !e.collection.startsWith('sys.'));
|
|
977
|
+
const shown = [];
|
|
978
|
+
for (const version of current) {
|
|
979
|
+
if (version.deleted && !options.includeDeleted)
|
|
980
|
+
continue;
|
|
981
|
+
shown.push({ version, createdAt: await createdAtOf(version) });
|
|
982
|
+
}
|
|
983
|
+
shown.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.version.key.localeCompare(b.version.key));
|
|
984
|
+
if (options.newestFirst)
|
|
985
|
+
shown.reverse();
|
|
986
|
+
const page = options.limit === undefined ? shown : shown.slice(0, options.limit);
|
|
987
|
+
return Promise.all(page.map(({ version }) => view(version)));
|
|
988
|
+
},
|
|
989
|
+
get,
|
|
990
|
+
async put(collection, body, options = {}) {
|
|
991
|
+
guard(collection);
|
|
992
|
+
if (options.key !== undefined && !RECORD_KEY_PATTERN.test(options.key)) {
|
|
993
|
+
throw new Error('A record key is 1–128 characters of a–z, 0–9 and : . _ -');
|
|
994
|
+
}
|
|
995
|
+
const links = options.links ?? [];
|
|
996
|
+
// One per something: the key is derived from it, so writing again is the next version of the same record.
|
|
997
|
+
const onePer = (await catalog()).get(collection)?.definition.rules?.onePer;
|
|
998
|
+
if (onePer && options.key === undefined) {
|
|
999
|
+
const derived = await onePerKey(collection, onePer, { root: deps.rootDid, links, body });
|
|
1000
|
+
if (!derived)
|
|
1001
|
+
throw new Error(`${collection} is one per ${onePer.join(' + ')} — give it every one of those`);
|
|
1002
|
+
return view(await write(collection, body, await after(derived), false, links, options.as ? { as: options.as } : {}));
|
|
1003
|
+
}
|
|
1004
|
+
return view(await writeFirst(collection, body, options.key ?? newRecordKey(), links, options.as));
|
|
1005
|
+
},
|
|
1006
|
+
async can(action, target) {
|
|
1007
|
+
if (action === 'create')
|
|
1008
|
+
return mayNow(target, 'create', null);
|
|
1009
|
+
const current = await currentOf(target);
|
|
1010
|
+
if (!current || current.deleted)
|
|
1011
|
+
return false;
|
|
1012
|
+
return mayNow(current.collection, action, await genesisOf(current));
|
|
1013
|
+
},
|
|
1014
|
+
upsertSystem: (collection, recordKey, body) => upsert(collection, recordKey, body),
|
|
1015
|
+
async profiles() {
|
|
1016
|
+
return [...(await profileMap()).values()].sort((a, b) => a.name.localeCompare(b.name) || a.did.localeCompare(b.did));
|
|
1017
|
+
},
|
|
1018
|
+
async publishProfile(profile) {
|
|
1019
|
+
// Someone following a space without a role in it cannot write there, and says nothing.
|
|
1020
|
+
if (await cannotWrite())
|
|
1021
|
+
return;
|
|
1022
|
+
const name = profile.name.trim().slice(0, 64);
|
|
1023
|
+
if (!name || (await profileMap()).get(deps.rootDid)?.name === name)
|
|
1024
|
+
return;
|
|
1025
|
+
await upsert(PROFILE_COLLECTION, await profileKey(deps.rootDid), { name });
|
|
1026
|
+
},
|
|
1027
|
+
async update(recordKey, body, options = {}) {
|
|
1028
|
+
const current = await requireLive(recordKey);
|
|
1029
|
+
guard(current.collection);
|
|
1030
|
+
// Links carry over unless replaced: ticking a todo should not unhook it from anything.
|
|
1031
|
+
const links = options.links ?? (await openBody(current)).links;
|
|
1032
|
+
return view(await write(current.collection, body, await after(recordKey), false, links, options.as ? { as: options.as } : {}));
|
|
1033
|
+
},
|
|
1034
|
+
async linked(recordKey, options = {}) {
|
|
1035
|
+
const pointing = (await linkIndex()).get(recordKey) ?? [];
|
|
1036
|
+
const keys = [...new Set(pointing.filter((p) => !options.rel || p.rel === options.rel).map((p) => p.from))];
|
|
1037
|
+
const found = [];
|
|
1038
|
+
for (const from of keys) {
|
|
1039
|
+
const current = await currentOf(from);
|
|
1040
|
+
if (!current || current.deleted)
|
|
1041
|
+
continue;
|
|
1042
|
+
if (options.collection && current.collection !== options.collection)
|
|
1043
|
+
continue;
|
|
1044
|
+
found.push(await view(current));
|
|
1045
|
+
}
|
|
1046
|
+
return found.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.key.localeCompare(b.key));
|
|
1047
|
+
},
|
|
1048
|
+
async remove(recordKey, options = {}) {
|
|
1049
|
+
guard((await requireLive(recordKey)).collection);
|
|
1050
|
+
await removeKey(recordKey, options.as);
|
|
1051
|
+
},
|
|
1052
|
+
removeSystem: removeKey,
|
|
1053
|
+
async history(recordKey) {
|
|
1054
|
+
const versions = await storage.history(recordKey);
|
|
1055
|
+
return Promise.all(versions.map((version) => view(version)));
|
|
1056
|
+
},
|
|
1057
|
+
async collections() {
|
|
1058
|
+
const counts = new Map();
|
|
1059
|
+
for (const version of await everyCurrent()) {
|
|
1060
|
+
if (version.deleted)
|
|
1061
|
+
continue;
|
|
1062
|
+
// The protocol's own bookkeeping is not one of the space's kinds of thing.
|
|
1063
|
+
if (version.collection.startsWith('sys.'))
|
|
1064
|
+
continue;
|
|
1065
|
+
counts.set(version.collection, (counts.get(version.collection) ?? 0) + 1);
|
|
1066
|
+
}
|
|
1067
|
+
const described = await catalog();
|
|
1068
|
+
const names = [...new Set([...described.keys(), ...counts.keys()])].sort();
|
|
1069
|
+
return names.map((name) => describe(name, described.get(name) ?? null, counts.get(name) ?? 0));
|
|
1070
|
+
},
|
|
1071
|
+
async define(input) {
|
|
1072
|
+
const recordKey = `collection:${input.name}`;
|
|
1073
|
+
const current = (await catalog()).get(input.name) ?? null;
|
|
1074
|
+
const version = input.version ?? (current ? current.definition.version + 1 : 1);
|
|
1075
|
+
const definition = {
|
|
1076
|
+
name: input.name,
|
|
1077
|
+
...(input.title !== undefined ? { title: input.title } : {}),
|
|
1078
|
+
...(input.description !== undefined ? { description: input.description } : {}),
|
|
1079
|
+
schema: toJsonSchema(input.schema),
|
|
1080
|
+
version,
|
|
1081
|
+
...(input.history !== undefined ? { history: input.history } : {}),
|
|
1082
|
+
...(input.links !== undefined ? { links: input.links } : {}),
|
|
1083
|
+
...(input.permissions !== undefined ? { permissions: input.permissions } : {}),
|
|
1084
|
+
...(input.rules !== undefined ? { rules: input.rules } : {}),
|
|
1085
|
+
...(input.screen !== undefined ? { screen: input.screen } : {}),
|
|
1086
|
+
};
|
|
1087
|
+
const problem = checkStoredCollection(definition);
|
|
1088
|
+
if (problem)
|
|
1089
|
+
throw new Error(problem);
|
|
1090
|
+
if (current && version <= current.definition.version) {
|
|
1091
|
+
throw new Error(`${input.name} is at version ${current.definition.version}; a new definition needs a higher one`);
|
|
1092
|
+
}
|
|
1093
|
+
await upsert(CATALOG_COLLECTION, recordKey, definition);
|
|
1094
|
+
const entry = (await catalog()).get(input.name) ?? null;
|
|
1095
|
+
const count = (await everyCurrent(input.name)).filter((e) => !e.deleted).length;
|
|
1096
|
+
return describe(input.name, entry, count);
|
|
1097
|
+
},
|
|
1098
|
+
async undefine(name) {
|
|
1099
|
+
if (!(await catalog()).has(name))
|
|
1100
|
+
throw new Error(`${name} is not defined in this space`);
|
|
1101
|
+
// Records left behind would lose their shape and their rules, so they go first.
|
|
1102
|
+
const left = (await everyCurrent(name)).filter((e) => !e.deleted).length;
|
|
1103
|
+
if (left)
|
|
1104
|
+
throw new Error(`${name} still has ${left} record${left === 1 ? '' : 's'}; delete them first`);
|
|
1105
|
+
await removeKey(`collection:${name}`);
|
|
1106
|
+
},
|
|
1107
|
+
async access() {
|
|
1108
|
+
const { history } = await access();
|
|
1109
|
+
const state = history.current;
|
|
1110
|
+
const role = standing(state, deps.rootDid);
|
|
1111
|
+
return Object.freeze({
|
|
1112
|
+
roles: [...state.roles.values()].sort((a, b) => b.rank - a.rank || a.name.localeCompare(b.name)),
|
|
1113
|
+
members: [...state.members]
|
|
1114
|
+
.map(([did, name]) => ({ did, role: name }))
|
|
1115
|
+
.sort((a, b) => (state.roles.get(b.role)?.rank ?? -1) - (state.roles.get(a.role)?.rank ?? -1) || a.did.localeCompare(b.did)),
|
|
1116
|
+
invites: [...state.invites].map(([inviteDid, invite]) => ({ key: inviteDid, role: invite.role, open: invite.open })),
|
|
1117
|
+
role,
|
|
1118
|
+
heads: history.heads(),
|
|
1119
|
+
});
|
|
1120
|
+
},
|
|
1121
|
+
async setMember(did, role) {
|
|
1122
|
+
const { history } = await access();
|
|
1123
|
+
const was = standing(history.current, did);
|
|
1124
|
+
const next = role === null ? null : history.current.roles.get(role);
|
|
1125
|
+
if (role !== null && !next)
|
|
1126
|
+
throw new Error(`There is no role "${role}" in "${space.name}"`);
|
|
1127
|
+
// Taking power away keeps what this node has seen them write.
|
|
1128
|
+
const lowers = was && (!next || next.rank < was.rank || was.permissions.some((p) => !next.permissions.includes(p)));
|
|
1129
|
+
const keep = lowers ? await keepFrom(new Set([did])) : [];
|
|
1130
|
+
await upsert(MEMBER_COLLECTION, await memberKey(did), { did, role, ...(keep.length ? { keep } : {}) });
|
|
1131
|
+
},
|
|
1132
|
+
async putRole(role) {
|
|
1133
|
+
const problem = checkRole(role);
|
|
1134
|
+
if (problem)
|
|
1135
|
+
throw new Error(problem);
|
|
1136
|
+
const { history } = await access();
|
|
1137
|
+
const was = history.current.roles.get(role.name);
|
|
1138
|
+
const lowers = was && (role.rank < was.rank || was.permissions.some((p) => !role.permissions.includes(p)));
|
|
1139
|
+
const holders = new Set([...history.current.members].filter(([, name]) => name === role.name).map(([did]) => did));
|
|
1140
|
+
const keep = lowers && holders.size ? await keepFrom(holders) : [];
|
|
1141
|
+
await upsert(ROLE_COLLECTION, roleKey(role.name), {
|
|
1142
|
+
name: role.name,
|
|
1143
|
+
...(role.title !== undefined ? { title: role.title } : {}),
|
|
1144
|
+
rank: role.rank,
|
|
1145
|
+
permissions: [...role.permissions],
|
|
1146
|
+
...(keep.length ? { keep } : {}),
|
|
1147
|
+
});
|
|
1148
|
+
},
|
|
1149
|
+
async removeRole(name) {
|
|
1150
|
+
const { history } = await access();
|
|
1151
|
+
if (!history.current.roles.has(name))
|
|
1152
|
+
throw new Error(`There is no role "${name}" in "${space.name}"`);
|
|
1153
|
+
const holders = new Set([...history.current.members].filter(([, held]) => held === name).map(([did]) => did));
|
|
1154
|
+
const keep = holders.size ? await keepFrom(holders) : [];
|
|
1155
|
+
await upsert(ROLE_COLLECTION, roleKey(name), { name, removed: true, ...(keep.length ? { keep } : {}) });
|
|
1156
|
+
},
|
|
1157
|
+
async openInvite(role) {
|
|
1158
|
+
const secret = generateInviteSecret();
|
|
1159
|
+
const pair = await deriveInviteKey(secret, provider);
|
|
1160
|
+
await upsert(INVITE_COLLECTION, await inviteRecordKey(pair.did), { key: pair.did, role, open: true });
|
|
1161
|
+
return { secret, key: pair.did };
|
|
1162
|
+
},
|
|
1163
|
+
async closeInvite(inviteDid) {
|
|
1164
|
+
const { history } = await access();
|
|
1165
|
+
const invite = history.current.invites.get(inviteDid);
|
|
1166
|
+
if (!invite)
|
|
1167
|
+
throw new Error('There is no such invite in this space');
|
|
1168
|
+
// Who joined with it before this node closed it stays: the close names what it saw, and the replay does the rest.
|
|
1169
|
+
await upsert(INVITE_COLLECTION, await inviteRecordKey(inviteDid), { key: inviteDid, role: invite.role, open: false });
|
|
1170
|
+
},
|
|
1171
|
+
async revoke(token) {
|
|
1172
|
+
const cid = await noteCid(token);
|
|
1173
|
+
const keep = [];
|
|
1174
|
+
for (const version of await everyCurrent()) {
|
|
1175
|
+
if (version.proof && (await noteCid(version.proof)) === cid)
|
|
1176
|
+
keep.push(version.id);
|
|
1177
|
+
}
|
|
1178
|
+
await upsert(REVOKE_COLLECTION, await revokeKey(cid), { note: token, ...(keep.length ? { keep: keep.slice(0, MAX_KEEP) } : {}) });
|
|
1179
|
+
},
|
|
1180
|
+
async isRevoked(token) {
|
|
1181
|
+
return (await access()).history.revoked(await noteCid(token)) !== null;
|
|
1182
|
+
},
|
|
1183
|
+
async join(secret) {
|
|
1184
|
+
const { history } = await access();
|
|
1185
|
+
if (standing(history.current, deps.rootDid)) {
|
|
1186
|
+
waitingInvite = false;
|
|
1187
|
+
deps.onJoined?.();
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
const pair = await deriveInviteKey(secret, provider);
|
|
1191
|
+
const invite = history.current.invites.get(pair.did);
|
|
1192
|
+
if (!invite?.open)
|
|
1193
|
+
return false;
|
|
1194
|
+
await upsert(MEMBER_COLLECTION, await memberKey(deps.rootDid), { did: deps.rootDid, role: invite.role, invite: { key: pair.did, signature: await signInvite(space.id, deps.rootDid, pair, provider) } }, { joining: true });
|
|
1195
|
+
waitingInvite = false;
|
|
1196
|
+
deps.onJoined?.();
|
|
1197
|
+
return true;
|
|
1198
|
+
},
|
|
1199
|
+
async status() {
|
|
1200
|
+
return {
|
|
1201
|
+
space: space.id,
|
|
1202
|
+
connection,
|
|
1203
|
+
peers: connectedPeers(),
|
|
1204
|
+
root: await storage.getRootCid(),
|
|
1205
|
+
rejected,
|
|
1206
|
+
};
|
|
1207
|
+
},
|
|
1208
|
+
async close() {
|
|
1209
|
+
if (watchTimer)
|
|
1210
|
+
clearInterval(watchTimer);
|
|
1211
|
+
if (announceTimer)
|
|
1212
|
+
clearTimeout(announceTimer);
|
|
1213
|
+
channel?.close();
|
|
1214
|
+
sync.stop();
|
|
1215
|
+
for (const network of networks)
|
|
1216
|
+
network.disconnect();
|
|
1217
|
+
await storage.close();
|
|
1218
|
+
},
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
//# sourceMappingURL=space-runtime.js.map
|