├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── main.js │ ├── search.js │ └── style.css ├── classes │ ├── nakama_js.Client.html │ ├── nakama_js.DefaultSocket.html │ ├── nakama_js.Session.html │ ├── nakama_js.WebSocketAdapterText.html │ ├── satori_js.Client.html │ └── satori_js.Session.html ├── index.html ├── interfaces │ ├── nakama_js.Channel.html │ ├── nakama_js.ChannelMessage.html │ ├── nakama_js.ChannelMessageAck.html │ ├── nakama_js.ChannelMessageList.html │ ├── nakama_js.ChannelPresenceEvent.html │ ├── nakama_js.Friend.html │ ├── nakama_js.FriendOfFriend.html │ ├── nakama_js.Friends.html │ ├── nakama_js.FriendsOfFriends.html │ ├── nakama_js.Group.html │ ├── nakama_js.GroupList.html │ ├── nakama_js.GroupUser.html │ ├── nakama_js.GroupUserList.html │ ├── nakama_js.ISession.html │ ├── nakama_js.LeaderboardRecord.html │ ├── nakama_js.LeaderboardRecordList.html │ ├── nakama_js.Match.html │ ├── nakama_js.MatchData.html │ ├── nakama_js.MatchPresenceEvent.html │ ├── nakama_js.MatchmakerMatched.html │ ├── nakama_js.MatchmakerTicket.html │ ├── nakama_js.MatchmakerUser.html │ ├── nakama_js.Notification.html │ ├── nakama_js.NotificationList.html │ ├── nakama_js.Party.html │ ├── nakama_js.PartyCreate.html │ ├── nakama_js.PartyData.html │ ├── nakama_js.PartyJoinRequest.html │ ├── nakama_js.PartyJoinRequestList.html │ ├── nakama_js.PartyLeader.html │ ├── nakama_js.PartyMatchmakerTicket.html │ ├── nakama_js.PartyPresenceEvent.html │ ├── nakama_js.Presence.html │ ├── nakama_js.RpcResponse.html │ ├── nakama_js.Socket.html │ ├── nakama_js.SocketCloseHandler.html │ ├── nakama_js.SocketError.html │ ├── nakama_js.SocketErrorHandler.html │ ├── nakama_js.SocketMessageHandler.html │ ├── nakama_js.SocketOpenHandler.html │ ├── nakama_js.Status.html │ ├── nakama_js.StatusPresenceEvent.html │ ├── nakama_js.StorageObject.html │ ├── nakama_js.StorageObjectList.html │ ├── nakama_js.StorageObjects.html │ ├── nakama_js.StreamData.html │ ├── nakama_js.StreamId.html │ ├── nakama_js.StreamPresenceEvent.html │ ├── nakama_js.SubscriptionList.html │ ├── nakama_js.Tournament.html │ ├── nakama_js.TournamentList.html │ ├── nakama_js.TournamentRecordList.html │ ├── nakama_js.User.html │ ├── nakama_js.UserGroup.html │ ├── nakama_js.UserGroupList.html │ ├── nakama_js.Users.html │ ├── nakama_js.ValidatedSubscription.html │ ├── nakama_js.WebSocketAdapter.html │ ├── nakama_js.WriteLeaderboardRecord.html │ ├── nakama_js.WriteStorageObject.html │ ├── nakama_js.WriteTournamentRecord.html │ └── satori_js.ISession.html ├── modules.html └── modules │ ├── nakama_js.html │ └── satori_js.html ├── openapi-gen ├── README.md └── main.go ├── package-lock.json ├── package.json ├── packages ├── nakama-js-iife-example │ ├── README.md │ ├── dist │ │ └── nakama-js-example.iife.js │ ├── index.html │ ├── index.ts │ └── package.json ├── nakama-js-protobuf │ ├── .gitignore │ ├── README.md │ ├── api │ │ └── api.ts │ ├── build.js │ ├── build.mjs │ ├── dist │ │ ├── nakama-js-protobuf.cjs.js │ │ ├── nakama-js-protobuf.esm.mjs │ │ ├── nakama-js-protobuf.iife.js │ │ ├── nakama-js-protobuf │ │ │ ├── api │ │ │ │ └── api.d.ts │ │ │ ├── google │ │ │ │ └── protobuf │ │ │ │ │ ├── timestamp.d.ts │ │ │ │ │ └── wrappers.d.ts │ │ │ ├── index.d.ts │ │ │ ├── rtapi │ │ │ │ └── realtime.d.ts │ │ │ └── web_socket_adapter_pb.d.ts │ │ └── nakama-js │ │ │ └── web_socket_adapter.d.ts │ ├── google │ │ └── protobuf │ │ │ ├── timestamp.ts │ │ │ └── wrappers.ts │ ├── index.ts │ ├── package.json │ ├── rtapi │ │ └── realtime.ts │ ├── tsconfig.json │ └── web_socket_adapter_pb.ts ├── nakama-js-test │ ├── client-authenticate.test.ts │ ├── client-friend.test.ts │ ├── client-group.test.ts │ ├── client-leaderboard.test.ts │ ├── client-link.test.ts │ ├── client-rpc.test.ts │ ├── client-storage.test.ts │ ├── client-user.test.ts │ ├── client.test.ts │ ├── index.html │ ├── jest.config.js │ ├── package.json │ ├── session.test.ts │ ├── socket-channel.test.ts │ ├── socket-match.test.ts │ ├── socket-matchmaker.test.ts │ ├── socket-notification.test.ts │ ├── socket-party.test.ts │ ├── socket-status.test.ts │ ├── socket.test.ts │ ├── tsconfig.test.json │ └── utils.ts ├── nakama-js-webpack-example │ ├── README.md │ ├── dist │ │ └── main.js │ ├── index.html │ ├── index.ts │ ├── package.json │ ├── tsconfig.json │ └── webpack.config.js ├── nakama-js │ ├── .gitignore │ ├── README.md │ ├── api.gen.ts │ ├── build.mjs │ ├── client.ts │ ├── dist │ │ ├── api.gen.d.ts │ │ ├── client.d.ts │ │ ├── index.d.ts │ │ ├── nakama-js.cjs.js │ │ ├── nakama-js.esm.mjs │ │ ├── nakama-js.iife.js │ │ ├── nakama-js.umd.js │ │ ├── session.d.ts │ │ ├── socket.d.ts │ │ ├── utils.d.ts │ │ └── web_socket_adapter.d.ts │ ├── index.ts │ ├── package.json │ ├── rollup.config.js │ ├── session.ts │ ├── socket.ts │ ├── tsconfig.json │ ├── utils.ts │ └── web_socket_adapter.ts ├── satori-js-test │ ├── client.test.ts │ ├── index.html │ ├── jest.config.js │ ├── package.json │ ├── tsconfig.test.json │ └── utils.ts └── satori-js │ ├── README.md │ ├── api.gen.ts │ ├── build.mjs │ ├── client.ts │ ├── dist │ ├── api.gen.d.ts │ ├── client.d.ts │ ├── index.d.ts │ ├── satori-js.cjs.js │ ├── satori-js.esm.mjs │ ├── satori-js.iife.js │ ├── satori-js.umd.js │ ├── session.d.ts │ └── utils.d.ts │ ├── index.ts │ ├── package.json │ ├── rollup.config.js │ ├── session.ts │ ├── tsconfig.json │ └── utils.ts ├── tsconfig.base.json ├── tsconfig.typedoc.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node,linux,macos,windows,sublimetext,webstorm+all 2 | 3 | .rpt2_cache/ 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### macOS ### 21 | *.DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | # Icon must end with two \r 26 | Icon 27 | 28 | # Thumbnails 29 | ._* 30 | 31 | # Files that might appear in the root of a volume 32 | .DocumentRevisions-V100 33 | .fseventsd 34 | .Spotlight-V100 35 | .TemporaryItems 36 | .Trashes 37 | .VolumeIcon.icns 38 | .com.apple.timemachine.donotpresent 39 | 40 | # Directories potentially created on remote AFP share 41 | .AppleDB 42 | .AppleDesktop 43 | Network Trash Folder 44 | Temporary Items 45 | .apdisk 46 | 47 | ### Node ### 48 | # Logs 49 | logs 50 | *.log 51 | npm-debug.log* 52 | yarn-debug.log* 53 | yarn-error.log* 54 | 55 | # Runtime data 56 | pids 57 | *.pid 58 | *.seed 59 | *.pid.lock 60 | 61 | # Directory for instrumented libs generated by jscoverage/JSCover 62 | lib-cov 63 | 64 | # Coverage directory used by tools like istanbul 65 | coverage 66 | 67 | # nyc test coverage 68 | .nyc_output 69 | 70 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 71 | .grunt 72 | 73 | # Bower dependency directory (https://bower.io/) 74 | bower_components 75 | 76 | # node-waf configuration 77 | .lock-wscript 78 | 79 | # Compiled binary addons (http://nodejs.org/api/addons.html) 80 | build/Release 81 | 82 | # Dependency directories 83 | node_modules/ 84 | jspm_packages/ 85 | 86 | # Typescript v1 declaration files 87 | typings/ 88 | 89 | # Optional npm cache directory 90 | .npm 91 | 92 | # Optional eslint cache 93 | .eslintcache 94 | 95 | # Optional REPL history 96 | .node_repl_history 97 | 98 | # Output of 'npm pack' 99 | *.tgz 100 | 101 | # Yarn 102 | .yarn-integrity 103 | 104 | **/.yarn 105 | !.yarn/releases 106 | !.yarn/plugins 107 | !.yarn/sdks 108 | !.yarn/versions 109 | .pnp.* 110 | 111 | # dotenv environment variables file 112 | .env 113 | 114 | 115 | ### SublimeText ### 116 | # cache files for sublime text 117 | *.tmlanguage.cache 118 | *.tmPreferences.cache 119 | *.stTheme.cache 120 | 121 | # workspace files are user-specific 122 | *.sublime-workspace 123 | 124 | # project files should be checked into the repository, unless a significant 125 | # proportion of contributors will probably not be using SublimeText 126 | # *.sublime-project 127 | 128 | # sftp configuration file 129 | sftp-config.json 130 | 131 | # Package control specific files 132 | Package Control.last-run 133 | Package Control.ca-list 134 | Package Control.ca-bundle 135 | Package Control.system-ca-bundle 136 | Package Control.cache/ 137 | Package Control.ca-certs/ 138 | Package Control.merged-ca-bundle 139 | Package Control.user-ca-bundle 140 | oscrypto-ca-bundle.crt 141 | bh_unicode_properties.cache 142 | 143 | # Sublime-github package stores a github token in this file 144 | # https://packagecontrol.io/packages/sublime-github 145 | GitHub.sublime-settings 146 | 147 | ### WebStorm+all ### 148 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 149 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 150 | 151 | # User-specific stuff: 152 | .idea/**/workspace.xml 153 | .idea/**/tasks.xml 154 | .idea/dictionaries 155 | 156 | # Sensitive or high-churn files: 157 | .idea/**/dataSources/ 158 | .idea/**/dataSources.ids 159 | .idea/**/dataSources.xml 160 | .idea/**/dataSources.local.xml 161 | .idea/**/sqlDataSources.xml 162 | .idea/**/dynamic.xml 163 | .idea/**/uiDesigner.xml 164 | 165 | # Gradle: 166 | .idea/**/gradle.xml 167 | .idea/**/libraries 168 | 169 | # CMake 170 | cmake-build-debug/ 171 | 172 | # Mongo Explorer plugin: 173 | .idea/**/mongoSettings.xml 174 | 175 | ## File-based project format: 176 | *.iws 177 | 178 | ## Plugin-specific files: 179 | 180 | # IntelliJ 181 | /out/ 182 | 183 | # mpeltonen/sbt-idea plugin 184 | .idea_modules/ 185 | 186 | # JIRA plugin 187 | atlassian-ide-plugin.xml 188 | 189 | # Cursive Clojure plugin 190 | .idea/replstate.xml 191 | 192 | # Ruby plugin and RubyMine 193 | /.rakeTasks 194 | 195 | # Crashlytics plugin (for Android Studio and IntelliJ) 196 | com_crashlytics_export_strings.xml 197 | crashlytics.properties 198 | crashlytics-build.properties 199 | fabric.properties 200 | 201 | ### WebStorm+all Patch ### 202 | # Ignores the whole idea folder 203 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 204 | 205 | .idea/ 206 | 207 | ### Windows ### 208 | # Windows thumbnail cache files 209 | Thumbs.db 210 | ehthumbs.db 211 | ehthumbs_vista.db 212 | 213 | # Folder config file 214 | Desktop.ini 215 | 216 | # Recycle Bin used on file shares 217 | $RECYCLE.BIN/ 218 | 219 | # Windows Installer files 220 | *.cab 221 | *.msi 222 | *.msm 223 | *.msp 224 | 225 | # Windows shortcuts 226 | *.lnk 227 | 228 | # End of https://www.gitignore.io/api/node,linux,macos,windows,sublimetext,webstorm+all 229 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node,linux,macos,windows,sublimetext,webstorm+all 2 | 3 | src/ 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### macOS ### 21 | *.DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | # Icon must end with two \r 26 | Icon 27 | 28 | # Thumbnails 29 | ._* 30 | 31 | # Files that might appear in the root of a volume 32 | .DocumentRevisions-V100 33 | .fseventsd 34 | .Spotlight-V100 35 | .TemporaryItems 36 | .Trashes 37 | .VolumeIcon.icns 38 | .com.apple.timemachine.donotpresent 39 | 40 | # Directories potentially created on remote AFP share 41 | .AppleDB 42 | .AppleDesktop 43 | Network Trash Folder 44 | Temporary Items 45 | .apdisk 46 | 47 | ### Node ### 48 | # Logs 49 | logs 50 | *.log 51 | npm-debug.log* 52 | yarn-debug.log* 53 | yarn-error.log* 54 | 55 | # Runtime data 56 | pids 57 | *.pid 58 | *.seed 59 | *.pid.lock 60 | 61 | # Directory for instrumented libs generated by jscoverage/JSCover 62 | lib-cov 63 | 64 | # Coverage directory used by tools like istanbul 65 | coverage 66 | 67 | # nyc test coverage 68 | .nyc_output 69 | 70 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 71 | .grunt 72 | 73 | # Bower dependency directory (https://bower.io/) 74 | bower_components 75 | 76 | # node-waf configuration 77 | .lock-wscript 78 | 79 | # Compiled binary addons (http://nodejs.org/api/addons.html) 80 | build/Release 81 | 82 | # Dependency directories 83 | node_modules/ 84 | jspm_packages/ 85 | 86 | # Typescript v1 declaration files 87 | typings/ 88 | 89 | # Optional npm cache directory 90 | .npm 91 | 92 | # Optional eslint cache 93 | .eslintcache 94 | 95 | # Optional REPL history 96 | .node_repl_history 97 | 98 | # Output of 'npm pack' 99 | *.tgz 100 | 101 | # Yarn Integrity file 102 | .yarn-integrity 103 | 104 | # dotenv environment variables file 105 | .env 106 | 107 | 108 | ### SublimeText ### 109 | # cache files for sublime text 110 | *.tmlanguage.cache 111 | *.tmPreferences.cache 112 | *.stTheme.cache 113 | 114 | # workspace files are user-specific 115 | *.sublime-workspace 116 | 117 | # project files should be checked into the repository, unless a significant 118 | # proportion of contributors will probably not be using SublimeText 119 | # *.sublime-project 120 | 121 | # sftp configuration file 122 | sftp-config.json 123 | 124 | # Package control specific files 125 | Package Control.last-run 126 | Package Control.ca-list 127 | Package Control.ca-bundle 128 | Package Control.system-ca-bundle 129 | Package Control.cache/ 130 | Package Control.ca-certs/ 131 | Package Control.merged-ca-bundle 132 | Package Control.user-ca-bundle 133 | oscrypto-ca-bundle.crt 134 | bh_unicode_properties.cache 135 | 136 | # Sublime-github package stores a github token in this file 137 | # https://packagecontrol.io/packages/sublime-github 138 | GitHub.sublime-settings 139 | 140 | ### WebStorm+all ### 141 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 142 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 143 | 144 | # User-specific stuff: 145 | .idea/**/workspace.xml 146 | .idea/**/tasks.xml 147 | .idea/dictionaries 148 | 149 | # Sensitive or high-churn files: 150 | .idea/**/dataSources/ 151 | .idea/**/dataSources.ids 152 | .idea/**/dataSources.xml 153 | .idea/**/dataSources.local.xml 154 | .idea/**/sqlDataSources.xml 155 | .idea/**/dynamic.xml 156 | .idea/**/uiDesigner.xml 157 | 158 | # Gradle: 159 | .idea/**/gradle.xml 160 | .idea/**/libraries 161 | 162 | # CMake 163 | cmake-build-debug/ 164 | 165 | # Mongo Explorer plugin: 166 | .idea/**/mongoSettings.xml 167 | 168 | ## File-based project format: 169 | *.iws 170 | 171 | ## Plugin-specific files: 172 | 173 | # IntelliJ 174 | /out/ 175 | 176 | # mpeltonen/sbt-idea plugin 177 | .idea_modules/ 178 | 179 | # JIRA plugin 180 | atlassian-ide-plugin.xml 181 | 182 | # Cursive Clojure plugin 183 | .idea/replstate.xml 184 | 185 | # Ruby plugin and RubyMine 186 | /.rakeTasks 187 | 188 | # Crashlytics plugin (for Android Studio and IntelliJ) 189 | com_crashlytics_export_strings.xml 190 | crashlytics.properties 191 | crashlytics-build.properties 192 | fabric.properties 193 | 194 | ### WebStorm+all Patch ### 195 | # Ignores the whole idea folder 196 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 197 | 198 | .idea/ 199 | 200 | ### Windows ### 201 | # Windows thumbnail cache files 202 | Thumbs.db 203 | ehthumbs.db 204 | ehthumbs_vista.db 205 | 206 | # Folder config file 207 | Desktop.ini 208 | 209 | # Recycle Bin used on file shares 210 | $RECYCLE.BIN/ 211 | 212 | # Windows Installer files 213 | *.cab 214 | *.msi 215 | *.msm 216 | *.msp 217 | 218 | # Windows shortcuts 219 | *.lnk 220 | 221 | # End of https://www.gitignore.io/api/node,linux,macos,windows,sublimetext,webstorm+all 222 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/nakama-js/README.md -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #000000; 3 | --dark-hl-0: #D4D4D4; 4 | --light-hl-1: #A31515; 5 | --dark-hl-1: #CE9178; 6 | --light-hl-2: #AF00DB; 7 | --dark-hl-2: #C586C0; 8 | --light-hl-3: #001080; 9 | --dark-hl-3: #9CDCFE; 10 | --light-hl-4: #0000FF; 11 | --dark-hl-4: #569CD6; 12 | --light-hl-5: #008000; 13 | --dark-hl-5: #6A9955; 14 | --light-hl-6: #795E26; 15 | --dark-hl-6: #DCDCAA; 16 | --light-hl-7: #0070C1; 17 | --dark-hl-7: #4FC1FF; 18 | --light-hl-8: #098658; 19 | --dark-hl-8: #B5CEA8; 20 | --light-hl-9: #267F99; 21 | --dark-hl-9: #4EC9B0; 22 | --light-hl-10: #EE0000; 23 | --dark-hl-10: #D7BA7D; 24 | --light-code-background: #FFFFFF; 25 | --dark-code-background: #1E1E1E; 26 | } 27 | 28 | @media (prefers-color-scheme: light) { :root { 29 | --hl-0: var(--light-hl-0); 30 | --hl-1: var(--light-hl-1); 31 | --hl-2: var(--light-hl-2); 32 | --hl-3: var(--light-hl-3); 33 | --hl-4: var(--light-hl-4); 34 | --hl-5: var(--light-hl-5); 35 | --hl-6: var(--light-hl-6); 36 | --hl-7: var(--light-hl-7); 37 | --hl-8: var(--light-hl-8); 38 | --hl-9: var(--light-hl-9); 39 | --hl-10: var(--light-hl-10); 40 | --code-background: var(--light-code-background); 41 | } } 42 | 43 | @media (prefers-color-scheme: dark) { :root { 44 | --hl-0: var(--dark-hl-0); 45 | --hl-1: var(--dark-hl-1); 46 | --hl-2: var(--dark-hl-2); 47 | --hl-3: var(--dark-hl-3); 48 | --hl-4: var(--dark-hl-4); 49 | --hl-5: var(--dark-hl-5); 50 | --hl-6: var(--dark-hl-6); 51 | --hl-7: var(--dark-hl-7); 52 | --hl-8: var(--dark-hl-8); 53 | --hl-9: var(--dark-hl-9); 54 | --hl-10: var(--dark-hl-10); 55 | --code-background: var(--dark-code-background); 56 | } } 57 | 58 | :root[data-theme='light'] { 59 | --hl-0: var(--light-hl-0); 60 | --hl-1: var(--light-hl-1); 61 | --hl-2: var(--light-hl-2); 62 | --hl-3: var(--light-hl-3); 63 | --hl-4: var(--light-hl-4); 64 | --hl-5: var(--light-hl-5); 65 | --hl-6: var(--light-hl-6); 66 | --hl-7: var(--light-hl-7); 67 | --hl-8: var(--light-hl-8); 68 | --hl-9: var(--light-hl-9); 69 | --hl-10: var(--light-hl-10); 70 | --code-background: var(--light-code-background); 71 | } 72 | 73 | :root[data-theme='dark'] { 74 | --hl-0: var(--dark-hl-0); 75 | --hl-1: var(--dark-hl-1); 76 | --hl-2: var(--dark-hl-2); 77 | --hl-3: var(--dark-hl-3); 78 | --hl-4: var(--dark-hl-4); 79 | --hl-5: var(--dark-hl-5); 80 | --hl-6: var(--dark-hl-6); 81 | --hl-7: var(--dark-hl-7); 82 | --hl-8: var(--dark-hl-8); 83 | --hl-9: var(--dark-hl-9); 84 | --hl-10: var(--dark-hl-10); 85 | --code-background: var(--dark-code-background); 86 | } 87 | 88 | .hl-0 { color: var(--hl-0); } 89 | .hl-1 { color: var(--hl-1); } 90 | .hl-2 { color: var(--hl-2); } 91 | .hl-3 { color: var(--hl-3); } 92 | .hl-4 { color: var(--hl-4); } 93 | .hl-5 { color: var(--hl-5); } 94 | .hl-6 { color: var(--hl-6); } 95 | .hl-7 { color: var(--hl-7); } 96 | .hl-8 { color: var(--hl-8); } 97 | .hl-9 { color: var(--hl-9); } 98 | .hl-10 { color: var(--hl-10); } 99 | pre, code { background: var(--code-background); } 100 | -------------------------------------------------------------------------------- /docs/modules.html: -------------------------------------------------------------------------------- 1 | @heroiclabs/nakama-js-base
2 |
3 | 10 |
11 |
12 |
13 |
14 |

@heroiclabs/nakama-js-base

15 |
16 |
17 |

