@mudlet/mudlet-web 0.4.5 → 1.0.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 +1 -1
- package/dist-lib/analytics.d.ts +27 -0
- package/dist-lib/assets/fonts/Vera.woff2 +0 -0
- package/dist-lib/assets/fonts/VeraBI.woff2 +0 -0
- package/dist-lib/assets/fonts/VeraBd.woff2 +0 -0
- package/dist-lib/assets/fonts/VeraIt.woff2 +0 -0
- package/dist-lib/assets/mapParser.worker-DrVZ3wg_.js +9 -0
- package/dist-lib/backupImport-wt8r7JXI.js +233 -0
- package/dist-lib/branding.d.ts +111 -13
- package/dist-lib/db/sqliteClient.d.ts +195 -7
- package/dist-lib/hooks/useAutoReconnect.d.ts +61 -0
- package/dist-lib/hooks/useDocumentFullscreen.d.ts +23 -0
- package/dist-lib/hooks/useKeyboardShortcuts.d.ts +37 -0
- package/dist-lib/hooks/useRecordLastPlayed.d.ts +10 -0
- package/dist-lib/import/applyMudletProfile.d.ts +35 -9
- package/dist-lib/import/backupImport.d.ts +118 -0
- package/dist-lib/import/collectProfileExport.d.ts +33 -2
- package/dist-lib/import/defaultPackages.d.ts +57 -14
- package/dist-lib/import/defaults/CF-loader/CF-loader.mpackage +0 -0
- package/dist-lib/import/defaults/MedBootstrap/MedBootstrap.mpackage +0 -0
- package/dist-lib/import/defaults/deleteOldProfiles/deleteOldProfiles.mpackage +0 -0
- package/dist-lib/import/defaults/echo/echo.mpackage +0 -0
- package/dist-lib/import/defaults/enable-accessibility/enable-accessibility.mpackage +0 -0
- package/dist-lib/import/defaults/generic_mapper/generic_mapper.mpackage +0 -0
- package/dist-lib/import/defaults/gui-drop/gui-drop.mpackage +0 -0
- package/dist-lib/import/defaults/icesus-loader/icesus-loader.mpackage +0 -0
- package/dist-lib/import/defaults/mg-loader/mg-loader.mpackage +0 -0
- package/dist-lib/import/defaults/mudlet-base-ui/mudlet-base-ui.mpackage +0 -0
- package/dist-lib/import/defaults/mudlet-mapper.xml +1215 -389
- package/dist-lib/import/mudletControlChars.d.ts +14 -0
- package/dist-lib/import/mudletHost.d.ts +73 -10
- package/dist-lib/import/mudletLink.d.ts +53 -2
- package/dist-lib/import/mudletProfileExport.d.ts +38 -5
- package/dist-lib/import/mudletProfileImport.d.ts +110 -12
- package/dist-lib/import/mudletVariables.d.ts +4 -3
- package/dist-lib/import/mudletWriteback.d.ts +15 -1
- package/dist-lib/import/mudletXmlImport.d.ts +46 -0
- package/dist-lib/import/packageFonts.d.ts +33 -0
- package/dist-lib/import/packageInstaller.d.ts +111 -7
- package/dist-lib/import/qtProfileData.d.ts +10 -0
- package/dist-lib/import/remotePackageInstall.d.ts +23 -7
- package/dist-lib/import/zipEntries.d.ts +45 -0
- package/dist-lib/index.d.ts +7 -3
- package/dist-lib/index.js +50660 -29735
- package/dist-lib/logging/SessionLogger.d.ts +75 -15
- package/dist-lib/map/MapSelectionOverlay.d.ts +2 -2
- package/dist-lib/map/MapStore.d.ts +432 -59
- package/dist-lib/map/MudletHighlightOverlay.d.ts +2 -2
- package/dist-lib/map/{MudixMapReader.d.ts → MudletMapReader.d.ts} +2 -2
- package/dist-lib/map/areaGraph.d.ts +120 -0
- package/dist-lib/map/environmentKit.d.ts +36 -0
- package/dist-lib/map/labelPixmap.d.ts +90 -0
- package/dist-lib/map/mapImageExport.d.ts +36 -2
- package/dist-lib/map/mapStyle.d.ts +11 -0
- package/dist-lib/map/mapVersion.d.ts +66 -0
- package/dist-lib/map/pathfinding.d.ts +2 -2
- package/dist-lib/mud/CharLoginController.d.ts +230 -0
- package/dist-lib/mud/ItemIdSequence.d.ts +1 -1
- package/dist-lib/mud/MudSession.d.ts +279 -22
- package/dist-lib/mud/PatternEngine.d.ts +153 -14
- package/dist-lib/mud/aliases/AliasEngine.d.ts +55 -9
- package/dist-lib/mud/autoLogin.d.ts +59 -0
- package/dist-lib/mud/commandSplit.d.ts +9 -0
- package/dist-lib/mud/connection/LineAssembler.d.ts +38 -11
- package/dist-lib/mud/connection/MudClient.d.ts +291 -53
- package/dist-lib/mud/connection/SubnegotiationRepair.d.ts +24 -0
- package/dist-lib/mud/connection/TelnetNegotiator.d.ts +208 -68
- package/dist-lib/mud/connection/telnetDebug.d.ts +8 -8
- package/dist-lib/mud/events.d.ts +102 -23
- package/dist-lib/mud/games/bundledGames.d.ts +12 -7
- package/dist-lib/mud/games/gameLinks.d.ts +40 -0
- package/dist-lib/mud/games/icons/3Kingdoms.png +0 -0
- package/dist-lib/mud/games/icons/3Scapes.png +0 -0
- package/dist-lib/mud/games/icons/achaea.png +0 -0
- package/dist-lib/mud/games/icons/aetolia.png +0 -0
- package/dist-lib/mud/games/icons/banner_astaria.png +0 -0
- package/dist-lib/mud/games/icons/banner_darkmists.png +0 -0
- package/dist-lib/mud/games/icons/banner_dreamland.png +0 -0
- package/dist-lib/mud/games/icons/banner_petria.jpg +0 -0
- package/dist-lib/mud/games/icons/banner_ropmud.png +0 -0
- package/dist-lib/mud/games/icons/blackmud.png +0 -0
- package/dist-lib/mud/games/icons/carrionfields.png +0 -0
- package/dist-lib/mud/games/icons/dragonfire_icon.png +0 -0
- package/dist-lib/mud/games/icons/fed2-logo.png +0 -0
- package/dist-lib/mud/games/icons/icesus_480x120.png +0 -0
- package/dist-lib/mud/games/icons/imperian.png +0 -0
- package/dist-lib/mud/games/icons/infinity_480x120.png +0 -0
- package/dist-lib/mud/games/icons/lusternia.png +0 -0
- package/dist-lib/mud/games/icons/morgengrauen_480x120.png +0 -0
- package/dist-lib/mud/games/icons/mudlet-tutorial.png +0 -0
- package/dist-lib/mud/games/icons/phoenixmud_480x120.png +0 -0
- package/dist-lib/mud/games/icons/starmourn.png +0 -0
- package/dist-lib/mud/games/icons/voidmud_icon.png +0 -0
- package/dist-lib/mud/games/icons/wotmudicon.png +0 -0
- package/dist-lib/mud/games/websiteLinks.d.ts +21 -0
- package/dist-lib/mud/keybindings/KeyEngine.d.ts +110 -4
- package/dist-lib/mud/keybindings/browserReservedKeys.d.ts +2 -2
- package/dist-lib/mud/keybindings/keyEventTarget.d.ts +61 -0
- package/dist-lib/mud/keybindings/qtKeys.d.ts +91 -12
- package/dist-lib/mud/protocol/atcp.d.ts +19 -0
- package/dist-lib/mud/protocol/byteString.d.ts +24 -0
- package/dist-lib/mud/protocol/charLoginFlow.d.ts +243 -16
- package/dist-lib/mud/protocol/charset.d.ts +111 -46
- package/dist-lib/mud/protocol/codePages.d.ts +10 -0
- package/dist-lib/mud/protocol/constants.d.ts +17 -10
- package/dist-lib/mud/protocol/echo.d.ts +23 -27
- package/dist-lib/mud/protocol/gmcp.d.ts +36 -6
- package/dist-lib/mud/protocol/index.d.ts +3 -2
- package/dist-lib/mud/protocol/luaLiteral.d.ts +12 -0
- package/dist-lib/mud/protocol/mccp.d.ts +83 -1
- package/dist-lib/mud/protocol/mnes.d.ts +41 -58
- package/dist-lib/mud/protocol/msdp.d.ts +11 -4
- package/dist-lib/mud/protocol/msp.d.ts +3 -2
- package/dist-lib/mud/protocol/mssp.d.ts +9 -4
- package/dist-lib/mud/protocol/multiByte.d.ts +51 -0
- package/dist-lib/mud/protocol/multiByteEncode.d.ts +42 -0
- package/dist-lib/mud/protocol/mxp.d.ts +193 -14
- package/dist-lib/mud/protocol/oauthClientFlow.d.ts +128 -0
- package/dist-lib/mud/protocol/qtCodePages.generated.d.ts +1 -0
- package/dist-lib/mud/protocol/tlsCodes.d.ts +40 -3
- package/dist-lib/mud/replay/ReplayPlayer.d.ts +16 -1
- package/dist-lib/mud/replay/replayFormat.d.ts +29 -2
- package/dist-lib/mud/text/Console.d.ts +255 -49
- package/dist-lib/mud/text/FormatState.d.ts +137 -14
- package/dist-lib/mud/text/ansiEscapes.d.ts +32 -16
- package/dist-lib/mud/text/colorParsers.d.ts +14 -2
- package/dist-lib/mud/text/colorSpaceId.d.ts +2 -0
- package/dist-lib/mud/text/colors.d.ts +17 -10
- package/dist-lib/mud/text/controlCharacterMode.d.ts +1 -1
- package/dist-lib/mud/text/hyperlinkVisibility.d.ts +1 -1
- package/dist-lib/mud/text/lineBreak.d.ts +5 -0
- package/dist-lib/mud/text/serverWrap.d.ts +2 -2
- package/dist-lib/mud/text/textAnalyzer.d.ts +79 -0
- package/dist-lib/mud/text/unicodeRuns.d.ts +11 -0
- package/dist-lib/mud/text/unicodeTables.generated.d.ts +3 -0
- package/dist-lib/mud/text/wcwidth.d.ts +27 -22
- package/dist-lib/mud/timers/TimerEngine.d.ts +163 -11
- package/dist-lib/mud/timers/workerClock.d.ts +67 -0
- package/dist-lib/mud/triggers/TriggerEngine.d.ts +449 -47
- package/dist-lib/mud/triggers/legacyColorPatterns.d.ts +58 -0
- package/dist-lib/mud/triggers/pcre/Pcre2.d.ts +83 -1
- package/dist-lib/oauth-callback.html +94 -0
- package/dist-lib/pcre2Wasm.d.ts +54 -0
- package/dist-lib/pcre2Wasm.js +241 -0
- package/dist-lib/scripting/EngineHost.d.ts +54 -21
- package/dist-lib/scripting/IScriptingRuntime.d.ts +103 -27
- package/dist-lib/scripting/MxpFrameManager.d.ts +10 -1
- package/dist-lib/scripting/ScriptingAPI.d.ts +1039 -213
- package/dist-lib/scripting/ScriptingEngine.d.ts +753 -103
- package/dist-lib/scripting/StopwatchManager.d.ts +97 -36
- package/dist-lib/scripting/http/HttpService.d.ts +9 -1
- package/dist-lib/scripting/http/userUrl.d.ts +41 -0
- package/dist-lib/scripting/lua/LuaRuntime.d.ts +352 -77
- package/dist-lib/scripting/lua/argContracts.d.ts +71 -0
- package/dist-lib/scripting/lua/bindings/automation.d.ts +1 -1
- package/dist-lib/scripting/lua/bindings/commands.d.ts +13 -0
- package/dist-lib/scripting/lua/bindings/context.d.ts +20 -2
- package/dist-lib/scripting/lua/bindings/diagnostics.d.ts +1 -1
- package/dist-lib/scripting/lua/bindings/map.d.ts +1 -1
- package/dist-lib/scripting/lua/bindings/media.d.ts +2 -2
- package/dist-lib/scripting/lua/bindings/output.d.ts +1 -1
- package/dist-lib/scripting/lua/bindings/session.d.ts +2 -2
- package/dist-lib/scripting/lua/bindings/textEdit.d.ts +1 -1
- package/dist-lib/scripting/lua/bindings/userWindows.d.ts +1 -1
- package/dist-lib/scripting/lua/byteArmor.d.ts +36 -0
- package/dist-lib/scripting/lua/imageSize.d.ts +7 -0
- package/dist-lib/scripting/lua/rex.d.ts +4 -2
- package/dist-lib/scripting/lua/sqlRowEncoder.d.ts +6 -1
- package/dist-lib/scripting/lua/utf8CaseMap.d.ts +1 -0
- package/dist-lib/scripting/lua/utf8Patterns.d.ts +21 -1
- package/dist-lib/scripting/lua/wellFormedStrings.d.ts +40 -0
- package/dist-lib/scripting/lua/yajl.d.ts +43 -5
- package/dist-lib/scripting/profilesPresence.d.ts +20 -2
- package/dist-lib/scripting/vfs/ProfileVFS.d.ts +164 -10
- package/dist-lib/scripting/vfs/fsErrors.d.ts +28 -0
- package/dist-lib/scripting/vfs/idbCommit.d.ts +20 -0
- package/dist-lib/storage/appStore.d.ts +24 -9
- package/dist-lib/storage/hooks.d.ts +5 -1
- package/dist-lib/storage/index.d.ts +2 -2
- package/dist-lib/storage/mapStorage.d.ts +2 -0
- package/dist-lib/storage/persistentStorage.d.ts +3 -1
- package/dist-lib/storage/profileStorage.d.ts +15 -0
- package/dist-lib/storage/profileVfsData.d.ts +3 -3
- package/dist-lib/storage/schema.d.ts +521 -44
- package/dist-lib/storage/storageMigration.d.ts +82 -0
- package/dist-lib/storage-g5V1b-c1.js +2039 -0
- package/dist-lib/styles.css +1 -1
- package/dist-lib/ui/AboutModal.d.ts +1 -1
- package/dist-lib/ui/BrandLoginFields.d.ts +1 -1
- package/dist-lib/ui/BrowserSignInModal.d.ts +23 -0
- package/dist-lib/ui/CharLoginModal.d.ts +24 -15
- package/dist-lib/ui/CmdLineMenuRegistry.d.ts +14 -5
- package/dist-lib/ui/CommandBar.d.ts +8 -8
- package/dist-lib/ui/ConnectionFormModal.d.ts +25 -1
- package/dist-lib/ui/ConnectionScreen.d.ts +9 -1
- package/dist-lib/ui/GameLinkPrompt.d.ts +28 -0
- package/dist-lib/ui/HelpModal.d.ts +5 -1
- package/dist-lib/ui/MapSymbolUsageModal.d.ts +32 -0
- package/dist-lib/ui/MouseEventRegistry.d.ts +31 -4
- package/dist-lib/ui/NotepadModal.d.ts +22 -0
- package/dist-lib/ui/ResizableModal.d.ts +16 -1
- package/dist-lib/ui/SettingsModal.d.ts +10 -1
- package/dist-lib/ui/TlsAlertBanner.d.ts +3 -1
- package/dist-lib/ui/Toolbar.d.ts +22 -1
- package/dist-lib/ui/VaultManageButton.d.ts +20 -0
- package/dist-lib/ui/VaultModal.d.ts +5 -0
- package/dist-lib/ui/VaultUnlockPrompt.d.ts +15 -0
- package/dist-lib/ui/buttons/ButtonsBar.d.ts +3 -1
- package/dist-lib/ui/buttons/inactiveButtons.d.ts +25 -0
- package/dist-lib/ui/cmdline/CommandLineManager.d.ts +19 -0
- package/dist-lib/ui/cmdline/plainText.d.ts +16 -0
- package/dist-lib/ui/cmdline/subCommandLine.d.ts +78 -0
- package/dist-lib/ui/cmdline/useCmdLineSelection.d.ts +26 -0
- package/dist-lib/ui/cmdline/useSubCommandLineKeys.d.ts +33 -0
- package/dist-lib/ui/codemirror/lineParagraphMarks.d.ts +19 -0
- package/dist-lib/ui/codemirror/luaHover.d.ts +7 -0
- package/dist-lib/ui/codemirror/options.d.ts +33 -0
- package/dist-lib/ui/codemirror/theme.d.ts +29 -5
- package/dist-lib/ui/commands/addonCommands.d.ts +134 -0
- package/dist-lib/ui/commands/appShortcuts.d.ts +86 -0
- package/dist-lib/ui/commands/hostCommands.d.ts +88 -0
- package/dist-lib/ui/commands/keyMatch.d.ts +20 -0
- package/dist-lib/ui/commands/keySequence.d.ts +57 -0
- package/dist-lib/ui/components/ContextMenu.d.ts +21 -1
- package/dist-lib/ui/components/FontPicker.d.ts +5 -1
- package/dist-lib/ui/components/useModalFocus.d.ts +13 -1
- package/dist-lib/ui/helpTopics.d.ts +15 -0
- package/dist-lib/ui/labels/LabelManager.d.ts +172 -9
- package/dist-lib/ui/labels/LabelOverlay.d.ts +19 -0
- package/dist-lib/ui/labels/backgroundImageSize.d.ts +19 -0
- package/dist-lib/ui/labels/labelModel.d.ts +64 -0
- package/dist-lib/ui/labels/qColor.d.ts +13 -0
- package/dist-lib/ui/labels/qtCss.d.ts +12 -4
- package/dist-lib/ui/landmarks.d.ts +9 -0
- package/dist-lib/ui/launcher/ActionMenu.d.ts +19 -0
- package/dist-lib/ui/launcher/GameDetails.d.ts +21 -0
- package/dist-lib/ui/launcher/GameSection.d.ts +18 -0
- package/dist-lib/ui/launcher/LauncherSearch.d.ts +23 -0
- package/dist-lib/ui/launcher/ProfileArt.d.ts +20 -0
- package/dist-lib/ui/launcher/ProfileSection.d.ts +43 -0
- package/dist-lib/ui/launcher/launcherModel.d.ts +72 -0
- package/dist-lib/ui/layout/overlayLayerOrder.d.ts +2 -2
- package/dist-lib/ui/menu/MenuBar.d.ts +31 -0
- package/dist-lib/ui/menu/SplitButton.d.ts +39 -0
- package/dist-lib/ui/menu/anchoredPopup.d.ts +74 -0
- package/dist-lib/ui/menu/appMenus.d.ts +65 -0
- package/dist-lib/ui/menu/menuModel.d.ts +88 -0
- package/dist-lib/ui/menu/toolbarModel.d.ts +56 -0
- package/dist-lib/ui/notepad.d.ts +76 -0
- package/dist-lib/ui/openSignInPage.d.ts +14 -0
- package/dist-lib/ui/output/OutputContextMenu.d.ts +14 -1
- package/dist-lib/ui/output/OutputRenderer.d.ts +5 -0
- package/dist-lib/ui/output/OutputSearchBar.d.ts +5 -1
- package/dist-lib/ui/output/StickyOutputPanel.d.ts +17 -3
- package/dist-lib/ui/output/TextAnalyzerModal.d.ts +25 -0
- package/dist-lib/ui/output/caretMode.d.ts +3 -2
- package/dist-lib/ui/output/clearSplit.d.ts +11 -0
- package/dist-lib/ui/output/outputCopy.d.ts +41 -2
- package/dist-lib/ui/output/outputRows.d.ts +43 -0
- package/dist-lib/ui/output/selectionGesture.d.ts +35 -0
- package/dist-lib/ui/output/wordSelection.d.ts +33 -0
- package/dist-lib/ui/search/matcher.d.ts +14 -4
- package/dist-lib/ui/settings/SettingsShell.d.ts +91 -0
- package/dist-lib/ui/settings/ShortcutEditor.d.ts +13 -0
- package/dist-lib/ui/sound/SoundManager.d.ts +210 -44
- package/dist-lib/ui/sound/closedCaption.d.ts +4 -1
- package/dist-lib/ui/sound/mediaDirection.d.ts +35 -0
- package/dist-lib/ui/tabCompletion.d.ts +53 -0
- package/dist-lib/ui/textedit/TextEditManager.d.ts +11 -2
- package/dist-lib/ui/useOpenProfiles.d.ts +1 -1
- package/dist-lib/ui/useVaultSaver.d.ts +26 -0
- package/dist-lib/ui/video/VideoManager.d.ts +7 -0
- package/dist-lib/ui/windows/WindowManager.d.ts +452 -37
- package/dist-lib/ui/windows/panels/ScriptEditorPanel.d.ts +14 -0
- package/dist-lib/ui/windows/panels/ScriptSearch.d.ts +6 -1
- package/dist-lib/ui/windows/panels/TextPanel.d.ts +4 -1
- package/dist-lib/ui/windows/panels/VariablesView.d.ts +37 -7
- package/dist-lib/ui/windows/panels/WindowCmdLine.d.ts +4 -2
- package/dist-lib/ui/windows/panels/aliasLoop.d.ts +24 -0
- package/dist-lib/ui/windows/panels/editorClipboard.d.ts +45 -0
- package/dist-lib/ui/windows/panels/editorUndo.d.ts +79 -0
- package/dist-lib/ui/windows/panels/mapAreaChange.d.ts +10 -0
- package/dist-lib/utils/appShellCache.d.ts +25 -0
- package/dist-lib/utils/fontFamilyName.d.ts +13 -0
- package/dist-lib/utils/fontLoader.d.ts +31 -0
- package/dist-lib/utils/formatBytes.d.ts +4 -0
- package/dist-lib/utils/mustBeOneOf.d.ts +9 -0
- package/dist-lib/utils/profileLock.d.ts +1 -1
- package/dist-lib/utils/storedCredentials.d.ts +31 -0
- package/dist-lib/utils/systemTheme.d.ts +24 -0
- package/dist-lib/utils/timeZone.d.ts +10 -0
- package/dist-lib/vault/CredentialVault.d.ts +197 -0
- package/dist-lib/vault/deviceUnlock.d.ts +47 -0
- package/dist-lib/vault/passkeyPrf.d.ts +56 -0
- package/dist-lib/vault/useVault.d.ts +8 -0
- package/dist-lib/vault/vaultAccess.d.ts +80 -0
- package/dist-lib/vault/vaultCrypto.d.ts +56 -0
- package/dist-lib/vault/vaultHelpTopic.d.ts +9 -0
- package/dist-lib/vault/vaultRecord.d.ts +92 -0
- package/dist-lib/vfs-sw.js +220 -11
- package/dist-lib/vite.d.ts +1 -1
- package/dist-lib/vite.js +66 -43
- package/package.json +22 -14
- package/dist-lib/assets/mapParser.worker-t-3rd1zR.js +0 -9
- package/dist-lib/mud/connection/PingTracker.d.ts +0 -41
- package/dist-lib/ui/BundledGameGrid.d.ts +0 -21
- package/dist-lib/ui/ConnectionGrid.d.ts +0 -21
- package/dist-lib/ui/bufferWords.d.ts +0 -38
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The credential vault: per-profile login passwords, encrypted at rest under a
|
|
3
|
+
* key the browser cannot produce on its own.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists rather than a "remember my password" checkbox: the app store
|
|
6
|
+
* is plaintext localStorage, so the old checkbox had to ship a warning telling
|
|
7
|
+
* the user that the convenience it had just offered was unsafe (issue #25). And
|
|
8
|
+
* handing the job to the browser's password manager doesn't work either —
|
|
9
|
+
* managers scope credentials by *origin*, so ten profiles on one host produce
|
|
10
|
+
* ten indistinguishable entries in one dropdown, every time, for every game.
|
|
11
|
+
*
|
|
12
|
+
* The vault inverts that. Secrets live here, encrypted; the single thing the
|
|
13
|
+
* password manager is asked to remember is the vault's unlock passphrase — one
|
|
14
|
+
* entry per origin, never ambiguous — or nothing at all when the unlocker is a
|
|
15
|
+
* passkey. Envelope encryption (see `vaultCrypto`) lets a vault carry several
|
|
16
|
+
* unlockers at once, so a passkey can be primary with a passphrase as the
|
|
17
|
+
* recovery path.
|
|
18
|
+
*
|
|
19
|
+
* **Scope of the protection.** Ciphertext at rest defends against everything
|
|
20
|
+
* that reads storage without running in the page: disk images, backups, profile
|
|
21
|
+
* sync, another user on the machine, an extension trawling localStorage. It does
|
|
22
|
+
* not defend against script running in the page *while the vault is unlocked* —
|
|
23
|
+
* no in-page vault can, and neither can a password manager with an open vault.
|
|
24
|
+
* The unlocked state is per page load and lives only in memory — unless the
|
|
25
|
+
* player opts into keeping it unlocked on this device (see `deviceUnlock`).
|
|
26
|
+
*
|
|
27
|
+
* State changes notify `subscribe` listeners so React can render off
|
|
28
|
+
* `getSnapshot` through `useSyncExternalStore`.
|
|
29
|
+
*/
|
|
30
|
+
import { type DeviceUnlockStore } from './deviceUnlock';
|
|
31
|
+
import { type VaultRecord, type VaultSignIn, type VaultUnlocker } from './vaultRecord';
|
|
32
|
+
export type { VaultSignIn } from './vaultRecord';
|
|
33
|
+
/** What a listener needs to render. Immutable; identity changes on every
|
|
34
|
+
* mutation so `useSyncExternalStore` re-renders exactly when something did. */
|
|
35
|
+
export interface VaultSnapshot {
|
|
36
|
+
/** A vault has been set up on this device. */
|
|
37
|
+
exists: boolean;
|
|
38
|
+
/** Set up but not opened this page load. `false` when `exists` is false. */
|
|
39
|
+
locked: boolean;
|
|
40
|
+
/** Profiles with a stored password — readable while locked. */
|
|
41
|
+
entryIds: readonly string[];
|
|
42
|
+
/** Profiles with a saved GMCP sign-in token — readable while locked. */
|
|
43
|
+
signInIds: readonly string[];
|
|
44
|
+
/** Unlock methods, without their key material. */
|
|
45
|
+
unlockers: readonly {
|
|
46
|
+
id: string;
|
|
47
|
+
kind: VaultUnlocker['kind'];
|
|
48
|
+
label: string;
|
|
49
|
+
createdAt: string;
|
|
50
|
+
}[];
|
|
51
|
+
/** Epoch ms until which this device opens the vault by itself, when the
|
|
52
|
+
* player chose that. Known only once the vault is open. */
|
|
53
|
+
rememberedUntil: number | null;
|
|
54
|
+
}
|
|
55
|
+
/** Failure modes worth telling apart in the UI. */
|
|
56
|
+
export type VaultErrorCode = 'unavailable' | 'no-vault' | 'locked' | 'no-prf' | 'no-passkey' | 'last-unlocker';
|
|
57
|
+
export declare class VaultError extends Error {
|
|
58
|
+
code: VaultErrorCode;
|
|
59
|
+
constructor(code: VaultErrorCode, message: string);
|
|
60
|
+
}
|
|
61
|
+
interface VaultIO {
|
|
62
|
+
load(): VaultRecord | null;
|
|
63
|
+
save(record: VaultRecord | null): void;
|
|
64
|
+
}
|
|
65
|
+
export declare class CredentialVault {
|
|
66
|
+
private io;
|
|
67
|
+
private device;
|
|
68
|
+
private record;
|
|
69
|
+
/** Recovered at unlock, dropped at lock. Never persisted. */
|
|
70
|
+
private masterKey;
|
|
71
|
+
/** Decrypted secrets while unlocked, keyed by connection id. */
|
|
72
|
+
private secrets;
|
|
73
|
+
/** Decrypted GMCP sign-ins while unlocked, keyed by connection id. */
|
|
74
|
+
private signIns;
|
|
75
|
+
private listeners;
|
|
76
|
+
private snapshot;
|
|
77
|
+
/** See {@link VaultSnapshot.rememberedUntil}. */
|
|
78
|
+
private rememberedUntil;
|
|
79
|
+
constructor(io?: VaultIO, device?: DeviceUnlockStore | null);
|
|
80
|
+
subscribe: (listener: () => void) => (() => void);
|
|
81
|
+
getSnapshot: () => VaultSnapshot;
|
|
82
|
+
/** Whether this browser can do vault crypto at all (secure context). */
|
|
83
|
+
get available(): boolean;
|
|
84
|
+
/** Whether a passkey unlocker can even be offered here. */
|
|
85
|
+
get passkeysSupported(): boolean;
|
|
86
|
+
get exists(): boolean;
|
|
87
|
+
get locked(): boolean;
|
|
88
|
+
/** True when this profile has a stored password — answerable while locked,
|
|
89
|
+
* which is what lets the app decide whether an unlock is worth asking for. */
|
|
90
|
+
hasEntry(connectionId: string): boolean;
|
|
91
|
+
/** The stored password, or `undefined` when there is none *or* the vault is
|
|
92
|
+
* locked. Callers treat both the same way: fall back to asking the user. */
|
|
93
|
+
getPassword(connectionId: string): string | undefined;
|
|
94
|
+
/** True when this profile has a saved sign-in *token* — answerable while
|
|
95
|
+
* locked. A token-less resume hint does not count: it is not worth asking
|
|
96
|
+
* the player to unlock for. */
|
|
97
|
+
hasSignIn(connectionId: string): boolean;
|
|
98
|
+
/** The saved GMCP sign-in (a copy), or `undefined` when there is none or
|
|
99
|
+
* the vault is locked. */
|
|
100
|
+
getSignIn(connectionId: string): VaultSignIn | undefined;
|
|
101
|
+
/** Whether this browser can keep the vault unlocked between visits. */
|
|
102
|
+
get canRemember(): boolean;
|
|
103
|
+
/** Whether a secret written now would actually be kept: a vault exists and
|
|
104
|
+
* is open. What the GMCP `token_storage` field reports. */
|
|
105
|
+
get canStore(): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Create a vault whose first unlocker is a passphrase.
|
|
108
|
+
*
|
|
109
|
+
* `seed` migrates any passwords that were already sitting in plaintext on
|
|
110
|
+
* the connection records — the caller strips them from the store once this
|
|
111
|
+
* resolves, so the plaintext copy does not outlive its replacement.
|
|
112
|
+
*/
|
|
113
|
+
createWithPassphrase(passphrase: string, seed?: Record<string, string>): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* Create a vault whose first unlocker is a passkey. Two authenticator
|
|
116
|
+
* prompts: one to register the credential, one to evaluate PRF for its KEK
|
|
117
|
+
* (Chrome does not return PRF output at registration).
|
|
118
|
+
*
|
|
119
|
+
* Throws `no-prf` when the authenticator cannot do PRF — the caller falls
|
|
120
|
+
* back to `createWithPassphrase`.
|
|
121
|
+
*/
|
|
122
|
+
createWithPasskey(appName: string, label: string, seed?: Record<string, string>): Promise<void>;
|
|
123
|
+
/** Open the vault with a passphrase. `false` = wrong passphrase (the only
|
|
124
|
+
* outcome a user can act on); anything else throws. */
|
|
125
|
+
unlockWithPassphrase(passphrase: string): Promise<boolean>;
|
|
126
|
+
/** Open the vault with a registered passkey. Throws on cancel (WebAuthn's
|
|
127
|
+
* own error) or when no passkey unlocker exists. */
|
|
128
|
+
unlockWithPasskey(): Promise<boolean>;
|
|
129
|
+
/** Drop the master key and every decrypted secret. The record stays. A
|
|
130
|
+
* vault kept unlocked on this device stops being so: "Lock" means locked
|
|
131
|
+
* on the next visit too. */
|
|
132
|
+
lock(): void;
|
|
133
|
+
/**
|
|
134
|
+
* Open the vault from a remembered unlock, if this device has one for it.
|
|
135
|
+
* Called once at startup, before anything reads a password. A stale entry —
|
|
136
|
+
* expired, for a vault since replaced, or one that will not decrypt — is
|
|
137
|
+
* dropped. Never throws: the worst case is the usual unlock prompt.
|
|
138
|
+
*/
|
|
139
|
+
restoreOnThisDevice(): Promise<boolean>;
|
|
140
|
+
/** Keep the open vault unlocked on this device until the expiry. Asking
|
|
141
|
+
* again renews it. Requires unlocked. */
|
|
142
|
+
rememberOnThisDevice(): Promise<void>;
|
|
143
|
+
/** Stop keeping the vault unlocked on this device. It stays open for the
|
|
144
|
+
* rest of this visit. */
|
|
145
|
+
forgetOnThisDevice(): Promise<void>;
|
|
146
|
+
/** Remove the vault and everything in it, irreversibly. */
|
|
147
|
+
deleteVault(): void;
|
|
148
|
+
/** Store (or, with `null`, forget) one profile's password. Requires unlocked. */
|
|
149
|
+
setPassword(connectionId: string, password: string | null): Promise<void>;
|
|
150
|
+
/**
|
|
151
|
+
* Forget every trace of a profile — called when the profile is deleted.
|
|
152
|
+
*
|
|
153
|
+
* A locked vault cannot rewrite its ciphertext, so the entry is left in
|
|
154
|
+
* place and swept on the next unlock by {@link pruneMissing}. Returning
|
|
155
|
+
* `false` says so; nothing leaks either way, since a deleted profile's
|
|
156
|
+
* password is unreachable.
|
|
157
|
+
*/
|
|
158
|
+
forgetConnection(connectionId: string): Promise<boolean>;
|
|
159
|
+
/**
|
|
160
|
+
* Store (or, with `null`, forget) one profile's GMCP sign-in. Requires
|
|
161
|
+
* unlocked: whether a token can be kept at all is decided from
|
|
162
|
+
* {@link canStore} *before* the game is told (`token_storage`), so a locked
|
|
163
|
+
* vault here is a race for the caller to report, not a silent drop.
|
|
164
|
+
*/
|
|
165
|
+
setSignIn(connectionId: string, entry: VaultSignIn | null): Promise<void>;
|
|
166
|
+
/** Drop stored passwords for profiles that no longer exist. No-op while
|
|
167
|
+
* locked; the caller runs it right after an unlock. */
|
|
168
|
+
pruneMissing(existingConnectionIds: Iterable<string>): Promise<void>;
|
|
169
|
+
/** Add a passphrase to an already-open vault (recovery path for a
|
|
170
|
+
* passkey-first vault, or a new phrase alongside the old one). */
|
|
171
|
+
addPassphrase(passphrase: string, label?: string): Promise<void>;
|
|
172
|
+
/** Add a passkey to an already-open vault. */
|
|
173
|
+
addPasskey(appName: string, label: string): Promise<void>;
|
|
174
|
+
/** Remove an unlock method. Refuses to remove the last one — that would
|
|
175
|
+
* leave a vault nobody can open, which is data loss wearing a button. */
|
|
176
|
+
removeUnlocker(id: string): void;
|
|
177
|
+
/** Replace every passphrase unlocker with one derived from `next`. The
|
|
178
|
+
* vault must be open, so this is "change", not "reset" — a forgotten
|
|
179
|
+
* passphrase is recovered with a passkey or not at all. */
|
|
180
|
+
changePassphrase(next: string, label?: string): Promise<void>;
|
|
181
|
+
private requireAvailable;
|
|
182
|
+
private requireRecord;
|
|
183
|
+
private requireMaster;
|
|
184
|
+
private requireUnlocked;
|
|
185
|
+
private buildPassphraseUnlocker;
|
|
186
|
+
private buildPasskeyUnlocker;
|
|
187
|
+
private installNewVault;
|
|
188
|
+
private adoptMasterKey;
|
|
189
|
+
private persistSecrets;
|
|
190
|
+
private writeRecord;
|
|
191
|
+
private buildSnapshot;
|
|
192
|
+
private emit;
|
|
193
|
+
}
|
|
194
|
+
/** The app's vault. One per page — the unlocked state is deliberately shared
|
|
195
|
+
* across profiles, so opening a second game after unlocking the first asks for
|
|
196
|
+
* nothing. */
|
|
197
|
+
export declare const credentialVault: CredentialVault;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Keep unlocked on this device": the vault opens by itself on later visits.
|
|
3
|
+
*
|
|
4
|
+
* The master key is wrapped under a *device key* — an AES-GCM key created with
|
|
5
|
+
* `extractable: false` and kept in IndexedDB, which stores a `CryptoKey` by
|
|
6
|
+
* structured clone. Script on this origin can ask the browser to use it but can
|
|
7
|
+
* never read its bytes, so the wrapped copy is useless anywhere but this
|
|
8
|
+
* browser profile: not in a backup, not on another machine, not to an extension
|
|
9
|
+
* that copies localStorage (where the vault record lives).
|
|
10
|
+
*
|
|
11
|
+
* What it gives up, and what the player is told: anyone who can use this
|
|
12
|
+
* browser profile can open the vault, as with the browser's own saved
|
|
13
|
+
* passwords. Script in the page could already use an unlocked vault, so that
|
|
14
|
+
* exposure is unchanged. Someone copying the profile directory off the disk may
|
|
15
|
+
* recover the key, since browsers do not all encrypt IndexedDB at rest. Hence
|
|
16
|
+
* opt-in, and an expiry ({@link REMEMBER_UNLOCK_DAYS}) that re-asking renews.
|
|
17
|
+
*
|
|
18
|
+
* Kept in its own database so dropping it never touches anything else, and so
|
|
19
|
+
* no backup or storage sweep that names the app's other databases picks it up.
|
|
20
|
+
*/
|
|
21
|
+
import { type Ciphertext } from './vaultCrypto';
|
|
22
|
+
/** How long a remembered unlock lasts before the vault asks again. */
|
|
23
|
+
export declare const REMEMBER_UNLOCK_DAYS = 30;
|
|
24
|
+
/** One remembered unlock — at most one per browser profile, like the vault. */
|
|
25
|
+
export interface RememberedUnlock {
|
|
26
|
+
/** Non-extractable; never leaves IndexedDB as bytes. */
|
|
27
|
+
deviceKey: CryptoKey;
|
|
28
|
+
/** The vault master key, encrypted under `deviceKey`. */
|
|
29
|
+
wrapped: Ciphertext;
|
|
30
|
+
/** Which vault this opens — its `prfSalt`, random per vault. A vault
|
|
31
|
+
* deleted and set up again must not be "opened" by a stale entry. */
|
|
32
|
+
vaultTag: string;
|
|
33
|
+
/** Epoch ms after which it is discarded unused. */
|
|
34
|
+
expiresAt: number;
|
|
35
|
+
}
|
|
36
|
+
export interface DeviceUnlockStore {
|
|
37
|
+
load(): Promise<RememberedUnlock | null>;
|
|
38
|
+
save(entry: RememberedUnlock): Promise<void>;
|
|
39
|
+
clear(): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/** Wrap `master` under a fresh device key. */
|
|
42
|
+
export declare function rememberMasterKey(master: CryptoKey, vaultTag: string, now?: number): Promise<RememberedUnlock>;
|
|
43
|
+
/** The master key back, or `null` when the entry is for another vault, has
|
|
44
|
+
* expired, or will not decrypt. */
|
|
45
|
+
export declare function recallMasterKey(entry: RememberedUnlock, vaultTag: string, now?: number): Promise<CryptoKey | null>;
|
|
46
|
+
/** The IndexedDB-backed store, or `null` where there is no IndexedDB. */
|
|
47
|
+
export declare function idbDeviceUnlockStore(): DeviceUnlockStore | null;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passkey-backed vault unlocking, via the WebAuthn **PRF extension**.
|
|
3
|
+
*
|
|
4
|
+
* PRF turns an authenticator into a keyed pseudo-random function: given a salt,
|
|
5
|
+
* the credential returns the same 32 secret bytes every time, and those bytes
|
|
6
|
+
* never leave the authenticator's control without a user-verification gesture
|
|
7
|
+
* (Touch ID, Windows Hello, a security key's PIN). We use them as the KEK for
|
|
8
|
+
* one vault unlocker — so "unlock" is a fingerprint rather than a typed secret,
|
|
9
|
+
* and the browser's password manager holds a single passkey for the app instead
|
|
10
|
+
* of one saved login per game.
|
|
11
|
+
*
|
|
12
|
+
* There is no server here. The challenges are client-side randomness and the
|
|
13
|
+
* signature is never verified: we are deriving a key, not authenticating. That
|
|
14
|
+
* is why registration invents a local user handle.
|
|
15
|
+
*
|
|
16
|
+
* Support is feature-detected at registration: `create()` reports `prf.enabled`,
|
|
17
|
+
* and an authenticator that says no cannot back a vault, so the caller falls
|
|
18
|
+
* back to a passphrase.
|
|
19
|
+
*/
|
|
20
|
+
/** Registered credential, as persisted in the vault record. */
|
|
21
|
+
export interface PasskeyRegistration {
|
|
22
|
+
/** Raw credential id, base64 — replayed in `allowCredentials` at unlock. */
|
|
23
|
+
credentialId: string;
|
|
24
|
+
}
|
|
25
|
+
/** True when the browser exposes WebAuthn at all. PRF support is only knowable
|
|
26
|
+
* after a registration attempt, so this is a pre-filter, not a guarantee. */
|
|
27
|
+
export declare function passkeysAvailable(): boolean;
|
|
28
|
+
/** True when this device has a built-in authenticator (Touch ID, Hello, Android
|
|
29
|
+
* screen lock) — used only to word the offer, never to gate it: security keys
|
|
30
|
+
* do PRF too. */
|
|
31
|
+
export declare function platformAuthenticatorAvailable(): Promise<boolean>;
|
|
32
|
+
/**
|
|
33
|
+
* Register a new passkey for the vault and check that it can do PRF.
|
|
34
|
+
*
|
|
35
|
+
* Returns `null` when the credential was created but the authenticator refused
|
|
36
|
+
* PRF — the caller must then fall back to a passphrase. Throws when the user
|
|
37
|
+
* cancels or registration fails outright.
|
|
38
|
+
*/
|
|
39
|
+
export declare function registerVaultPasskey(appName: string, label: string): Promise<PasskeyRegistration | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Evaluate the vault's PRF salt against whichever registered passkey the user
|
|
42
|
+
* presents, and return the 32 secret bytes it answers with.
|
|
43
|
+
*
|
|
44
|
+
* All known credentials go into one `allowCredentials` list so a vault holding
|
|
45
|
+
* a laptop's Touch ID *and* a phone opens from either in a single prompt. That
|
|
46
|
+
* works because the salt belongs to the vault, not to a credential: one
|
|
47
|
+
* assertion cannot evaluate a different salt per credential, and the KEKs stay
|
|
48
|
+
* distinct regardless, since PRF output is keyed by the credential's own secret.
|
|
49
|
+
*
|
|
50
|
+
* Returns `null` when the authenticator answered without a PRF result. A cancel
|
|
51
|
+
* or timeout throws, as WebAuthn does.
|
|
52
|
+
*/
|
|
53
|
+
export declare function evaluateVaultPrf(registrations: PasskeyRegistration[], prfSalt: Uint8Array): Promise<{
|
|
54
|
+
credentialId: string;
|
|
55
|
+
secret: Uint8Array;
|
|
56
|
+
} | null>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { VaultSnapshot } from './CredentialVault';
|
|
2
|
+
/**
|
|
3
|
+
* Render off the credential vault's state. Returns the "no vault" snapshot in
|
|
4
|
+
* builds that never installed one (see `vaultAccess`), so every consumer can
|
|
5
|
+
* call this unconditionally and simply render nothing when `exists` is false
|
|
6
|
+
* and the vault cannot be created.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useVault(): VaultSnapshot;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seam between the app and the credential vault.
|
|
3
|
+
*
|
|
4
|
+
* The vault is a **stock-app feature only**. A branded build ships for one game,
|
|
5
|
+
* so the problem it solves — ten profiles' logins piling up indistinguishably in
|
|
6
|
+
* one origin's password-manager dropdown — does not exist there, and branded
|
|
7
|
+
* builds keep credentials in memory for the page (`utils/sessionCredentials`)
|
|
8
|
+
* rather than storing them at all.
|
|
9
|
+
*
|
|
10
|
+
* So the library entry (`src/index.ts` → `MudletWebApp`) must not reach the
|
|
11
|
+
* vault, and this module is how: it is the only vault file the shared UI
|
|
12
|
+
* imports, it names the vault and its modal through `import type` alone, and
|
|
13
|
+
* `main.tsx` — which the standalone app loads and the library does not — is what
|
|
14
|
+
* puts the real ones in. Nothing here pulls in crypto, WebAuthn or the modal, so
|
|
15
|
+
* a consumer build contains none of it; every `getVault()` there is `null` and
|
|
16
|
+
* the callers render nothing.
|
|
17
|
+
*/
|
|
18
|
+
import type { ComponentType } from 'react';
|
|
19
|
+
import type { CredentialVault, VaultSignIn, VaultSnapshot } from './CredentialVault';
|
|
20
|
+
/** Props of the vault's modal. Declared here, not in the component, so this
|
|
21
|
+
* module can describe the UI it hands out without importing it. */
|
|
22
|
+
export interface VaultUIProps {
|
|
23
|
+
/** Which step to show: create a vault, open one, or manage one. */
|
|
24
|
+
mode: 'setup' | 'unlock' | 'manage';
|
|
25
|
+
/** Why an unlock is being asked for — e.g. the profile being opened. */
|
|
26
|
+
reason?: string;
|
|
27
|
+
/** Plaintext passwords to fold into a *new* vault, keyed by connection id. */
|
|
28
|
+
seed?: Record<string, string>;
|
|
29
|
+
/** Profile id → display name, for the saved-logins list. */
|
|
30
|
+
connectionNames?: Record<string, string>;
|
|
31
|
+
onDone: (result: 'created' | 'unlocked' | 'closed') => void;
|
|
32
|
+
}
|
|
33
|
+
export type VaultUI = ComponentType<VaultUIProps>;
|
|
34
|
+
/** What a consumer without a vault sees. Frozen and shared: `useSyncExternalStore`
|
|
35
|
+
* needs a stable identity from a snapshot that has not changed. */
|
|
36
|
+
export declare const NO_VAULT: VaultSnapshot;
|
|
37
|
+
/**
|
|
38
|
+
* Make a vault (and the UI that drives it) available to the app. Called once
|
|
39
|
+
* from `main.tsx`, before the first render, so credential reads on a cold
|
|
40
|
+
* auto-connect never race the install.
|
|
41
|
+
*/
|
|
42
|
+
export declare function installVault(vault: CredentialVault, ui: VaultUI): void;
|
|
43
|
+
/** Drop the installed vault. Tests only — the app installs once per page. */
|
|
44
|
+
export declare function uninstallVault(): void;
|
|
45
|
+
/** The app's vault, or `null` in a build that never installed one. */
|
|
46
|
+
export declare function getVault(): CredentialVault | null;
|
|
47
|
+
/** The vault's modal, or `null` — render it only when this is non-null. */
|
|
48
|
+
export declare function getVaultUI(): VaultUI | null;
|
|
49
|
+
export declare function subscribeVault(listener: () => void): () => void;
|
|
50
|
+
export declare function vaultSnapshot(): VaultSnapshot;
|
|
51
|
+
/** A stored password, or `undefined` when there is no vault, no entry, or the
|
|
52
|
+
* vault is locked. All three mean the same thing to a caller: ask the user. */
|
|
53
|
+
export declare function vaultPassword(connectionId: string): string | undefined;
|
|
54
|
+
/** Whether this profile has a stored password — true even while locked, which
|
|
55
|
+
* is what tells the app an unlock is worth offering. */
|
|
56
|
+
export declare function vaultHasEntry(connectionId: string): boolean;
|
|
57
|
+
/** Whether opening this profile should ask for an unlock first — it has a
|
|
58
|
+
* saved password or a saved GMCP sign-in token, and the vault is shut. */
|
|
59
|
+
export declare function vaultNeedsUnlock(connectionId: string): boolean;
|
|
60
|
+
/** The profile's saved GMCP `Char.Login` sign-in, or `undefined` when there is
|
|
61
|
+
* no vault, no entry, or the vault is locked. */
|
|
62
|
+
export declare function vaultSignIn(connectionId: string): VaultSignIn | undefined;
|
|
63
|
+
/** Whether a vault could be set up here but none exists yet. False with no
|
|
64
|
+
* vault installed (a branded build) or no crypto (an insecure context). */
|
|
65
|
+
export declare function vaultMissing(): boolean;
|
|
66
|
+
/** Whether a vault exists and is locked. */
|
|
67
|
+
export declare function vaultLocked(): boolean;
|
|
68
|
+
/** Whether this profile has a saved sign-in token — true even while locked. */
|
|
69
|
+
export declare function vaultHasSignIn(connectionId: string): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Whether a sign-in token handed to us right now would be kept — the honest
|
|
72
|
+
* answer to GMCP's `token_storage`. With no vault (a branded build, or a player
|
|
73
|
+
* who never set one up) or a locked one, a token would be thrown away, and the
|
|
74
|
+
* standard asks the client to say so up front, so the game does not offer the
|
|
75
|
+
* player a "remember me" that cannot happen.
|
|
76
|
+
*/
|
|
77
|
+
export declare function vaultCanStoreSignIn(): boolean;
|
|
78
|
+
/** Save (or with `null` forget) a profile's sign-in. Resolves `false` when it
|
|
79
|
+
* could not — no vault, locked, or the write failed. */
|
|
80
|
+
export declare function saveVaultSignIn(connectionId: string, entry: VaultSignIn | null): Promise<boolean>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebCrypto primitives for the credential vault.
|
|
3
|
+
*
|
|
4
|
+
* Envelope encryption: one random 256-bit AES-GCM **master key** encrypts the
|
|
5
|
+
* secrets blob, and every *unlocker* (a passphrase, a passkey) wraps a copy of
|
|
6
|
+
* that master key with a key-encryption key it derives on its own. Adding or
|
|
7
|
+
* removing an unlock method therefore rewraps one small blob instead of
|
|
8
|
+
* re-encrypting the secrets, and two methods can coexist without either being
|
|
9
|
+
* able to read the other's derivation input.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is pure WebCrypto over plain values — no storage, no DOM —
|
|
12
|
+
* so the format can be tested end-to-end without IndexedDB or a browser.
|
|
13
|
+
*/
|
|
14
|
+
/** AES-GCM ciphertext plus its nonce, both base64 for JSON storage. */
|
|
15
|
+
export interface Ciphertext {
|
|
16
|
+
/** 12-byte GCM nonce, freshly random for every encryption. */
|
|
17
|
+
iv: string;
|
|
18
|
+
/** Ciphertext with the 16-byte GCM tag appended (WebCrypto's layout). */
|
|
19
|
+
ct: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* PBKDF2 work factor for passphrase unlockers. OWASP's 2023 floor for
|
|
23
|
+
* PBKDF2-HMAC-SHA256; ~0.3s on a mid-range laptop, which is the right trade for
|
|
24
|
+
* something typed once per page rather than once per keystroke. Stored *in* the
|
|
25
|
+
* record so the number can be raised later without stranding existing vaults.
|
|
26
|
+
*/
|
|
27
|
+
export declare const PBKDF2_ITERATIONS = 600000;
|
|
28
|
+
/** True when this context can do vault crypto at all. */
|
|
29
|
+
export declare function cryptoAvailable(): boolean;
|
|
30
|
+
export declare function randomBytes(length: number): Uint8Array;
|
|
31
|
+
export declare function toBase64(bytes: ArrayBuffer | Uint8Array): string;
|
|
32
|
+
export declare function fromBase64(b64: string): Uint8Array;
|
|
33
|
+
/** A fresh master key. Extractable — it has to be, to be wrapped by unlockers. */
|
|
34
|
+
export declare function generateMasterKey(): Promise<CryptoKey>;
|
|
35
|
+
/**
|
|
36
|
+
* Stretch a passphrase into a key-encryption key. The KEK never encrypts
|
|
37
|
+
* secrets directly — it only wraps the master key — so changing the passphrase
|
|
38
|
+
* costs one rewrap.
|
|
39
|
+
*/
|
|
40
|
+
export declare function deriveKeyFromPassphrase(passphrase: string, salt: Uint8Array, iterations?: number): Promise<CryptoKey>;
|
|
41
|
+
/**
|
|
42
|
+
* Stretch a WebAuthn PRF result into a key-encryption key. The PRF output is
|
|
43
|
+
* already 32 uniformly-random bytes bound to the authenticator, so this is a
|
|
44
|
+
* domain-separating HKDF rather than a cost function — there is nothing to
|
|
45
|
+
* brute-force without the authenticator.
|
|
46
|
+
*/
|
|
47
|
+
export declare function deriveKeyFromPrf(prfOutput: ArrayBuffer | Uint8Array): Promise<CryptoKey>;
|
|
48
|
+
export declare function encryptBytes(key: CryptoKey, plaintext: Uint8Array): Promise<Ciphertext>;
|
|
49
|
+
/** Decrypt, or throw — GCM authentication failure is how a wrong passphrase is detected. */
|
|
50
|
+
export declare function decryptBytes(key: CryptoKey, box: Ciphertext): Promise<Uint8Array>;
|
|
51
|
+
export declare function encryptJson(key: CryptoKey, value: unknown): Promise<Ciphertext>;
|
|
52
|
+
export declare function decryptJson<T>(key: CryptoKey, box: Ciphertext): Promise<T>;
|
|
53
|
+
/** Wrap the master key for one unlocker. */
|
|
54
|
+
export declare function wrapMasterKey(kek: CryptoKey, master: CryptoKey): Promise<Ciphertext>;
|
|
55
|
+
/** Recover the master key from an unlocker's wrapped copy. Throws on a bad KEK. */
|
|
56
|
+
export declare function unwrapMasterKey(kek: CryptoKey, wrapped: Ciphertext): Promise<CryptoKey>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The manual page for the credential vault.
|
|
3
|
+
*
|
|
4
|
+
* It lives here rather than in `ui/helpTopics.ts` for the same reason the vault
|
|
5
|
+
* itself does: a branded build ships for one game, has no vault, and must not
|
|
6
|
+
* carry a help topic explaining a feature its players cannot find. Registered
|
|
7
|
+
* from `main.tsx` at boot — the standalone app's entry, never the library's.
|
|
8
|
+
*/
|
|
9
|
+
export declare function installVaultHelpTopic(): void;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk shape of the credential vault, and its (synchronous) persistence.
|
|
3
|
+
*
|
|
4
|
+
* The record lives in `localStorage` rather than IndexedDB on purpose: it is
|
|
5
|
+
* small, and everything sensitive in it is already AES-GCM ciphertext, so the
|
|
6
|
+
* weakness that made plaintext `charLoginPassword` a bad idea — localStorage is
|
|
7
|
+
* readable by anything running on the page — costs nothing here. In exchange
|
|
8
|
+
* the vault's *shape* (does one exist, which profiles have a saved password) is
|
|
9
|
+
* readable synchronously during the first render, with no async init race and
|
|
10
|
+
* no loading state in every consumer.
|
|
11
|
+
*
|
|
12
|
+
* It is deliberately NOT part of the Zustand store: `mudix_v1` is exported,
|
|
13
|
+
* imported and reset by profile tooling, and vault material must not travel
|
|
14
|
+
* with a profile export.
|
|
15
|
+
*/
|
|
16
|
+
import type { Ciphertext } from './vaultCrypto';
|
|
17
|
+
export declare const VAULT_STORAGE_KEY = "mudlet_vault_v1";
|
|
18
|
+
interface UnlockerBase {
|
|
19
|
+
/** Stable local id, so the manage UI can remove one unambiguously. */
|
|
20
|
+
id: string;
|
|
21
|
+
/** User-facing name ("Passphrase", "MacBook Touch ID"). */
|
|
22
|
+
label: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
/** The vault's master key, wrapped by the KEK this unlocker derives. */
|
|
25
|
+
wrapped: Ciphertext;
|
|
26
|
+
}
|
|
27
|
+
export interface PassphraseUnlocker extends UnlockerBase {
|
|
28
|
+
kind: 'passphrase';
|
|
29
|
+
/** PBKDF2 salt, base64. */
|
|
30
|
+
salt: string;
|
|
31
|
+
/** PBKDF2 iteration count *as used*, so the cost can be raised for new
|
|
32
|
+
* vaults without stranding existing ones. */
|
|
33
|
+
iterations: number;
|
|
34
|
+
}
|
|
35
|
+
export interface PasskeyUnlocker extends UnlockerBase {
|
|
36
|
+
kind: 'passkey';
|
|
37
|
+
/** WebAuthn credential id, base64. */
|
|
38
|
+
credentialId: string;
|
|
39
|
+
}
|
|
40
|
+
export type VaultUnlocker = PassphraseUnlocker | PasskeyUnlocker;
|
|
41
|
+
export interface VaultRecord {
|
|
42
|
+
version: 1;
|
|
43
|
+
/** 32-byte PRF salt shared by every passkey unlocker, base64. See
|
|
44
|
+
* `evaluateVaultPrf` for why it is per-vault and not per-credential. */
|
|
45
|
+
prfSalt: string;
|
|
46
|
+
unlockers: VaultUnlocker[];
|
|
47
|
+
/** Connection ids that have a stored password. Plaintext by design — it is
|
|
48
|
+
* what lets a *locked* vault answer "does this profile have a saved login?"
|
|
49
|
+
* so the app knows whether to offer an unlock at all. */
|
|
50
|
+
entryIds: string[];
|
|
51
|
+
/** AES-GCM under the master key over `Record<connectionId, password>`.
|
|
52
|
+
* `null` before the first secret is stored. */
|
|
53
|
+
payload: Ciphertext | null;
|
|
54
|
+
/** Profiles holding a saved GMCP `Char.Login` sign-in token (see
|
|
55
|
+
* {@link VaultSignIn}). Plaintext for the same reason as `entryIds`: a
|
|
56
|
+
* locked vault has to be able to say a profile has something worth
|
|
57
|
+
* unlocking for. Absent in a record written before sign-ins existed. */
|
|
58
|
+
signInIds?: string[];
|
|
59
|
+
/** AES-GCM under the master key over `Record<connectionId, VaultSignIn>`.
|
|
60
|
+
* A field of its own rather than more keys in `payload`, so a build from
|
|
61
|
+
* before sign-ins existed still reads its passwords unchanged, and carries
|
|
62
|
+
* this across its own writes untouched (it spreads the record). */
|
|
63
|
+
signInPayload?: Ciphertext | null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A saved GMCP `Char.Login` v2 sign-in: the reconnect token a game issued with
|
|
67
|
+
* `Char.Login.Token`, the account it names, and what is needed to replay it
|
|
68
|
+
* safely — or to resume the same provider once it stops working.
|
|
69
|
+
*
|
|
70
|
+
* Mudlet keeps the same entry in the OS keychain (`GMCPAuthenticator`'s
|
|
71
|
+
* metadata + token keys). The token is a bearer secret — it signs the account
|
|
72
|
+
* in without a password — so it lives encrypted here or not at all.
|
|
73
|
+
*/
|
|
74
|
+
export interface VaultSignIn {
|
|
75
|
+
/** The account the token was issued for; `Char.Login.Reconnect` names it. */
|
|
76
|
+
account: string;
|
|
77
|
+
/** Provider id (`discord`, `google`, …) learned from `Char.Login.URL`, so a
|
|
78
|
+
* dead token can turn into a resume of that provider's sign-in. */
|
|
79
|
+
provider?: string;
|
|
80
|
+
/** The bearer token. Absent in a resume hint left behind by a dead one. */
|
|
81
|
+
token?: string;
|
|
82
|
+
/** The token may only be replayed over an encrypted game connection — the
|
|
83
|
+
* issuing server's requirement, stored because the connection that set it
|
|
84
|
+
* is long gone by replay time. Missing reads as true, never widening. */
|
|
85
|
+
secureOnly?: boolean;
|
|
86
|
+
}
|
|
87
|
+
/** Parse defensively — a corrupt record must degrade to "no vault", never to a
|
|
88
|
+
* crash on boot that locks the user out of the app as well as the vault. */
|
|
89
|
+
export declare function parseVaultRecord(raw: string | null): VaultRecord | null;
|
|
90
|
+
export declare function loadVaultRecord(): VaultRecord | null;
|
|
91
|
+
export declare function saveVaultRecord(record: VaultRecord | null): void;
|
|
92
|
+
export {};
|