@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,783 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module session/auth
|
|
3
|
+
* Signing in, as one object any UI can draw.
|
|
4
|
+
*
|
|
5
|
+
* `createWeaveAuth` holds the whole flow — where the data lives, which account,
|
|
6
|
+
* the ways into it, creating one, arriving from a phone-pairing link — as a
|
|
7
|
+
* state you read and actions you call. The `<weave-auth>` element draws it, a
|
|
8
|
+
* React hook follows it, and an app that wants its own screens can draw it
|
|
9
|
+
* itself. None of them see the seed: it stays inside this object.
|
|
10
|
+
*
|
|
11
|
+
* The credential is a generated 26-character code. It is the seed, written out,
|
|
12
|
+
* which is what makes it work on a domain that has never heard of you — there is
|
|
13
|
+
* nothing stored for it to unlock. A password manager holds it exactly as it
|
|
14
|
+
* holds any generated password. The passkey and the short password are
|
|
15
|
+
* conveniences layered on top, local to one site, so the code only has to be
|
|
16
|
+
* fetched once per site.
|
|
17
|
+
*
|
|
18
|
+
* The identity key never signs a record. Signing in starts a node, which makes
|
|
19
|
+
* a throwaway session key and asks the identity for one note saying that key
|
|
20
|
+
* may write for it; everything after is signed by the session key.
|
|
21
|
+
*/
|
|
22
|
+
import { createIdentityManager } from '../identity/identity-manager.js';
|
|
23
|
+
import { createLocalRootSigner } from '../identity/root-signer.js';
|
|
24
|
+
import { AGENT_FACT } from '../identity/agent-note.js';
|
|
25
|
+
import { accountDataPath, newAccountId, createBrowserAccountStore } from '../identity/account-store.js';
|
|
26
|
+
import { createVault } from '../identity/folder-account.js';
|
|
27
|
+
import { withWrap, wrapSeedWithDeviceKey, unwrapSeedWithDeviceKey, unwrapSeedWithPassphrase, deviceWrapsFor, deriveVaultKey, deriveVaultKeyBytes, } from '../identity/account-vault.js';
|
|
28
|
+
import { createDeviceKey, getDeviceKey, deleteDeviceKey } from '../identity/device-key.js';
|
|
29
|
+
import { registerPasskey, authenticatePasskey, hasPlatformAuthenticator, renamePasskey } from '../identity/webauthn.js';
|
|
30
|
+
import { generateSeed, seedToRecoveryCode, recoveryCodeToSeed, isValidRecoveryCode } from '../identity/recovery-code.js';
|
|
31
|
+
import { isFolderStorageAvailable } from '../storage/directory-access.js';
|
|
32
|
+
import { base64UrlEncode } from '../utils/encoding.js';
|
|
33
|
+
import { createNode } from '../node/node.js';
|
|
34
|
+
import { copyAccountData } from '../node/copy.js';
|
|
35
|
+
import { isProtocolError, protocolError } from '../utils/errors.js';
|
|
36
|
+
import { deleteBrowserData, forgetPod, inspectPod, listAccounts, pickPod, recallPod, rememberPod, storesFor, } from './places.js';
|
|
37
|
+
import { createStaySignedIn } from './stay-signed-in.js';
|
|
38
|
+
import { grantCapabilities, MAX_GRANT_DAYS } from './connect.js';
|
|
39
|
+
import { clearPairingTicket, collectFromDesktop, offerToPhone as offerPairing, readPairingTicket, } from './pairing.js';
|
|
40
|
+
/** Turns a thrown value into something worth showing, ignoring a dismissed prompt. */
|
|
41
|
+
function describe(error) {
|
|
42
|
+
if (error instanceof DOMException && (error.name === 'NotAllowedError' || error.name === 'AbortError')) {
|
|
43
|
+
return null; // the person dismissed a passkey or folder prompt
|
|
44
|
+
}
|
|
45
|
+
if (isProtocolError(error)) {
|
|
46
|
+
return { message: error.message, code: error.code, ...(error.hint ? { hint: error.hint } : {}) };
|
|
47
|
+
}
|
|
48
|
+
return { message: error instanceof Error ? error.message : 'Something went wrong' };
|
|
49
|
+
}
|
|
50
|
+
const afterStorage = (accounts) => (accounts.length > 0 ? 'signIn' : 'welcome');
|
|
51
|
+
/**
|
|
52
|
+
* Creates the sign-in flow for this page.
|
|
53
|
+
* @param config Where to connect once signed in, and what to call the app
|
|
54
|
+
*/
|
|
55
|
+
export function createWeaveAuth(config = {}) {
|
|
56
|
+
const rpId = config.rpId ?? globalThis.location?.hostname ?? 'localhost';
|
|
57
|
+
const appName = config.appName ?? 'Weave';
|
|
58
|
+
const prefix = config.storageKey ?? 'weave';
|
|
59
|
+
const storage = config.storage !== undefined ? config.storage : (globalThis.localStorage ?? null);
|
|
60
|
+
const stay = createStaySignedIn(storage, rpId, prefix);
|
|
61
|
+
const browserAccounts = config.browser?.accounts ?? createBrowserAccountStore;
|
|
62
|
+
const browserStores = config.browser?.stores ?? ((account) => storesFor(account));
|
|
63
|
+
const LAST_ACCOUNT = `${prefix}.last-account`;
|
|
64
|
+
const BROWSER_CHOSEN = `${prefix}.storage-choice`;
|
|
65
|
+
const get = (key) => {
|
|
66
|
+
try {
|
|
67
|
+
return storage?.getItem(key) ?? null;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const set = (key, value) => {
|
|
74
|
+
try {
|
|
75
|
+
storage?.setItem(key, value);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Remembering is a convenience, never a requirement.
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
let state = Object.freeze({
|
|
82
|
+
stage: 'starting',
|
|
83
|
+
place: null,
|
|
84
|
+
accounts: [],
|
|
85
|
+
selectedId: null,
|
|
86
|
+
entry: null,
|
|
87
|
+
session: null,
|
|
88
|
+
freshCode: null,
|
|
89
|
+
busy: false,
|
|
90
|
+
error: null,
|
|
91
|
+
folderAvailable: isFolderStorageAvailable(),
|
|
92
|
+
pairing: readPairingTicket(),
|
|
93
|
+
pairingStage: null,
|
|
94
|
+
podChoice: null,
|
|
95
|
+
moved: null,
|
|
96
|
+
});
|
|
97
|
+
const listeners = new Set();
|
|
98
|
+
const update = (patch) => {
|
|
99
|
+
state = Object.freeze({ ...state, ...patch });
|
|
100
|
+
for (const listener of listeners)
|
|
101
|
+
listener(state);
|
|
102
|
+
};
|
|
103
|
+
/** The seed behind the open session. Never leaves this closure except as the account password. */
|
|
104
|
+
let seed = null;
|
|
105
|
+
let vaultKey = null;
|
|
106
|
+
let stopFollowingName = null;
|
|
107
|
+
let started = null;
|
|
108
|
+
const browserPlace = async () => ({ kind: 'browser', store: await browserAccounts(), directory: null });
|
|
109
|
+
const storesOf = (place, account, key) => place.directory ? storesFor(account, { directory: place.directory, vaultKey: key }) : browserStores(account);
|
|
110
|
+
// ─── Accounts in a place ───────────────────────────────────────────
|
|
111
|
+
async function openEntry(place, id) {
|
|
112
|
+
const [summary, vault] = await Promise.all([
|
|
113
|
+
listAccounts(place).then((accounts) => accounts.find((account) => account.id === id)),
|
|
114
|
+
place.store.read(id),
|
|
115
|
+
]);
|
|
116
|
+
if (!summary || !vault)
|
|
117
|
+
return null;
|
|
118
|
+
// A wrap whose key is missing from this browser cannot be offered, however
|
|
119
|
+
// confidently the vault lists it.
|
|
120
|
+
const usable = await Promise.all(deviceWrapsFor(vault, rpId).map(async (wrap) => ((await getDeviceKey(wrap.deviceKeyId).catch(() => null)) ? wrap : null)));
|
|
121
|
+
return {
|
|
122
|
+
summary,
|
|
123
|
+
vault,
|
|
124
|
+
shortcuts: usable.filter((wrap) => wrap !== null),
|
|
125
|
+
hasPassword: vault.wraps.some((wrap) => wrap.kind === 'passphrase'),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** Re-reads the accounts in a place and picks one to expand. */
|
|
129
|
+
async function refresh(place) {
|
|
130
|
+
const accounts = await listAccounts(place);
|
|
131
|
+
const remembered = get(LAST_ACCOUNT);
|
|
132
|
+
const chosen = accounts.find((account) => account.id === remembered) ?? accounts[0] ?? null;
|
|
133
|
+
update({
|
|
134
|
+
place,
|
|
135
|
+
accounts,
|
|
136
|
+
selectedId: chosen?.id ?? null,
|
|
137
|
+
entry: chosen ? await openEntry(place, chosen.id) : null,
|
|
138
|
+
});
|
|
139
|
+
return accounts;
|
|
140
|
+
}
|
|
141
|
+
async function refreshEntry() {
|
|
142
|
+
if (state.place && state.session)
|
|
143
|
+
update({ entry: await openEntry(state.place, state.session.account.id) });
|
|
144
|
+
}
|
|
145
|
+
// ─── Sessions ──────────────────────────────────────────────────────
|
|
146
|
+
/**
|
|
147
|
+
* Stops the open session's node. The session stays in the state until
|
|
148
|
+
* something replaces it — a restart swaps one session for the next, and
|
|
149
|
+
* signing out moves off the signed-in screens in the same change — so a
|
|
150
|
+
* screen never sees "ready" with no session.
|
|
151
|
+
*/
|
|
152
|
+
async function stopNode(node = state.session?.node) {
|
|
153
|
+
stopFollowingName?.();
|
|
154
|
+
stopFollowingName = null;
|
|
155
|
+
seed = null;
|
|
156
|
+
vaultKey = null;
|
|
157
|
+
await node?.close();
|
|
158
|
+
}
|
|
159
|
+
/** Starts the node for an unlocked account. */
|
|
160
|
+
async function startSession(place, account, unlocked) {
|
|
161
|
+
await stopNode();
|
|
162
|
+
const manager = createIdentityManager();
|
|
163
|
+
const identity = await manager.fromSeed(unlocked);
|
|
164
|
+
const key = await deriveVaultKey(unlocked);
|
|
165
|
+
const node = await createNode({
|
|
166
|
+
signer: createLocalRootSigner(identity, manager.getProvider()),
|
|
167
|
+
accountKey: await deriveVaultKeyBytes(unlocked),
|
|
168
|
+
stores: storesOf(place, account, key),
|
|
169
|
+
...(config.network ? { network: config.network } : {}),
|
|
170
|
+
});
|
|
171
|
+
seed = unlocked;
|
|
172
|
+
vaultKey = key;
|
|
173
|
+
const session = Object.freeze({ account, did: identity.did, sessionDid: node.sessionDid, node });
|
|
174
|
+
// The account's name follows it: a rename on another device or site
|
|
175
|
+
// arrives through the account registry and is taken here too.
|
|
176
|
+
const adopt = async () => {
|
|
177
|
+
const profile = await node.account.profile().catch(() => null);
|
|
178
|
+
const current = state.session;
|
|
179
|
+
if (!profile || !current || current.node !== node || profile.name === current.account.name)
|
|
180
|
+
return;
|
|
181
|
+
await adoptName(profile.name).catch(() => { });
|
|
182
|
+
};
|
|
183
|
+
stopFollowingName = node.subscribe((event) => {
|
|
184
|
+
if (event.type === 'account')
|
|
185
|
+
void adopt();
|
|
186
|
+
});
|
|
187
|
+
void adopt();
|
|
188
|
+
return session;
|
|
189
|
+
}
|
|
190
|
+
/** Records that an account was just used, and starts its session. */
|
|
191
|
+
async function begin(place, summary, unlocked) {
|
|
192
|
+
const used = { ...summary, lastUsedAt: new Date().toISOString() };
|
|
193
|
+
try {
|
|
194
|
+
const vault = await place.store.read(summary.id);
|
|
195
|
+
if (vault)
|
|
196
|
+
await place.store.write(used, vault);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// The order the picker opens in is not worth failing a sign-in over.
|
|
200
|
+
}
|
|
201
|
+
set(LAST_ACCOUNT, summary.id);
|
|
202
|
+
const session = await startSession(place, used, unlocked);
|
|
203
|
+
// So a refresh does not ask again — for as long as the setting allows.
|
|
204
|
+
await stay.remember(summary.id, place.kind, unlocked).catch(() => { });
|
|
205
|
+
return session;
|
|
206
|
+
}
|
|
207
|
+
/** Runs something that may fail, showing the failure; true when it worked. */
|
|
208
|
+
async function run(work) {
|
|
209
|
+
update({ busy: true, error: null });
|
|
210
|
+
try {
|
|
211
|
+
await work();
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
update({ error: describe(error) });
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
update({ busy: false });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** Runs a way in, and goes to the app when it ends in a session. */
|
|
223
|
+
const enter = (open) => run(async () => {
|
|
224
|
+
const session = await open();
|
|
225
|
+
update({ session, stage: 'ready' });
|
|
226
|
+
}).then(() => undefined);
|
|
227
|
+
// ─── Passkeys ──────────────────────────────────────────────────────
|
|
228
|
+
/**
|
|
229
|
+
* Runs a passkey ceremony as a gate.
|
|
230
|
+
*
|
|
231
|
+
* Nothing is read out of it. The ceremony proves a person with the
|
|
232
|
+
* authenticator is present, and the key that actually opens the account
|
|
233
|
+
* lives in this site's storage — which is why every passkey provider works,
|
|
234
|
+
* including the ones that store passkeys without the PRF extension.
|
|
235
|
+
*
|
|
236
|
+
* Steers to this device's own authenticator when it has one: the key this
|
|
237
|
+
* gates never leaves the browser, so a passkey synced by a manager gates
|
|
238
|
+
* nothing anywhere else.
|
|
239
|
+
*/
|
|
240
|
+
async function passkeyGate(mode, options = {}) {
|
|
241
|
+
const preferPlatform = mode === 'create' ? await hasPlatformAuthenticator() : false;
|
|
242
|
+
const steer = preferPlatform ? { hints: ['client-device'] } : {};
|
|
243
|
+
if (mode === 'create') {
|
|
244
|
+
const registration = await registerPasskey({
|
|
245
|
+
rpId,
|
|
246
|
+
rpName: appName,
|
|
247
|
+
userName: options.label ?? appName,
|
|
248
|
+
...steer,
|
|
249
|
+
...(preferPlatform ? { attachment: 'platform' } : {}),
|
|
250
|
+
});
|
|
251
|
+
return { credentialId: registration.credentialId, userHandle: registration.userHandle };
|
|
252
|
+
}
|
|
253
|
+
const auth = await authenticatePasskey(options.credentialId, { rpId, ...steer });
|
|
254
|
+
return { credentialId: auth.credentialId };
|
|
255
|
+
}
|
|
256
|
+
/** Rewrites the open account's vault. */
|
|
257
|
+
async function updateVault(make) {
|
|
258
|
+
const { place, session } = state;
|
|
259
|
+
if (!place || !session)
|
|
260
|
+
throw new Error('Not signed in.');
|
|
261
|
+
const current = await place.store.read(session.account.id);
|
|
262
|
+
if (!current)
|
|
263
|
+
throw new Error('That account is no longer here.');
|
|
264
|
+
await place.store.write(session.account, await make(current));
|
|
265
|
+
}
|
|
266
|
+
/** Takes a name for the open account here: the label it is filed under, and its passkey's label. */
|
|
267
|
+
async function adoptName(name) {
|
|
268
|
+
const { place, session } = state;
|
|
269
|
+
if (!place || !session)
|
|
270
|
+
throw new Error('Not signed in.');
|
|
271
|
+
const renamed = { ...session.account, name: name.trim() || session.account.name };
|
|
272
|
+
const vault = await place.store.read(session.account.id);
|
|
273
|
+
if (!vault)
|
|
274
|
+
throw new Error('That account is no longer here.');
|
|
275
|
+
await place.store.write(renamed, { ...vault, label: renamed.name });
|
|
276
|
+
// Recent browsers let a site relabel its passkey; older ones ignore it.
|
|
277
|
+
for (const wrap of deviceWrapsFor(vault, rpId)) {
|
|
278
|
+
if (wrap.userHandle)
|
|
279
|
+
await renamePasskey({ rpId, userHandle: wrap.userHandle, name: renamed.name }).catch(() => false);
|
|
280
|
+
}
|
|
281
|
+
update({ session: Object.freeze({ ...session, account: renamed }), accounts: await listAccounts(place) });
|
|
282
|
+
return renamed;
|
|
283
|
+
}
|
|
284
|
+
/** Changes to the open account's shortcuts, then re-reads them. */
|
|
285
|
+
const changeShortcut = (apply) => run(async () => {
|
|
286
|
+
if (!seed)
|
|
287
|
+
throw new Error('Not signed in.');
|
|
288
|
+
await apply(seed);
|
|
289
|
+
await refreshEntry();
|
|
290
|
+
});
|
|
291
|
+
/** Connected apps are remembered per account, on this device */
|
|
292
|
+
function writeConnections(connections) {
|
|
293
|
+
const account = state.session?.account.id;
|
|
294
|
+
if (account)
|
|
295
|
+
set(`${prefix}.connections:${account}`, JSON.stringify(connections));
|
|
296
|
+
update({});
|
|
297
|
+
}
|
|
298
|
+
// ─── The flow ──────────────────────────────────────────────────────
|
|
299
|
+
const auth = {
|
|
300
|
+
getState: () => state,
|
|
301
|
+
subscribe(listener) {
|
|
302
|
+
listeners.add(listener);
|
|
303
|
+
return () => listeners.delete(listener);
|
|
304
|
+
},
|
|
305
|
+
start() {
|
|
306
|
+
started ??= (async () => {
|
|
307
|
+
try {
|
|
308
|
+
// Asks nothing of the person: only whether what was granted before is still live.
|
|
309
|
+
const pod = await recallPod(false).catch(() => null);
|
|
310
|
+
const place = pod ?? (await browserPlace());
|
|
311
|
+
const accounts = await refresh(place);
|
|
312
|
+
if (accounts.length > 0) {
|
|
313
|
+
const kept = await stay.recall(place.kind).catch(() => null);
|
|
314
|
+
const summary = kept && accounts.find((account) => account.id === kept.accountId);
|
|
315
|
+
if (kept && summary) {
|
|
316
|
+
update({ session: await begin(place, summary, kept.seed), stage: 'ready' });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (state.pairing) {
|
|
321
|
+
update({ stage: 'pair' });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
// Nothing here yet and the storage question never answered: ask it first.
|
|
325
|
+
const undecided = !pod && accounts.length === 0 && state.folderAvailable && get(BROWSER_CHOSEN) !== 'browser';
|
|
326
|
+
update({ stage: undecided ? 'where' : afterStorage(accounts) });
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
update({ error: describe(error), stage: 'welcome' });
|
|
330
|
+
}
|
|
331
|
+
})();
|
|
332
|
+
return started;
|
|
333
|
+
},
|
|
334
|
+
async choosePod() {
|
|
335
|
+
await run(async () => {
|
|
336
|
+
const pod = await pickPod();
|
|
337
|
+
const { session, place } = state;
|
|
338
|
+
// Signed in: look before leaping. The picker needed the click, but
|
|
339
|
+
// switching waits for an answer.
|
|
340
|
+
if (session && place) {
|
|
341
|
+
const contents = await inspectPod(pod, place, session.account.did);
|
|
342
|
+
if (contents.same)
|
|
343
|
+
throw new Error('That is the pod you are already using.');
|
|
344
|
+
update({ podChoice: { pod, contents, from: place } });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
await rememberPod(pod);
|
|
348
|
+
update({ stage: afterStorage(await refresh(pod)) });
|
|
349
|
+
});
|
|
350
|
+
},
|
|
351
|
+
async useBrowser() {
|
|
352
|
+
set(BROWSER_CHOSEN, 'browser');
|
|
353
|
+
await run(async () => {
|
|
354
|
+
if (state.place?.kind === 'folder')
|
|
355
|
+
await forgetPod();
|
|
356
|
+
const place = state.place?.kind === 'browser' ? state.place : await browserPlace();
|
|
357
|
+
update({ stage: afterStorage(await refresh(place)) });
|
|
358
|
+
});
|
|
359
|
+
},
|
|
360
|
+
changeStorage() {
|
|
361
|
+
update({ error: null, stage: 'where' });
|
|
362
|
+
},
|
|
363
|
+
async select(accountId) {
|
|
364
|
+
update({ selectedId: accountId, error: null });
|
|
365
|
+
if (state.place)
|
|
366
|
+
update({ entry: await openEntry(state.place, accountId) });
|
|
367
|
+
},
|
|
368
|
+
signInWithCode(code) {
|
|
369
|
+
return enter(async () => {
|
|
370
|
+
const place = state.place;
|
|
371
|
+
if (!place)
|
|
372
|
+
throw new Error('No place to sign in to yet.');
|
|
373
|
+
if (!isValidRecoveryCode(code)) {
|
|
374
|
+
throw protocolError('VAULT_UNLOCK_FAILED', 'That does not look like an account password.', 'It is 26 letters and digits, usually shown in groups of four.');
|
|
375
|
+
}
|
|
376
|
+
const unlocked = recoveryCodeToSeed(code);
|
|
377
|
+
const identity = await createIdentityManager().fromSeed(unlocked);
|
|
378
|
+
const expected = state.accounts.find((account) => account.id === state.selectedId);
|
|
379
|
+
if (expected && identity.did !== expected.did) {
|
|
380
|
+
throw protocolError('VAULT_UNLOCK_FAILED', `That password opens a different account, not ${expected.name}.`, 'With more than one account saved for this site, a password manager fills whichever it saw last ' +
|
|
381
|
+
`unless you pick. Choose the entry named ${expected.name}, or open the account it did fill.`);
|
|
382
|
+
}
|
|
383
|
+
const known = (await listAccounts(place)).find((account) => account.did === identity.did);
|
|
384
|
+
if (known)
|
|
385
|
+
return begin(place, known, unlocked);
|
|
386
|
+
// New to this place — the second-site case. File it, so next time there
|
|
387
|
+
// is something to add a passkey to.
|
|
388
|
+
const id = newAccountId();
|
|
389
|
+
const summary = {
|
|
390
|
+
id,
|
|
391
|
+
name: expected?.name ?? 'My account',
|
|
392
|
+
did: identity.did,
|
|
393
|
+
createdAt: new Date().toISOString(),
|
|
394
|
+
dataPath: accountDataPath(id),
|
|
395
|
+
};
|
|
396
|
+
await place.store.write(summary, createVault({ did: identity.did, label: summary.name, wraps: [] }));
|
|
397
|
+
return begin(place, summary, unlocked);
|
|
398
|
+
});
|
|
399
|
+
},
|
|
400
|
+
signInWithPassword(password) {
|
|
401
|
+
return enter(async () => {
|
|
402
|
+
const { place, entry } = state;
|
|
403
|
+
const wrap = entry?.vault.wraps.find((candidate) => candidate.kind === 'passphrase');
|
|
404
|
+
if (!place || !entry || !wrap || wrap.kind !== 'passphrase') {
|
|
405
|
+
throw protocolError('VAULT_UNLOCK_FAILED', 'No password is set for this account here.', 'Use your account password instead.');
|
|
406
|
+
}
|
|
407
|
+
return begin(place, entry.summary, await unwrapSeedWithPassphrase(wrap, password));
|
|
408
|
+
});
|
|
409
|
+
},
|
|
410
|
+
signInWithPasskey() {
|
|
411
|
+
return enter(async () => {
|
|
412
|
+
const { place, entry } = state;
|
|
413
|
+
const target = entry?.shortcuts[entry.shortcuts.length - 1];
|
|
414
|
+
if (!place || !entry || !target) {
|
|
415
|
+
throw protocolError('VAULT_UNLOCK_FAILED', 'No passkey on this device can open that account.', 'Use your account password, and you can set one up afterwards.');
|
|
416
|
+
}
|
|
417
|
+
// The gate first, so the key is never reached for without someone present.
|
|
418
|
+
if (target.credentialId)
|
|
419
|
+
await passkeyGate('get', { credentialId: target.credentialId });
|
|
420
|
+
const key = await getDeviceKey(target.deviceKeyId);
|
|
421
|
+
if (!key) {
|
|
422
|
+
throw protocolError('VAULT_UNLOCK_FAILED', 'The key for that passkey is not in this browser any more.', 'Clearing site data removes it. Your account password still works, and a new passkey can be set up afterwards.');
|
|
423
|
+
}
|
|
424
|
+
return begin(place, entry.summary, await unwrapSeedWithDeviceKey(target, key));
|
|
425
|
+
});
|
|
426
|
+
},
|
|
427
|
+
startCreating() {
|
|
428
|
+
update({ error: null, stage: 'create' });
|
|
429
|
+
},
|
|
430
|
+
showSignIn() {
|
|
431
|
+
update({ error: null, stage: 'signIn' });
|
|
432
|
+
},
|
|
433
|
+
async createAccount(name) {
|
|
434
|
+
await run(async () => {
|
|
435
|
+
const place = state.place;
|
|
436
|
+
if (!place)
|
|
437
|
+
throw new Error('No place to keep the account yet.');
|
|
438
|
+
// The seed is random, and the password shown afterwards is that seed
|
|
439
|
+
// written out — not a backup of it.
|
|
440
|
+
const unlocked = generateSeed();
|
|
441
|
+
const identity = await createIdentityManager().fromSeed(unlocked);
|
|
442
|
+
const id = newAccountId();
|
|
443
|
+
const summary = {
|
|
444
|
+
id,
|
|
445
|
+
name: name.trim(),
|
|
446
|
+
did: identity.did,
|
|
447
|
+
createdAt: new Date().toISOString(),
|
|
448
|
+
dataPath: accountDataPath(id),
|
|
449
|
+
};
|
|
450
|
+
// No wraps yet: the password is the way in, and it needs none.
|
|
451
|
+
await place.store.write(summary, createVault({ did: identity.did, label: summary.name, wraps: [] }));
|
|
452
|
+
const session = await begin(place, summary, unlocked);
|
|
453
|
+
// The name travels with the account, so the next site it is opened on
|
|
454
|
+
// shows it. Only at creation and on rename — never on a plain start,
|
|
455
|
+
// where a device that has not synced yet would publish a stale name.
|
|
456
|
+
await session.node.account.setName(summary.name).catch(() => { });
|
|
457
|
+
update({ session, freshCode: seedToRecoveryCode(unlocked), accounts: await listAccounts(place) });
|
|
458
|
+
});
|
|
459
|
+
},
|
|
460
|
+
codeSaved() {
|
|
461
|
+
update({ freshCode: null, stage: 'ready' });
|
|
462
|
+
},
|
|
463
|
+
async acceptPairing() {
|
|
464
|
+
const ticket = state.pairing;
|
|
465
|
+
if (!ticket)
|
|
466
|
+
return;
|
|
467
|
+
if (!state.place)
|
|
468
|
+
update({ place: await browserPlace() });
|
|
469
|
+
await auth.signInWithCode(ticket.code);
|
|
470
|
+
const session = state.session;
|
|
471
|
+
if (!session)
|
|
472
|
+
return;
|
|
473
|
+
clearPairingTicket();
|
|
474
|
+
await collectFromDesktop(session.node, ticket, (stage) => update({ pairingStage: stage }));
|
|
475
|
+
update({ pairing: null });
|
|
476
|
+
},
|
|
477
|
+
dismissPairing() {
|
|
478
|
+
clearPairingTicket();
|
|
479
|
+
update({ pairing: null, pairingStage: null, stage: state.session ? 'ready' : afterStorage(state.accounts) });
|
|
480
|
+
},
|
|
481
|
+
clearError() {
|
|
482
|
+
update({ error: null });
|
|
483
|
+
},
|
|
484
|
+
async rename(name) {
|
|
485
|
+
const session = state.session;
|
|
486
|
+
if (!session)
|
|
487
|
+
return false;
|
|
488
|
+
return run(async () => {
|
|
489
|
+
const renamed = await adoptName(name);
|
|
490
|
+
// Every other device and site that opens the account follows.
|
|
491
|
+
await session.node.account.setName(renamed.name).catch(() => { });
|
|
492
|
+
});
|
|
493
|
+
},
|
|
494
|
+
addPasskey: () => changeShortcut(async (unlocked) => {
|
|
495
|
+
const session = state.session;
|
|
496
|
+
const place = state.place;
|
|
497
|
+
const { credentialId, userHandle } = await passkeyGate('create', { label: session.account.name });
|
|
498
|
+
const deviceKey = await createDeviceKey();
|
|
499
|
+
const wrap = await wrapSeedWithDeviceKey(unlocked, deviceKey, {
|
|
500
|
+
rpId,
|
|
501
|
+
credentialId,
|
|
502
|
+
...(userHandle ? { userHandle } : {}),
|
|
503
|
+
label: rpId,
|
|
504
|
+
});
|
|
505
|
+
// Replacing an older passkey leaves its key behind, opening nothing.
|
|
506
|
+
const previous = deviceWrapsFor((await place.store.read(session.account.id)), rpId);
|
|
507
|
+
await updateVault(async (vault) => withWrap(vault, wrap));
|
|
508
|
+
for (const stale of previous)
|
|
509
|
+
await deleteDeviceKey(stale.deviceKeyId);
|
|
510
|
+
}),
|
|
511
|
+
removeShortcut: (kind) => changeShortcut(async () => {
|
|
512
|
+
const session = state.session;
|
|
513
|
+
if (kind === 'passkey') {
|
|
514
|
+
const vault = await state.place.store.read(session.account.id);
|
|
515
|
+
for (const wrap of vault ? deviceWrapsFor(vault, rpId) : [])
|
|
516
|
+
await deleteDeviceKey(wrap.deviceKeyId);
|
|
517
|
+
}
|
|
518
|
+
// Removing every shortcut leaves the account as a new one starts:
|
|
519
|
+
// openable by its password. Another site's passkey is not this one's to remove.
|
|
520
|
+
await updateVault(async (vault) => ({
|
|
521
|
+
...vault,
|
|
522
|
+
wraps: vault.wraps.filter((wrap) => kind === 'passkey' ? !(wrap.kind === 'device' && wrap.rpId === rpId) : wrap.kind !== 'passphrase'),
|
|
523
|
+
}));
|
|
524
|
+
}),
|
|
525
|
+
async confirmPod(how) {
|
|
526
|
+
const choice = state.podChoice;
|
|
527
|
+
const session = state.session;
|
|
528
|
+
if (!choice || !session || !seed || !vaultKey)
|
|
529
|
+
return;
|
|
530
|
+
const unlocked = seed;
|
|
531
|
+
const key = vaultKey;
|
|
532
|
+
await run(async () => {
|
|
533
|
+
const { pod, contents, from } = choice;
|
|
534
|
+
if (!pod.directory)
|
|
535
|
+
throw new Error('That is not a folder.');
|
|
536
|
+
if (how === 'switch' && contents.account) {
|
|
537
|
+
// Use the pod's own copy and bring nothing; what was only here stays here.
|
|
538
|
+
await rememberPod(pod);
|
|
539
|
+
const started = await begin(pod, contents.account, unlocked);
|
|
540
|
+
update({ session: started, place: pod, accounts: await listAccounts(pod), podChoice: null, moved: null });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
// Combine: every record is signed and named by its content, and deletes
|
|
544
|
+
// are records too, so two copies merge by keeping everything from both.
|
|
545
|
+
const existing = contents.account;
|
|
546
|
+
const id = existing?.id ?? newAccountId();
|
|
547
|
+
const summary = existing
|
|
548
|
+
? { ...existing, lastUsedAt: new Date().toISOString() }
|
|
549
|
+
: { ...session.account, id, dataPath: accountDataPath(id), lastUsedAt: new Date().toISOString() };
|
|
550
|
+
// Ways of unlocking: the pod's, plus any from here it lacks.
|
|
551
|
+
const here = await from.store.read(session.account.id);
|
|
552
|
+
let vault = (existing ? await pod.store.read(existing.id) : null) ??
|
|
553
|
+
createVault({ did: session.account.did, label: session.account.name, wraps: [] });
|
|
554
|
+
for (const wrap of here?.wraps ?? []) {
|
|
555
|
+
if (!vault.wraps.some((kept) => kept.id === wrap.id))
|
|
556
|
+
vault = withWrap(vault, wrap);
|
|
557
|
+
}
|
|
558
|
+
await pod.store.write(summary, vault);
|
|
559
|
+
const copied = await copyAccountData({
|
|
560
|
+
from: storesOf(from, session.account, key),
|
|
561
|
+
to: storesOf(pod, summary, key),
|
|
562
|
+
did: session.account.did,
|
|
563
|
+
accountKey: await deriveVaultKeyBytes(unlocked),
|
|
564
|
+
});
|
|
565
|
+
await rememberPod(pod);
|
|
566
|
+
const started = await begin(pod, summary, unlocked);
|
|
567
|
+
update({
|
|
568
|
+
session: started,
|
|
569
|
+
place: pod,
|
|
570
|
+
accounts: await listAccounts(pod),
|
|
571
|
+
podChoice: null,
|
|
572
|
+
moved: {
|
|
573
|
+
merged: existing !== null,
|
|
574
|
+
spacesAdded: copied.spacesAdded,
|
|
575
|
+
recordsAdded: copied.recordsAdded,
|
|
576
|
+
from: from.kind === 'folder' ? (from.directory?.name ?? 'your old pod') : null,
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
},
|
|
581
|
+
cancelPod() {
|
|
582
|
+
update({ podChoice: null, error: null });
|
|
583
|
+
},
|
|
584
|
+
async forgetBrowserCopy() {
|
|
585
|
+
const session = state.session;
|
|
586
|
+
if (!session)
|
|
587
|
+
return;
|
|
588
|
+
await run(async () => {
|
|
589
|
+
const browser = await browserAccounts();
|
|
590
|
+
const copy = (await browser.list()).find((entry) => entry.did === session.account.did);
|
|
591
|
+
if (copy) {
|
|
592
|
+
await browser.remove(copy.id);
|
|
593
|
+
await deleteBrowserData(copy);
|
|
594
|
+
}
|
|
595
|
+
update({ moved: null });
|
|
596
|
+
});
|
|
597
|
+
},
|
|
598
|
+
dismissMoved() {
|
|
599
|
+
update({ moved: null });
|
|
600
|
+
},
|
|
601
|
+
accountPassword() {
|
|
602
|
+
return seed ? seedToRecoveryCode(seed) : null;
|
|
603
|
+
},
|
|
604
|
+
offerToPhone(onStage) {
|
|
605
|
+
const session = state.session;
|
|
606
|
+
if (!session || !seed)
|
|
607
|
+
return Promise.reject(new Error('Sign in first.'));
|
|
608
|
+
const location = globalThis.location;
|
|
609
|
+
return offerPairing({
|
|
610
|
+
node: session.node,
|
|
611
|
+
seed,
|
|
612
|
+
relays: config.network?.relays ?? [],
|
|
613
|
+
link: config.pairingLink ?? (location ? `${location.origin}${location.pathname}` : ''),
|
|
614
|
+
}, onStage);
|
|
615
|
+
},
|
|
616
|
+
async grant(choice) {
|
|
617
|
+
const session = state.session;
|
|
618
|
+
if (!session || !seed)
|
|
619
|
+
throw new Error('Sign in first.');
|
|
620
|
+
const { node } = session;
|
|
621
|
+
const { request } = choice;
|
|
622
|
+
if (request.access === 'carry')
|
|
623
|
+
throw new Error('A carrier is given passes, not a note — use grantCarry.');
|
|
624
|
+
const access = request.access;
|
|
625
|
+
const agent = request.agent === true;
|
|
626
|
+
if (agent && request.create?.length)
|
|
627
|
+
throw new Error('An agent works in spaces that exist — none are made for it.');
|
|
628
|
+
const whole = request.scope === 'account';
|
|
629
|
+
const created = [];
|
|
630
|
+
for (const params of request.create ?? [])
|
|
631
|
+
created.push(await node.spaces.create(params));
|
|
632
|
+
const ids = [...new Set([...choice.spaceIds, ...created.map((space) => space.id)])];
|
|
633
|
+
const spaces = [];
|
|
634
|
+
for (const id of ids) {
|
|
635
|
+
const space = await node.spaces.get(id);
|
|
636
|
+
if (!space)
|
|
637
|
+
throw new Error(`No space ${id} in this account.`);
|
|
638
|
+
// Read-only: what lets the app write is the note, under this account's own role — never a secret of the space's.
|
|
639
|
+
const invite = await node.spaces.invite(id, { write: false });
|
|
640
|
+
spaces.push({ id, name: space.name, invite });
|
|
641
|
+
}
|
|
642
|
+
const days = Math.min(Math.max(choice.days ?? request.days ?? 7, 1 / 24), MAX_GRANT_DAYS);
|
|
643
|
+
const expiresAt = Math.floor(Date.now() / 1000) + Math.round(days * 24 * 3600);
|
|
644
|
+
const manager = createIdentityManager();
|
|
645
|
+
const root = createLocalRootSigner(await manager.fromSeed(seed), manager.getProvider());
|
|
646
|
+
const token = await root.delegate({
|
|
647
|
+
audience: request.audience,
|
|
648
|
+
capabilities: grantCapabilities(access, whole ? 'all' : ids),
|
|
649
|
+
expiration: expiresAt,
|
|
650
|
+
...(agent ? { facts: [AGENT_FACT] } : {}),
|
|
651
|
+
});
|
|
652
|
+
const connection = {
|
|
653
|
+
origin: choice.origin,
|
|
654
|
+
name: request.name?.slice(0, 80) ?? null,
|
|
655
|
+
audience: request.audience,
|
|
656
|
+
access,
|
|
657
|
+
scope: whole ? 'account' : 'spaces',
|
|
658
|
+
spaces: spaces.map(({ id, name }) => ({ id, name })),
|
|
659
|
+
grantedAt: new Date().toISOString(),
|
|
660
|
+
expiresAt,
|
|
661
|
+
token: token.encoded,
|
|
662
|
+
...(agent ? { agent: true } : {}),
|
|
663
|
+
};
|
|
664
|
+
// Connecting again replaces the old note. An agent is its own key: connecting one replaces only that one.
|
|
665
|
+
const replaced = (known) => (agent ? known.audience === request.audience : known.origin === choice.origin && !known.agent);
|
|
666
|
+
writeConnections([connection, ...auth.connections().filter((known) => !replaced(known))]);
|
|
667
|
+
return {
|
|
668
|
+
v: 1,
|
|
669
|
+
did: session.did,
|
|
670
|
+
name: session.account.name,
|
|
671
|
+
token: token.encoded,
|
|
672
|
+
access,
|
|
673
|
+
scope: whole ? 'account' : 'spaces',
|
|
674
|
+
spaces,
|
|
675
|
+
...(whole ? { accountKey: base64UrlEncode(await deriveVaultKeyBytes(seed)) } : {}),
|
|
676
|
+
...(config.network?.relays?.length ? { relays: [...config.network.relays] } : {}),
|
|
677
|
+
expiresAt,
|
|
678
|
+
...(agent ? { agent: true } : {}),
|
|
679
|
+
};
|
|
680
|
+
},
|
|
681
|
+
async grantCarry({ origin, request }) {
|
|
682
|
+
const session = state.session;
|
|
683
|
+
if (!session || !seed)
|
|
684
|
+
throw new Error('Sign in first.');
|
|
685
|
+
const { node } = session;
|
|
686
|
+
// Connecting again replaces the old one: one carrier per origin.
|
|
687
|
+
const previous = auth.connections().find((known) => known.origin === origin);
|
|
688
|
+
if (previous?.carrySpace)
|
|
689
|
+
await node.carriers.remove(previous.carrySpace).catch(() => { });
|
|
690
|
+
const name = request.name?.slice(0, 80) || 'Browser extension';
|
|
691
|
+
const carry = await node.carriers.add({ did: request.audience, name });
|
|
692
|
+
const connection = {
|
|
693
|
+
origin,
|
|
694
|
+
name,
|
|
695
|
+
audience: request.audience,
|
|
696
|
+
access: 'carry',
|
|
697
|
+
scope: 'account',
|
|
698
|
+
spaces: [],
|
|
699
|
+
grantedAt: new Date().toISOString(),
|
|
700
|
+
// Carrying has no end date: a pass reads nothing, and the carrier writes nothing.
|
|
701
|
+
expiresAt: 0,
|
|
702
|
+
carrySpace: carry.space,
|
|
703
|
+
};
|
|
704
|
+
writeConnections([connection, ...auth.connections().filter((known) => known.origin !== origin)]);
|
|
705
|
+
const place = state.place;
|
|
706
|
+
return {
|
|
707
|
+
v: 1,
|
|
708
|
+
kind: 'carry',
|
|
709
|
+
did: session.did,
|
|
710
|
+
name: session.account.name,
|
|
711
|
+
carry,
|
|
712
|
+
pod: place?.kind === 'folder' ? { dataPath: session.account.dataPath, folder: place.directory?.name ?? 'your pod' } : null,
|
|
713
|
+
...(config.network?.relays?.length ? { relays: [...config.network.relays] } : {}),
|
|
714
|
+
};
|
|
715
|
+
},
|
|
716
|
+
connections() {
|
|
717
|
+
const account = state.session?.account.id;
|
|
718
|
+
if (!account)
|
|
719
|
+
return [];
|
|
720
|
+
try {
|
|
721
|
+
return JSON.parse(get(`${prefix}.connections:${account}`) ?? '[]');
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
return [];
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
connectedElsewhere(origin) {
|
|
728
|
+
const current = state.session?.account.id;
|
|
729
|
+
return state.accounts
|
|
730
|
+
.filter((account) => account.id !== current)
|
|
731
|
+
.filter((account) => {
|
|
732
|
+
try {
|
|
733
|
+
const known = JSON.parse(get(`${prefix}.connections:${account.id}`) ?? '[]');
|
|
734
|
+
return known.some((connection) => connection.origin === origin);
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
})
|
|
740
|
+
.map((account) => ({ id: account.id, name: account.name }));
|
|
741
|
+
},
|
|
742
|
+
async disconnect(origin, options = {}) {
|
|
743
|
+
// The app goes with the agents connected through it; an agent can go alone.
|
|
744
|
+
const goes = (known) => options.audience !== undefined
|
|
745
|
+
? known.audience === options.audience
|
|
746
|
+
: known.origin === origin && (!options.agent || !!known.agent);
|
|
747
|
+
const going = auth.connections().filter(goes);
|
|
748
|
+
const node = state.session?.node;
|
|
749
|
+
for (const connection of going) {
|
|
750
|
+
if (connection.carrySpace && node)
|
|
751
|
+
await node.carriers.remove(connection.carrySpace);
|
|
752
|
+
if (connection.token && connection.access === 'write' && node) {
|
|
753
|
+
const covered = connection.scope === 'account'
|
|
754
|
+
? (await node.spaces.list()).filter((space) => space.writable).map((space) => space.id)
|
|
755
|
+
: connection.spaces.map((space) => space.id);
|
|
756
|
+
for (const spaceId of covered) {
|
|
757
|
+
// A space that is gone, or that this account no longer writes in, has nothing to revoke.
|
|
758
|
+
await node.spaces.revoke(spaceId, connection.token).catch(() => { });
|
|
759
|
+
}
|
|
760
|
+
// A whole-account app could also add spaces to the account's list, and rename it.
|
|
761
|
+
if (connection.scope === 'account')
|
|
762
|
+
await node.account.revoke(connection.token).catch(() => { });
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
writeConnections(auth.connections().filter((known) => !goes(known)));
|
|
766
|
+
},
|
|
767
|
+
staySignedIn: {
|
|
768
|
+
choice: () => stay.choice(),
|
|
769
|
+
setChoice: (choice) => stay.setChoice(choice),
|
|
770
|
+
until: () => stay.until(),
|
|
771
|
+
},
|
|
772
|
+
async signOut() {
|
|
773
|
+
await stay.forget();
|
|
774
|
+
const leaving = state.session?.node;
|
|
775
|
+
update({ stage: 'starting', session: null, entry: null, freshCode: null, podChoice: null, moved: null });
|
|
776
|
+
await stopNode(leaving).catch(() => { });
|
|
777
|
+
const place = state.place;
|
|
778
|
+
update({ stage: place ? afterStorage(await refresh(place)) : 'welcome' });
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
return auth;
|
|
782
|
+
}
|
|
783
|
+
//# sourceMappingURL=auth.js.map
|