Index

18 |
19 |

Modules

20 |
nakama-js 21 | satori-js 22 |
23 |
46 |
47 |

Generated using TypeDoc

48 |
-------------------------------------------------------------------------------- /docs/modules/satori_js.html: -------------------------------------------------------------------------------- 1 | satori-js | @heroiclabs/nakama-js-base
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Module satori-js

20 |
21 |
22 |
23 |
24 |

Index

25 |
26 |

Classes

27 |
Client 28 | Session 29 |
30 |
31 |

Interfaces

32 |
ISession 33 |
34 |
62 |
63 |

Generated using TypeDoc

64 |
-------------------------------------------------------------------------------- /openapi-gen/README.md: -------------------------------------------------------------------------------- 1 | openapi-gen 2 | =========== 3 | 4 | > A util command to generate Nakama server's API client from the Swagger specification. 5 | 6 | ## Usage 7 | 8 | ### Nakama 9 | 10 | ```shell 11 | go run main.go "$GOPATH/src/github.com/heroiclabs/nakama/apigrpc/apigrpc.swagger.json" "Nakama" > ../packages/nakama-js/api.gen.ts 12 | ``` 13 | 14 | ### Satori 15 | 16 | ```shell 17 | go run main.go "$GOPATH/src/github.com/heroiclabs/satori/api/satori.swagger.json" "Satori" > ../packages/satori-js/api.gen.ts 18 | ``` 19 | 20 | ### Rationale 21 | 22 | The TypeScript generator available with swagger-codegen depends on Node's `"url"` package. The usage in the generated code does not warrant the need for it's inclusion. We wanted to generate lean and simple code output with minimal dependencies so we built our own. This gives us complete control over the dependencies required by the Nakama JS client. 23 | 24 | The only dependencies with the generated code is implicit usage of `"fetch"` which can be resolved with a polyfill and `"base64-js"`. 25 | 26 | ### Limitations 27 | 28 | The code generator has __only__ been checked against the Swagger specification generated for Nakama server. YMMV. 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heroiclabs/nakama-js-base", 3 | "keywords": [ 4 | "app server", 5 | "client library", 6 | "game server", 7 | "nakama", 8 | "realtime", 9 | "realtime chat" 10 | ], 11 | "scripts": { 12 | "docs": "typedoc --tsconfig tsconfig.typedoc.json --entryPoints ./packages/nakama-js/index.ts --entryPoints ./packages/satori-js/index.ts --gaID UA-89839802-1 --out docs" 13 | }, 14 | "repository": "https://github.com/heroiclabs/nakama-js", 15 | "homepage": "https://heroiclabs.com", 16 | "bugs": "https://github.com/heroiclabs/nakama-js/issues", 17 | "author": "Chris Molozian ", 18 | "contributors": [ 19 | "Andrei Mihu ", 20 | "Mo Firouz " 21 | ], 22 | "license": "Apache-2.0", 23 | "devDependencies": { 24 | "esbuild": "^0.24.0", 25 | "typedoc": "^0.26.11", 26 | "typescript": "^5.6.3" 27 | }, 28 | "private": true, 29 | "workspaces": [ 30 | "packages/nakama-js", 31 | "packages/nakama-js-test", 32 | "packages/satori-js", 33 | "packages/satori-js-test", 34 | "packages/nakama-js-iife-example", 35 | "packages/nakama-js-protobuf", 36 | "packages/nakama-js-webpack-example" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /packages/nakama-js-iife-example/README.md: -------------------------------------------------------------------------------- 1 | To run this example, run `yarn build` and open `index.html`. -------------------------------------------------------------------------------- /packages/nakama-js-iife-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Nakama JS Example Browser Test 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/nakama-js-iife-example/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Client} from "@heroiclabs/nakama-js"; 18 | 19 | var useSSL = false; // Enable if server is run with an SSL certificate. 20 | var client = new Client("defaultkey", "127.0.0.1", "7350", useSSL); 21 | 22 | client.authenticateCustom("test_id").then( 23 | session => { console.log("authenticated."); 24 | }).catch(e => { 25 | console.log("error authenticating."); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/nakama-js-iife-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heroiclabs/nakama-js-iife-example", 3 | "version": "1.0.0", 4 | "description": "An example project that utilizes nakama-js", 5 | "private": true, 6 | "license": "Apache-2.0", 7 | "dependencies": { 8 | "@heroiclabs/nakama-js": "2.8.0" 9 | }, 10 | "scripts": { 11 | "build": "npx esbuild --bundle index.ts --target=es6 --format=iife --outfile=dist/nakama-js-example.iife.js" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/nakama-js-protobuf/.gitignore: -------------------------------------------------------------------------------- 1 | dist/**/*.js 2 | dist/**/*.js.map 3 | -------------------------------------------------------------------------------- /packages/nakama-js-protobuf/README.md: -------------------------------------------------------------------------------- 1 | Nakama JavaScript Protobuf adapter 2 | ======================== 3 | 4 | > Websocket adapter adding protocol buffer support to the [nakama-js](https://www.npmjs.com/package/@heroiclabs/nakama-js) client. 5 | 6 | [Nakama](https://github.com/heroiclabs/nakama) is an open-source server designed to power modern games and apps. Features include user accounts, chat, social, matchmaker, realtime multiplayer, and much [more](https://heroiclabs.com). 7 | 8 | 9 | ## Getting Started 10 | 11 | 1. Import the adapter into your project: 12 | 13 | ```shell 14 | yarn add "@heroiclabs/nakama-js-protobuf" 15 | ``` 16 | 17 | 2. Pass the Protobuf adapter to build the socket object. 18 | 19 | ```js 20 | import {Client} from "@heroiclabs/nakama-js"; 21 | import {WebSocketAdapterPb} from "@heroiclabs/nakama-js-protobuf" 22 | 23 | const useSSL = false; // Enable if server is run with an SSL certificate. 24 | const client = new Client("defaultkey", "127.0.0.1", 7350, useSSL); 25 | 26 | const trace = false; 27 | const socket = client.createSocket(useSSL, trace, new WebSocketAdapterPb()); 28 | ``` 29 | 30 | 3. Use the WebSocket: 31 | 32 | ```js 33 | socket.ondisconnect = (evt) => { 34 | console.info("Disconnected", evt); 35 | }; 36 | 37 | const session = await socket.connect(session); 38 | // Socket is open. 39 | ``` 40 | 41 | ### License 42 | 43 | This project is licensed under the [Apache-2 License](https://github.com/heroiclabs/nakama-js/blob/master/LICENSE). 44 | -------------------------------------------------------------------------------- /packages/nakama-js-protobuf/build.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Nakama Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const { execSync } = require("child_process"); 16 | 17 | function esbuild(args) { 18 | execSync("npx esbuild --bundle index.ts --target=es6 --global-name=nakamajsprotobuf " + args) 19 | } 20 | 21 | // emit .d.ts files and perform type checking 22 | execSync("npx typescript --project tsconfig.json", {stdio: 'inherit'}) 23 | 24 | esbuild(" --format=cjs --outfile=dist/nakama-js-protobuf.cjs.js") 25 | esbuild(" --format=esm --outfile=dist/nakama-js-protobuf.esm.mjs") 26 | esbuild(" --format=iife --outfile=dist/nakama-js-protobuf.iife.js") 27 | -------------------------------------------------------------------------------- /packages/nakama-js-protobuf/build.mjs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Nakama Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import esbuild from 'esbuild'; 16 | 17 | // Shared esbuild config 18 | const config = { 19 | logLevel: 'info', 20 | entryPoints: ['index.ts'], 21 | bundle: true, 22 | target: 'es6', 23 | globalName: 'nakamajsprotobuf' 24 | }; 25 | 26 | // Build CommonJS 27 | await esbuild.build({ 28 | ...config, 29 | format: 'cjs', 30 | outfile: 'dist/nakama-js-protobuf.cjs.js' 31 | }); 32 | 33 | // Build ESM 34 | await esbuild.build({ 35 | ...config, 36 | format: 'esm', 37 | outfile: 'dist/nakama-js-protobuf.esm.mjs' 38 | }); 39 | 40 | // Build IIFE 41 | await esbuild.build({ 42 | ...config, 43 | format: 'iife', 44 | outfile: 'dist/nakama-js-protobuf.iife.js' 45 | }); -------------------------------------------------------------------------------- /packages/nakama-js-protobuf/dist/nakama-js-protobuf/google/protobuf/timestamp.d.ts: -------------------------------------------------------------------------------- 1 | import { Writer, Reader } from 'protobufjs/minimal'; 2 | /** 3 | * A Timestamp represents a point in time independent of any time zone or local 4 | * calendar, encoded as a count of seconds and fractions of seconds at 5 | * nanosecond resolution. The count is relative to an epoch at UTC midnight on 6 | * January 1, 1970, in the proleptic Gregorian calendar which extends the 7 | * Gregorian calendar backwards to year one. 8 | * 9 | * All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap 10 | * second table is needed for interpretation, using a [24-hour linear 11 | * smear](https://developers.google.com/time/smear). 12 | * 13 | * The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By 14 | * restricting to that range, we ensure that we can convert to and from [RFC 15 | * 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. 16 | * 17 | * # Examples 18 | * 19 | * Example 1: Compute Timestamp from POSIX `time()`. 20 | * 21 | * Timestamp timestamp; 22 | * timestamp.set_seconds(time(NULL)); 23 | * timestamp.set_nanos(0); 24 | * 25 | * Example 2: Compute Timestamp from POSIX `gettimeofday()`. 26 | * 27 | * struct timeval tv; 28 | * gettimeofday(&tv, NULL); 29 | * 30 | * Timestamp timestamp; 31 | * timestamp.set_seconds(tv.tv_sec); 32 | * timestamp.set_nanos(tv.tv_usec * 1000); 33 | * 34 | * Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. 35 | * 36 | * FILETIME ft; 37 | * GetSystemTimeAsFileTime(&ft); 38 | * UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; 39 | * 40 | * // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z 41 | * // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. 42 | * Timestamp timestamp; 43 | * timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); 44 | * timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); 45 | * 46 | * Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. 47 | * 48 | * long millis = System.currentTimeMillis(); 49 | * 50 | * Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) 51 | * .setNanos((int) ((millis % 1000) * 1000000)).build(); 52 | * 53 | * 54 | * Example 5: Compute Timestamp from current time in Python. 55 | * 56 | * timestamp = Timestamp() 57 | * timestamp.GetCurrentTime() 58 | * 59 | * # JSON Mapping 60 | * 61 | * In JSON format, the Timestamp type is encoded as a string in the 62 | * [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the 63 | * format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" 64 | * where {year} is always expressed using four digits while {month}, {day}, 65 | * {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional 66 | * seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), 67 | * are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone 68 | * is required. A proto3 JSON serializer should always use UTC (as indicated by 69 | * "Z") when printing the Timestamp type and a proto3 JSON parser should be 70 | * able to accept both UTC and other timezones (as indicated by an offset). 71 | * 72 | * For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past 73 | * 01:30 UTC on January 15, 2017. 74 | * 75 | * In JavaScript, one can convert a Date object to this format using the 76 | * standard 77 | * [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) 78 | * method. In Python, a standard `datetime.datetime` object can be converted 79 | * to this format using 80 | * [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with 81 | * the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use 82 | * the Joda Time's [`ISODateTimeFormat.dateTime()`]( 83 | * http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D 84 | * ) to obtain a formatter capable of generating timestamps in this format. 85 | * 86 | * 87 | */ 88 | export interface Timestamp { 89 | /** 90 | * Represents seconds of UTC time since Unix epoch 91 | * 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to 92 | * 9999-12-31T23:59:59Z inclusive. 93 | */ 94 | seconds: number; 95 | /** 96 | * Non-negative fractions of a second at nanosecond resolution. Negative 97 | * second values with fractions must still have non-negative nanos values 98 | * that count forward in time. Must be from 0 to 999,999,999 99 | * inclusive. 100 | */ 101 | nanos: number; 102 | } 103 | export declare const protobufPackage = "google.protobuf"; 104 | export declare const Timestamp: { 105 | encode(message: Timestamp, writer?: Writer): Writer; 106 | decode(input: Uint8Array | Reader, length?: number): Timestamp; 107 | fromJSON(object: any): Timestamp; 108 | fromPartial(object: DeepPartial): Timestamp; 109 | toJSON(message: Timestamp): unknown; 110 | }; 111 | type Builtin = Date | Function | Uint8Array | string | number | undefined; 112 | export type DeepPartial = T extends Builtin ? T : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends { 113 | $case: string; 114 | } ? { 115 | [K in keyof Omit]?: DeepPartial; 116 | } & { 117 | $case: T['$case']; 118 | } : T extends {} ? { 119 | [K in keyof T]?: DeepPartial; 120 | } : Partial; 121 | export {}; 122 | -------------------------------------------------------------------------------- /packages/nakama-js-protobuf/dist/nakama-js-protobuf/google/protobuf/wrappers.d.ts: -------------------------------------------------------------------------------- 1 | import { Writer, Reader } from 'protobufjs/minimal'; 2 | /** 3 | * Wrapper message for `double`. 4 | * 5 | * The JSON representation for `DoubleValue` is JSON number. 6 | */ 7 | export interface DoubleValue { 8 | /** 9 | * The double value. 10 | */ 11 | value: number; 12 | } 13 | /** 14 | * Wrapper message for `float`. 15 | * 16 | * The JSON representation for `FloatValue` is JSON number. 17 | */ 18 | export interface FloatValue { 19 | /** 20 | * The float value. 21 | */ 22 | value: number; 23 | } 24 | /** 25 | * Wrapper message for `int64`. 26 | * 27 | * The JSON representation for `Int64Value` is JSON string. 28 | */ 29 | export interface Int64Value { 30 | /** 31 | * The int64 value. 32 | */ 33 | value: number; 34 | } 35 | /** 36 | * Wrapper message for `uint64`. 37 | * 38 | * The JSON representation for `UInt64Value` is JSON string. 39 | */ 40 | export interface UInt64Value { 41 | /** 42 | * The uint64 value. 43 | */ 44 | value: number; 45 | } 46 | /** 47 | * Wrapper message for `int32`. 48 | * 49 | * The JSON representation for `Int32Value` is JSON number. 50 | */ 51 | export interface Int32Value { 52 | /** 53 | * The int32 value. 54 | */ 55 | value: number; 56 | } 57 | /** 58 | * Wrapper message for `uint32`. 59 | * 60 | * The JSON representation for `UInt32Value` is JSON number. 61 | */ 62 | export interface UInt32Value { 63 | /** 64 | * The uint32 value. 65 | */ 66 | value: number; 67 | } 68 | /** 69 | * Wrapper message for `bool`. 70 | * 71 | * The JSON representation for `BoolValue` is JSON `true` and `false`. 72 | */ 73 | export interface BoolValue { 74 | /** 75 | * The bool value. 76 | */ 77 | value: boolean; 78 | } 79 | /** 80 | * Wrapper message for `string`. 81 | * 82 | * The JSON representation for `StringValue` is JSON string. 83 | */ 84 | export interface StringValue { 85 | /** 86 | * The string value. 87 | */ 88 | value: string; 89 | } 90 | /** 91 | * Wrapper message for `bytes`. 92 | * 93 | * The JSON representation for `BytesValue` is JSON string. 94 | */ 95 | export interface BytesValue { 96 | /** 97 | * The bytes value. 98 | */ 99 | value: Uint8Array; 100 | } 101 | export declare const protobufPackage = "google.protobuf"; 102 | export declare const DoubleValue: { 103 | encode(message: DoubleValue, writer?: Writer): Writer; 104 | decode(input: Uint8Array | Reader, length?: number): DoubleValue; 105 | fromJSON(object: any): DoubleValue; 106 | fromPartial(object: DeepPartial): DoubleValue; 107 | toJSON(message: DoubleValue): unknown; 108 | }; 109 | export declare const FloatValue: { 110 | encode(message: FloatValue, writer?: Writer): Writer; 111 | decode(input: Uint8Array | Reader, length?: number): FloatValue; 112 | fromJSON(object: any): FloatValue; 113 | fromPartial(object: DeepPartial): FloatValue; 114 | toJSON(message: FloatValue): unknown; 115 | }; 116 | export declare const Int64Value: { 117 | encode(message: Int64Value, writer?: Writer): Writer; 118 | decode(input: Uint8Array | Reader, length?: number): Int64Value; 119 | fromJSON(object: any): Int64Value; 120 | fromPartial(object: DeepPartial): Int64Value; 121 | toJSON(message: Int64Value): unknown; 122 | }; 123 | export declare const UInt64Value: { 124 | encode(message: UInt64Value, writer?: Writer): Writer; 125 | decode(input: Uint8Array | Reader, length?: number): UInt64Value; 126 | fromJSON(object: any): UInt64Value; 127 | fromPartial(object: DeepPartial): UInt64Value; 128 | toJSON(message: UInt64Value): unknown; 129 | }; 130 | export declare const Int32Value: { 131 | encode(message: Int32Value, writer?: Writer): Writer; 132 | decode(input: Uint8Array | Reader, length?: number): Int32Value; 133 | fromJSON(object: any): Int32Value; 134 | fromPartial(object: DeepPartial): Int32Value; 135 | toJSON(message: Int32Value): unknown; 136 | }; 137 | export declare const UInt32Value: { 138 | encode(message: UInt32Value, writer?: Writer): Writer; 139 | decode(input: Uint8Array | Reader, length?: number): UInt32Value; 140 | fromJSON(object: any): UInt32Value; 141 | fromPartial(object: DeepPartial): UInt32Value; 142 | toJSON(message: UInt32Value): unknown; 143 | }; 144 | export declare const BoolValue: { 145 | encode(message: BoolValue, writer?: Writer): Writer; 146 | decode(input: Uint8Array | Reader, length?: number): BoolValue; 147 | fromJSON(object: any): BoolValue; 148 | fromPartial(object: DeepPartial): BoolValue; 149 | toJSON(message: BoolValue): unknown; 150 | }; 151 | export declare const StringValue: { 152 | encode(message: StringValue, writer?: Writer): Writer; 153 | decode(input: Uint8Array | Reader, length?: number): StringValue; 154 | fromJSON(object: any): StringValue; 155 | fromPartial(object: DeepPartial): StringValue; 156 | toJSON(message: StringValue): unknown; 157 | }; 158 | export declare const BytesValue: { 159 | encode(message: BytesValue, writer?: Writer): Writer; 160 | decode(input: Uint8Array | Reader, length?: number): BytesValue; 161 | fromJSON(object: any): BytesValue; 162 | fromPartial(object: DeepPartial): BytesValue; 163 | toJSON(message: BytesValue): unknown; 164 | }; 165 | type Builtin = Date | Function | Uint8Array | string | number | undefined; 166 | export type DeepPartial = T extends Builtin ? T : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends { 167 | $case: string; 168 | } ? { 169 | [K in keyof Omit]?: DeepPartial; 170 | } & { 171 | $case: T['$case']; 172 | } : T extends {} ? { 173 | [K in keyof T]?: DeepPartial; 174 | } : Partial; 175 | export {}; 176 | -------------------------------------------------------------------------------- /packages/nakama-js-protobuf/dist/nakama-js-protobuf/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./web_socket_adapter_pb"; 2 | -------------------------------------------------------------------------------- /packages/nakama-js-protobuf/dist/nakama-js-protobuf/web_socket_adapter_pb.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { WebSocketAdapter, SocketCloseHandler, SocketErrorHandler, SocketMessageHandler, SocketOpenHandler } from "../nakama-js/web_socket_adapter"; 17 | /** 18 | * A protocol buffer socket adapter that accepts and transmits payloads using the protobuf binary wire format. 19 | */ 20 | export declare class WebSocketAdapterPb implements WebSocketAdapter { 21 | private _socket?; 22 | constructor(); 23 | get onClose(): SocketCloseHandler | null; 24 | set onClose(value: SocketCloseHandler | null); 25 | get onError(): SocketErrorHandler | null; 26 | set onError(value: SocketErrorHandler | null); 27 | get onMessage(): SocketMessageHandler | null; 28 | set onMessage(value: SocketMessageHandler | null); 29 | get onOpen(): SocketOpenHandler | null; 30 | set onOpen(value: SocketOpenHandler | null); 31 | isOpen(): boolean; 32 | close(): void; 33 | connect(scheme: string, host: string, port: string, createStatus: boolean, token: string): void; 34 | send(msg: any): void; 35 | } 36 | -------------------------------------------------------------------------------- /packages/nakama-js-protobuf/dist/nakama-js/web_socket_adapter.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * An interface used by Nakama's web socket to determine the payload protocol. 18 | */ 19 | export interface WebSocketAdapter { 20 | /** 21 | * Dispatched when the web socket closes. 22 | */ 23 | onClose: SocketCloseHandler | null; 24 | /** 25 | * Dispatched when the web socket receives an error. 26 | */ 27 | onError: SocketErrorHandler | null; 28 | /** 29 | * Dispatched when the web socket receives a normal message. 30 | */ 31 | onMessage: SocketMessageHandler | null; 32 | /** 33 | * Dispatched when the web socket opens. 34 | */ 35 | onOpen: SocketOpenHandler | null; 36 | isOpen(): boolean; 37 | close(): void; 38 | connect(scheme: string, host: string, port: string, createStatus: boolean, token: string): void; 39 | send(message: any): void; 40 | } 41 | /** 42 | * SocketCloseHandler defines a lambda that handles WebSocket close events. 43 | */ 44 | export interface SocketCloseHandler { 45 | (this: WebSocket, evt: CloseEvent): void; 46 | } 47 | /** 48 | * SocketErrorHandler defines a lambda that handles responses from the server via WebSocket 49 | * that indicate an error. 50 | */ 51 | export interface SocketErrorHandler { 52 | (this: WebSocket, evt: Event): void; 53 | } 54 | /** 55 | * SocketMessageHandler defines a lambda that handles valid WebSocket messages. 56 | */ 57 | export interface SocketMessageHandler { 58 | (message: any): void; 59 | } 60 | /** 61 | * SocketOpenHandler defines a lambda that handles WebSocket open events. 62 | */ 63 | export interface SocketOpenHandler { 64 | (this: WebSocket, evt: Event): void; 65 | } 66 | /** 67 | * A text-based socket adapter that accepts and transmits payloads over UTF-8. 68 | */ 69 | export declare class WebSocketAdapterText implements WebSocketAdapter { 70 | private _socket?; 71 | get onClose(): SocketCloseHandler | null; 72 | set onClose(value: SocketCloseHandler | null); 73 | get onError(): SocketErrorHandler | null; 74 | set onError(value: SocketErrorHandler | null); 75 | get onMessage(): SocketMessageHandler | null; 76 | set onMessage(value: SocketMessageHandler | null); 77 | get onOpen(): SocketOpenHandler | null; 78 | set onOpen(value: SocketOpenHandler | null); 79 | isOpen(): boolean; 80 | connect(scheme: string, host: string, port: string, createStatus: boolean, token: string): void; 81 | close(): void; 82 | send(msg: any): void; 83 | } 84 | -------------------------------------------------------------------------------- /packages/nakama-js-protobuf/google/protobuf/timestamp.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-ignore 3 | import * as Long from 'long'; 4 | import { Writer, Reader, util, configure } from 'protobufjs/minimal'; 5 | 6 | 7 | /** 8 | * A Timestamp represents a point in time independent of any time zone or local 9 | * calendar, encoded as a count of seconds and fractions of seconds at 10 | * nanosecond resolution. The count is relative to an epoch at UTC midnight on 11 | * January 1, 1970, in the proleptic Gregorian calendar which extends the 12 | * Gregorian calendar backwards to year one. 13 | * 14 | * All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap 15 | * second table is needed for interpretation, using a [24-hour linear 16 | * smear](https://developers.google.com/time/smear). 17 | * 18 | * The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By 19 | * restricting to that range, we ensure that we can convert to and from [RFC 20 | * 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. 21 | * 22 | * # Examples 23 | * 24 | * Example 1: Compute Timestamp from POSIX `time()`. 25 | * 26 | * Timestamp timestamp; 27 | * timestamp.set_seconds(time(NULL)); 28 | * timestamp.set_nanos(0); 29 | * 30 | * Example 2: Compute Timestamp from POSIX `gettimeofday()`. 31 | * 32 | * struct timeval tv; 33 | * gettimeofday(&tv, NULL); 34 | * 35 | * Timestamp timestamp; 36 | * timestamp.set_seconds(tv.tv_sec); 37 | * timestamp.set_nanos(tv.tv_usec * 1000); 38 | * 39 | * Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. 40 | * 41 | * FILETIME ft; 42 | * GetSystemTimeAsFileTime(&ft); 43 | * UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; 44 | * 45 | * // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z 46 | * // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. 47 | * Timestamp timestamp; 48 | * timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); 49 | * timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); 50 | * 51 | * Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. 52 | * 53 | * long millis = System.currentTimeMillis(); 54 | * 55 | * Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) 56 | * .setNanos((int) ((millis % 1000) * 1000000)).build(); 57 | * 58 | * 59 | * Example 5: Compute Timestamp from current time in Python. 60 | * 61 | * timestamp = Timestamp() 62 | * timestamp.GetCurrentTime() 63 | * 64 | * # JSON Mapping 65 | * 66 | * In JSON format, the Timestamp type is encoded as a string in the 67 | * [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the 68 | * format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" 69 | * where {year} is always expressed using four digits while {month}, {day}, 70 | * {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional 71 | * seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), 72 | * are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone 73 | * is required. A proto3 JSON serializer should always use UTC (as indicated by 74 | * "Z") when printing the Timestamp type and a proto3 JSON parser should be 75 | * able to accept both UTC and other timezones (as indicated by an offset). 76 | * 77 | * For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past 78 | * 01:30 UTC on January 15, 2017. 79 | * 80 | * In JavaScript, one can convert a Date object to this format using the 81 | * standard 82 | * [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) 83 | * method. In Python, a standard `datetime.datetime` object can be converted 84 | * to this format using 85 | * [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with 86 | * the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use 87 | * the Joda Time's [`ISODateTimeFormat.dateTime()`]( 88 | * http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D 89 | * ) to obtain a formatter capable of generating timestamps in this format. 90 | * 91 | * 92 | */ 93 | export interface Timestamp { 94 | /** 95 | * Represents seconds of UTC time since Unix epoch 96 | * 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to 97 | * 9999-12-31T23:59:59Z inclusive. 98 | */ 99 | seconds: number; 100 | /** 101 | * Non-negative fractions of a second at nanosecond resolution. Negative 102 | * second values with fractions must still have non-negative nanos values 103 | * that count forward in time. Must be from 0 to 999,999,999 104 | * inclusive. 105 | */ 106 | nanos: number; 107 | } 108 | 109 | const baseTimestamp: object = { 110 | seconds: 0, 111 | nanos: 0, 112 | }; 113 | 114 | function longToNumber(long: Long) { 115 | if (long.gt(Number.MAX_SAFE_INTEGER)) { 116 | throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); 117 | } 118 | return long.toNumber(); 119 | } 120 | 121 | export const protobufPackage = 'google.protobuf' 122 | 123 | export const Timestamp = { 124 | encode(message: Timestamp, writer: Writer = Writer.create()): Writer { 125 | writer.uint32(8).int64(message.seconds); 126 | writer.uint32(16).int32(message.nanos); 127 | return writer; 128 | }, 129 | decode(input: Uint8Array | Reader, length?: number): Timestamp { 130 | const reader = input instanceof Uint8Array ? new Reader(input) : input; 131 | let end = length === undefined ? reader.len : reader.pos + length; 132 | const message = { ...baseTimestamp } as Timestamp; 133 | while (reader.pos < end) { 134 | const tag = reader.uint32(); 135 | switch (tag >>> 3) { 136 | case 1: 137 | message.seconds = longToNumber(reader.int64() as Long); 138 | break; 139 | case 2: 140 | message.nanos = reader.int32(); 141 | break; 142 | default: 143 | reader.skipType(tag & 7); 144 | break; 145 | } 146 | } 147 | return message; 148 | }, 149 | fromJSON(object: any): Timestamp { 150 | const message = { ...baseTimestamp } as Timestamp; 151 | if (object.seconds !== undefined && object.seconds !== null) { 152 | message.seconds = Number(object.seconds); 153 | } 154 | if (object.nanos !== undefined && object.nanos !== null) { 155 | message.nanos = Number(object.nanos); 156 | } 157 | return message; 158 | }, 159 | fromPartial(object: DeepPartial): Timestamp { 160 | const message = { ...baseTimestamp } as Timestamp; 161 | if (object.seconds !== undefined && object.seconds !== null) { 162 | message.seconds = object.seconds; 163 | } 164 | if (object.nanos !== undefined && object.nanos !== null) { 165 | message.nanos = object.nanos; 166 | } 167 | return message; 168 | }, 169 | toJSON(message: Timestamp): unknown { 170 | const obj: any = {}; 171 | message.seconds !== undefined && (obj.seconds = message.seconds); 172 | message.nanos !== undefined && (obj.nanos = message.nanos); 173 | return obj; 174 | }, 175 | }; 176 | 177 | if (util.Long !== Long as any) { 178 | util.Long = Long as any; 179 | configure(); 180 | } 181 | 182 | type Builtin = Date | Function | Uint8Array | string | number | undefined; 183 | export type DeepPartial = T extends Builtin 184 | ? T 185 | : T extends Array 186 | ? Array> 187 | : T extends ReadonlyArray 188 | ? ReadonlyArray> 189 | : T extends { $case: string } 190 | ? { [K in keyof Omit]?: DeepPartial } & { $case: T['$case'] } 191 | : T extends {} 192 | ? { [K in keyof T]?: DeepPartial } 193 | : Partial; -------------------------------------------------------------------------------- /packages/nakama-js-protobuf/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./web_socket_adapter_pb"; 2 | -------------------------------------------------------------------------------- /packages/nakama-js-protobuf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heroiclabs/nakama-js-protobuf", 3 | "version": "1.5.0", 4 | "description": "Websocket adapter adding protocol buffer support to the Nakama Javascript client.", 5 | "main": "dist/nakama-js-protobuf.cjs.js", 6 | "module": "dist/nakama-js-protobuf.esm.mjs", 7 | "types": "dist/nakama-js-protobuf/index.d.ts", 8 | "exports": { 9 | "./package.json": "./package.json", 10 | ".": { 11 | "import": "./dist/nakama-js-protobuf.esm.mjs", 12 | "require": "./dist/nakama-js-protobuf.cjs.js" 13 | } 14 | }, 15 | "scripts": { 16 | "build": "npx tsc && node build.mjs" 17 | }, 18 | "devDependencies": { 19 | "ts-proto": "^1.80.1", 20 | "ts-proto-descriptors": "1.2.1" 21 | }, 22 | "keywords": [ 23 | "app server", 24 | "client library", 25 | "game server", 26 | "nakama", 27 | "realtime", 28 | "realtime chat" 29 | ], 30 | "repository": "https://github.com/heroiclabs/nakama-js", 31 | "homepage": "https://heroiclabs.com", 32 | "bugs": "https://github.com/heroiclabs/nakama-js/issues", 33 | "author": "Chris Molozian ", 34 | "contributors": [ 35 | "Andrei Mihu ", 36 | "Mo Firouz ", 37 | "Milton Candelero " 38 | ], 39 | "license": "Apache-2.0", 40 | "dependencies": { 41 | "@scarf/scarf": "^1.4.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/nakama-js-protobuf/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": [ 4 | "./*", 5 | ], 6 | "compilerOptions": { 7 | "outDir": "dist" 8 | }, 9 | "declarationDir": "dist" 10 | } 11 | -------------------------------------------------------------------------------- /packages/nakama-js-protobuf/web_socket_adapter_pb.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { WebSocketAdapter, SocketCloseHandler, SocketErrorHandler, SocketMessageHandler, SocketOpenHandler } from "../nakama-js/web_socket_adapter" 18 | import * as tsproto from "./rtapi/realtime" 19 | 20 | /** 21 | * A protocol buffer socket adapter that accepts and transmits payloads using the protobuf binary wire format. 22 | */ 23 | export class WebSocketAdapterPb implements WebSocketAdapter { 24 | 25 | private _socket?: WebSocket; 26 | 27 | constructor() { 28 | } 29 | 30 | get onClose(): SocketCloseHandler | null { 31 | return this._socket!.onclose; 32 | } 33 | 34 | set onClose(value: SocketCloseHandler | null) { 35 | this._socket!.onclose = value; 36 | } 37 | 38 | get onError(): SocketErrorHandler | null { 39 | return this._socket!.onerror; 40 | } 41 | 42 | set onError(value: SocketErrorHandler | null) { 43 | this._socket!.onerror = value; 44 | } 45 | 46 | get onMessage(): SocketMessageHandler | null { 47 | return this._socket!.onmessage; 48 | } 49 | 50 | set onMessage(value: SocketMessageHandler | null) { 51 | 52 | if (value) { 53 | this._socket!.onmessage = (evt: MessageEvent) => { 54 | const buffer: ArrayBuffer = evt.data; 55 | const uintBuffer: Uint8Array = new Uint8Array(buffer); 56 | const envelope = tsproto.Envelope.decode(uintBuffer); 57 | 58 | if (envelope.channel_message) { 59 | if (envelope.channel_message.code == undefined) { 60 | //protobuf plugin does not default-initialize missing Int32Value fields 61 | envelope.channel_message.code = 0; 62 | } 63 | if (envelope.channel_message.persistent == undefined) { 64 | //protobuf plugin does not default-initialize missing BoolValue fields 65 | envelope.channel_message.persistent = false; 66 | } 67 | } 68 | 69 | value!(envelope); 70 | }; 71 | } 72 | else { 73 | value = null; 74 | } 75 | } 76 | 77 | get onOpen(): SocketOpenHandler | null { 78 | return this._socket!.onopen; 79 | } 80 | 81 | set onOpen(value: SocketOpenHandler | null) { 82 | this._socket!.onopen = value; 83 | } 84 | 85 | isOpen(): boolean { 86 | return this._socket?.readyState == WebSocket.OPEN; 87 | } 88 | 89 | close() { 90 | this._socket!.close(); 91 | this._socket = undefined; 92 | } 93 | 94 | connect(scheme: string, host: string, port: string, createStatus: boolean, token: string): void { 95 | const url = `${scheme}${host}:${port}/ws?lang=en&status=${encodeURIComponent(createStatus.toString())}&token=${encodeURIComponent(token)}&format=protobuf`; 96 | this._socket = new WebSocket(url); 97 | this._socket.binaryType = "arraybuffer"; 98 | } 99 | 100 | send(msg: any): void { 101 | 102 | if (msg.match_data_send) { 103 | let payload = msg.match_data_send.data; 104 | // can't send a string over protobuf 105 | if (typeof payload == "string") { 106 | msg.match_data_send.data = new TextEncoder().encode(payload); 107 | } 108 | } else if (msg.party_data_send) { 109 | let payload = msg.party_data_send.data; 110 | // can't send a string over protobuf 111 | if (typeof payload == "string") { 112 | msg.party_data_send.data = new TextEncoder().encode(payload); 113 | } 114 | } 115 | 116 | const envelopeWriter = tsproto.Envelope.encode(tsproto.Envelope.fromPartial(msg)); 117 | const encodedMsg = envelopeWriter.finish(); 118 | this._socket!.send(encodedMsg); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /packages/nakama-js-test/client-authenticate.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Page} from "puppeteer"; 18 | import * as nakamajs from "@heroiclabs/nakama-js"; 19 | import {createPage, createFacebookInstantGameAuthToken, generateid} from "./utils"; 20 | import {describe, expect, it} from '@jest/globals' 21 | 22 | describe('Authenticate Tests', () => { 23 | 24 | it('should authenticate with email', async () => { 25 | const page : Page = await createPage(); 26 | 27 | const email = generateid() + "@example.com"; 28 | const password = generateid(); 29 | 30 | const session = await page.evaluate(async (email, password) => { 31 | const client = new nakamajs.Client(); 32 | const promise = client.authenticateEmail(email, password); 33 | return promise; 34 | }, email, password); 35 | 36 | expect(session).not.toBeNull(); 37 | expect(session.token).not.toBeNull(); 38 | }); 39 | 40 | it('should authenticate with device id', async () => { 41 | const page : Page = await createPage(); 42 | 43 | const deviceid = generateid(); 44 | 45 | const session = await page.evaluate((deviceid) => { 46 | const client = new nakamajs.Client(); 47 | return client.authenticateDevice(deviceid); 48 | }, deviceid); 49 | 50 | expect(session).not.toBeNull(); 51 | expect(session.token).not.toBeNull(); 52 | }); 53 | 54 | it('should authenticate with custom id', async () => { 55 | const page : Page = await createPage(); 56 | 57 | const customid = generateid(); 58 | 59 | const session = await page.evaluate((customid) => { 60 | const client = new nakamajs.Client(); 61 | return client.authenticateCustom(customid); 62 | }, customid); 63 | 64 | expect(session).not.toBeNull(); 65 | expect(session.token).not.toBeNull(); 66 | }); 67 | 68 | it('should authenticate with user variables', async () => { 69 | const page : Page = await createPage(); 70 | 71 | const customid = generateid(); 72 | const customUsername = generateid(); 73 | 74 | const session = await page.evaluate((customid, customUsername) => { 75 | let vars = {}; 76 | vars["testString"] = "testValue"; 77 | 78 | const client = new nakamajs.Client(); 79 | return client.authenticateCustom(customid, true, customUsername, vars); 80 | }, customid, customUsername); 81 | 82 | expect(session).not.toBeNull(); 83 | expect(session.token).not.toBeNull(); 84 | expect(session.vars).not.toBeNull(); 85 | expect(session.vars["testString"]).toBe("testValue"); 86 | 87 | }); 88 | 89 | it('should fail to authenticate with new custom id', async () => { 90 | const page : Page = await createPage(); 91 | 92 | const customid = generateid(); 93 | const result = await page.evaluate(async (customid) => { 94 | const client = new nakamajs.Client(); 95 | try { 96 | // Expects exception. 97 | return await client.authenticateCustom(customid, false); 98 | } catch (err) { 99 | return err; 100 | } 101 | }, customid); 102 | 103 | expect(result).not.toBeNull(); 104 | }); 105 | 106 | it('should authenticate with custom id twice', async () => { 107 | const page : Page = await createPage(); 108 | 109 | const customid = "someuniquecustomid"; 110 | 111 | const session = await page.evaluate(async (customid) => { 112 | const client = new nakamajs.Client(); 113 | await client.authenticateCustom(customid); 114 | return await client.authenticateCustom(customid); 115 | }, customid); 116 | 117 | expect(session).not.toBeNull(); 118 | expect(session.token).not.toBeNull(); 119 | }); 120 | 121 | it('should fail authenticate with custom id', async () => { 122 | const page : Page = await createPage(); 123 | 124 | const result = await page.evaluate(async () => { 125 | const client = new nakamajs.Client(); 126 | try { 127 | // Expects exception. 128 | return await client.authenticateCustom(""); 129 | } catch (err) { 130 | return err; 131 | } 132 | }); 133 | 134 | expect(result).not.toBeNull(); 135 | }); 136 | 137 | it.skip('should authenticate with facebook instant games', async () => { 138 | let token : string = createFacebookInstantGameAuthToken("a_player_id"); 139 | const page : Page = await createPage(); 140 | 141 | const session = await page.evaluate((token) => { 142 | const client = new nakamajs.Client(); 143 | return client.authenticateFacebookInstantGame(token); 144 | }, token); 145 | 146 | expect(session).not.toBeNull(); 147 | expect(session.token).not.toBeNull(); 148 | }); 149 | }); -------------------------------------------------------------------------------- /packages/nakama-js-test/client-friend.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Page} from "puppeteer" 18 | import * as nakamajs from "@heroiclabs/nakama-js/client"; 19 | import {createFacebookInstantGameAuthToken, createPage, generateid} from "./utils"; 20 | import {describe, expect, it} from '@jest/globals' 21 | 22 | describe('Friend Tests', () => { 23 | 24 | it('should add friend, then list', async () => { 25 | const page : Page = await createPage(); 26 | 27 | const customid1 = generateid(); 28 | const customid2 = generateid(); 29 | 30 | const result = await page.evaluate(async (customid1, customid2) => { 31 | const client1 = new nakamajs.Client(); 32 | const session1 = await client1.authenticateCustom(customid1); 33 | const client2 = new nakamajs.Client(); 34 | const session2 = await client2.authenticateCustom(customid2); 35 | 36 | await client1.addFriends(session1, [session2.user_id]); 37 | return await client1.listFriends(session1); 38 | }, customid1, customid2); 39 | 40 | expect(result).not.toBeNull(); 41 | expect(result.friends!.length).toBe(1); 42 | expect(result.friends![0].state).toBe(1); 43 | }); 44 | 45 | it('should receive friend invite, then list', async () => { 46 | const page : Page = await createPage(); 47 | 48 | const customid1 = generateid(); 49 | const customid2 = generateid(); 50 | 51 | const result = await page.evaluate(async (customid1, customid2) => { 52 | const client1 = new nakamajs.Client(); 53 | const session1 = await client1.authenticateCustom(customid1); 54 | const client2 = new nakamajs.Client(); 55 | const session2 = await client2.authenticateCustom(customid2); 56 | 57 | await client1.addFriends(session1, [session2.user_id]); 58 | return await client2.listFriends(session2); 59 | }, customid1, customid2); 60 | 61 | expect(result).not.toBeNull(); 62 | expect(result.friends!.length).toBe(1); 63 | expect(result.friends![0].state).toBe(2); 64 | }); 65 | 66 | it('should block friend, then list', async () => { 67 | const page : Page = await createPage(); 68 | 69 | const customid1 = generateid(); 70 | const customid2 = generateid(); 71 | 72 | const result = await page.evaluate(async (customid1, customid2) => { 73 | const client1 = new nakamajs.Client(); 74 | const session1 = await client1.authenticateCustom(customid1); 75 | const client2 = new nakamajs.Client(); 76 | const session2 = await client2.authenticateCustom(customid2); 77 | 78 | await client1.blockFriends(session1, [session2.user_id]); 79 | return await client1.listFriends(session1); 80 | }, customid1, customid2); 81 | 82 | expect(result).not.toBeNull(); 83 | expect(result.friends!.length).toBe(1); 84 | expect(result.friends![0].state).toBe(3); 85 | }); 86 | 87 | it('should add friend, accept, then list', async () => { 88 | const page : Page = await createPage(); 89 | 90 | const customid1 = generateid(); 91 | const customid2 = generateid(); 92 | 93 | const result = await page.evaluate(async (customid1, customid2) => { 94 | const client1 = new nakamajs.Client(); 95 | const session1 = await client1.authenticateCustom(customid1); 96 | const client2 = new nakamajs.Client(); 97 | const session2 = await client2.authenticateCustom(customid2); 98 | 99 | await client1.addFriends(session1, [session2.user_id]); 100 | await client2.addFriends(session2, [session1.user_id]); 101 | return await client1.listFriends(session1); 102 | }, customid1, customid2); 103 | 104 | expect(result).not.toBeNull(); 105 | expect(result.friends!.length).toBe(1); 106 | expect(result.friends![0].state).toBe(0); 107 | }); 108 | 109 | it('should add friend, reject, then list', async () => { 110 | const page : Page = await createPage(); 111 | 112 | const customid1 = generateid(); 113 | const customid2 = generateid(); 114 | 115 | const result = await page.evaluate(async (customid1, customid2) => { 116 | const client1 = new nakamajs.Client(); 117 | const session1 = await client1.authenticateCustom(customid1); 118 | const client2 = new nakamajs.Client(); 119 | const session2 = await client2.authenticateCustom(customid2); 120 | 121 | await client1.addFriends(session1, [session2.user_id]); 122 | await client2.deleteFriends(session2, [session1.user_id]); 123 | return await client1.listFriends(session1); 124 | }, customid1, customid2); 125 | 126 | expect(result).not.toBeNull(); 127 | expect(result.friends!.length).toBe(0); 128 | }); 129 | 130 | it.skip('should add friend authenticated via facebook instant, then list', async () => { 131 | const page : Page = await createPage(); 132 | 133 | const customid1 = generateid(); 134 | const customid2 = generateid(); 135 | 136 | const result = await page.evaluate(async (customid1, token2) => { 137 | const client1 = new nakamajs.Client(); 138 | const session1 = await client1.authenticateCustom(customid1); 139 | const client2 = new nakamajs.Client(); 140 | const session2 = await client2.authenticateFacebookInstantGame(token2, true); 141 | await client1.addFriends(session1, [session2.user_id]); 142 | return await client1.listFriends(session1); 143 | }, customid1, createFacebookInstantGameAuthToken(customid2)); 144 | 145 | expect(result.friends![0]).not.toBeNull(); 146 | expect(result.friends![0].user.facebook_instant_game_id).toEqual(customid2); 147 | }); 148 | 149 | }); 150 | -------------------------------------------------------------------------------- /packages/nakama-js-test/client-leaderboard.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Page} from "puppeteer" 18 | import * as nakamajs from "@heroiclabs/nakama-js"; 19 | import {createPage, generateid} from "./utils" 20 | import {describe, expect, it} from '@jest/globals' 21 | 22 | describe('Leaderboard Tests', () => { 23 | 24 | it('should create leaderboard with Set operator and write record', async () => { 25 | const page : Page = await createPage(); 26 | 27 | const customid = generateid(); 28 | const rpcid = "clientrpc.create_leaderboard" 29 | const operator = "set" 30 | const score = { 31 | score: "10", 32 | subscore: "1", 33 | metadata: {"key": "value"} 34 | }; 35 | 36 | const result = await page.evaluate(async (customid, rpcid, operator, score) => { 37 | const client = new nakamajs.Client(); 38 | const session = await client.authenticateCustom(customid) 39 | const result = await client.rpc(session, rpcid, {"operator": operator}); 40 | const leaderboardId =(result.payload).leaderboard_id; 41 | return await client.writeLeaderboardRecord(session, leaderboardId, score) 42 | }, customid, rpcid, operator, score); 43 | 44 | expect(result).not.toBeNull(); 45 | expect(result.score).toBe(10); 46 | expect(result.subscore).toBe(1); 47 | expect(result.metadata).not.toBeNull(); 48 | expect(( result.metadata).key).toBe("value"); 49 | }); 50 | 51 | it('should create leaderboard with Best operator and write record', async () => { 52 | const page : Page = await createPage(); 53 | 54 | const customid = generateid(); 55 | const rpcid = "clientrpc.create_leaderboard" 56 | const operator = "best" 57 | const score = { 58 | score: "10", 59 | subscore: "1", 60 | metadata: {"key": "value"} 61 | }; 62 | 63 | const result = await page.evaluate(async (customid, rpcid, operator, score) => { 64 | const client = new nakamajs.Client(); 65 | const session = await client.authenticateCustom(customid) 66 | const result = await client.rpc(session, rpcid, {"operator": operator}); 67 | const leaderboardId = (result.payload).leaderboard_id; 68 | await client.writeLeaderboardRecord(session, leaderboardId, score) 69 | 70 | score.score = "1"; 71 | score.subscore = "20"; 72 | return await client.writeLeaderboardRecord(session, leaderboardId, score) 73 | }, customid, rpcid, operator, score); 74 | 75 | expect(result).not.toBeNull(); 76 | expect(result.score).toBe(10); 77 | expect(result.subscore).toBe(20); 78 | expect(result.metadata).not.toBeNull(); 79 | expect(( result.metadata).key).toBe("value"); 80 | }); 81 | 82 | it('should create leaderboard with Incr operator and write record', async () => { 83 | const page : Page = await createPage(); 84 | 85 | const customid = generateid(); 86 | const rpcid = "clientrpc.create_leaderboard" 87 | const operator = "incr" 88 | const score = { 89 | score: "10", 90 | subscore: "1", 91 | metadata: {"key": "value"} 92 | }; 93 | 94 | const result = await page.evaluate(async (customid, rpcid, operator, score) => { 95 | const client = new nakamajs.Client(); 96 | const session = await client.authenticateCustom(customid); 97 | const result = await client.rpc(session, rpcid, {"operator": operator}); 98 | const leaderboardId = (result.payload).leaderboard_id; 99 | await client.writeLeaderboardRecord(session, leaderboardId, score); 100 | 101 | score.score = "1"; 102 | score.subscore = "5"; 103 | return await client.writeLeaderboardRecord(session, leaderboardId, score) 104 | }, customid, rpcid, operator, score); 105 | 106 | expect(result).not.toBeNull(); 107 | expect(result.score).toBe(11); 108 | expect(result.subscore).toBe(6); 109 | expect(result.metadata).not.toBeNull(); 110 | expect(( result.metadata).key).toBe("value"); 111 | }); 112 | 113 | it('should create leaderboard with Set operator and then list leaderboard records', async () => { 114 | const page : Page = await createPage(); 115 | 116 | const customid = generateid(); 117 | const rpcid = "clientrpc.create_leaderboard" 118 | const operator = "set" 119 | const score = { 120 | score: "10", 121 | subscore: "1", 122 | metadata: {"key": "value"} 123 | }; 124 | 125 | const result = await page.evaluate(async (customid, rpcid, operator, score) => { 126 | const client = new nakamajs.Client(); 127 | const session = await client.authenticateCustom(customid); 128 | 129 | const result = await client.rpc(session, rpcid, {"operator": operator}); 130 | const leaderboardId = (result.payload).leaderboard_id; 131 | await client.writeLeaderboardRecord(session, leaderboardId, score); 132 | return await client.listLeaderboardRecords(session, leaderboardId); 133 | }, customid, rpcid, operator, score); 134 | 135 | expect(result).not.toBeNull(); 136 | expect(result.records![0].score).toBe(10); 137 | expect(result.records![0].subscore).toBe(1); 138 | expect(result.records![0].metadata).not.toBeNull(); 139 | expect(( result.records![0].metadata).key).toBe("value"); 140 | expect(result.records![0].rank).toBe(1); 141 | }); 142 | 143 | }); 144 | -------------------------------------------------------------------------------- /packages/nakama-js-test/client-link.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const base64url = require("base64url"); 18 | const crypto = require("crypto"); 19 | import {Page} from "puppeteer" 20 | import * as nakamajs from "@heroiclabs/nakama-js"; 21 | import {createPage, generateid} from "./utils"; 22 | import {describe, expect, it} from '@jest/globals' 23 | 24 | describe('Link / Unlink Tests', () => { 25 | 26 | it('should link device ID', async () => { 27 | const page : Page = await createPage(); 28 | 29 | const customid = generateid(); 30 | const deviceid = generateid(); 31 | 32 | const account = await page.evaluate(async (customid, deviceid) => { 33 | const client = new nakamajs.Client(); 34 | const session = await client.authenticateCustom(customid) 35 | await client.linkDevice(session, { id: deviceid }); 36 | return await client.getAccount(session); 37 | }, customid, deviceid); 38 | 39 | expect(account).not.toBeNull(); 40 | expect(account.custom_id).not.toBeNull(); 41 | expect(account.devices![0]).not.toBeNull(); 42 | }); 43 | 44 | it('should unlink device ID', async () => { 45 | const page : Page = await createPage(); 46 | 47 | const customid = generateid(); 48 | const deviceid = generateid(); 49 | 50 | const account = await page.evaluate(async (customid, deviceid) => { 51 | const client = new nakamajs.Client(); 52 | const session = await client.authenticateCustom(customid); 53 | await client.linkDevice(session, { id: deviceid }); 54 | await client.unlinkDevice(session, {id: deviceid }); 55 | return await client.getAccount(session); 56 | }, customid, deviceid); 57 | 58 | expect(account).not.toBeNull(); 59 | expect(account.custom_id).not.toBeNull(); 60 | expect(account.hasOwnProperty("devices")).toBe(false); 61 | }); 62 | 63 | //optional test 64 | it.skip('should link to facebook instant games', async () => { 65 | const page : Page = await createPage(); 66 | 67 | const fbid = generateid(); 68 | 69 | const testSecret = "fb-instant-test-secret"; 70 | 71 | const mockFbInstantPayload = JSON.stringify({ 72 | algorithm: "HMAC-SHA256", 73 | issued_at: 1594867628, 74 | player_id: fbid, 75 | request_payload: "" 76 | }); 77 | 78 | const encodedPayload = base64url(mockFbInstantPayload); 79 | 80 | const signature = crypto.createHmac('sha256', testSecret).update(encodedPayload).digest(); 81 | const encodedSignature = base64url(signature); 82 | 83 | const token = encodedSignature + "." + encodedPayload; 84 | 85 | const customid = generateid(); 86 | 87 | const account = await page.evaluate(async (customid, token) => { 88 | const client = new nakamajs.Client(); 89 | const session = await client.authenticateCustom(customid); 90 | await client.linkFacebookInstantGame(session, { signed_player_info: token }); 91 | return await client.getAccount(session); 92 | }, customid, token); 93 | 94 | ("account user is..."); 95 | (account.user); 96 | expect(account).not.toBeNull(); 97 | expect(account.user!.facebook_instant_game_id).not.toBeUndefined(); 98 | 99 | }); 100 | 101 | //optional test 102 | it.skip('should unlink to facebook instant games', async () => { 103 | const page : Page = await createPage(); 104 | 105 | const fbid = generateid(); 106 | 107 | const testSecret = "fb-instant-test-secret"; 108 | 109 | const mockFbInstantPayload = JSON.stringify({ 110 | algorithm: "HMAC-SHA256", 111 | issued_at: 1594867628, 112 | player_id: fbid, 113 | request_payload: "" 114 | }); 115 | 116 | const encodedPayload = base64url(mockFbInstantPayload); 117 | 118 | const signature = crypto.createHmac('sha256', testSecret).update(encodedPayload).digest(); 119 | const encodedSignature = base64url(signature); 120 | 121 | const token = encodedSignature + "." + encodedPayload; 122 | 123 | const customid = generateid(); 124 | 125 | const account = await page.evaluate(async (customid, token) => { 126 | const client = new nakamajs.Client(); 127 | const session = await client.authenticateCustom(customid); 128 | await client.linkFacebookInstantGame(session, { signed_player_info: token }); 129 | await client.unlinkFacebookInstantGame(session, { signed_player_info: token }); 130 | 131 | return await client.getAccount(session); 132 | }, customid, token); 133 | 134 | expect(account).not.toBeNull(); 135 | expect(account.user!.facebook_instant_game_id).toBeUndefined(); 136 | }) 137 | 138 | }); 139 | -------------------------------------------------------------------------------- /packages/nakama-js-test/client-rpc.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | import {Page} from "puppeteer" 19 | import * as nakamajs from "@heroiclabs/nakama-js"; 20 | import {createPage, generateid} from "./utils" 21 | import {describe, expect, it} from '@jest/globals' 22 | 23 | describe('RPC Tests', () => { 24 | 25 | it('should send rpc', async () => { 26 | const page : Page = await createPage(); 27 | 28 | const customid = generateid(); 29 | const rpcid = "clientrpc.rpc_get"; 30 | 31 | const rpcResult = await page.evaluate(async (customid, rpcid) => { 32 | const client = new nakamajs.Client(); 33 | const session = await client.authenticateCustom(customid) 34 | return await client.rpc(session, rpcid, {}); 35 | }, customid, rpcid); 36 | 37 | expect(rpcResult).not.toBeNull(); 38 | }); 39 | 40 | it('should send rpc with payload', async () => { 41 | const page : Page = await createPage(); 42 | 43 | const customid = generateid(); 44 | const rpcid = "clientrpc.rpc"; 45 | const request = { 46 | "hello": "world" 47 | }; 48 | 49 | const rpcResult = await page.evaluate(async (customid, rpcid, request) => { 50 | const client = new nakamajs.Client(); 51 | const session = await client.authenticateCustom(customid) 52 | return await client.rpc(session, rpcid, request); 53 | }, customid, rpcid, request); 54 | 55 | expect(rpcResult).not.toBeNull(); 56 | expect(rpcResult.payload).not.toBeNull(); 57 | expect(rpcResult.payload).toEqual(request); 58 | }); 59 | 60 | it('should send rpc with httpKey', async() => { 61 | const page : Page = await createPage(); 62 | 63 | const rpcid = "clientrpc.rpc_get"; 64 | const HTTP_KEY = "defaulthttpkey"; 65 | 66 | const rpcResult = await page.evaluate(async (rpcid, HTTP_KEY) => { 67 | const client = new nakamajs.Client(); 68 | return await client.rpcHttpKey(HTTP_KEY, rpcid, null!); 69 | }, rpcid, HTTP_KEY); 70 | 71 | expect(rpcResult).not.toBeNull(); 72 | }) 73 | }); 74 | -------------------------------------------------------------------------------- /packages/nakama-js-test/client-storage.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | import {Page} from "puppeteer" 19 | import * as nakamajs from "@heroiclabs/nakama-js"; 20 | import {createPage, generateid} from "./utils" 21 | import {describe, expect, it} from '@jest/globals' 22 | 23 | describe('Storage Tests', () => { 24 | 25 | it('should write and read storage', async () => { 26 | 27 | const page : Page = await createPage(); 28 | 29 | const customid = generateid(); 30 | const collection = "testcollection"; 31 | const key = "testkey"; 32 | const value = {"hello": "world"}; 33 | 34 | const result = await page.evaluate(async (customid, collection, key, value) => { 35 | const client = new nakamajs.Client(); 36 | const session = await client.authenticateCustom(customid); 37 | await client.writeStorageObjects(session, [ 38 | { 39 | "collection": collection, 40 | "key": key, 41 | "value": value 42 | } 43 | ]); 44 | return await client.readStorageObjects(session, { 45 | object_ids: [{ 46 | "collection": collection, 47 | "key": key, 48 | "user_id": session.user_id 49 | }] 50 | }); 51 | }, customid, collection, key, value); 52 | 53 | expect(result).not.toBeNull(); 54 | expect(result.objects.length).toBe(1); 55 | expect(result.objects[0]).not.toBeNull(); 56 | expect(result.objects[0].collection).toBe(collection); 57 | expect(result.objects[0].key).toBe(key); 58 | expect(result.objects[0].value).toEqual(value); 59 | expect(result.objects[0].permission_read).toBe(1); 60 | expect(result.objects[0].permission_write).toBe(1); 61 | expect(result.objects[0].version).not.toBeNull(); 62 | }); 63 | 64 | it('should write and delete storage', async () => { 65 | const page : Page = await createPage(); 66 | 67 | const customid = generateid(); 68 | const collection = "testcollection"; 69 | const key = "testkey"; 70 | const value = {"hello": "world"}; 71 | 72 | const result = await page.evaluate(async (customid, collection, key, value) => { 73 | const client = new nakamajs.Client(); 74 | const session = await client.authenticateCustom(customid); 75 | await client.writeStorageObjects(session,[ 76 | { 77 | "collection": collection, 78 | "key": key, 79 | "value": value 80 | } 81 | ]); 82 | 83 | await client.deleteStorageObjects(session, { 84 | object_ids: [{ 85 | "collection": collection, 86 | "key": key 87 | }] 88 | }); 89 | 90 | return await client.readStorageObjects(session, { 91 | object_ids: [{ 92 | "collection": collection, 93 | "key": key, 94 | "user_id": session.user_id 95 | }] 96 | }) 97 | }, customid, collection, key, value); 98 | 99 | expect(result).not.toBeNull(); 100 | expect(result.objects.length).toBe(0); 101 | }); 102 | 103 | it('should write and list storage', async () => { 104 | const page : Page = await createPage(); 105 | 106 | const customid = generateid(); 107 | const collection = "testcollection"; 108 | const key = "testkey"; 109 | const value = {"hello": "world"}; 110 | 111 | const result = await page.evaluate(async (customid, collection, key, value) => { 112 | const client = new nakamajs.Client(); 113 | const session = await client.authenticateCustom(customid); 114 | 115 | await client.writeStorageObjects(session,[ 116 | { 117 | "collection": collection, 118 | "key": key, 119 | "value": value, 120 | "permission_read": 2, 121 | "permission_write": 1 122 | } 123 | ]); 124 | 125 | return await client.listStorageObjects(session, collection, session.user_id); 126 | 127 | }, customid, collection, key, value); 128 | 129 | expect(result).not.toBeNull(); 130 | expect(result.objects.length).toBe(1); 131 | expect(result.objects[0]).not.toBeNull(); 132 | expect(result.objects[0].collection).toBe(collection); 133 | expect(result.objects[0].key).toBe(key); 134 | expect(result.objects[0].value).toEqual(value); 135 | expect(result.objects[0].permission_read).toBe(2); 136 | expect(result.objects[0].permission_write).toBe(1); 137 | expect(result.objects[0].version).not.toBeNull(); 138 | }); 139 | 140 | }); 141 | -------------------------------------------------------------------------------- /packages/nakama-js-test/client-user.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | import {Page} from "puppeteer" 19 | import * as nakamajs from "@heroiclabs/nakama-js"; 20 | import {createPage, generateid} from "./utils"; 21 | import {describe, expect, it} from '@jest/globals' 22 | 23 | describe('User Tests', () => { 24 | 25 | it('should return current user account', async () => { 26 | const page : Page = await createPage(); 27 | 28 | const customid = generateid(); 29 | 30 | const account = await page.evaluate(async (customid) => { 31 | const client = new nakamajs.Client(); 32 | return client.authenticateCustom(customid) 33 | .then(session => { 34 | return client.getAccount(session); 35 | }); 36 | }, customid); 37 | 38 | expect(account).not.toBeNull(); 39 | expect(account.custom_id).not.toBeNull(); 40 | expect(account.wallet).not.toBeNull(); 41 | expect(account.wallet).toBe("{}"); 42 | expect(account.user).not.toBeNull(); 43 | expect(account.user.id).not.toBeNull(); 44 | expect(account.user.username).not.toBeNull(); 45 | expect(account.user.lang_tag).not.toBeNull(); 46 | expect(account.user.lang_tag).toBe("en"); 47 | expect(account.user.metadata).not.toBeNull(); 48 | expect(account.user.metadata).toBe("{}"); 49 | }); 50 | 51 | it('should update current user account', async () => { 52 | 53 | const page : Page = await createPage(); 54 | 55 | const customid = generateid(); 56 | const displayName = "display"; 57 | const avatar = "avatar"; 58 | const lang = "fa"; 59 | const loc = "california"; 60 | 61 | const account = await page.evaluate(async (customid, displayName, avatar, lang, loc) => { 62 | const client = new nakamajs.Client(); 63 | const session = await client.authenticateCustom(customid); 64 | 65 | await client.updateAccount(session, { 66 | display_name: displayName, 67 | avatar_url: avatar, 68 | lang_tag: lang, 69 | location: loc 70 | }); 71 | 72 | return await client.getAccount(session); 73 | }, customid, displayName, avatar, lang, loc); 74 | 75 | expect(account).not.toBeNull(); 76 | expect(account.user.display_name).not.toBeNull(); 77 | expect(account.user.display_name).toBe(displayName); 78 | expect(account.user.avatar_url).not.toBeNull(); 79 | expect(account.user.avatar_url).toBe(avatar); 80 | expect(account.user.lang_tag).not.toBeNull(); 81 | expect(account.user.lang_tag).toBe(lang); 82 | expect(account.user.location).not.toBeNull(); 83 | expect(account.user.location).toBe(loc); 84 | }); 85 | 86 | it('should update current user with same username', async () => { 87 | const page : Page = await createPage(); 88 | 89 | const customid = generateid(); 90 | 91 | const account = await page.evaluate(async (customid) => { 92 | const client = new nakamajs.Client(); 93 | const session = await client.authenticateCustom(customid); 94 | const success = await client.updateAccount(session, { 95 | username: session.username 96 | }); 97 | return client.getAccount(session); 98 | }, customid); 99 | 100 | expect(account).not.toBeNull(); 101 | expect(account.custom_id).toBe(customid); 102 | }); 103 | 104 | it('should return two users', async () => { 105 | const page : Page = await createPage(); 106 | 107 | const customid = generateid(); 108 | const customid2 = generateid(); 109 | 110 | const users = await page.evaluate(async (customid, customid2) => { 111 | const client = new nakamajs.Client(); 112 | const session1 = await client.authenticateCustom(customid); 113 | const session2 = await client.authenticateCustom(customid2) 114 | return await client.getUsers(session2, [session1.user_id], [session2.username], []); 115 | }, customid, customid2); 116 | 117 | expect(users).not.toBeNull(); 118 | expect(users[0]).not.toBeNull(); 119 | expect(users[1]).not.toBeNull(); 120 | }); 121 | 122 | it('should return no users', async () => { 123 | const page : Page = await createPage(); 124 | 125 | const customid = generateid(); 126 | 127 | const response = await page.evaluate(async (customid) => { 128 | const client = new nakamajs.Client(); 129 | const session = await client.authenticateCustom(customid); 130 | return await client.getUsers(session, [], [], []); 131 | }, customid); 132 | 133 | expect(response).not.toBeNull(); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /packages/nakama-js-test/client.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Page} from "puppeteer" 18 | import * as nakamajs from "@heroiclabs/nakama-js"; 19 | import {createPage} from "./utils" 20 | import {describe, expect, it} from '@jest/globals' 21 | 22 | describe('Client Tests', () => { 23 | 24 | it('should create object with defaults', async () => { 25 | const page : Page = await createPage(); 26 | 27 | const client = await page.evaluate(() => { 28 | return new nakamajs.Client(); 29 | }); 30 | 31 | expect(client).not.toBeNull(); 32 | expect(client.serverkey).toBe("defaultkey"); 33 | expect(client.host).toBe("127.0.0.1"); 34 | expect(client.port).toBe("7350"); 35 | expect(client.useSSL).toBe(false); 36 | expect(client.timeout).toBe(7000); 37 | }); 38 | 39 | it('should create object with configuration', async () => { 40 | const page : Page = await createPage(); 41 | 42 | const SERVER_KEY = "somesecret!"; 43 | const HOST = "127.0.0.2"; 44 | const PORT = "8080"; 45 | const SSL = true; 46 | const TIMEOUT = 8000; 47 | 48 | const client = await page.evaluate((SERVER_KEY, HOST, PORT, SSL, TIMEOUT) => { 49 | return new nakamajs.Client(SERVER_KEY, HOST, PORT, SSL, TIMEOUT); 50 | }, SERVER_KEY, HOST, PORT, SSL, TIMEOUT); 51 | 52 | expect(client).not.toBeNull(); 53 | expect(client.serverkey).toBe(SERVER_KEY); 54 | expect(client.host).toBe(HOST); 55 | expect(client.port).toBe(PORT); 56 | expect(client.useSSL).toBe(SSL); 57 | expect(client.timeout).toBe(TIMEOUT); 58 | }); 59 | 60 | it('should obey timeout configuration option', async () => { 61 | const page : Page = await createPage(); 62 | 63 | const err = await page.evaluate(() => { 64 | const client = new nakamajs.Client("defaultkey", "127.0.0.1", "7350", false, 0); 65 | return client.authenticateCustom("timeoutuseridentifier") 66 | .catch(err => err); 67 | }); 68 | 69 | expect(err).not.toBeNull(); 70 | expect(err).toBe("Request timed out."); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /packages/nakama-js-test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Nakama JS Browser Test 7 | 8 | 9 | 10 | 11 | 12 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /packages/nakama-js-test/jest.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('merge') 2 | const ts_preset = require('ts-jest/jest-preset') 3 | const puppeteer_preset = require('jest-puppeteer/jest-preset') 4 | 5 | //use multiple jest presets by merging and exporting them as a single object 6 | module.exports = merge.recursive(ts_preset, puppeteer_preset, { 7 | globals: { 8 | 'ts-jest': { 9 | tsConfig: 'tsconfig.test.json' 10 | } 11 | } 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /packages/nakama-js-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heroiclabs/nakama-js-test", 3 | "version": "1.0.0", 4 | "description": "A subproject that houses tests for Nakama JS", 5 | "private": true, 6 | "license": "Apache-2.0", 7 | "dependencies": { 8 | "@heroiclabs/nakama-js": "file:../nakama-js", 9 | "@heroiclabs/nakama-js-protobuf": "file:../nakama-js-protobuf", 10 | "base64url": "3.0.1", 11 | "merge": "^2.1.1" 12 | }, 13 | "devDependencies": { 14 | "@types/expect-puppeteer": "^5.0.6", 15 | "@types/jest": "^29.5.14", 16 | "@types/jest-environment-puppeteer": "^5.0.6", 17 | "@types/node": "18.19.66", 18 | "@types/puppeteer": "^7.0.4", 19 | "jest": "29.7.0", 20 | "jest-puppeteer": "10.1.4", 21 | "puppeteer": "23.9.0", 22 | "ts-jest": "29.2.5" 23 | }, 24 | "scripts": { 25 | "test": "npx tsc --project tsconfig.test.json && jest --runInBand" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/nakama-js-test/session.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | import * as nakamajs from "@heroiclabs/nakama-js"; 19 | import {createPage, generateid} from "./utils"; 20 | import {Page} from "puppeteer"; 21 | import {describe, expect, it} from '@jest/globals' 22 | 23 | describe('Session Tests', () => { 24 | 25 | it('should be expired', async () => { 26 | const page : Page = await createPage(); 27 | 28 | const expired = await page.evaluate(() => { 29 | const nowUnixEpoch = Math.floor(Date.now() / 1000); 30 | const expiredJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTY5MTA5NzMsInVpZCI6ImY0MTU4ZjJiLTgwZjMtNDkyNi05NDZiLWE4Y2NmYzE2NTQ5MCIsInVzbiI6InZUR2RHSHl4dmwifQ.gzLaMQPaj5wEKoskOSALIeJLOYXEVFoPx3KY0Jm1EVU"; 31 | const session = nakamajs.Session.restore(expiredJwt, expiredJwt); 32 | return session.isexpired(nowUnixEpoch); 33 | }); 34 | 35 | expect(expired).toBeTruthy(); 36 | }); 37 | 38 | it('should have username and userId', async () => { 39 | const page : Page = await createPage(); 40 | 41 | const session = await page.evaluate(() => { 42 | const expiredJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTY5MTA5NzMsInVpZCI6ImY0MTU4ZjJiLTgwZjMtNDkyNi05NDZiLWE4Y2NmYzE2NTQ5MCIsInVzbiI6InZUR2RHSHl4dmwifQ.gzLaMQPaj5wEKoskOSALIeJLOYXEVFoPx3KY0Jm1EVU"; 43 | return nakamajs.Session.restore(expiredJwt, expiredJwt); 44 | }); 45 | 46 | expect(session.username).not.toBeNull(); 47 | expect(session.username).toBe("vTGdGHyxvl"); 48 | expect(session.user_id).not.toBeNull(); 49 | expect(session.user_id).toBe("f4158f2b-80f3-4926-946b-a8ccfc165490"); 50 | }); 51 | 52 | it("restored should have refresh token", async () => { 53 | const page : Page = await createPage(); 54 | 55 | const session = await page.evaluate(() => { 56 | const expiredJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTY5MTA5NzMsInVpZCI6ImY0MTU4ZjJiLTgwZjMtNDkyNi05NDZiLWE4Y2NmYzE2NTQ5MCIsInVzbiI6InZUR2RHSHl4dmwifQ.gzLaMQPaj5wEKoskOSALIeJLOYXEVFoPx3KY0Jm1EVU"; 57 | return nakamajs.Session.restore(expiredJwt, expiredJwt); 58 | }); 59 | 60 | expect(session.refresh_token).not.toBeNull(); 61 | expect(session.refresh_token).not.toBeUndefined(); 62 | expect(session.refresh_token).not.toBe(""); 63 | }); 64 | 65 | it("restored should handle null refresh token", async () => { 66 | const page : Page = await createPage(); 67 | 68 | await page.evaluate(() => { 69 | const expiredJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTY5MTA5NzMsInVpZCI6ImY0MTU4ZjJiLTgwZjMtNDkyNi05NDZiLWE4Y2NmYzE2NTQ5MCIsInVzbiI6InZUR2RHSHl4dmwifQ.gzLaMQPaj5wEKoskOSALIeJLOYXEVFoPx3KY0Jm1EVU"; 70 | return nakamajs.Session.restore(expiredJwt, null); 71 | }); 72 | }); 73 | 74 | it("should refresh", async () => { 75 | const page : Page = await createPage(); 76 | const customId = generateid(); 77 | 78 | const tokens : any = await page.evaluate(async (customId) => { 79 | 80 | /* @ts-ignore */ 81 | function timeoutPromise(ms : number) : Promise { 82 | return new Promise(resolve => setTimeout(resolve, ms)); 83 | } 84 | 85 | const client = new nakamajs.Client(); 86 | const session = await client.authenticateCustom(customId); 87 | const firstToken = session.token; 88 | 89 | await timeoutPromise(1000); 90 | const secondSession = await client.sessionRefresh(session); 91 | const secondToken = secondSession.token; 92 | return {"firstToken": firstToken, "secondToken": secondToken}; 93 | }, customId); 94 | 95 | expect(tokens.secondToken).not.toBeNull(); 96 | expect(tokens.secondToken).not.toBeUndefined(); 97 | expect(tokens.secondToken).not.toEqual(tokens.firstToken); 98 | }); 99 | 100 | it("should logout", async () => { 101 | const page : Page = await createPage(); 102 | const customId = generateid(); 103 | 104 | const status : any = await page.evaluate(async (customId) => { 105 | const client = new nakamajs.Client(); 106 | const session = await client.authenticateCustom(customId); 107 | await client.sessionLogout(session, session.token, session.refresh_token); 108 | 109 | const obj = { 110 | "collection": "collection", 111 | "key": "this should fail", 112 | "value": {} 113 | }; 114 | 115 | try 116 | { 117 | await client.writeStorageObjects(session, [obj]); 118 | return null; 119 | } catch (e) { 120 | return e.status; 121 | } 122 | }, customId); 123 | 124 | expect(status).toEqual(401); 125 | }); 126 | 127 | it("should autorefresh session", async () => { 128 | const page : Page = await createPage(); 129 | const customId = generateid(); 130 | 131 | const tokens : any = await page.evaluate(async (customId) => { 132 | 133 | function timeoutPromise(ms : number) : Promise { 134 | return new Promise(resolve => setTimeout(resolve, ms)); 135 | } 136 | 137 | const client = new nakamajs.Client(); 138 | const session = await client.authenticateCustom(customId); 139 | const firstToken = session.token; 140 | session.expires_at = (Date.now() + client.expiredTimespanMs)/1000 - 1; 141 | 142 | /* @ts-ignore */ 143 | await timeoutPromise(1000); 144 | 145 | const obj = { 146 | "collection": "collection", 147 | "key": "this should succeed", 148 | "value": {} 149 | }; 150 | 151 | await client.writeStorageObjects(session, [obj]); 152 | 153 | const secondToken = session.token; 154 | return {"firstToken": firstToken, "secondToken": secondToken}; 155 | }, customId); 156 | 157 | expect(tokens.secondToken).not.toBeNull(); 158 | expect(tokens.secondToken).not.toBeUndefined(); 159 | expect(tokens.secondToken).not.toEqual(tokens.firstToken); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /packages/nakama-js-test/socket-channel.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | import * as nakamajs from "@heroiclabs/nakama-js"; 19 | import * as nakamajsprotobuf from "../nakama-js-protobuf"; 20 | import {generateid, createPage, adapters, AdapterType} from "./utils"; 21 | import {describe, expect, it} from '@jest/globals' 22 | 23 | describe('Channel Tests', () => { 24 | 25 | it.each(adapters)('should join a channel', async (adapter) => { 26 | 27 | const page = await createPage(); 28 | const customid = generateid(); 29 | const channelid = generateid(); 30 | 31 | const response = await page.evaluate(async (customid, channelid, adapter) => { 32 | 33 | const client = new nakamajs.Client(); 34 | 35 | const socket = client.createSocket(false, false, 36 | adapter == AdapterType.Protobuf ? new nakamajsprotobuf.WebSocketAdapterPb() : new nakamajs.WebSocketAdapterText()); 37 | 38 | const session = await client.authenticateCustom(customid) 39 | await socket.connect(session, false); 40 | 41 | //chat type: 1 = room, 2 = Direct Message 3 = Group 42 | return await socket.joinChat(channelid, 1, true, false); 43 | }, customid, channelid, adapter); 44 | 45 | expect(response).not.toBeNull(); 46 | expect(response.id).not.toBeNull(); 47 | expect(response.presences).not.toBeNull(); 48 | expect(response.self).not.toBeNull(); 49 | }); 50 | 51 | it.each(adapters)('should join a room, then leave it', async (adapter) => { 52 | const page = await createPage(); 53 | 54 | const customid = generateid(); 55 | const channelid = generateid(); 56 | 57 | const response = await page.evaluate(async (customid, channelid, adapter) => { 58 | 59 | const client = new nakamajs.Client(); 60 | const socket = client.createSocket(false, true, 61 | adapter == AdapterType.Protobuf ? new nakamajsprotobuf.WebSocketAdapterPb() : new nakamajs.WebSocketAdapterText()); 62 | 63 | const session = await client.authenticateCustom(customid) 64 | await socket.connect(session, false); 65 | //chat type: 1 = room, 2 = Direct Message 3 = Group 66 | const channel = await socket.joinChat(channelid, 1, true, false); 67 | 68 | return await socket.leaveChat(channel.id); 69 | }, customid, channelid, adapter); 70 | 71 | expect(response).not.toBeNull(); 72 | }); 73 | 74 | it.each(adapters)('should create a group, join group chat, then leave it', async (adapter) => { 75 | const page = await createPage(); 76 | 77 | const customid = generateid(); 78 | const group_name = generateid(); 79 | 80 | const response = await page.evaluate(async (customid, group_name, adapter) => { 81 | 82 | const client = new nakamajs.Client(); 83 | const socket = client.createSocket(false, false, 84 | adapter == AdapterType.Protobuf ? new nakamajsprotobuf.WebSocketAdapterPb() : new nakamajs.WebSocketAdapterText()); 85 | 86 | const session = await client.authenticateCustom(customid) 87 | await socket.connect(session, false); 88 | 89 | const group = await client.createGroup(session, { name: group_name, open: true }); 90 | //chat type: 1 = room, 2 = Direct Message 3 = Group 91 | const channel = await socket.joinChat(group.id!, 3, true, false); 92 | return await socket.leaveChat(channel.id); 93 | }, customid, group_name, adapter); 94 | 95 | expect(response).not.toBeNull(); 96 | }); 97 | 98 | it.each(adapters)('should join a room, then send message, receive message', async (adapter) => { 99 | 100 | const page = await createPage(); 101 | 102 | const customid = generateid(); 103 | const channelid = generateid(); 104 | const payload = { "hello": "world" }; 105 | 106 | const message : nakamajs.ChannelMessage | null = await page.evaluate(async (customid, channelid, payload, adapter) => { 107 | const client = new nakamajs.Client(); 108 | const socket = client.createSocket(false, false, 109 | adapter == AdapterType.Protobuf ? new nakamajsprotobuf.WebSocketAdapterPb() : new nakamajs.WebSocketAdapterText()); 110 | 111 | var promise1 = new Promise((resolve, reject) => { 112 | socket.onchannelmessage = (channelmessage) => { 113 | resolve(channelmessage); 114 | } 115 | }); 116 | 117 | const session = await client.authenticateCustom(customid) 118 | await socket.connect(session, false); 119 | 120 | // chat type: 1 = room, 2 = Direct Message 3 = Group 121 | const channel = await socket.joinChat(channelid, 1, false, false); 122 | 123 | await socket.writeChatMessage(channel.id, payload); 124 | 125 | var promise2 = new Promise((resolve, reject) => { 126 | setTimeout(reject, 5000, "did not receive channel message - timed out.") 127 | }); 128 | 129 | return Promise.race([promise1, promise2]); 130 | }, customid, channelid, payload, adapter); 131 | 132 | expect(message).not.toBeNull(); 133 | expect(message!.channel_id).not.toBeNull(); 134 | expect(message!.message_id).not.toBeNull(); 135 | expect(message!.sender_id).not.toBeNull(); 136 | expect(message!.content).toEqual(payload); 137 | expect(message!.code).toBe(0); 138 | expect(message!.persistent).toBe(false); 139 | }); 140 | 141 | it.each(adapters)('should join a room, then send message, update message, then list messages', async (adapter) => { 142 | const page = await createPage(); 143 | 144 | const customid = generateid(); 145 | const channelid = generateid(); 146 | const payload = { "hello": "world" }; 147 | const updatedPayload = { "hello": "world2" }; 148 | 149 | const response = await page.evaluate(async (customid, channelid, payload, updatedPayload, adapter) => { 150 | const client = new nakamajs.Client(); 151 | const socket = client.createSocket(false, false, 152 | adapter == AdapterType.Protobuf ? new nakamajsprotobuf.WebSocketAdapterPb() : new nakamajs.WebSocketAdapterText()); 153 | 154 | const session = await client.authenticateCustom(customid) 155 | await socket.connect(session, false); 156 | 157 | //chat type: 1 = room, 2 = Direct Message 3 = Group 158 | const channel = await socket.joinChat(channelid, 1, true, false); 159 | 160 | const ack = await socket.writeChatMessage(channel.id, payload); 161 | const ack2 = await socket.updateChatMessage( 162 | ack.channel_id, 163 | ack.message_id, 164 | updatedPayload); 165 | 166 | return await client.listChannelMessages(session, ack2.channel_id, 10) 167 | }, customid, channelid, payload, updatedPayload, adapter); 168 | 169 | console.log(response); 170 | 171 | expect(response).not.toBeNull(); 172 | expect(response.messages).not.toBeNull(); 173 | expect(response.messages?.length).toBe(1); 174 | expect(response.cacheable_cursor).not.toBeNull(); 175 | expect(response.cacheable_cursor).not.toBeUndefined(); 176 | 177 | response.messages?.forEach(message => { 178 | expect(message.content).toEqual(updatedPayload); 179 | expect(message.code).toEqual(0); 180 | expect(message.persistent).toBe(true); 181 | }) 182 | }); 183 | 184 | it.each(adapters)('should join a room, then send message, remove message, then list messages', async (adapter) => { 185 | const page = await createPage(); 186 | 187 | const customid = generateid(); 188 | const channelid = generateid(); 189 | const payload = { "hello": "world" }; 190 | 191 | const response = await page.evaluate(async (customid, channelid, payload, adapter) => { 192 | 193 | const client = new nakamajs.Client(); 194 | const socket = client.createSocket(false, false, 195 | adapter == AdapterType.Protobuf ? new nakamajsprotobuf.WebSocketAdapterPb() : new nakamajs.WebSocketAdapterText()); 196 | 197 | const session = await client.authenticateCustom(customid) 198 | await socket.connect(session, false); 199 | 200 | // chat type: 1 = room, 2 = Direct Message 3 = Group 201 | const channel = await socket.joinChat(channelid, 1, true, false); 202 | 203 | const ack = await socket.writeChatMessage(channel.id, payload); 204 | 205 | await socket.removeChatMessage(ack.channel_id, 206 | ack.message_id); 207 | 208 | return await client.listChannelMessages(session, ack.channel_id, 10) 209 | }, customid, channelid, payload, adapter); 210 | 211 | expect(response).not.toBeNull(); 212 | expect(response.messages).not.toBeNull(); 213 | expect(response.messages!.length).toBe(0); 214 | }); 215 | 216 | }); 217 | -------------------------------------------------------------------------------- /packages/nakama-js-test/socket-notification.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | import {Page} from "puppeteer"; 19 | import * as nakamajs from "@heroiclabs/nakama-js"; 20 | import {createPage, generateid} from "./utils"; 21 | import {describe, expect, it} from '@jest/globals' 22 | 23 | describe('Notification Tests', () => { 24 | 25 | it('should rpc and list notifications', async () => { 26 | const page : Page = await createPage(); 27 | 28 | const customid = generateid(); 29 | 30 | const notifications = await page.evaluate(async (customid) => { 31 | const client = new nakamajs.Client(); 32 | const session = await client.authenticateCustom(customid); 33 | await client.rpc(session, "clientrpc.send_notification", {"user_id": session.user_id}); 34 | await client.rpc(session, "clientrpc.send_notification", {"user_id": session.user_id}); 35 | var notificationList = await client.listNotifications(session, 1, ""); 36 | var notificationList = await client.listNotifications(session, 1, notificationList.cacheable_cursor); 37 | 38 | return notificationList; 39 | }, customid); 40 | 41 | expect(notifications).not.toBeNull(); 42 | expect(notifications.notifications[0].id).not.toBeNull(); 43 | expect(notifications.notifications[0].content).toEqual({reward_coins: 1000}); 44 | expect(notifications.notifications[0].subject).toEqual("You've unlocked level 100!"); 45 | expect(notifications.notifications[0].code).toEqual(1); 46 | }); 47 | 48 | it('should rpc and delete notification', async () => { 49 | const page : Page = await createPage(); 50 | 51 | const customid = generateid(); 52 | 53 | const response = await page.evaluate(async (customid) => { 54 | const client = new nakamajs.Client(); 55 | const session = await client.authenticateCustom(customid); 56 | const rpcSuccess = await client.rpc(session, "clientrpc.send_notification", {"user_id": session.user_id}); 57 | const notificationsList = await client.listNotifications(session, 100, ""); 58 | 59 | var notificationsDelete = [] 60 | notificationsList.notifications.forEach((notification) => { 61 | notificationsDelete.push(notification.id); 62 | }); 63 | 64 | await client.deleteNotifications(session, notificationsDelete); 65 | return client.listNotifications(session, 1, ""); 66 | }, customid); 67 | 68 | expect(response).not.toBeNull(); 69 | expect(response.notifications).not.toBeNull(); 70 | expect(response.notifications.length).toEqual(0); 71 | }); 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /packages/nakama-js-test/socket-status.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as nakamajs from "@heroiclabs/nakama-js"; 18 | import {StatusPresenceEvent} from "@heroiclabs/nakama-js/socket"; 19 | import * as nakamajsprotobuf from "../nakama-js-protobuf"; 20 | import {generateid, createPage, adapters, AdapterType} from "./utils"; 21 | import {describe, expect, it} from '@jest/globals' 22 | 23 | describe('Status Tests', () => { 24 | 25 | it.each(adapters)('should create status, and then update it', async (adapter) => { 26 | const page = await createPage(); 27 | 28 | const customid = generateid(); 29 | 30 | const response = await page.evaluate(async (customid, adapter) => { 31 | 32 | const client = new nakamajs.Client(); 33 | const socket = client.createSocket(false, false, 34 | adapter == AdapterType.Protobuf ? new nakamajsprotobuf.WebSocketAdapterPb() : new nakamajs.WebSocketAdapterText()); 35 | 36 | const session = await client.authenticateCustom(customid); 37 | await socket.connect(session, true); 38 | 39 | return socket.updateStatus("hello-world"); 40 | }, customid, adapter); 41 | 42 | expect(response).not.toBeNull(); 43 | }); 44 | 45 | it.each(adapters)('should follow user2, and get status update when coming online', async (adapter) => { 46 | const page = await createPage(); 47 | 48 | const customid1 = generateid(); 49 | const customid2 = generateid(); 50 | 51 | const response = await page.evaluate(async (customid1, customid2, adapter) => { 52 | const client1 = new nakamajs.Client(); 53 | const client2 = new nakamajs.Client(); 54 | const socket2 = client2.createSocket(false, false, 55 | adapter == AdapterType.Protobuf ? new nakamajsprotobuf.WebSocketAdapterPb() : new nakamajs.WebSocketAdapterText()); 56 | 57 | const socket1 = client1.createSocket(false, false, 58 | adapter == AdapterType.Protobuf ? new nakamajsprotobuf.WebSocketAdapterPb() : new nakamajs.WebSocketAdapterText()); 59 | 60 | const session1 = await client1.authenticateCustom(customid1); 61 | const session2 = await client2.authenticateCustom(customid2); 62 | await socket1.connect(session1, true); 63 | await socket1.followUsers([session2.user_id]); 64 | 65 | var promise1 = new Promise((resolve, reject) => { 66 | socket1.onstatuspresence = (statusPresence) => { 67 | resolve(statusPresence); 68 | } 69 | }); 70 | 71 | const promise2 = socket2.connect(session2, true).then((session) => { 72 | return new Promise((resolve, reject) => { 73 | setTimeout(reject, 2500, "did not receive match data - timed out."); 74 | }); 75 | }); 76 | 77 | return Promise.race([promise1, promise2]); 78 | }, customid1, customid2, adapter); 79 | 80 | expect(response).not.toBeNull(); 81 | expect(response.joins.length).toEqual(1); 82 | }); 83 | 84 | it.each(adapters)('should follow user2, and unfollow user2', async (adapter) => { 85 | const page = await createPage(); 86 | 87 | const customid1 = generateid(); 88 | const customid2 = generateid(); 89 | 90 | const response = await page.evaluate(async (customid1, customid2, adapter) => { 91 | const client1 = new nakamajs.Client(); 92 | const client2 = new nakamajs.Client(); 93 | const socket1 = client1.createSocket(false, false, 94 | adapter == AdapterType.Protobuf ? new nakamajsprotobuf.WebSocketAdapterPb() : new nakamajs.WebSocketAdapterText()); 95 | 96 | const session1 = await client1.authenticateCustom(customid1); 97 | const session2 = await client2.authenticateCustom(customid2); 98 | await socket1.connect(session1, true); 99 | await socket1.followUsers([session2.user_id]); 100 | return socket1.unfollowUsers([session2.user_id]); 101 | }, customid1, customid2, adapter); 102 | 103 | expect(response).not.toBeNull(); 104 | }); 105 | 106 | }); 107 | -------------------------------------------------------------------------------- /packages/nakama-js-test/socket.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as nakamajs from "@heroiclabs/nakama-js"; 18 | import {StreamData} from "@heroiclabs/nakama-js/socket" 19 | import * as nakamajsprotobuf from "../nakama-js-protobuf"; 20 | import {generateid, createPage, adapters, AdapterType} from "./utils" 21 | import {describe, expect, it} from '@jest/globals' 22 | 23 | describe('Socket Message Tests', () => { 24 | 25 | it.each(adapters)('should connect', async (adapter) => { 26 | const page = await createPage(); 27 | 28 | const customid = generateid(); 29 | 30 | const session = await page.evaluate(async (customid, adapter) => { 31 | const client = new nakamajs.Client(); 32 | const session = await client.authenticateCustom(customid); 33 | 34 | const socket = client.createSocket(false, false, 35 | adapter == AdapterType.Protobuf ? new nakamajsprotobuf.WebSocketAdapterPb() : new nakamajs.WebSocketAdapterText()); 36 | 37 | await socket.connect(session, false); 38 | socket.disconnect(false); 39 | }, customid, adapter); 40 | }); 41 | 42 | it.each(adapters)('should rpc and receive stream data', async (adapter) => { 43 | const page = await createPage(); 44 | 45 | const customid = generateid(); 46 | const ID = "clientrpc.send_stream_data"; 47 | const PAYLOAD = JSON.stringify({ "hello": "world" }); 48 | 49 | const response = await page.evaluate(async (customid, id, payload, adapter) => { 50 | const client = new nakamajs.Client(); 51 | const socket = client.createSocket(false, false, 52 | adapter == AdapterType.Protobuf ? new nakamajsprotobuf.WebSocketAdapterPb() : new nakamajs.WebSocketAdapterText()); 53 | 54 | var promise1 = new Promise((resolve, reject) => { 55 | socket.onstreamdata = (streamdata) => { 56 | resolve(streamdata); 57 | } 58 | }); 59 | 60 | const session = await client.authenticateCustom(customid) 61 | await socket.connect(session, false); 62 | await socket.rpc(id, payload); 63 | var promise2 = new Promise((resolve, reject) => { 64 | setTimeout(reject, 5000, "did not receive stream data - timed out.") 65 | }); 66 | 67 | return Promise.race([promise1, promise2]); 68 | }, customid, ID, PAYLOAD, adapter); 69 | 70 | expect(response).not.toBeNull(); 71 | expect(response.data).toBe(PAYLOAD); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /packages/nakama-js-test/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "es2015", 6 | "es2016", 7 | "es2017" 8 | ], 9 | "target": "es2018", 10 | "module": "es2015", 11 | "removeComments": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "moduleResolution": "node", 14 | "noEmitHelpers": false, 15 | "importHelpers": false, 16 | "baseUrl": "./", 17 | "noEmit": true, 18 | "allowSyntheticDefaultImports": true 19 | }, 20 | "include": [ 21 | "./*", 22 | ], 23 | "exclude": [] 24 | } 25 | -------------------------------------------------------------------------------- /packages/nakama-js-test/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Page, Browser} from "puppeteer"; 18 | const fs = require("fs"); 19 | const crypto = require("crypto"); 20 | const base64url = require("base64url"); 21 | 22 | // automatically assigned by puppeteer + Jest 23 | declare var browser : Browser; 24 | 25 | // automatically assigned by puppeteer + Jest 26 | declare var browser : Browser; 27 | 28 | // util to generate a random id. 29 | export function generateid(): string { 30 | const arr: string[] = []; 31 | 32 | for (let i: number = 0; i < 30; i++) { 33 | arr.push(Math.random().toString(36)[3]); 34 | } 35 | 36 | return arr.join(""); 37 | }; 38 | 39 | export async function createPage(): Promise { 40 | 41 | const page = await browser.newPage(); 42 | 43 | page.on('console', msg => console.log('LOG:', msg.text())); 44 | page.on('error', handlePageError); 45 | page.on('pageerror', handlePageError); 46 | 47 | const nakamaJsLib = fs.readFileSync(__dirname + '/../nakama-js/dist/nakama-js.iife.js', 'utf8'); 48 | const nakamaJsProtobufLib = fs.readFileSync(__dirname + '/../nakama-js-protobuf/dist/nakama-js-protobuf.iife.js', 'utf8'); 49 | 50 | await page.evaluateOnNewDocument(nakamaJsLib); 51 | await page.evaluateOnNewDocument(nakamaJsProtobufLib); 52 | await page.evaluateOnNewDocument(() => { 53 | globalThis.timeoutPromise = function(ms) { 54 | return new Promise(resolve => setTimeout(resolve, ms)); 55 | } 56 | }) 57 | 58 | await page.goto('about:blank'); 59 | 60 | return page; 61 | } 62 | 63 | function handlePageError(err) { 64 | 65 | let msg: string; 66 | 67 | if (err instanceof Object) { 68 | msg = JSON.stringify(err); 69 | } 70 | else { 71 | msg = err; 72 | } 73 | 74 | console.error('ERR:', msg); 75 | } 76 | 77 | export const enum AdapterType { 78 | Text = 0, 79 | Protobuf = 1 80 | } 81 | 82 | export const adapters = [AdapterType.Text, AdapterType.Protobuf]; 83 | 84 | export function createFacebookInstantGameAuthToken(id : string) : string { 85 | const testSecret = "fb-instant-test-secret"; 86 | 87 | const mockFbInstantPayload = JSON.stringify({ 88 | algorithm: "HMAC-SHA256", 89 | issued_at: 1594867628, 90 | player_id: id, 91 | request_payload: "" 92 | }); 93 | 94 | const encodedPayload = base64url(mockFbInstantPayload); 95 | 96 | const signature = crypto.createHmac('sha256', testSecret).update(encodedPayload).digest(); 97 | const encodedSignature = base64url(signature); 98 | 99 | const token = encodedSignature + "." + encodedPayload; 100 | return token; 101 | } 102 | 103 | export const matchmakerTimeout = 20000; 104 | -------------------------------------------------------------------------------- /packages/nakama-js-webpack-example/README.md: -------------------------------------------------------------------------------- 1 | To run this example, run `yarn build` and open `index.html`. -------------------------------------------------------------------------------- /packages/nakama-js-webpack-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Nakama JS Example Browser Test 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/nakama-js-webpack-example/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Client} from "@heroiclabs/nakama-js"; 18 | 19 | var useSSL = false; // Enable if server is run with an SSL certificate. 20 | var client = new Client("defaultkey", "127.0.0.1", "7350", useSSL); 21 | 22 | client.authenticateCustom("test_id").then( 23 | session => { console.log("authenticated."); 24 | }).catch(e => { 25 | console.log("error authenticating."); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/nakama-js-webpack-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heroiclabs/nakama-js-webpack-example", 3 | "version": "1.0.0", 4 | "description": "An example project that utilizes nakama-js and Webpack", 5 | "private": true, 6 | "license": "Apache-2.0", 7 | "dependencies": { 8 | "@heroiclabs/nakama-js": "file:../nakama-js", 9 | "ts-loader": "^9.5.1", 10 | "webpack": "^5.96.1", 11 | "webpack-cli": "^5.1.4" 12 | }, 13 | "scripts": { 14 | "build": "npx webpack --config webpack.config.js" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/nakama-js-webpack-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | }, 4 | "files": ["index.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/nakama-js-webpack-example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './index.ts', 5 | output: { 6 | filename: 'main.js', 7 | path: path.resolve(__dirname, 'dist'), 8 | }, 9 | module: { 10 | rules: [{ 11 | test: /\.tsx?$/, 12 | use: 'ts-loader', 13 | }] 14 | } 15 | }; -------------------------------------------------------------------------------- /packages/nakama-js/.gitignore: -------------------------------------------------------------------------------- 1 | dist/*.js 2 | dist/*.js.map 3 | -------------------------------------------------------------------------------- /packages/nakama-js/README.md: -------------------------------------------------------------------------------- 1 | Nakama JavaScript client 2 | ======================== 3 | 4 | > JavaScript client for Nakama server written in TypeScript. For browser and React Native projects. 5 | 6 | [Nakama](https://github.com/heroiclabs/nakama) is an open-source server designed to power modern games and apps. Features include user accounts, chat, social, matchmaker, realtime multiplayer, and much [more](https://heroiclabs.com). 7 | 8 | This client implements the full API and socket options with the server. It's written in TypeScript with minimal dependencies to be compatible with all modern browsers and React Native. 9 | 10 | Full documentation is online - https://heroiclabs.com/docs/nakama/client-libraries/javascript/ 11 | 12 | ## Getting Started 13 | 14 | You'll need to setup the server and database before you can connect with the client. The simplest way is to use Docker but have a look at the [server documentation](https://github.com/heroiclabs/nakama#getting-started) for other options. 15 | 16 | 1. Install and run the servers. Follow these [instructions](https://heroiclabs.com/docs/nakama/getting-started/install/docker/). 17 | 18 | 2. Import the client into your project. It's [available on NPM](https://www.npmjs.com/package/@heroiclabs/nakama-js) and can be also be added to a project with Bower or other package managers. 19 | 20 | ```shell 21 | npm install @heroiclabs/nakama-js 22 | ``` 23 | 24 | You'll now see the code in the "node_modules" folder and package listed in your "package.json". 25 | 26 | Optionally, if you would like to use the Protocol Buffers wire format with your sockets, you can import the adapter found in this package: 27 | 28 | ```shell 29 | npm install @heroiclabs/nakama-js-protobuf 30 | ``` 31 | 32 | 3. Use the connection credentials to build a client object. 33 | 34 | ```js 35 | import {Client} from "@heroiclabs/nakama-js"; 36 | 37 | var useSSL = false; // Enable if server is run with an SSL certificate. 38 | var client = new Client("defaultkey", "127.0.0.1", "7350", useSSL); 39 | ``` 40 | 41 | ## Usage 42 | 43 | The client object has many methods to execute various features in the server or open realtime socket connections with the server. 44 | 45 | ### Authenticate 46 | 47 | There's a variety of ways to [authenticate](https://heroiclabs.com/docs/authentication) with the server. Authentication can create a user if they don't already exist with those credentials. It's also easy to authenticate with a social profile from Google Play Games, Facebook, Game Center, etc. 48 | 49 | ```js 50 | var email = "super@heroes.com"; 51 | var password = "batsignal"; 52 | const session = await client.authenticateEmail(email, password); 53 | console.info(session); 54 | ``` 55 | 56 | ### Sessions 57 | 58 | When authenticated the server responds with an auth token (JWT) which contains useful properties and gets deserialized into a `Session` object. 59 | 60 | ```js 61 | console.info(session.token); // raw JWT token 62 | console.info(session.refreshToken); // refresh token 63 | console.info(session.userId); 64 | console.info(session.username); 65 | console.info("Session has expired?", session.isexpired(Date.now() / 1000)); 66 | const expiresat = session.expires_at; 67 | console.warn("Session will expire at", new Date(expiresat * 1000).toISOString()); 68 | ``` 69 | 70 | It is recommended to store the auth token from the session and check at startup if it has expired. If the token has expired you must reauthenticate. The expiry time of the token can be changed as a setting in the server. 71 | 72 | ```js 73 | // Assume we've stored the auth token in browser Web Storage. 74 | const authtoken = window.localStorage.getItem("nkauthtoken"); 75 | const refreshtoken = window.localStorage.getItem("nkrefreshtoken"); 76 | 77 | let session = nakamajs.Session.restore(authtoken, refreshtoken); 78 | 79 | // Check whether a session is close to expiry. 80 | 81 | const unixTimeInFuture = Date.now() + 8.64e+7; // one day from now 82 | 83 | if (session.isexpired(unixTimeInFuture / 1000)) { 84 | try 85 | { 86 | session = await client.sessionRefresh(session); 87 | } 88 | catch (e) 89 | { 90 | console.info("Session can no longer be refreshed. Must reauthenticate!"); 91 | } 92 | } 93 | ``` 94 | 95 | ### Requests 96 | 97 | The client includes lots of builtin APIs for various features of the game server. These can be accessed with the methods which return Promise objects. It can also call custom logic as RPC functions on the server. These can also be executed with a socket object. 98 | 99 | All requests are sent with a session object which authorizes the client. 100 | 101 | ```js 102 | const account = await client.getAccount(session); 103 | console.info(account.user.id); 104 | console.info(account.user.username); 105 | console.info(account.wallet); 106 | ``` 107 | 108 | ### Socket 109 | 110 | The client can create one or more sockets with the server. Each socket can have it's own event listeners registered for responses received from the server. 111 | 112 | ```js 113 | const secure = false; // Enable if server is run with an SSL certificate 114 | const trace = false; 115 | const socket = client.createSocket(secure, trace); 116 | socket.ondisconnect = (evt) => { 117 | console.info("Disconnected", evt); 118 | }; 119 | 120 | const session = await socket.connect(session); 121 | // Socket is open. 122 | ``` 123 | 124 | If you are using the optional protocol buffer adapter, pass the adapter to the Socket object during construction: 125 | 126 | ```js 127 | import {WebSocketAdapterPb} from "@heroiclabs/nakama-js-protobuf" 128 | 129 | const secure = false; // Enable if server is run with an SSL certificate 130 | const trace = false; 131 | const socket = client.createSocket(secure, trace, new WebSocketAdapterPb()); 132 | ``` 133 | 134 | There's many messages for chat, realtime, status events, notifications, etc. which can be sent or received from the socket. 135 | 136 | ```js 137 | socket.onchannelmessage = (message) => { 138 | console.info("Message received from channel", message.channel_id); 139 | console.info("Received message", message); 140 | }; 141 | 142 | 143 | // 1 = room, 2 = Direct Message, 3 = Group 144 | const roomname = "mychannel"; 145 | const type: number = 1; 146 | const persistence: boolean = false; 147 | const hidden: boolean = false; 148 | 149 | const channel = await socket.joinChat(roomname, type, persistence, hidden); 150 | 151 | const message = { hello: "world" }; 152 | socket.writeChatMessage(channel.id, message); 153 | ``` 154 | 155 | ## Handling errors 156 | 157 | For any errors in client requests, we return the original error objects from the Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API 158 | 159 | In order to capture the Nakama server response associated with the error, when you wrap your client requests in a `try...catch` statement you can invoke `await error.json()` on the `error` object in the catch block: 160 | 161 | ```js 162 | try { 163 | const account = await client.getAccount(session); 164 | console.info(account.user.id); 165 | console.info(account.user.username); 166 | console.info(account.wallet); 167 | } catch (error) { 168 | console.info("Inner Nakama error", await error.json()); 169 | } 170 | ``` 171 | 172 | ## Contribute 173 | 174 | The development roadmap is managed as GitHub issues and pull requests are welcome. If you're interested in enhancing the code please open an issue to discuss the changes or drop in and discuss it in the [community forum](https://forum.heroiclabs.com). 175 | 176 | ### Source Builds 177 | 178 | Ensure you are using Node v18>. 179 | 180 | The codebase is multi-package monorepo written in TypeScript and can be built with [esbuild](https://github.com/evanw/esbuild). All dependencies are managed with NPM. 181 | 182 | To build from source, first install all workspace dependencies from the repository root with `npm install`. 183 | 184 | Then to build a specific workspace, pass the `--workspace` flag to your build command, for example: 185 | 186 | ```shell 187 | npm run build --workspace=@heroiclabs/nakama-js 188 | ``` 189 | 190 | ### Run Tests 191 | 192 | To run tests you will need to run the server and database. Most tests are written as integration tests which execute against the server. A quick approach we use with our test workflow is to use the Docker compose file described in the [documentation](https://heroiclabs.com/docs/nakama/getting-started/install/docker/). 193 | 194 | Tests are run against each workspace bundle; if you have made source code changes, you should `npm run build --workspace=` prior to running tests. 195 | 196 | ```shell 197 | docker-compose -f ./docker-compose.yml up 198 | npm run test --workspace=@heroiclabs/nakama-js-test 199 | ``` 200 | 201 | ### Protocol Buffer Web Socket Adapter 202 | 203 | To update the generated Typescript required for using the protocol buffer adapter, `cd` into 204 | `packages/nakama-js-protobuf` and run the following: 205 | 206 | ```shell 207 | npx protoc \ 208 | --plugin="./node_modules/.bin/protoc-gen-ts_proto" \ 209 | --proto_path=$GOPATH/src/github.com/heroiclabs/nakama-common \ 210 | --ts_proto_out=. \ 211 | --ts_proto_opt=snakeToCamel=false \ 212 | --ts_proto_opt=esModuleInterop=true \ 213 | $GOPATH/src/github.com/heroiclabs/nakama-common/rtapi/realtime.proto \ 214 | $GOPATH/src/github.com/heroiclabs/nakama-common/api/api.proto 215 | ``` 216 | 217 | ### Release Process 218 | 219 | To release onto NPM if you have access to the "@heroiclabs" organization you can use NPM. 220 | 221 | ```shell 222 | npm run build --workspace= && npm publish --access=public --workspace= 223 | ``` 224 | 225 | ### Generate Docs 226 | 227 | API docs are generated with typedoc and deployed to GitHub pages. 228 | 229 | To run typedoc: 230 | 231 | ``` 232 | npm install && npm run docs 233 | ``` 234 | 235 | ### License 236 | 237 | This project is licensed under the [Apache-2 License](https://github.com/heroiclabs/nakama-js/blob/master/LICENSE). 238 | -------------------------------------------------------------------------------- /packages/nakama-js/build.mjs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Nakama Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import esbuild from 'esbuild'; 16 | 17 | // Shared esbuild config 18 | const config = { 19 | logLevel: 'info', 20 | entryPoints: ['index.ts'], 21 | bundle: true, 22 | target: 'es6', 23 | globalName: 'nakamajs' 24 | }; 25 | 26 | // Build CommonJS 27 | await esbuild.build({ 28 | ...config, 29 | format: 'cjs', 30 | outfile: 'dist/nakama-js.cjs.js' 31 | }); 32 | 33 | // Build ESM 34 | await esbuild.build({ 35 | ...config, 36 | format: 'esm', 37 | outfile: 'dist/nakama-js.esm.mjs' 38 | }); 39 | 40 | // Build IIFE 41 | await esbuild.build({ 42 | ...config, 43 | format: 'iife', 44 | outfile: 'dist/nakama-js.iife.js' 45 | }); -------------------------------------------------------------------------------- /packages/nakama-js/dist/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import "whatwg-fetch"; 17 | export * from "./client"; 18 | export * from "./session"; 19 | export * from "./socket"; 20 | export * from "./web_socket_adapter"; 21 | /** 22 | * Reexported due to duplicate definition of ChannelMessage in [Client]{@link ./client.ts} and [Session]{@link ./session.ts} 23 | */ 24 | export { ChannelMessage } from "./client"; 25 | -------------------------------------------------------------------------------- /packages/nakama-js/dist/session.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** A session authenticated for a user with Nakama server. */ 17 | export interface ISession { 18 | /** Claims */ 19 | /** The authorization token used to construct this session. */ 20 | token: string; 21 | /** If the user account for this session was just created. */ 22 | created: boolean; 23 | /** The UNIX timestamp when this session was created. */ 24 | readonly created_at: number; 25 | /** The UNIX timestamp when this session will expire. */ 26 | expires_at?: number; 27 | /** The UNIX timestamp when the refresh token will expire. */ 28 | refresh_expires_at?: number; 29 | /** Refresh token that can be used for session token renewal. */ 30 | refresh_token: string; 31 | /** The username of the user who owns this session. */ 32 | username?: string; 33 | /** The ID of the user who owns this session. */ 34 | user_id?: string; 35 | /** Any custom properties associated with this session. */ 36 | vars?: object; 37 | /** Validate token */ 38 | /** If the session has expired. */ 39 | isexpired(currenttime: number): boolean; 40 | /** If the refresh token has expired. */ 41 | isrefreshexpired(currenttime: number): boolean; 42 | } 43 | export declare class Session implements ISession { 44 | readonly created: boolean; 45 | token: string; 46 | readonly created_at: number; 47 | expires_at?: number; 48 | refresh_expires_at?: number; 49 | refresh_token: string; 50 | username?: string; 51 | user_id?: string; 52 | vars?: object; 53 | constructor(token: string, refresh_token: string, created: boolean); 54 | isexpired(currenttime: number): boolean; 55 | isrefreshexpired(currenttime: number): boolean; 56 | update(token: string, refreshToken: string): void; 57 | decodeJWT(token: string): any; 58 | static restore(token: string, refreshToken: string): Session; 59 | } 60 | -------------------------------------------------------------------------------- /packages/nakama-js/dist/utils.d.ts: -------------------------------------------------------------------------------- 1 | export declare function buildFetchOptions(method: string, options: any, bodyJson: string): any; 2 | export declare function b64EncodeUnicode(str: string): string; 3 | export declare function b64DecodeUnicode(str: string): string; 4 | -------------------------------------------------------------------------------- /packages/nakama-js/dist/web_socket_adapter.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * An interface used by Nakama's web socket to determine the payload protocol. 18 | */ 19 | export interface WebSocketAdapter { 20 | /** 21 | * Dispatched when the web socket closes. 22 | */ 23 | onClose: SocketCloseHandler | null; 24 | /** 25 | * Dispatched when the web socket receives an error. 26 | */ 27 | onError: SocketErrorHandler | null; 28 | /** 29 | * Dispatched when the web socket receives a normal message. 30 | */ 31 | onMessage: SocketMessageHandler | null; 32 | /** 33 | * Dispatched when the web socket opens. 34 | */ 35 | onOpen: SocketOpenHandler | null; 36 | isOpen(): boolean; 37 | close(): void; 38 | connect(scheme: string, host: string, port: string, createStatus: boolean, token: string): void; 39 | send(message: any): void; 40 | } 41 | /** 42 | * SocketCloseHandler defines a lambda that handles WebSocket close events. 43 | */ 44 | export interface SocketCloseHandler { 45 | (this: WebSocket, evt: CloseEvent): void; 46 | } 47 | /** 48 | * SocketErrorHandler defines a lambda that handles responses from the server via WebSocket 49 | * that indicate an error. 50 | */ 51 | export interface SocketErrorHandler { 52 | (this: WebSocket, evt: Event): void; 53 | } 54 | /** 55 | * SocketMessageHandler defines a lambda that handles valid WebSocket messages. 56 | */ 57 | export interface SocketMessageHandler { 58 | (message: any): void; 59 | } 60 | /** 61 | * SocketOpenHandler defines a lambda that handles WebSocket open events. 62 | */ 63 | export interface SocketOpenHandler { 64 | (this: WebSocket, evt: Event): void; 65 | } 66 | /** 67 | * A text-based socket adapter that accepts and transmits payloads over UTF-8. 68 | */ 69 | export declare class WebSocketAdapterText implements WebSocketAdapter { 70 | private _socket?; 71 | get onClose(): SocketCloseHandler | null; 72 | set onClose(value: SocketCloseHandler | null); 73 | get onError(): SocketErrorHandler | null; 74 | set onError(value: SocketErrorHandler | null); 75 | get onMessage(): SocketMessageHandler | null; 76 | set onMessage(value: SocketMessageHandler | null); 77 | get onOpen(): SocketOpenHandler | null; 78 | set onOpen(value: SocketOpenHandler | null); 79 | isOpen(): boolean; 80 | connect(scheme: string, host: string, port: string, createStatus: boolean, token: string): void; 81 | close(): void; 82 | send(msg: any): void; 83 | } 84 | -------------------------------------------------------------------------------- /packages/nakama-js/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import "whatwg-fetch"; 18 | 19 | export * from "./client"; 20 | export * from "./session"; 21 | export * from "./socket"; 22 | export * from "./web_socket_adapter"; 23 | 24 | /** 25 | * Reexported due to duplicate definition of ChannelMessage in [Client]{@link ./client.ts} and [Session]{@link ./session.ts} 26 | */ 27 | export { ChannelMessage } from "./client"; 28 | -------------------------------------------------------------------------------- /packages/nakama-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heroiclabs/nakama-js", 3 | "version": "2.9.0", 4 | "scripts": { 5 | "build": "npx tsc && npx rollup -c --bundleConfigAsCjs && node build.mjs" 6 | }, 7 | "description": "JavaScript client for Nakama server written in TypeScript.", 8 | "main": "dist/nakama-js.cjs.js", 9 | "module": "dist/nakama-js.esm.mjs", 10 | "types": "dist/index.d.ts", 11 | "exports": { 12 | "./package.json": "./package.json", 13 | ".": { 14 | "types": "./dist/index.d.ts", 15 | "import": "./dist/nakama-js.esm.mjs", 16 | "require": "./dist/nakama-js.cjs.js" 17 | } 18 | }, 19 | "keywords": [ 20 | "app server", 21 | "client library", 22 | "game server", 23 | "nakama", 24 | "realtime", 25 | "realtime chat" 26 | ], 27 | "repository": "https://github.com/heroiclabs/nakama-js", 28 | "homepage": "https://heroiclabs.com", 29 | "bugs": "https://github.com/heroiclabs/nakama-js/issues", 30 | "author": "Chris Molozian ", 31 | "contributors": [ 32 | "Andrei Mihu ", 33 | "Mo Firouz " 34 | ], 35 | "license": "Apache-2.0", 36 | "dependencies": { 37 | "@scarf/scarf": "^1.4.0", 38 | "base64-arraybuffer": "^1.0.2", 39 | "js-base64": "^3.7.7", 40 | "whatwg-fetch": "^3.6.20" 41 | }, 42 | "devDependencies": { 43 | "@rollup/plugin-node-resolve": "^15.3.0", 44 | "@rollup/plugin-typescript": "^12.1.1", 45 | "rollup": "^4.27.4", 46 | "tslib": "^2.8.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/nakama-js/rollup.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Heroic Labs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Rollup is the legacy build system for nakama-js and is only used for cocos2d-x-js support. 18 | 19 | import typescript from '@rollup/plugin-typescript'; 20 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 21 | 22 | export default { 23 | input: './index.ts', 24 | output: { 25 | format: 'umd', 26 | name: 'nakamajs', 27 | dir: "dist", 28 | entryFileNames: "nakama-js.umd.js" // workaround for TS requirement that dir is specified in config 29 | }, 30 | plugins: [ 31 | typescript({ 32 | include: ["**/*.ts"], 33 | target: "es5" 34 | }), 35 | nodeResolve() 36 | ], 37 | moduleContext: { 38 | [require.resolve('whatwg-fetch')]: 'window' 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /packages/nakama-js/session.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | import * as base64 from "js-base64" 19 | 20 | /** A session authenticated for a user with Nakama server. */ 21 | export interface ISession { 22 | /** Claims */ 23 | /** The authorization token used to construct this session. */ 24 | token: string; 25 | /** If the user account for this session was just created. */ 26 | created: boolean 27 | /** The UNIX timestamp when this session was created. */ 28 | readonly created_at: number; 29 | /** The UNIX timestamp when this session will expire. */ 30 | expires_at?: number; 31 | /** The UNIX timestamp when the refresh token will expire. */ 32 | refresh_expires_at?: number; 33 | /** Refresh token that can be used for session token renewal. */ 34 | refresh_token: string; 35 | /** The username of the user who owns this session. */ 36 | username?: string; 37 | /** The ID of the user who owns this session. */ 38 | user_id?: string; 39 | /** Any custom properties associated with this session. */ 40 | vars?: object; 41 | 42 | /** Validate token */ 43 | /** If the session has expired. */ 44 | isexpired(currenttime: number): boolean; 45 | /** If the refresh token has expired. */ 46 | isrefreshexpired(currenttime: number): boolean; 47 | } 48 | 49 | export class Session implements ISession { 50 | 51 | token : string; 52 | readonly created_at: number; 53 | expires_at?: number; 54 | refresh_expires_at?: number; 55 | refresh_token: string; 56 | username?: string; 57 | user_id?: string; 58 | vars?: object; 59 | 60 | constructor( 61 | token: string, 62 | refresh_token: string, 63 | readonly created: boolean) { 64 | this.token = token; 65 | this.refresh_token = refresh_token; 66 | this.created_at = Math.floor(new Date().getTime() / 1000); 67 | this.update(token, refresh_token); 68 | } 69 | 70 | isexpired(currenttime: number): boolean { 71 | return (this.expires_at! - currenttime) < 0; 72 | } 73 | 74 | isrefreshexpired(currenttime: number): boolean { 75 | return (this.refresh_expires_at! - currenttime) < 0; 76 | } 77 | 78 | update(token: string, refreshToken: string) { 79 | const tokenDecoded = this.decodeJWT(token); 80 | const tokenExpiresAt = Math.floor(parseInt(tokenDecoded['exp'])); 81 | 82 | /** clients that have just updated to the refresh tokens */ 83 | /** client release will not have a cached refresh token */ 84 | if (refreshToken) { 85 | const refreshTokenDecoded = this.decodeJWT(refreshToken); 86 | const refreshTokenExpiresAt = Math.floor(parseInt(refreshTokenDecoded['exp'])); 87 | this.refresh_expires_at = refreshTokenExpiresAt; 88 | this.refresh_token = refreshToken; 89 | } 90 | 91 | this.token = token; 92 | this.expires_at = tokenExpiresAt; 93 | this.username = tokenDecoded['usn']; 94 | this.user_id = tokenDecoded['uid']; 95 | this.vars = tokenDecoded['vrs']; 96 | } 97 | 98 | decodeJWT(token: string) { 99 | const { 1: base64Raw } = token.split('.') 100 | const _base64 = base64Raw.replace(/-/g, '+').replace(/_/g, '/') 101 | const jsonPayload = decodeURIComponent(base64.atob(_base64).split('').map((c) => { 102 | return `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}` 103 | }).join('')) 104 | 105 | return JSON.parse(jsonPayload) 106 | } 107 | 108 | static restore(token: string, refreshToken: string): Session { 109 | return new Session(token, refreshToken, false); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /packages/nakama-js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": [ 4 | "./*", 5 | ], 6 | "compilerOptions": { 7 | "outDir": "dist" 8 | }, 9 | "declarationDir": "dist" 10 | } 11 | -------------------------------------------------------------------------------- /packages/nakama-js/utils.ts: -------------------------------------------------------------------------------- 1 | import {encode, decode} from "js-base64" 2 | 3 | export function buildFetchOptions(method: string, options: any, bodyJson: string) { 4 | const fetchOptions = {...{ method: method }, ...options}; 5 | fetchOptions.headers = {...options.headers}; 6 | 7 | if (typeof XMLHttpRequest !== "undefined") { 8 | const descriptor = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, "withCredentials"); 9 | 10 | // in Cocos Creator, XMLHttpRequest.withCredentials is not writable, so make the fetch 11 | // polyfill avoid writing to it. 12 | if (!descriptor?.set) { 13 | fetchOptions.credentials = 'cocos-ignore'; // string value is arbitrary, cannot be 'omit' or 'include 14 | } 15 | } 16 | 17 | if(!Object.keys(fetchOptions.headers).includes("Accept")) { 18 | fetchOptions.headers["Accept"] = "application/json"; 19 | } 20 | 21 | if(!Object.keys(fetchOptions.headers).includes("Content-Type")) { 22 | fetchOptions.headers["Content-Type"] = "application/json"; 23 | } 24 | 25 | Object.keys(fetchOptions.headers).forEach((key: string) => { 26 | if (!fetchOptions.headers[key]) { 27 | delete fetchOptions.headers[key]; 28 | } 29 | }); 30 | 31 | if (bodyJson) { 32 | fetchOptions.body = bodyJson; 33 | } 34 | 35 | return fetchOptions; 36 | } 37 | 38 | export function b64EncodeUnicode(str:string) { 39 | return encode(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, 40 | function toSolidBytes(_match:string, p1) { 41 | return String.fromCharCode(Number('0x' + p1)); 42 | })); 43 | } 44 | 45 | export function b64DecodeUnicode(str: string) { 46 | return decodeURIComponent(decode(str).split('').map(function(c) { 47 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); 48 | }).join('')); 49 | } 50 | -------------------------------------------------------------------------------- /packages/nakama-js/web_socket_adapter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { decode, encode } from "base64-arraybuffer"; 18 | import { btoa } from "js-base64" 19 | 20 | /** 21 | * An interface used by Nakama's web socket to determine the payload protocol. 22 | */ 23 | export interface WebSocketAdapter { 24 | 25 | /** 26 | * Dispatched when the web socket closes. 27 | */ 28 | onClose : SocketCloseHandler | null; 29 | 30 | /** 31 | * Dispatched when the web socket receives an error. 32 | */ 33 | onError : SocketErrorHandler | null; 34 | 35 | /** 36 | * Dispatched when the web socket receives a normal message. 37 | */ 38 | onMessage : SocketMessageHandler | null; 39 | 40 | /** 41 | * Dispatched when the web socket opens. 42 | */ 43 | onOpen : SocketOpenHandler | null; 44 | 45 | isOpen(): boolean; 46 | close() : void; 47 | connect(scheme: string, host: string, port : string, createStatus: boolean, token : string) : void; 48 | send(message: any) : void; 49 | } 50 | 51 | /** 52 | * SocketCloseHandler defines a lambda that handles WebSocket close events. 53 | */ 54 | export interface SocketCloseHandler { 55 | (this : WebSocket, evt: CloseEvent): void; 56 | } 57 | 58 | /** 59 | * SocketErrorHandler defines a lambda that handles responses from the server via WebSocket 60 | * that indicate an error. 61 | */ 62 | export interface SocketErrorHandler { 63 | (this : WebSocket, evt: Event): void; 64 | } 65 | 66 | /** 67 | * SocketMessageHandler defines a lambda that handles valid WebSocket messages. 68 | */ 69 | export interface SocketMessageHandler { 70 | (message: any): void; 71 | } 72 | 73 | /** 74 | * SocketOpenHandler defines a lambda that handles WebSocket open events. 75 | */ 76 | export interface SocketOpenHandler { 77 | (this : WebSocket, evt : Event) : void 78 | } 79 | 80 | /** 81 | * A text-based socket adapter that accepts and transmits payloads over UTF-8. 82 | */ 83 | export class WebSocketAdapterText implements WebSocketAdapter { 84 | private _socket?: WebSocket; 85 | 86 | get onClose(): SocketCloseHandler | null { 87 | return this._socket!.onclose; 88 | } 89 | 90 | set onClose(value: SocketCloseHandler | null) { 91 | this._socket!.onclose = value; 92 | } 93 | 94 | get onError(): SocketErrorHandler | null { 95 | return this._socket!.onerror; 96 | } 97 | 98 | set onError(value: SocketErrorHandler | null) { 99 | this._socket!.onerror = value; 100 | } 101 | 102 | get onMessage(): SocketMessageHandler | null { 103 | return this._socket!.onmessage; 104 | } 105 | 106 | set onMessage(value: SocketMessageHandler | null) { 107 | if (value) { 108 | this._socket!.onmessage = (evt: MessageEvent) => { 109 | const message: any = JSON.parse(evt.data); 110 | 111 | if (message.match_data && message.match_data.data) { 112 | message.match_data.data = new Uint8Array(decode(message.match_data.data)); 113 | } else if (message.party_data && message.party_data.data) { 114 | message.party_data.data = new Uint8Array(decode(message.party_data.data)); 115 | } 116 | 117 | value!(message); 118 | }; 119 | } 120 | else { 121 | value = null; 122 | } 123 | } 124 | 125 | get onOpen(): SocketOpenHandler | null { 126 | return this._socket!.onopen; 127 | } 128 | 129 | set onOpen(value: SocketOpenHandler | null) { 130 | this._socket!.onopen = value; 131 | } 132 | 133 | isOpen(): boolean { 134 | return this._socket?.readyState == WebSocket.OPEN; 135 | } 136 | 137 | connect(scheme: string, host: string, port: string, createStatus: boolean, token: string): void { 138 | const url = `${scheme}${host}:${port}/ws?lang=en&status=${encodeURIComponent(createStatus.toString())}&token=${encodeURIComponent(token)}`; 139 | this._socket = new WebSocket(url); 140 | } 141 | 142 | close() { 143 | this._socket!.close(); 144 | this._socket = undefined; 145 | } 146 | 147 | send(msg: any): void { 148 | if (msg.match_data_send) { 149 | // according to protobuf docs, int64 is encoded to JSON as string. 150 | msg.match_data_send.op_code = msg.match_data_send.op_code.toString(); 151 | let payload = msg.match_data_send.data; 152 | if (payload && payload instanceof Uint8Array) { 153 | msg.match_data_send.data = encode(payload.buffer); 154 | } else if (payload) { // it's a string 155 | msg.match_data_send.data = btoa(payload); 156 | } 157 | } else if (msg.party_data_send) { 158 | // according to protobuf docs, int64 is encoded to JSON as string. 159 | msg.party_data_send.op_code = msg.party_data_send.op_code.toString(); 160 | let payload = msg.party_data_send.data; 161 | if (payload && payload instanceof Uint8Array) { 162 | msg.party_data_send.data = encode(payload.buffer); 163 | } else if (payload) { // it's a string 164 | msg.party_data_send.data = btoa(payload); 165 | } 166 | } 167 | 168 | this._socket!.send(JSON.stringify(msg)); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /packages/satori-js-test/client.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Page} from "puppeteer" 18 | import * as satorijs from "@heroiclabs/satori-js"; 19 | import {createPage} from "./utils" 20 | import {describe, expect, it} from '@jest/globals' 21 | 22 | describe('Client Tests', () => { 23 | 24 | it('should create object with defaults', async () => { 25 | const page : Page = await createPage(); 26 | 27 | const client = await page.evaluate(() => { 28 | return new satorijs.Client(); 29 | }); 30 | 31 | expect(client).not.toBeNull(); 32 | expect(client.serverkey).toBe("defaultkey"); 33 | expect(client.host).toBe("127.0.0.1"); 34 | expect(client.port).toBe("7350"); 35 | expect(client.useSSL).toBe(false); 36 | expect(client.timeout).toBe(7000); 37 | }); 38 | 39 | it('should create object with configuration', async () => { 40 | const page : Page = await createPage(); 41 | 42 | const SERVER_KEY = "somesecret!"; 43 | const HOST = "127.0.0.2"; 44 | const PORT = "8080"; 45 | const SSL = true; 46 | const TIMEOUT = 8000; 47 | 48 | const client = await page.evaluate((SERVER_KEY, HOST, PORT, SSL, TIMEOUT) => { 49 | return new satorijs.Client(SERVER_KEY, HOST, PORT, SSL, TIMEOUT); 50 | }, SERVER_KEY, HOST, PORT, SSL, TIMEOUT); 51 | 52 | expect(client).not.toBeNull(); 53 | expect(client.serverkey).toBe(SERVER_KEY); 54 | expect(client.host).toBe(HOST); 55 | expect(client.port).toBe(PORT); 56 | expect(client.useSSL).toBe(SSL); 57 | expect(client.timeout).toBe(TIMEOUT); 58 | }); 59 | 60 | it('should obey timeout configuration option', async () => { 61 | const page : Page = await createPage(); 62 | 63 | const err = await page.evaluate(() => { 64 | const client = new satorijs.Client("defaultkey", "127.0.0.1", "7350", false, 0); 65 | return client.authenticate("timeoutuseridentifier") 66 | .catch(err => err); 67 | }); 68 | 69 | expect(err).not.toBeNull(); 70 | expect(err).toBe("Request timed out."); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /packages/satori-js-test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Satori JS Browser Test 7 | 8 | 9 | 10 | 11 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /packages/satori-js-test/jest.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('merge') 2 | const ts_preset = require('ts-jest/jest-preset') 3 | const puppeteer_preset = require('jest-puppeteer/jest-preset') 4 | 5 | //use multiple jest presets by merging and exporting them as a single object 6 | module.exports = merge.recursive(ts_preset, puppeteer_preset, { 7 | globals: { 8 | 'ts-jest': { 9 | tsConfig: 'tsconfig.test.json' 10 | } 11 | } 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /packages/satori-js-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heroiclabs/satori-js-test", 3 | "version": "1.0.0", 4 | "description": "A subproject that houses tests for Satori JS", 5 | "private": true, 6 | "license": "Apache-2.0", 7 | "dependencies": { 8 | "@heroiclabs/satori-js": "file:../satori-js", 9 | "base64url": "3.0.1" 10 | }, 11 | "scripts": { 12 | "test": "npx typescript --project tsconfig.test.json && jest --runInBand" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/satori-js-test/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "es2015", 6 | "es2016", 7 | "es2017" 8 | ], 9 | "target": "es2018", 10 | "module": "es2015", 11 | "removeComments": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "moduleResolution": "node", 14 | "noEmitHelpers": false, 15 | "importHelpers": false, 16 | "baseUrl": "./", 17 | "noEmit": true, 18 | "allowSyntheticDefaultImports": true 19 | }, 20 | "include": [ 21 | "./*", 22 | ], 23 | "exclude": [] 24 | } 25 | -------------------------------------------------------------------------------- /packages/satori-js-test/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Page, Browser} from "puppeteer"; 18 | const fs = require("fs"); 19 | const crypto = require("crypto"); 20 | const base64url = require("base64url"); 21 | 22 | // automatically assigned by puppeteer + Jest 23 | declare var browser : Browser; 24 | 25 | // automatically assigned by puppeteer + Jest 26 | declare var browser : Browser; 27 | 28 | // util to generate a random id. 29 | export function generateid(): string { 30 | const arr: string[] = []; 31 | 32 | for (let i: number = 0; i < 30; i++) { 33 | arr.push(Math.random().toString(36)[3]); 34 | } 35 | 36 | return arr.join(""); 37 | }; 38 | 39 | export async function createPage(): Promise { 40 | 41 | const page = await browser.newPage(); 42 | 43 | page.on('console', msg => console.log('LOG:', msg.text())); 44 | page.on('error', handlePageError); 45 | page.on('pageerror', handlePageError); 46 | 47 | const nakamaJsLib = fs.readFileSync(__dirname + '/../nakama-js/dist/nakama-js.iife.js', 'utf8'); 48 | const nakamaJsProtobufLib = fs.readFileSync(__dirname + '/../nakama-js-protobuf/dist/nakama-js-protobuf.iife.js', 'utf8'); 49 | 50 | await page.evaluateOnNewDocument(nakamaJsLib); 51 | await page.evaluateOnNewDocument(nakamaJsProtobufLib); 52 | await page.evaluateOnNewDocument(() => { 53 | globalThis.timeoutPromise = function(ms) { 54 | return new Promise(resolve => setTimeout(resolve, ms)); 55 | } 56 | }) 57 | 58 | await page.goto('about:blank'); 59 | 60 | return page; 61 | } 62 | 63 | function handlePageError(err) { 64 | 65 | let msg: string; 66 | 67 | if (err instanceof Object) { 68 | msg = JSON.stringify(err); 69 | } 70 | else { 71 | msg = err; 72 | } 73 | 74 | console.error('ERR:', msg); 75 | } 76 | 77 | export const enum AdapterType { 78 | Text = 0, 79 | Protobuf = 1 80 | } 81 | 82 | export const adapters = [AdapterType.Text, AdapterType.Protobuf]; 83 | 84 | export function createFacebookInstantGameAuthToken(id : string) : string { 85 | const testSecret = "fb-instant-test-secret"; 86 | 87 | const mockFbInstantPayload = JSON.stringify({ 88 | algorithm: "HMAC-SHA256", 89 | issued_at: 1594867628, 90 | player_id: id, 91 | request_payload: "" 92 | }); 93 | 94 | const encodedPayload = base64url(mockFbInstantPayload); 95 | 96 | const signature = crypto.createHmac('sha256', testSecret).update(encodedPayload).digest(); 97 | const encodedSignature = base64url(signature); 98 | 99 | const token = encodedSignature + "." + encodedPayload; 100 | return token; 101 | } 102 | 103 | export const matchmakerTimeout = 20000; 104 | -------------------------------------------------------------------------------- /packages/satori-js/README.md: -------------------------------------------------------------------------------- 1 | Satori JavaScript Client 2 | ======================== 3 | 4 | > JavaScript client for Satori server written in TypeScript. For browser and React Native projects. 5 | 6 | This client implements the full API for interacting with Satori server. It's written in TypeScript with minimal dependencies to be compatible with all modern browsers and React Native. 7 | 8 | Full documentation is online - https://heroiclabs.com/docs/javascript-client-guide 9 | 10 | ## Getting Started 11 | 12 | You'll need access to an instance of the Satori server before you can connect with the client. 13 | 14 | 1. Import the client into your project. It's [available on NPM](https://www.npmjs/package/@heroiclabs/satori-js). 15 | 16 | ```shell 17 | npm install @heroiclabs/satori-js 18 | ``` 19 | 20 | You'll now see the code in the "node_modules" folder and package listed in your "package.json". 21 | 22 | 2. Use the connection credentials to build a client object. 23 | 24 | ```js 25 | import {Client} from "@heroiclabs/satori-js"; 26 | 27 | const useSSL = false; 28 | const client = new Client("apiKey", "127.0.0.1", 7450, useSSL); 29 | ``` 30 | 31 | ## Usage 32 | 33 | The client object has many method to execute various features in the server. 34 | 35 | ### Authenticate 36 | 37 | To authenticate with the Satori server you must provide an identifier for the user. 38 | 39 | ```js 40 | const userId = ""; 41 | 42 | client.authenticate(userId) 43 | .then(session => { 44 | _session = session; 45 | console.info("Authenticated:", session); 46 | }).catch(error => { 47 | console.error("Error:", error); 48 | }); 49 | ``` 50 | 51 | ### Sessions 52 | 53 | When authenticated the server responds with an auth token (JWT) which contains useful properties and gets deserialized into a `Session` object. 54 | 55 | ```js 56 | console.info(session.token); // raw JWT token 57 | console.info(session.refreshToken); // refresh token 58 | console.info("Session has expired?", session.isexpired(Date.now() / 1000)); 59 | const expiresAt = session.expires_at; 60 | console.warn("Session will expire at:", new Date(expiresAt * 1000).toISOString()); 61 | ``` 62 | 63 | It is recommended to store the auth token from the session and check at startup if it has expired. If the token has expired you must reauthenticate. The expiry time of the token can be changed as a setting in the server. 64 | 65 | ```js 66 | // Assume we've stored the auth token in browser Web Storage. 67 | const authtoken = window.localStorage.getItem("satori_authtoken"); 68 | const refreshtoken = window.localStorage.getItem("satori_refreshtoken"); 69 | 70 | let session = satorijs.Session.restore(authtoken, refreshtoken); 71 | 72 | // Check whether a session is close to expiry. 73 | 74 | const unixTimeInFuture = Date.now() + 8.64e+7; // one day from now 75 | 76 | if (session.isexpired(unixTimeInFuture / 1000)) { 77 | try 78 | { 79 | session = await client.sessionRefresh(session); 80 | } 81 | catch (e) 82 | { 83 | console.info("Session can no longer be refreshed. Must reauthenticate!"); 84 | } 85 | } 86 | ``` 87 | 88 | ### Requests 89 | 90 | The client includes lots of builtin APIs for various featyures of the Satori server. These can be accessed with the methods which return Promise objects. 91 | 92 | Most requests are sent with a session object which authorizes the client. 93 | 94 | ```js 95 | const flags = await client.getFlags(session); 96 | console.info("Flags:", flags); 97 | ``` 98 | 99 | ## Contribute 100 | 101 | The development roadmap is managed as GitHub issues and pull requests are welcome. If you're interested in enhancing the code please open an issue to discuss the changes or drop in and discuss it in the [community forum](https://forum.heroiclabs.com). 102 | 103 | ### Source Builds 104 | 105 | Ensure you are using Node v18>. 106 | 107 | The codebase is multi-package monorepo written in TypeScript and can be built with [esbuild](https://github.com/evanw/esbuild). All dependencies are managed with Yarn. 108 | 109 | To build from source, install dependencies and build the `satori-js` package: 110 | 111 | ```shell 112 | npm install --workspace=@heroiclabs/satori-js && npm run build --workspace=@heroiclabs/satori-js 113 | ``` 114 | 115 | ### Run Tests 116 | 117 | To run tests you will need access to an instance of the Satori server. 118 | 119 | Tests are run against each workspace bundle; if you have made source code changes, you should `npm run build --workspace=` prior to running tests. 120 | 121 | ```shell 122 | npm run test --workspace=@heroiclabs/satori-js-test 123 | ``` 124 | 125 | ### Release Process 126 | 127 | To release onto NPM if you have access to the "@heroiclabs" organization you can use NPM. 128 | 129 | ```shell 130 | npm run build --workspace= && npm publish --access=public --workspace= 131 | ``` 132 | 133 | ### Generate Docs 134 | 135 | API docs are generated with typedoc and deployed to GitHub pages. 136 | 137 | To run typedoc: 138 | 139 | ``` 140 | npm install && npm run docs 141 | ``` 142 | 143 | ### License 144 | 145 | This project is licensed under the [Apache-2 License](https://github.com/heroiclabs/nakama-js/blob/master/LICENSE). -------------------------------------------------------------------------------- /packages/satori-js/build.mjs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Nakama Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import esbuild from 'esbuild'; 16 | 17 | // Shared esbuild config 18 | const config = { 19 | logLevel: 'info', 20 | entryPoints: ['index.ts'], 21 | bundle: true, 22 | target: 'es6', 23 | globalName: 'satorijs' 24 | }; 25 | 26 | // Build CommonJS 27 | await esbuild.build({ 28 | ...config, 29 | format: 'cjs', 30 | outfile: 'dist/satori-js.cjs.js' 31 | }); 32 | 33 | // Build ESM 34 | await esbuild.build({ 35 | ...config, 36 | format: 'esm', 37 | outfile: 'dist/satori-js.esm.mjs' 38 | }); 39 | 40 | // Build IIFE 41 | await esbuild.build({ 42 | ...config, 43 | format: 'iife', 44 | outfile: 'dist/satori-js.iife.js' 45 | }); -------------------------------------------------------------------------------- /packages/satori-js/dist/api.gen.d.ts: -------------------------------------------------------------------------------- 1 | /** Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user. */ 2 | export interface ApiAuthenticateLogoutRequest { 3 | refresh_token?: string; 4 | token?: string; 5 | } 6 | /** Authenticate against the server with a refresh token. */ 7 | export interface ApiAuthenticateRefreshRequest { 8 | refresh_token?: string; 9 | } 10 | /** */ 11 | export interface ApiAuthenticateRequest { 12 | custom?: Record; 13 | default?: Record; 14 | id?: string; 15 | } 16 | /** A single event. Usually, but not necessarily, part of a batch. */ 17 | export interface ApiEvent { 18 | id?: string; 19 | metadata?: Record; 20 | name?: string; 21 | timestamp?: string; 22 | value?: string; 23 | } 24 | /** */ 25 | export interface ApiEventRequest { 26 | events?: Array; 27 | } 28 | /** An experiment that this user is partaking. */ 29 | export interface ApiExperiment { 30 | name?: string; 31 | value?: string; 32 | } 33 | /** All experiments that this identity is involved with. */ 34 | export interface ApiExperimentList { 35 | experiments?: Array; 36 | } 37 | /** Feature flag available to the identity. */ 38 | export interface ApiFlag { 39 | condition_changed?: boolean; 40 | name?: string; 41 | value?: string; 42 | } 43 | /** */ 44 | export interface ApiFlagList { 45 | flags?: Array; 46 | } 47 | /** A response containing all the messages for an identity. */ 48 | export interface ApiGetMessageListResponse { 49 | cacheable_cursor?: string; 50 | messages?: Array; 51 | next_cursor?: string; 52 | prev_cursor?: string; 53 | } 54 | /** Enrich/replace the current session with a new ID. */ 55 | export interface ApiIdentifyRequest { 56 | custom?: Record; 57 | default?: Record; 58 | id?: string; 59 | } 60 | /** A single live event. */ 61 | export interface ApiLiveEvent { 62 | active_end_time_sec?: string; 63 | active_start_time_sec?: string; 64 | description?: string; 65 | id?: string; 66 | name?: string; 67 | value?: string; 68 | } 69 | /** List of Live events. */ 70 | export interface ApiLiveEventList { 71 | live_events?: Array; 72 | } 73 | /** A scheduled message. */ 74 | export interface ApiMessage { 75 | consume_time?: string; 76 | create_time?: string; 77 | metadata?: Record; 78 | read_time?: string; 79 | schedule_id?: string; 80 | send_time?: string; 81 | text?: string; 82 | update_time?: string; 83 | } 84 | /** Properties associated with an identity. */ 85 | export interface ApiProperties { 86 | computed?: Record; 87 | custom?: Record; 88 | default?: Record; 89 | } 90 | /** A session. */ 91 | export interface ApiSession { 92 | properties?: ApiProperties; 93 | refresh_token?: string; 94 | token?: string; 95 | } 96 | /** The request to update the status of a message. */ 97 | export interface ApiUpdateMessageRequest { 98 | consume_time?: string; 99 | id?: string; 100 | read_time?: string; 101 | } 102 | /** Update Properties associated with this identity. */ 103 | export interface ApiUpdatePropertiesRequest { 104 | custom?: Record; 105 | default?: Record; 106 | recompute?: boolean; 107 | } 108 | /** */ 109 | export interface ProtobufAny { 110 | type_url?: string; 111 | value?: string; 112 | } 113 | /** */ 114 | export interface RpcStatus { 115 | code?: number; 116 | details?: Array; 117 | message?: string; 118 | } 119 | export declare class SatoriApi { 120 | readonly apiKey: string; 121 | readonly basePath: string; 122 | readonly timeoutMs: number; 123 | constructor(apiKey: string, basePath: string, timeoutMs: number); 124 | /** A healthcheck which load balancers can use to check the service. */ 125 | satoriHealthcheck(bearerToken: string, options?: any): Promise; 126 | /** A readycheck which load balancers can use to check the service. */ 127 | satoriReadycheck(bearerToken: string, options?: any): Promise; 128 | /** Authenticate against the server. */ 129 | satoriAuthenticate(basicAuthUsername: string, basicAuthPassword: string, body: ApiAuthenticateRequest, options?: any): Promise; 130 | /** Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user. */ 131 | satoriAuthenticateLogout(bearerToken: string, body: ApiAuthenticateLogoutRequest, options?: any): Promise; 132 | /** Refresh a user's session using a refresh token retrieved from a previous authentication request. */ 133 | satoriAuthenticateRefresh(basicAuthUsername: string, basicAuthPassword: string, body: ApiAuthenticateRefreshRequest, options?: any): Promise; 134 | /** Publish an event for this session. */ 135 | satoriEvent(bearerToken: string, body: ApiEventRequest, options?: any): Promise; 136 | /** Get or list all available experiments for this identity. */ 137 | satoriGetExperiments(bearerToken: string, names?: Array, options?: any): Promise; 138 | /** List all available flags for this identity. */ 139 | satoriGetFlags(bearerToken: string, basicAuthUsername: string, basicAuthPassword: string, names?: Array, options?: any): Promise; 140 | /** Enrich/replace the current session with new identifier. */ 141 | satoriIdentify(bearerToken: string, body: ApiIdentifyRequest, options?: any): Promise; 142 | /** Delete the caller's identity and associated data. */ 143 | satoriDeleteIdentity(bearerToken: string, options?: any): Promise; 144 | /** List available live events. */ 145 | satoriGetLiveEvents(bearerToken: string, names?: Array, options?: any): Promise; 146 | /** Get the list of messages for the identity. */ 147 | satoriGetMessageList(bearerToken: string, limit?: number, forward?: boolean, cursor?: string, options?: any): Promise; 148 | /** Deletes a message for an identity. */ 149 | satoriDeleteMessage(bearerToken: string, id: string, options?: any): Promise; 150 | /** Updates a message for an identity. */ 151 | satoriUpdateMessage(bearerToken: string, id: string, body: ApiUpdateMessageRequest, options?: any): Promise; 152 | /** List properties associated with this identity. */ 153 | satoriListProperties(bearerToken: string, options?: any): Promise; 154 | /** Update identity properties. */ 155 | satoriUpdateProperties(bearerToken: string, body: ApiUpdatePropertiesRequest, options?: any): Promise; 156 | buildFullUrl(basePath: string, fragment: string, queryParams: Map): string; 157 | } 158 | -------------------------------------------------------------------------------- /packages/satori-js/dist/client.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { SatoriApi, ApiEvent } from "./api.gen"; 17 | import { Session } from "./session"; 18 | /** A client for Satori server. */ 19 | export declare class Client { 20 | readonly apiKey: string; 21 | readonly host: string; 22 | readonly port: string; 23 | readonly useSSL: boolean; 24 | readonly timeout: number; 25 | readonly autoRefreshSession: boolean; 26 | /** The expired timespan used to check session lifetime. */ 27 | expiredTimespanMs: number; 28 | /** The low level API client for Nakama server. */ 29 | readonly apiClient: SatoriApi; 30 | constructor(apiKey?: string, host?: string, port?: string, useSSL?: boolean, timeout?: number, autoRefreshSession?: boolean); 31 | /** Authenticate a user with an ID against the server. */ 32 | authenticate(id: string, customProperties?: Record, defaultProperties?: Record): Promise; 33 | /** Refresh a user's session using a refresh token retrieved from a previous authentication request. */ 34 | sessionRefresh(session: Session): Promise; 35 | /** Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user. */ 36 | logout(session: Session): Promise; 37 | /** Publish an event for this session. */ 38 | event(session: Session, event: ApiEvent): Promise; 39 | /** Publish multiple events for this session */ 40 | events(session: Session, events: Array): Promise; 41 | /** Get or list all available experiments for this identity. */ 42 | getExperiments(session: Session, names?: Array): Promise; 43 | /** Get a single flag for this identity. Throws an error when the flag does not exist. */ 44 | getFlag(session: Session, name: string): Promise; 45 | /** Get a single flag for this identity. */ 46 | getFlagWithFallback(session: Session, name: string, fallbackValue?: string): Promise<{ 47 | name: string; 48 | value: string | undefined; 49 | }>; 50 | /** Get a single flag with its configured default value. Throws an error when the flag does not exist. */ 51 | getFlagDefault(name: string): Promise; 52 | /** Get a single flag with its configured default value. */ 53 | getFlagDefaultWithFallback(name: string, fallbackValue?: string): Promise<{ 54 | name: string; 55 | value: string | undefined; 56 | }>; 57 | /** List all available flags for this identity. */ 58 | getFlags(session: Session, names?: Array): Promise; 59 | /** List all available default flags. */ 60 | getFlagsDefault(names?: Array): Promise; 61 | /** Enrich/replace the current session with new identifier. */ 62 | identify(session: Session, id: string, defaultProperties?: Record, customProperties?: Record): Promise; 63 | /** List available live events. */ 64 | getLiveEvents(session: Session, names?: Array): Promise; 65 | /** List properties associated with this identity. */ 66 | listProperties(session: Session): Promise; 67 | /** Update identity properties. */ 68 | updateProperties(session: Session, defaultProperties?: Record, customProperties?: Record, recompute?: boolean): Promise; 69 | /** Delete the caller's identity and associated data. */ 70 | deleteIdentity(session: Session): Promise; 71 | getMessageList(session: Session): Promise; 72 | deleteMessage(session: Session, id: string): Promise; 73 | updateMessage(session: Session, id: string, consume_time?: string, read_time?: string): Promise; 74 | } 75 | -------------------------------------------------------------------------------- /packages/satori-js/dist/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import "whatwg-fetch"; 17 | export * from "./client"; 18 | export * from "./session"; 19 | -------------------------------------------------------------------------------- /packages/satori-js/dist/session.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** A session authenticated for a user with Satori server. */ 17 | export interface ISession { 18 | /** Claims */ 19 | /** The authorization token used to construct this session. */ 20 | token: string; 21 | /** The UNIX timestamp when this session was created. */ 22 | readonly created_at: number; 23 | /** The UNIX timestamp when this session will expire. */ 24 | expires_at?: number; 25 | /** The UNIX timestamp when the refresh token will expire. */ 26 | refresh_expires_at?: number; 27 | /** Refresh token that can be used for session token renewal. */ 28 | refresh_token: string; 29 | /** The ID of the user who owns this session. */ 30 | user_id?: string; 31 | /** Any custom properties associated with this session. */ 32 | vars?: object; 33 | /** Validate token */ 34 | /** If the session has expired. */ 35 | isexpired(currenttime: number): boolean; 36 | /** If the refresh token has expired. */ 37 | isrefreshexpired(currenttime: number): boolean; 38 | } 39 | export declare class Session implements ISession { 40 | token: string; 41 | readonly created_at: number; 42 | expires_at?: number; 43 | refresh_expires_at?: number; 44 | refresh_token: string; 45 | user_id?: string; 46 | vars?: object; 47 | constructor(token: string, refresh_token: string); 48 | isexpired(currenttime: number): boolean; 49 | isrefreshexpired(currenttime: number): boolean; 50 | update(token: string, refreshToken: string): void; 51 | decodeJWT(token: string): any; 52 | static restore(token: string, refreshToken: string): Session; 53 | } 54 | -------------------------------------------------------------------------------- /packages/satori-js/dist/utils.d.ts: -------------------------------------------------------------------------------- 1 | export declare function buildFetchOptions(method: string, options: any, bodyJson: string): any; 2 | export declare function b64EncodeUnicode(str: string): string; 3 | export declare function b64DecodeUnicode(str: string): string; 4 | -------------------------------------------------------------------------------- /packages/satori-js/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import "whatwg-fetch"; 18 | 19 | export * from "./client"; 20 | export * from "./session"; -------------------------------------------------------------------------------- /packages/satori-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@heroiclabs/satori-js", 3 | "version": "2.9.0", 4 | "scripts": { 5 | "build": "npx tsc && npx rollup -c --bundleConfigAsCjs && node build.mjs", 6 | "docs": "typedoc index.ts --gaID UA-89839802-1 --out ../../docs" 7 | }, 8 | "description": "JavaScript client for Satori server written in TypeScript.", 9 | "main": "dist/satori-js.cjs.js", 10 | "module": "dist/satori-js.esm.mjs", 11 | "types": "dist/index.d.ts", 12 | "exports": { 13 | "./package.json": "./package.json", 14 | ".": { 15 | "types": "./dist/index.d.ts", 16 | "import": "./dist/satori-js.esm.mjs", 17 | "require": "./dist/satori-js.cjs.js" 18 | }, 19 | "./api": { 20 | "types": "./dist/api.gen.d.ts" 21 | } 22 | }, 23 | "keywords": [ 24 | "app server", 25 | "client library", 26 | "game server", 27 | "satori", 28 | "live ops" 29 | ], 30 | "repository": "https://github.com/heroiclabs/nakama-js", 31 | "homepage": "https://heroiclabs.com", 32 | "bugs": "https://github.com/heroiclabs/nakama-js/issues", 33 | "author": "Chris Molozian ", 34 | "contributors": [ 35 | "Andrei Mihu ", 36 | "Mo Firouz " 37 | ], 38 | "license": "Apache-2.0", 39 | "devDependencies": { 40 | "@rollup/plugin-node-resolve": "^15.3.0", 41 | "@rollup/plugin-typescript": "^12.1.1", 42 | "rollup": "^4.27.4", 43 | "tslib": "^2.8.1" 44 | }, 45 | "dependencies": { 46 | "@scarf/scarf": "^1.4.0", 47 | "base64-arraybuffer": "^1.0.2", 48 | "js-base64": "^3.7.7", 49 | "whatwg-fetch": "^3.6.20" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/satori-js/rollup.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Heroic Labs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Rollup is the legacy build system for nakama-js and is only used for cocos2d-x-js support. 18 | 19 | import typescript from '@rollup/plugin-typescript'; 20 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 21 | 22 | export default { 23 | input: './index.ts', 24 | output: { 25 | format: 'umd', 26 | name: 'satorijs', 27 | dir: "dist", 28 | entryFileNames: "satori-js.umd.js" // workaround for TS requirement that dir is specified in config 29 | }, 30 | plugins: [ 31 | typescript({ 32 | include: ["**/*.ts"], 33 | target: "es5" 34 | }), 35 | nodeResolve() 36 | ], 37 | moduleContext: { 38 | [require.resolve('whatwg-fetch')]: 'window' 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /packages/satori-js/session.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 The Nakama Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | import * as base64 from "js-base64" 19 | 20 | /** A session authenticated for a user with Satori server. */ 21 | export interface ISession { 22 | /** Claims */ 23 | /** The authorization token used to construct this session. */ 24 | token: string; 25 | /** The UNIX timestamp when this session was created. */ 26 | readonly created_at: number; 27 | /** The UNIX timestamp when this session will expire. */ 28 | expires_at?: number; 29 | /** The UNIX timestamp when the refresh token will expire. */ 30 | refresh_expires_at?: number; 31 | /** Refresh token that can be used for session token renewal. */ 32 | refresh_token: string; 33 | /** The ID of the user who owns this session. */ 34 | user_id?: string; 35 | /** Any custom properties associated with this session. */ 36 | vars?: object; 37 | 38 | /** Validate token */ 39 | /** If the session has expired. */ 40 | isexpired(currenttime: number): boolean; 41 | /** If the refresh token has expired. */ 42 | isrefreshexpired(currenttime: number): boolean; 43 | } 44 | 45 | export class Session implements ISession { 46 | 47 | token : string; 48 | readonly created_at: number; 49 | expires_at?: number; 50 | refresh_expires_at?: number; 51 | refresh_token: string; 52 | user_id?: string; 53 | vars?: object; 54 | 55 | constructor( 56 | token: string, 57 | refresh_token: string) { 58 | this.token = token; 59 | this.refresh_token = refresh_token; 60 | this.created_at = Math.floor(new Date().getTime() / 1000); 61 | this.update(token, refresh_token); 62 | } 63 | 64 | isexpired(currenttime: number): boolean { 65 | return (this.expires_at! - currenttime) < 0; 66 | } 67 | 68 | isrefreshexpired(currenttime: number): boolean { 69 | return (this.refresh_expires_at! - currenttime) < 0; 70 | } 71 | 72 | update(token: string, refreshToken: string) { 73 | const tokenDecoded = this.decodeJWT(token); 74 | const tokenExpiresAt = Math.floor(parseInt(tokenDecoded['exp'])); 75 | 76 | /** clients that have just updated to the refresh tokens */ 77 | /** client release will not have a cached refresh token */ 78 | if (refreshToken) { 79 | const refreshTokenDecoded = this.decodeJWT(refreshToken); 80 | const refreshTokenExpiresAt = Math.floor(parseInt(refreshTokenDecoded['exp'])); 81 | this.refresh_expires_at = refreshTokenExpiresAt; 82 | this.refresh_token = refreshToken; 83 | } 84 | 85 | this.token = token; 86 | this.expires_at = tokenExpiresAt; 87 | this.user_id = tokenDecoded['uid']; 88 | this.vars = tokenDecoded['vrs']; 89 | } 90 | 91 | decodeJWT(token: string) { 92 | const { 1: base64Raw } = token.split('.') 93 | const _base64 = base64Raw.replace(/-/g, '+').replace(/_/g, '/') 94 | const jsonPayload = decodeURIComponent(base64.atob(_base64).split('').map((c) => { 95 | return `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}` 96 | }).join('')) 97 | 98 | return JSON.parse(jsonPayload) 99 | } 100 | 101 | static restore(token: string, refreshToken: string): Session { 102 | return new Session(token, refreshToken); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/satori-js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": [ 4 | "./*", 5 | ], 6 | "compilerOptions": { 7 | "outDir": "dist" 8 | }, 9 | "declarationDir": "dist" 10 | } 11 | -------------------------------------------------------------------------------- /packages/satori-js/utils.ts: -------------------------------------------------------------------------------- 1 | import {encode, decode} from "js-base64" 2 | 3 | export function buildFetchOptions(method: string, options: any, bodyJson: string) { 4 | const fetchOptions = {...{ method: method }, ...options}; 5 | fetchOptions.headers = {...options.headers}; 6 | 7 | const descriptor = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, "withCredentials"); 8 | 9 | // in Cocos Creator, XMLHttpRequest.withCredentials is not writable, so make the fetch 10 | // polyfill avoid writing to it. 11 | if (!descriptor?.set) { 12 | fetchOptions.credentials = 'cocos-ignore'; // string value is arbitrary, cannot be 'omit' or 'include 13 | } 14 | 15 | if(!Object.keys(fetchOptions.headers).includes("Accept")) { 16 | fetchOptions.headers["Accept"] = "application/json"; 17 | } 18 | 19 | if(!Object.keys(fetchOptions.headers).includes("Content-Type")) { 20 | fetchOptions.headers["Content-Type"] = "application/json"; 21 | } 22 | 23 | Object.keys(fetchOptions.headers).forEach((key: string) => { 24 | if (!fetchOptions.headers[key]) { 25 | delete fetchOptions.headers[key]; 26 | } 27 | }); 28 | 29 | if (bodyJson) { 30 | fetchOptions.body = bodyJson; 31 | } 32 | 33 | return fetchOptions; 34 | } 35 | 36 | export function b64EncodeUnicode(str:string) { 37 | return encode(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, 38 | function toSolidBytes(_match:string, p1) { 39 | return String.fromCharCode(Number('0x' + p1)); 40 | })); 41 | } 42 | 43 | export function b64DecodeUnicode(str: string) { 44 | return decodeURIComponent(decode(str).split('').map(function(c) { 45 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); 46 | }).join('')); 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "es2015", 6 | "es2016" 7 | ], 8 | "target": "es6", 9 | "module": "es2015", 10 | "declaration": true, 11 | "emitDeclarationOnly": true, 12 | "sourceMap": true, 13 | "removeComments": false, 14 | "downlevelIteration": true, 15 | "noImplicitAny": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "allowSyntheticDefaultImports": true, 22 | "moduleResolution": "node", 23 | "baseUrl": "./", 24 | "strict": true, 25 | "strictNullChecks": true, 26 | "strictFunctionTypes": true, 27 | "alwaysStrict": true, 28 | "skipLibCheck": true 29 | }, 30 | "exclude": [ 31 | "node_modules" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [ 4 | "packages/nakama-js/**/*", 5 | "packages/satori-js/**/*" 6 | ], 7 | "exclude": [ 8 | "node_modules" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:latest" 5 | ], 6 | "jsRules": {}, 7 | "rules": {}, 8 | "rulesDirectory": [] 9 | } 10 | --------------------------------------------------------------------------------