@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,872 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node
|
|
3
|
+
* A node: one identity, the spaces it holds, and everything needed to read,
|
|
4
|
+
* write and sync them.
|
|
5
|
+
*
|
|
6
|
+
* This is what a browser tab, a command line, an always-on daemon and an agent
|
|
7
|
+
* all have in common. Each of them is a thin layer over the same node — the
|
|
8
|
+
* tab draws it, the CLI prints it, the daemon keeps it running and serves
|
|
9
|
+
* sockets, the agent calls it through MCP or WebMCP.
|
|
10
|
+
*
|
|
11
|
+
* **The root key signs once.** Starting a node generates a session key and asks
|
|
12
|
+
* the root signer for a note saying that key may write for the next hour. The
|
|
13
|
+
* note is renewed before it runs out. Every record is signed by the session
|
|
14
|
+
* key, so the root — a seed in this page, an account home, whatever holds it — is asked
|
|
15
|
+
* for one signature an hour, never one per write.
|
|
16
|
+
*/
|
|
17
|
+
import { runQuery } from '../query/engine.js';
|
|
18
|
+
import { nameOf } from '../query/types.js';
|
|
19
|
+
import { createP256Provider } from '../identity/crypto-p256.js';
|
|
20
|
+
import { publicKeyToDid, P256_MULTICODEC } from '../identity/did.js';
|
|
21
|
+
import { delegateCapabilities, parseUCAN, verifyUCAN } from '../identity/ucan.js';
|
|
22
|
+
import { isAgentNote } from '../identity/agent-note.js';
|
|
23
|
+
import { createSigner } from '../schema/signer.js';
|
|
24
|
+
import { createSchemaEngine } from '../schema/schema-engine.js';
|
|
25
|
+
import { createSpaceManager, parseSpaceInvite } from '../space/space-manager.js';
|
|
26
|
+
import { meshFor, noteCid, openSpaceRuntime } from './space-runtime.js';
|
|
27
|
+
import { createServerAuth } from '../network/peer-auth.js';
|
|
28
|
+
import { deriveInviteKey } from '../space/space-access.js';
|
|
29
|
+
import { base64UrlDecode } from '../utils/encoding.js';
|
|
30
|
+
import { CARRIER_COLLECTION, deriveAccountRegistry, MEMBERSHIP_COLLECTION, PROFILE_COLLECTION, PROFILE_KEY, } from '../space/account-registry.js';
|
|
31
|
+
import { CARRY_CLOSED_KEY, makePass, PASS_COLLECTION, passKey } from '../space/pass.js';
|
|
32
|
+
/** Everything a session key may do, across every space the node holds */
|
|
33
|
+
export const SESSION_CAPABILITY = { with: '*', can: 'expression/*' };
|
|
34
|
+
const DEFAULT_TTL_SECONDS = 3600;
|
|
35
|
+
/** How long a removed carrier's space is kept open, for the carrier to hear it was removed */
|
|
36
|
+
const FORGET_CARRIER_AFTER_MS = 30 * 24 * 3600 * 1000;
|
|
37
|
+
function summarize(record) {
|
|
38
|
+
const { space, key } = record;
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
id: space.id,
|
|
41
|
+
name: space.name,
|
|
42
|
+
visibility: space.visibility,
|
|
43
|
+
creator: space.creator,
|
|
44
|
+
createdAt: space.createdAt,
|
|
45
|
+
readable: space.visibility === 'public' || key !== null,
|
|
46
|
+
writable: record.role !== null,
|
|
47
|
+
role: record.role,
|
|
48
|
+
joining: record.invite !== null,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Accepts a bare invite or a whole share link carrying one (`…#invite=…`). */
|
|
52
|
+
function bareInvite(invite) {
|
|
53
|
+
return /[#&?]invite=([^&\s]+)/.exec(invite)?.[1] ?? invite.trim();
|
|
54
|
+
}
|
|
55
|
+
/** Lets a long-lived timer not hold a process open on its own. */
|
|
56
|
+
function unref(timer) {
|
|
57
|
+
timer.unref?.();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Starts a node.
|
|
61
|
+
*
|
|
62
|
+
* @param config Who it acts for, where it keeps data, and how it reaches peers
|
|
63
|
+
* @returns The running node; call `close()` when done
|
|
64
|
+
*/
|
|
65
|
+
export async function createNode(config) {
|
|
66
|
+
const provider = config.provider ?? createP256Provider();
|
|
67
|
+
const signer = createSigner(provider);
|
|
68
|
+
const ttl = config.sessionTtlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
69
|
+
const schemas = createSchemaEngine();
|
|
70
|
+
for (const collection of config.collections ?? [])
|
|
71
|
+
schemas.registerCollection(collection);
|
|
72
|
+
// ─── Session ───────────────────────────────────────────────────────
|
|
73
|
+
const sessionKeys = config.sessionKey ?? (await provider.generateKeyPair());
|
|
74
|
+
const sessionDid = publicKeyToDid(await provider.exportPublicKey(sessionKeys.publicKey), P256_MULTICODEC);
|
|
75
|
+
const delegate = () => config.signer.delegate({
|
|
76
|
+
audience: sessionDid,
|
|
77
|
+
capabilities: [SESSION_CAPABILITY],
|
|
78
|
+
expiration: Math.floor(Date.now() / 1000) + ttl,
|
|
79
|
+
});
|
|
80
|
+
let current = await delegate();
|
|
81
|
+
let closed = false;
|
|
82
|
+
let renewTimer = null;
|
|
83
|
+
const scheduleRenewal = (inSeconds) => {
|
|
84
|
+
renewTimer = setTimeout(() => {
|
|
85
|
+
delegate()
|
|
86
|
+
.then((token) => {
|
|
87
|
+
current = token;
|
|
88
|
+
scheduleRenewal(ttl * 0.75);
|
|
89
|
+
})
|
|
90
|
+
.catch(() => {
|
|
91
|
+
// The signer said no or was unreachable. Keep the delegation we have
|
|
92
|
+
// and ask again soon; writes fail validation once it expires.
|
|
93
|
+
if (!closed)
|
|
94
|
+
scheduleRenewal(Math.min(60, ttl / 4));
|
|
95
|
+
});
|
|
96
|
+
}, inSeconds * 1000);
|
|
97
|
+
unref(renewTimer);
|
|
98
|
+
};
|
|
99
|
+
scheduleRenewal(ttl * 0.75);
|
|
100
|
+
const session = { did: sessionDid, key: sessionKeys.privateKey, proof: () => current.encoded };
|
|
101
|
+
const mesh = meshFor(config.network, sessionDid);
|
|
102
|
+
/**
|
|
103
|
+
* A node that writes under an agent's note — an agent on someone's computer
|
|
104
|
+
* running a node of its own. It follows the account, but never writes for
|
|
105
|
+
* it: no list of spaces, no passes, no name. Every peer would ignore those.
|
|
106
|
+
*/
|
|
107
|
+
const agentSession = isAgentNote(current.encoded);
|
|
108
|
+
// ─── Events ────────────────────────────────────────────────────────
|
|
109
|
+
const listeners = new Set();
|
|
110
|
+
const emit = (event) => {
|
|
111
|
+
for (const listener of listeners) {
|
|
112
|
+
try {
|
|
113
|
+
listener(event);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
console.error('Error in node event listener:', e);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
// ─── Spaces ────────────────────────────────────────────────────────
|
|
121
|
+
const registryStore = await config.stores('registry', { seal: true });
|
|
122
|
+
const registry = createSpaceManager(registryStore, provider);
|
|
123
|
+
const runtimes = new Map();
|
|
124
|
+
// The account's own space list, kept in a space every device of the account
|
|
125
|
+
// derives for itself. Hidden from `list`; everything else treats it as a space.
|
|
126
|
+
const account = config.accountKey ? await deriveAccountRegistry(config.accountKey, config.signer.did, provider) : null;
|
|
127
|
+
const accountSpaceId = account?.space.id ?? null;
|
|
128
|
+
async function findRecord(spaceId) {
|
|
129
|
+
return spaceId === accountSpaceId ? account : registry.get(spaceId);
|
|
130
|
+
}
|
|
131
|
+
async function requireRecord(spaceId) {
|
|
132
|
+
const record = await findRecord(spaceId);
|
|
133
|
+
if (!record)
|
|
134
|
+
throw new Error(`Unknown space: ${spaceId}`);
|
|
135
|
+
return record;
|
|
136
|
+
}
|
|
137
|
+
// Said once per space: the note this node writes under was revoked there.
|
|
138
|
+
const revokedIn = new Set();
|
|
139
|
+
async function checkRevoked(spaceId, open) {
|
|
140
|
+
if (revokedIn.has(spaceId) || !(await open.isRevoked(session.proof())))
|
|
141
|
+
return;
|
|
142
|
+
revokedIn.add(spaceId);
|
|
143
|
+
emit({ type: 'revoked', space: spaceId });
|
|
144
|
+
}
|
|
145
|
+
/** Runtime events pass through; a change to the account registry is also acted on. */
|
|
146
|
+
const fromRuntime = (event) => {
|
|
147
|
+
emit(event);
|
|
148
|
+
// Which peers are the account's own is read off the registry's peers, so
|
|
149
|
+
// a device coming or going there changes every open space's status too.
|
|
150
|
+
if (event.type === 'status' && event.space === accountSpaceId) {
|
|
151
|
+
for (const spaceId of runtimes.keys())
|
|
152
|
+
if (spaceId !== accountSpaceId)
|
|
153
|
+
emit({ type: 'status', space: spaceId });
|
|
154
|
+
}
|
|
155
|
+
if (event.type === 'records')
|
|
156
|
+
void runtimes.get(event.space)?.then((rt) => checkRevoked(event.space, rt)).catch(() => { });
|
|
157
|
+
// A space joined with an invite whose record had not arrived: perhaps it has now.
|
|
158
|
+
if (event.type === 'records' && event.space !== accountSpaceId)
|
|
159
|
+
void finishJoining(event.space);
|
|
160
|
+
if (event.type === 'records' && event.space === accountSpaceId) {
|
|
161
|
+
emit({ type: 'account' });
|
|
162
|
+
void reconcile();
|
|
163
|
+
// A rename on another device reaches the spaces open here too.
|
|
164
|
+
void publishProfileToOpenSpaces();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
/** The account's name, from its registry — null without an account key, or before one is set */
|
|
168
|
+
async function ownName() {
|
|
169
|
+
if (!accountSpaceId)
|
|
170
|
+
return null;
|
|
171
|
+
const profile = await (await runtime(accountSpaceId)).get(PROFILE_KEY);
|
|
172
|
+
return profile?.verified && profile.root === config.signer.did && typeof profile.body?.name === 'string' ? profile.body.name : null;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Tells a space who this account is. Done when the space opens and when the
|
|
176
|
+
* name changes — not by opening every space, which would start syncing them
|
|
177
|
+
* all; a space not open now hears on its next open.
|
|
178
|
+
*/
|
|
179
|
+
async function publishProfile(spaceId, open) {
|
|
180
|
+
if (spaceId === accountSpaceId || agentSession)
|
|
181
|
+
return;
|
|
182
|
+
const name = await ownName();
|
|
183
|
+
if (name)
|
|
184
|
+
await open.publishProfile({ name });
|
|
185
|
+
}
|
|
186
|
+
async function publishProfileToOpenSpaces() {
|
|
187
|
+
for (const [spaceId, open] of runtimes) {
|
|
188
|
+
await open.then((rt) => publishProfile(spaceId, rt)).catch(() => { });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function runtime(spaceId) {
|
|
192
|
+
if (closed)
|
|
193
|
+
return Promise.reject(new Error('Node is closed'));
|
|
194
|
+
let open = runtimes.get(spaceId);
|
|
195
|
+
if (!open) {
|
|
196
|
+
open = requireRecord(spaceId).then((record) => openSpaceRuntime({
|
|
197
|
+
record,
|
|
198
|
+
stores: config.stores,
|
|
199
|
+
provider,
|
|
200
|
+
signer,
|
|
201
|
+
schemas,
|
|
202
|
+
session,
|
|
203
|
+
rootDid: config.signer.did,
|
|
204
|
+
...(config.network ? { network: config.network } : {}),
|
|
205
|
+
...(mesh ? { mesh } : {}),
|
|
206
|
+
peopleOnly: spaceId === accountSpaceId || carrySpaces.has(spaceId),
|
|
207
|
+
watchIntervalMs: config.watchIntervalMs ?? 2000,
|
|
208
|
+
emit: fromRuntime,
|
|
209
|
+
onRole: (role) => {
|
|
210
|
+
if (spaceId === accountSpaceId)
|
|
211
|
+
return;
|
|
212
|
+
// Holding a role now — perhaps just joined — means the space may hear who this is.
|
|
213
|
+
if (role !== null)
|
|
214
|
+
void runtimes.get(spaceId)?.then((rt) => publishProfile(spaceId, rt)).catch(() => { });
|
|
215
|
+
if (record.role === role)
|
|
216
|
+
return;
|
|
217
|
+
void registry.setRole(spaceId, role).then(() => emit({ type: 'spaces' }));
|
|
218
|
+
},
|
|
219
|
+
onJoined: () => {
|
|
220
|
+
if (!record.invite)
|
|
221
|
+
return;
|
|
222
|
+
void registry.clearInvite(spaceId).then(() => emit({ type: 'spaces' }));
|
|
223
|
+
},
|
|
224
|
+
}));
|
|
225
|
+
// A failed open must not be cached, or the space stays broken until restart.
|
|
226
|
+
open.catch(() => runtimes.delete(spaceId));
|
|
227
|
+
void open.then((rt) => publishProfile(spaceId, rt)).catch(() => { });
|
|
228
|
+
// A revoke that arrived on an earlier visit.
|
|
229
|
+
void open.then((rt) => checkRevoked(spaceId, rt)).catch(() => { });
|
|
230
|
+
runtimes.set(spaceId, open);
|
|
231
|
+
}
|
|
232
|
+
return open;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Uses a waiting invite, once its record has reached this device. Asked
|
|
236
|
+
* again while it is still working, it runs once more afterwards — the
|
|
237
|
+
* record that makes the difference may be the one that arrived meanwhile.
|
|
238
|
+
*/
|
|
239
|
+
const joining = new Map();
|
|
240
|
+
async function finishJoining(spaceId) {
|
|
241
|
+
const running = joining.get(spaceId);
|
|
242
|
+
if (running) {
|
|
243
|
+
running.again = true;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const state = { again: true };
|
|
247
|
+
joining.set(spaceId, state);
|
|
248
|
+
try {
|
|
249
|
+
while (state.again && !closed) {
|
|
250
|
+
state.again = false;
|
|
251
|
+
const record = await registry.get(spaceId);
|
|
252
|
+
if (!record?.invite)
|
|
253
|
+
break;
|
|
254
|
+
try {
|
|
255
|
+
if (await (await runtime(spaceId)).join(record.invite))
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// Not yet; the next change to the space tries again.
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
joining.delete(spaceId);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function closeRuntime(spaceId) {
|
|
268
|
+
const open = runtimes.get(spaceId);
|
|
269
|
+
if (!open)
|
|
270
|
+
return;
|
|
271
|
+
runtimes.delete(spaceId);
|
|
272
|
+
await (await open).close();
|
|
273
|
+
}
|
|
274
|
+
// ─── Memberships ───────────────────────────────────────────────────
|
|
275
|
+
//
|
|
276
|
+
// One record per space in the registry, key `space:<id>`, holding its
|
|
277
|
+
// invite. Current and live: the account belongs to the space. Current and
|
|
278
|
+
// deleted: it left — and rejoining is simply the next version. Every device
|
|
279
|
+
// converges on the same answer, because it is the same record.
|
|
280
|
+
const membershipKey = (spaceId) => `space:${spaceId}`;
|
|
281
|
+
async function memberships() {
|
|
282
|
+
if (!accountSpaceId)
|
|
283
|
+
return [];
|
|
284
|
+
const records = await (await runtime(accountSpaceId)).list({ collection: MEMBERSHIP_COLLECTION, includeDeleted: true });
|
|
285
|
+
// Only the account itself may say what it belongs to.
|
|
286
|
+
return records.filter((record) => record.verified &&
|
|
287
|
+
record.root === config.signer.did &&
|
|
288
|
+
(record.deleted || (typeof record.body?.space === 'string' && record.key === membershipKey(record.body.space))));
|
|
289
|
+
}
|
|
290
|
+
async function remember(spaceId) {
|
|
291
|
+
if (!accountSpaceId || agentSession)
|
|
292
|
+
return;
|
|
293
|
+
const open = await runtime(accountSpaceId);
|
|
294
|
+
if (await open.get(membershipKey(spaceId)))
|
|
295
|
+
return;
|
|
296
|
+
// A view-only invite: what lets the account write is its role in the
|
|
297
|
+
// space, which every device reads there. Invite secrets are never kept.
|
|
298
|
+
const invite = await registry.createInvite(spaceId, config.signer.did);
|
|
299
|
+
await open.upsertSystem(MEMBERSHIP_COLLECTION, membershipKey(spaceId), { space: spaceId, invite });
|
|
300
|
+
}
|
|
301
|
+
async function forget(spaceId) {
|
|
302
|
+
if (!accountSpaceId)
|
|
303
|
+
return;
|
|
304
|
+
const open = await runtime(accountSpaceId);
|
|
305
|
+
if (await open.get(membershipKey(spaceId)))
|
|
306
|
+
await open.removeSystem(membershipKey(spaceId));
|
|
307
|
+
}
|
|
308
|
+
// ─── Carriers ──────────────────────────────────────────────────────
|
|
309
|
+
//
|
|
310
|
+
// One record per carrier in the registry, naming the carry space shared with
|
|
311
|
+
// it. Every device of the account opens that space and keeps a pass in it
|
|
312
|
+
// for each of the account's spaces (`space/pass.ts`) — so a space joined in
|
|
313
|
+
// any app is carried, with nothing to do in the home.
|
|
314
|
+
const carrierKey = async (spaceId) => (await passKey(spaceId)).replace(/^pass:/, 'carrier:');
|
|
315
|
+
/**
|
|
316
|
+
* Carrier records the account wrote, live and removed, each with its carry
|
|
317
|
+
* space — for a removed one, as its last version with a body said.
|
|
318
|
+
*/
|
|
319
|
+
async function carrierRecords() {
|
|
320
|
+
if (!accountSpaceId)
|
|
321
|
+
return [];
|
|
322
|
+
const open = await runtime(accountSpaceId);
|
|
323
|
+
const found = [];
|
|
324
|
+
for (const record of await open.list({ collection: CARRIER_COLLECTION, includeDeleted: true })) {
|
|
325
|
+
if (!record.verified || record.root !== config.signer.did)
|
|
326
|
+
continue;
|
|
327
|
+
if (!record.deleted) {
|
|
328
|
+
if (typeof record.body?.space === 'string' && typeof record.body.invite === 'string')
|
|
329
|
+
found.push({ record, space: record.body.space });
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const before = (await open.history(record.key)).find((version) => typeof version.body?.space === 'string');
|
|
333
|
+
if (before?.body)
|
|
334
|
+
found.push({ record, space: before.body.space });
|
|
335
|
+
}
|
|
336
|
+
return found;
|
|
337
|
+
}
|
|
338
|
+
/** Every carry space, live or not — kept out of the account's own list */
|
|
339
|
+
let carrySpaces = new Set();
|
|
340
|
+
/** Puts a pass in a carrier's space for each of the account's spaces, and takes away the rest. */
|
|
341
|
+
async function syncPasses(carrier) {
|
|
342
|
+
if (!account || agentSession)
|
|
343
|
+
return;
|
|
344
|
+
const wanted = new Map();
|
|
345
|
+
// The registry too, so a restore can come through the carrier.
|
|
346
|
+
wanted.set(await passKey(account.space.id), await makePass(account));
|
|
347
|
+
for (const membership of await memberships()) {
|
|
348
|
+
if (membership.deleted || !membership.body)
|
|
349
|
+
continue;
|
|
350
|
+
const spaceId = membership.body.space;
|
|
351
|
+
if (carrySpaces.has(spaceId))
|
|
352
|
+
continue;
|
|
353
|
+
const record = await registry.get(spaceId);
|
|
354
|
+
if (!record)
|
|
355
|
+
continue;
|
|
356
|
+
try {
|
|
357
|
+
wanted.set(await passKey(spaceId), await makePass(record));
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// Held without its key — nothing to pass on until it arrives.
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const carry = await runtime(carrier.space);
|
|
364
|
+
const held = new Map((await carry.list({ collection: PASS_COLLECTION, includeDeleted: true }))
|
|
365
|
+
.filter((record) => record.verified && record.root === config.signer.did && record.key.startsWith('pass:'))
|
|
366
|
+
.map((record) => [record.key, record]));
|
|
367
|
+
for (const [key, pass] of wanted) {
|
|
368
|
+
const current = held.get(key);
|
|
369
|
+
if (!current || current.deleted || JSON.stringify(current.body) !== JSON.stringify(pass)) {
|
|
370
|
+
await carry.upsertSystem(PASS_COLLECTION, key, pass);
|
|
371
|
+
}
|
|
372
|
+
held.delete(key);
|
|
373
|
+
}
|
|
374
|
+
for (const [key, record] of held)
|
|
375
|
+
if (!record.deleted)
|
|
376
|
+
await carry.removeSystem(key);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Makes this node's spaces match the account's: join what the account
|
|
380
|
+
* belongs to, leave what it left, and record anything held here that the
|
|
381
|
+
* registry has never heard of.
|
|
382
|
+
*/
|
|
383
|
+
async function reconcileOnce() {
|
|
384
|
+
if (!accountSpaceId || closed)
|
|
385
|
+
return;
|
|
386
|
+
const held = new Set((await registry.list()).map((record) => record.space.id));
|
|
387
|
+
const known = new Set();
|
|
388
|
+
let changed = false;
|
|
389
|
+
// Carry spaces first: they are the account's, but not spaces it uses.
|
|
390
|
+
const carriers = await carrierRecords();
|
|
391
|
+
carrySpaces = new Set([...carrySpaces, ...carriers.map(({ space }) => space)]);
|
|
392
|
+
for (const { record, space: spaceId } of carriers) {
|
|
393
|
+
if (record.deleted) {
|
|
394
|
+
// Removed on some device. Its carry space stays open a while, so the
|
|
395
|
+
// carrier — perhaps offline now — still hears that it should forget.
|
|
396
|
+
if (!held.has(spaceId) || Date.now() - Date.parse(record.updatedAt) < FORGET_CARRIER_AFTER_MS)
|
|
397
|
+
continue;
|
|
398
|
+
await closeRuntime(spaceId);
|
|
399
|
+
await registry.remove(spaceId);
|
|
400
|
+
held.delete(spaceId);
|
|
401
|
+
}
|
|
402
|
+
else if (!held.has(spaceId)) {
|
|
403
|
+
try {
|
|
404
|
+
await registry.join(record.body.invite);
|
|
405
|
+
held.add(spaceId);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// An unreadable invite; the next version written for it will do.
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
for (const spaceId of carrySpaces)
|
|
413
|
+
known.add(spaceId);
|
|
414
|
+
for (const membership of await memberships()) {
|
|
415
|
+
const spaceId = membership.key.slice('space:'.length);
|
|
416
|
+
known.add(spaceId);
|
|
417
|
+
if (!membership.deleted && !held.has(spaceId)) {
|
|
418
|
+
try {
|
|
419
|
+
await registry.join(membership.body.invite);
|
|
420
|
+
changed = true;
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// An unreadable invite; the next version written for it will do.
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
else if (membership.deleted && held.has(spaceId)) {
|
|
427
|
+
// The account left it, on some device.
|
|
428
|
+
await closeRuntime(spaceId);
|
|
429
|
+
await registry.remove(spaceId);
|
|
430
|
+
changed = true;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// Held here but never recorded — joined before the registry existed, or on
|
|
434
|
+
// a node without the account key. Recorded now, so other devices follow.
|
|
435
|
+
for (const spaceId of held) {
|
|
436
|
+
if (!known.has(spaceId))
|
|
437
|
+
await remember(spaceId);
|
|
438
|
+
}
|
|
439
|
+
for (const { record, space: spaceId } of carriers) {
|
|
440
|
+
if (record.deleted || !record.body || !held.has(spaceId))
|
|
441
|
+
continue;
|
|
442
|
+
// Only a device that may write in the carry space can do this; others leave it to one that can.
|
|
443
|
+
await syncPasses(record.body).catch(() => { });
|
|
444
|
+
}
|
|
445
|
+
if (changed)
|
|
446
|
+
emit({ type: 'spaces' });
|
|
447
|
+
}
|
|
448
|
+
let reconciling = Promise.resolve();
|
|
449
|
+
function reconcile() {
|
|
450
|
+
reconciling = reconciling.then(reconcileOnce).catch((error) => {
|
|
451
|
+
if (!closed)
|
|
452
|
+
console.error('Could not reconcile the account registry:', error);
|
|
453
|
+
});
|
|
454
|
+
return reconciling;
|
|
455
|
+
}
|
|
456
|
+
const spaces = Object.freeze({
|
|
457
|
+
async list() {
|
|
458
|
+
return (await registry.list()).filter((record) => !carrySpaces.has(record.space.id)).map(summarize);
|
|
459
|
+
},
|
|
460
|
+
async get(spaceId) {
|
|
461
|
+
const record = await findRecord(spaceId);
|
|
462
|
+
return record ? summarize(record) : null;
|
|
463
|
+
},
|
|
464
|
+
async create(params) {
|
|
465
|
+
const record = await registry.create({
|
|
466
|
+
name: params.name,
|
|
467
|
+
visibility: params.visibility,
|
|
468
|
+
creator: config.signer.did,
|
|
469
|
+
...(params.roles ? { roles: params.roles } : {}),
|
|
470
|
+
...(params.creatorRole ? { creatorRole: params.creatorRole } : {}),
|
|
471
|
+
});
|
|
472
|
+
await remember(record.space.id);
|
|
473
|
+
emit({ type: 'spaces' });
|
|
474
|
+
return summarize(record);
|
|
475
|
+
},
|
|
476
|
+
async invite(spaceId, options = {}) {
|
|
477
|
+
if (options.write === false)
|
|
478
|
+
return registry.createInvite(spaceId, config.signer.did);
|
|
479
|
+
const open = await runtime(spaceId);
|
|
480
|
+
const { roles, role: mine } = await open.access();
|
|
481
|
+
// By default the lowest role below your own; with none below you, a view-only invite.
|
|
482
|
+
const role = options.role ?? (mine ? [...roles].reverse().find((candidate) => candidate.rank < mine.rank)?.name : undefined);
|
|
483
|
+
if (!role) {
|
|
484
|
+
if (options.role !== undefined || !mine)
|
|
485
|
+
throw new Error(mine ? `There is no role "${options.role}"` : 'You hold no role here, so you can only share it to view');
|
|
486
|
+
return registry.createInvite(spaceId, config.signer.did);
|
|
487
|
+
}
|
|
488
|
+
const { secret } = await open.openInvite(role);
|
|
489
|
+
return registry.createInvite(spaceId, config.signer.did, { secret, role });
|
|
490
|
+
},
|
|
491
|
+
preview(invite) {
|
|
492
|
+
const parsed = parseSpaceInvite(bareInvite(invite));
|
|
493
|
+
const { id, name, visibility, creator, createdAt } = parsed.space;
|
|
494
|
+
return {
|
|
495
|
+
space: { id, name, visibility, creator, createdAt },
|
|
496
|
+
invitedBy: parsed.invitedBy,
|
|
497
|
+
carriesKey: typeof parsed.key === 'string',
|
|
498
|
+
carriesWrite: typeof parsed.invite === 'string',
|
|
499
|
+
role: typeof parsed.invite === 'string' ? (parsed.role ?? null) : null,
|
|
500
|
+
};
|
|
501
|
+
},
|
|
502
|
+
async join(invite) {
|
|
503
|
+
const record = await registry.join(bareInvite(invite));
|
|
504
|
+
// A runtime opened before the key arrived would still be unable to read.
|
|
505
|
+
await closeRuntime(record.space.id);
|
|
506
|
+
await remember(record.space.id);
|
|
507
|
+
// Uses the invite now if its record is here already — otherwise when it arrives.
|
|
508
|
+
await finishJoining(record.space.id);
|
|
509
|
+
emit({ type: 'spaces' });
|
|
510
|
+
return summarize((await registry.get(record.space.id)) ?? record);
|
|
511
|
+
},
|
|
512
|
+
async leave(spaceId) {
|
|
513
|
+
if (spaceId === accountSpaceId)
|
|
514
|
+
throw new Error('The account registry cannot be left');
|
|
515
|
+
await forget(spaceId);
|
|
516
|
+
await closeRuntime(spaceId);
|
|
517
|
+
await registry.remove(spaceId);
|
|
518
|
+
emit({ type: 'spaces' });
|
|
519
|
+
},
|
|
520
|
+
async open(spaceId) {
|
|
521
|
+
await runtime(spaceId);
|
|
522
|
+
},
|
|
523
|
+
close: closeRuntime,
|
|
524
|
+
async status(spaceId) {
|
|
525
|
+
const status = await (await runtime(spaceId)).status();
|
|
526
|
+
if (!accountSpaceId)
|
|
527
|
+
return { ...status, own: [], carriers: [] };
|
|
528
|
+
// A peer's key does not say whose it is. But only this account's devices
|
|
529
|
+
// and apps can read the account registry, so a peer there is one of ours
|
|
530
|
+
// — except a carrier, which the registry names by its key.
|
|
531
|
+
const carrierKeys = new Set((await carrierRecords()).filter(({ record }) => !record.deleted && record.body).map(({ record }) => record.body.did));
|
|
532
|
+
const inRegistry = new Set((await (await runtime(accountSpaceId)).status()).peers);
|
|
533
|
+
return {
|
|
534
|
+
...status,
|
|
535
|
+
own: status.peers.filter((peer) => inRegistry.has(peer) && !carrierKeys.has(peer)),
|
|
536
|
+
carriers: status.peers.filter((peer) => carrierKeys.has(peer)),
|
|
537
|
+
};
|
|
538
|
+
},
|
|
539
|
+
async profiles(spaceId) {
|
|
540
|
+
return (await runtime(spaceId)).profiles();
|
|
541
|
+
},
|
|
542
|
+
async access(spaceId) {
|
|
543
|
+
return (await runtime(spaceId)).access();
|
|
544
|
+
},
|
|
545
|
+
async setMember(spaceId, did, role) {
|
|
546
|
+
await (await runtime(spaceId)).setMember(did, role);
|
|
547
|
+
},
|
|
548
|
+
async putRole(spaceId, role) {
|
|
549
|
+
await (await runtime(spaceId)).putRole(role);
|
|
550
|
+
},
|
|
551
|
+
async removeRole(spaceId, name) {
|
|
552
|
+
await (await runtime(spaceId)).removeRole(name);
|
|
553
|
+
},
|
|
554
|
+
async closeInvite(spaceId, keyOrLink) {
|
|
555
|
+
let key = keyOrLink;
|
|
556
|
+
// The link itself will do: its secret names the invite.
|
|
557
|
+
if (!keyOrLink.startsWith('did:key:')) {
|
|
558
|
+
const secret = parseSpaceInvite(bareInvite(keyOrLink)).invite;
|
|
559
|
+
if (!secret)
|
|
560
|
+
throw new Error('That invite is view-only — there is nothing to close. To stop someone reading, the space needs a new key.');
|
|
561
|
+
key = (await deriveInviteKey(base64UrlDecode(secret), provider)).did;
|
|
562
|
+
}
|
|
563
|
+
await (await runtime(spaceId)).closeInvite(key);
|
|
564
|
+
},
|
|
565
|
+
async revoke(spaceId, token) {
|
|
566
|
+
await (await runtime(spaceId)).revoke(token);
|
|
567
|
+
},
|
|
568
|
+
async authenticator(spaceId) {
|
|
569
|
+
const record = await findRecord(spaceId);
|
|
570
|
+
if (!record)
|
|
571
|
+
return null;
|
|
572
|
+
// Every peer proves its own DID; in a private space, readers are also
|
|
573
|
+
// checked against the space's public read key. The welcome is signed by
|
|
574
|
+
// the key this node introduces itself with.
|
|
575
|
+
const readKey = record.space.visibility === 'private' ? (record.space.readKey ?? '') : null;
|
|
576
|
+
return createServerAuth(spaceId, readKey, sessionKeys.privateKey, provider);
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
// With an account key, start following the registry at once: it is how this
|
|
580
|
+
// node learns which spaces it belongs to.
|
|
581
|
+
if (accountSpaceId) {
|
|
582
|
+
await runtime(accountSpaceId);
|
|
583
|
+
await reconcile();
|
|
584
|
+
}
|
|
585
|
+
const carriers = Object.freeze({
|
|
586
|
+
async list() {
|
|
587
|
+
return (await carrierRecords())
|
|
588
|
+
.filter(({ record }) => !record.deleted && record.body)
|
|
589
|
+
.map(({ record: { body } }) => ({ space: body.space, did: body.did, name: body.name, since: body.since }));
|
|
590
|
+
},
|
|
591
|
+
async add(carrier) {
|
|
592
|
+
if (!accountSpaceId)
|
|
593
|
+
throw new Error('Using a carrier needs the account key');
|
|
594
|
+
const name = carrier.name.trim().slice(0, 80) || 'Carrier';
|
|
595
|
+
// Private, so the passes in it are sealed; the account alone may write there.
|
|
596
|
+
const record = await registry.create({ name: `Carried by ${name}`, visibility: 'private', creator: config.signer.did });
|
|
597
|
+
carrySpaces.add(record.space.id);
|
|
598
|
+
const invite = await registry.createInvite(record.space.id, config.signer.did);
|
|
599
|
+
const body = { space: record.space.id, invite, did: carrier.did, name, since: new Date().toISOString() };
|
|
600
|
+
await (await runtime(accountSpaceId)).upsertSystem(CARRIER_COLLECTION, await carrierKey(record.space.id), body);
|
|
601
|
+
await syncPasses(body);
|
|
602
|
+
return { space: record.space.id, invite };
|
|
603
|
+
},
|
|
604
|
+
async remove(spaceId) {
|
|
605
|
+
if (!accountSpaceId)
|
|
606
|
+
throw new Error('Removing a carrier needs the account key');
|
|
607
|
+
const found = (await carrierRecords()).find(({ record, space }) => !record.deleted && space === spaceId)?.record;
|
|
608
|
+
if (!found)
|
|
609
|
+
return;
|
|
610
|
+
const carry = await runtime(spaceId);
|
|
611
|
+
// Passes gone first, then the word to forget everything, then the carrier's record.
|
|
612
|
+
for (const pass of await carry.list({ collection: PASS_COLLECTION })) {
|
|
613
|
+
if (pass.verified && pass.root === config.signer.did && pass.key.startsWith('pass:'))
|
|
614
|
+
await carry.removeSystem(pass.key);
|
|
615
|
+
}
|
|
616
|
+
await carry.upsertSystem(PASS_COLLECTION, CARRY_CLOSED_KEY, { v: 1, closed: true });
|
|
617
|
+
await (await runtime(accountSpaceId)).removeSystem(found.key);
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
const collections = Object.freeze({
|
|
621
|
+
async list(spaceId) {
|
|
622
|
+
return (await runtime(spaceId)).collections();
|
|
623
|
+
},
|
|
624
|
+
async define(spaceId, definition) {
|
|
625
|
+
return (await runtime(spaceId)).define(definition);
|
|
626
|
+
},
|
|
627
|
+
async delete(spaceId, name) {
|
|
628
|
+
return (await runtime(spaceId)).undefine(name);
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
const accountApi = Object.freeze({
|
|
632
|
+
async profile() {
|
|
633
|
+
if (!accountSpaceId)
|
|
634
|
+
return null;
|
|
635
|
+
// One record, key `profile`: its current version is the name, by the
|
|
636
|
+
// ordering rule — the same on every device, whatever their clocks say.
|
|
637
|
+
const profile = await (await runtime(accountSpaceId)).get(PROFILE_KEY);
|
|
638
|
+
if (!profile?.verified || profile.root !== config.signer.did || typeof profile.body?.name !== 'string')
|
|
639
|
+
return null;
|
|
640
|
+
return { name: profile.body.name, updatedAt: profile.updatedAt };
|
|
641
|
+
},
|
|
642
|
+
async setName(name) {
|
|
643
|
+
if (!accountSpaceId)
|
|
644
|
+
throw new Error('Renaming across devices needs the account key');
|
|
645
|
+
const trimmed = name.trim();
|
|
646
|
+
if (!trimmed)
|
|
647
|
+
throw new Error('A name cannot be empty');
|
|
648
|
+
const written = await (await runtime(accountSpaceId)).upsertSystem(PROFILE_COLLECTION, PROFILE_KEY, { name: trimmed });
|
|
649
|
+
emit({ type: 'account' });
|
|
650
|
+
await publishProfileToOpenSpaces();
|
|
651
|
+
return { name: trimmed, updatedAt: written.updatedAt };
|
|
652
|
+
},
|
|
653
|
+
async revoke(token) {
|
|
654
|
+
if (!accountSpaceId)
|
|
655
|
+
throw new Error('Revoking in the account registry needs the account key');
|
|
656
|
+
await (await runtime(accountSpaceId)).revoke(token);
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
const records = Object.freeze({
|
|
660
|
+
async list(spaceId, options) {
|
|
661
|
+
return (await runtime(spaceId)).list(options);
|
|
662
|
+
},
|
|
663
|
+
async get(spaceId, key) {
|
|
664
|
+
return (await runtime(spaceId)).get(key);
|
|
665
|
+
},
|
|
666
|
+
async put(spaceId, collection, body, options) {
|
|
667
|
+
return (await runtime(spaceId)).put(nameOf(collection), body, options);
|
|
668
|
+
},
|
|
669
|
+
async update(spaceId, key, body, options) {
|
|
670
|
+
return (await runtime(spaceId)).update(key, body, options);
|
|
671
|
+
},
|
|
672
|
+
async linked(spaceId, key, options) {
|
|
673
|
+
return (await runtime(spaceId)).linked(key, options);
|
|
674
|
+
},
|
|
675
|
+
async delete(spaceId, key) {
|
|
676
|
+
await (await runtime(spaceId)).remove(key);
|
|
677
|
+
},
|
|
678
|
+
async history(spaceId, key) {
|
|
679
|
+
return (await runtime(spaceId)).history(key);
|
|
680
|
+
},
|
|
681
|
+
async can(spaceId, action, target) {
|
|
682
|
+
return (await runtime(spaceId)).can(action, target);
|
|
683
|
+
},
|
|
684
|
+
async query(spaceId, query) {
|
|
685
|
+
const space = await runtime(spaceId);
|
|
686
|
+
return runQuery({
|
|
687
|
+
list: (collection) => space.list({ collection }),
|
|
688
|
+
get: (key) => space.get(key),
|
|
689
|
+
linked: (key, options) => space.linked(key, options),
|
|
690
|
+
}, query);
|
|
691
|
+
},
|
|
692
|
+
watch(spaceId, query, onResult, onError) {
|
|
693
|
+
// Changes arrive in bursts during sync; one run at a time, and one more
|
|
694
|
+
// after it if anything changed meanwhile — never a queue of stale runs.
|
|
695
|
+
let stopped = false;
|
|
696
|
+
let running = false;
|
|
697
|
+
let again = false;
|
|
698
|
+
const run = async () => {
|
|
699
|
+
if (running) {
|
|
700
|
+
again = true;
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
running = true;
|
|
704
|
+
do {
|
|
705
|
+
again = false;
|
|
706
|
+
try {
|
|
707
|
+
const result = await records.query(spaceId, query);
|
|
708
|
+
if (!stopped)
|
|
709
|
+
onResult(result);
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
if (!stopped)
|
|
713
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
714
|
+
}
|
|
715
|
+
} while (again && !stopped);
|
|
716
|
+
running = false;
|
|
717
|
+
};
|
|
718
|
+
const listener = (event) => {
|
|
719
|
+
if (event.type === 'records' && event.space === spaceId)
|
|
720
|
+
void run();
|
|
721
|
+
};
|
|
722
|
+
listeners.add(listener);
|
|
723
|
+
void run();
|
|
724
|
+
return () => {
|
|
725
|
+
stopped = true;
|
|
726
|
+
listeners.delete(listener);
|
|
727
|
+
};
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
// ─── Agents ────────────────────────────────────────────────────────
|
|
731
|
+
async function asAgent(agent) {
|
|
732
|
+
const agentDid = publicKeyToDid(await provider.exportPublicKey(agent.keys.publicKey), P256_MULTICODEC);
|
|
733
|
+
const checked = await verifyUCAN(agent.note, provider);
|
|
734
|
+
if (!checked.valid)
|
|
735
|
+
throw new Error(`The agent's note does not check out: ${checked.reason ?? 'invalid'}`);
|
|
736
|
+
if (!isAgentNote(agent.note))
|
|
737
|
+
throw new Error('That note is not an agent\'s — ask the account home for one with `agent: true`.');
|
|
738
|
+
const note = { ...parseUCAN(agent.note), encoded: agent.note, cid: await noteCid(agent.note) };
|
|
739
|
+
if (note.payload.aud !== agentDid)
|
|
740
|
+
throw new Error('That note was made out to a different key.');
|
|
741
|
+
if (note.payload.iss !== config.signer.did)
|
|
742
|
+
throw new Error('That note is from a different account.');
|
|
743
|
+
const as = { did: agentDid, key: agent.keys.privateKey, proof: () => note.encoded };
|
|
744
|
+
// The spaces its note names; `*` is every space, which a home never gives an agent but a note could say.
|
|
745
|
+
const all = note.payload.att.some((capability) => capability.with === '*');
|
|
746
|
+
const named = new Set(note.payload.att.filter((c) => c.with.startsWith('space:')).map((c) => c.with.slice('space:'.length)));
|
|
747
|
+
const allowed = (spaceId) => all || named.has(spaceId);
|
|
748
|
+
const inside = (spaceId) => {
|
|
749
|
+
if (!allowed(spaceId))
|
|
750
|
+
throw new Error('The agent was not given this space. The person can give it more in their account home.');
|
|
751
|
+
};
|
|
752
|
+
const person = (what) => async () => {
|
|
753
|
+
throw new Error(`An agent can't ${what}. Ask the person to do it.`);
|
|
754
|
+
};
|
|
755
|
+
const agentSpaces = Object.freeze({
|
|
756
|
+
...spaces,
|
|
757
|
+
list: async () => (await spaces.list()).filter((space) => allowed(space.id)),
|
|
758
|
+
get: async (spaceId) => (allowed(spaceId) ? spaces.get(spaceId) : null),
|
|
759
|
+
create: person('make spaces'),
|
|
760
|
+
invite: person('invite anyone'),
|
|
761
|
+
join: person('join spaces'),
|
|
762
|
+
leave: person('leave spaces'),
|
|
763
|
+
setMember: person('change who is in a space'),
|
|
764
|
+
putRole: person('change roles'),
|
|
765
|
+
removeRole: person('change roles'),
|
|
766
|
+
closeInvite: person('close invites'),
|
|
767
|
+
revoke: person('revoke notes'),
|
|
768
|
+
access: async (spaceId) => (inside(spaceId), spaces.access(spaceId)),
|
|
769
|
+
open: async (spaceId) => (inside(spaceId), spaces.open(spaceId)),
|
|
770
|
+
status: async (spaceId) => (inside(spaceId), spaces.status(spaceId)),
|
|
771
|
+
profiles: async (spaceId) => (inside(spaceId), spaces.profiles(spaceId)),
|
|
772
|
+
authenticator: async () => null,
|
|
773
|
+
});
|
|
774
|
+
const agentRecords = Object.freeze({
|
|
775
|
+
list: async (spaceId, options) => (inside(spaceId), records.list(spaceId, options)),
|
|
776
|
+
get: async (spaceId, key) => (inside(spaceId), records.get(spaceId, key)),
|
|
777
|
+
put: async (spaceId, collection, body, options) => {
|
|
778
|
+
inside(spaceId);
|
|
779
|
+
return (await runtime(spaceId)).put(nameOf(collection), body, { ...options, as });
|
|
780
|
+
},
|
|
781
|
+
update: async (spaceId, key, body, options) => {
|
|
782
|
+
inside(spaceId);
|
|
783
|
+
return (await runtime(spaceId)).update(key, body, { ...options, as });
|
|
784
|
+
},
|
|
785
|
+
linked: async (spaceId, key, options) => (inside(spaceId), records.linked(spaceId, key, options)),
|
|
786
|
+
delete: async (spaceId, key) => {
|
|
787
|
+
inside(spaceId);
|
|
788
|
+
await (await runtime(spaceId)).remove(key, { as });
|
|
789
|
+
},
|
|
790
|
+
history: async (spaceId, key) => (inside(spaceId), records.history(spaceId, key)),
|
|
791
|
+
can: async (spaceId, action, target) => allowed(spaceId) && records.can(spaceId, action, target),
|
|
792
|
+
query: async (spaceId, query) => (inside(spaceId), records.query(spaceId, query)),
|
|
793
|
+
watch: (spaceId, query, onResult, onError) => {
|
|
794
|
+
if (!allowed(spaceId)) {
|
|
795
|
+
onError?.(new Error('The agent was not given this space.'));
|
|
796
|
+
return () => { };
|
|
797
|
+
}
|
|
798
|
+
return records.watch(spaceId, query, onResult, onError);
|
|
799
|
+
},
|
|
800
|
+
});
|
|
801
|
+
const agentCollections = Object.freeze({
|
|
802
|
+
list: async (spaceId) => (inside(spaceId), collections.list(spaceId)),
|
|
803
|
+
// The runtime refuses these too, and every peer ignores them — said early, with what to do instead.
|
|
804
|
+
define: async () => {
|
|
805
|
+
throw new Error('An agent can\'t add or change collections. Propose an app instead (apps_propose), and a person in the space adds it.');
|
|
806
|
+
},
|
|
807
|
+
delete: async () => {
|
|
808
|
+
throw new Error('An agent can\'t remove collections. Ask the person to do it.');
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
return Object.freeze({
|
|
812
|
+
did: config.signer.did,
|
|
813
|
+
sessionDid: agentDid,
|
|
814
|
+
spaces: agentSpaces,
|
|
815
|
+
records: agentRecords,
|
|
816
|
+
collections: agentCollections,
|
|
817
|
+
account: Object.freeze({ profile: accountApi.profile, setName: person('rename the account'), revoke: person('revoke notes') }),
|
|
818
|
+
carriers: Object.freeze({ list: carriers.list, add: person('add a carrier'), remove: person('remove a carrier') }),
|
|
819
|
+
delegation: () => note,
|
|
820
|
+
delegate: person('pass its access on'),
|
|
821
|
+
asAgent: person('start another agent'),
|
|
822
|
+
subscribe: (listener) => node.subscribe((event) => {
|
|
823
|
+
if (!('space' in event) || allowed(event.space))
|
|
824
|
+
listener(event);
|
|
825
|
+
}),
|
|
826
|
+
// The agent stops; the node it acts through keeps running.
|
|
827
|
+
close: async () => { },
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
const node = Object.freeze({
|
|
831
|
+
did: config.signer.did,
|
|
832
|
+
sessionDid,
|
|
833
|
+
spaces,
|
|
834
|
+
records,
|
|
835
|
+
collections,
|
|
836
|
+
account: accountApi,
|
|
837
|
+
carriers,
|
|
838
|
+
asAgent,
|
|
839
|
+
delegation: () => current,
|
|
840
|
+
async delegate(params) {
|
|
841
|
+
const token = await delegateCapabilities({
|
|
842
|
+
parent: current,
|
|
843
|
+
issuer: { did: sessionDid, privateKey: sessionKeys.privateKey },
|
|
844
|
+
audience: params.audience,
|
|
845
|
+
capabilities: params.capabilities,
|
|
846
|
+
...(params.expiration !== undefined ? { expiration: params.expiration } : {}),
|
|
847
|
+
}, provider);
|
|
848
|
+
return { token, proofs: [current.encoded] };
|
|
849
|
+
},
|
|
850
|
+
subscribe(listener) {
|
|
851
|
+
listeners.add(listener);
|
|
852
|
+
return () => {
|
|
853
|
+
listeners.delete(listener);
|
|
854
|
+
};
|
|
855
|
+
},
|
|
856
|
+
async close() {
|
|
857
|
+
if (closed)
|
|
858
|
+
return;
|
|
859
|
+
closed = true;
|
|
860
|
+
if (renewTimer)
|
|
861
|
+
clearTimeout(renewTimer);
|
|
862
|
+
const open = [...runtimes.keys()];
|
|
863
|
+
await Promise.all(open.map((spaceId) => closeRuntime(spaceId)));
|
|
864
|
+
// Let go of the registry too: an open database connection blocks the
|
|
865
|
+
// browser from ever deleting it.
|
|
866
|
+
await registryStore.close();
|
|
867
|
+
listeners.clear();
|
|
868
|
+
},
|
|
869
|
+
});
|
|
870
|
+
return node;
|
|
871
|
+
}
|
|
872
|
+
//# sourceMappingURL=node.js.map
|