├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
└── workflows
│ ├── codeql.yml
│ ├── comment-issue.yml
│ ├── lint.yml
│ └── testsuite.yml
├── .gitignore
├── .jsbeautifyrc
├── .vscode
├── settings.json
└── typings
│ └── meteor.d.ts
├── Contributing.md
├── History.md
├── LICENSE
├── README.md
├── client
└── monitor.js
├── demo
├── .gitignore
├── .meteor
│ ├── .finished-upgraders
│ ├── .gitignore
│ ├── .id
│ ├── identifier
│ ├── packages
│ ├── platforms
│ ├── release
│ └── versions
├── README.md
├── client
│ ├── main.css
│ ├── main.html
│ └── main.js
├── deploy.sh
├── package-lock.json
├── package.json
├── packages
│ ├── .gitignore
│ └── meteor-user-status
│ │ ├── client
│ │ └── monitor.js
│ │ ├── package.js
│ │ └── server
│ │ └── status.js
├── server
│ └── main.js
└── settings-example.json
├── docs
└── example.png
├── package-lock.json
├── package.js
├── package.json
├── server
└── status.js
└── tests
├── client_tests.js
├── insecure_login.js
├── monitor_tests.js
├── server_tests.js
├── setup.js
└── status_tests.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 | indent_style = space
11 | indent_size = 2
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | packages
3 | phantom_runner.js
4 | start_test.js
5 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "commonjs": true,
5 | "es6": true
6 | },
7 | "extends": "eslint:recommended",
8 | "globals": {
9 | "Atomics": "readonly",
10 | "SharedArrayBuffer": "readonly"
11 | },
12 | "parser": "@babel/eslint-parser",
13 | "parserOptions": {
14 | "ecmaVersion": 2018,
15 | "sourceType": "module",
16 | "allowImportExportEverywhere": true,
17 | "requireConfigFile": false
18 | },
19 | "rules": {
20 | "linebreak-style": [
21 | "error",
22 | "unix"
23 | ],
24 | "quotes": [
25 | "error",
26 | "single"
27 | ],
28 | "semi": [
29 | "error",
30 | "always"
31 | ],
32 | "no-console": "off"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ "master" ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ "master" ]
20 | schedule:
21 | - cron: '16 20 * * 1'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'javascript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Use only 'java' to analyze code written in Java, Kotlin or both
38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
40 |
41 | steps:
42 | - name: Checkout repository
43 | uses: actions/checkout@v3
44 |
45 | # Initializes the CodeQL tools for scanning.
46 | - name: Initialize CodeQL
47 | uses: github/codeql-action/init@v2
48 | with:
49 | languages: ${{ matrix.language }}
50 | # If you wish to specify custom queries, you can do so here or in a config file.
51 | # By default, queries listed here will override any specified in a config file.
52 | # Prefix the list here with "+" to use these queries and those in the config file.
53 |
54 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
55 | queries: security-extended,security-and-quality
56 |
57 |
58 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
59 | # If this step fails, then you should remove it and run the build manually (see below)
60 | - name: Autobuild
61 | uses: github/codeql-action/autobuild@v2
62 |
63 | # ℹ️ Command-line programs to run using the OS shell.
64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
65 |
66 | # If the Autobuild fails above, remove it and uncomment the following three lines.
67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
68 |
69 | # - run: |
70 | # echo "Run, Build Application using script"
71 | # ./location_of_script_within_repo/buildscript.sh
72 |
73 | - name: Perform CodeQL Analysis
74 | uses: github/codeql-action/analyze@v2
75 | with:
76 | category: "/language:${{matrix.language}}"
77 |
--------------------------------------------------------------------------------
/.github/workflows/comment-issue.yml:
--------------------------------------------------------------------------------
1 | name: Add immediate comment on new issues
2 |
3 | on:
4 | issues:
5 | types: [opened]
6 |
7 | jobs:
8 | createComment:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Create Comment
12 | uses: peter-evans/create-or-update-comment@v1.4.2
13 | with:
14 | issue-number: ${{ github.event.issue.number }}
15 | body: |
16 | Thank you for submitting this issue!
17 |
18 | We, the Members of Meteor Community Packages take every issue seriously.
19 | Our goal is to provide long-term lifecycles for packages and keep up
20 | with the newest changes in Meteor and the overall NodeJs/JavaScript ecosystem.
21 |
22 | However, we contribute to these packages mostly in our free time.
23 | Therefore, we can't guarantee you issues to be solved within certain time.
24 |
25 | If you think this issue is trivial to solve, don't hesitate to submit
26 | a pull request, too! We will accompany you in the process with reviews and hints
27 | on how to get development set up.
28 |
29 | Please also consider sponsoring the maintainers of the package.
30 | If you don't know who is currently maintaining this package, just leave a comment
31 | and we'll let you know
32 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: "Lint test"
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | jobs:
10 | lint:
11 | name: Javascript standard lint
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: actions/setup-node@v3
16 | with:
17 | node-version: 18
18 | - name: cache dependencies
19 | uses: actions/cache@v3
20 | with:
21 | path: ~/.npm
22 | key: ${{ runner.os }}-node-18-${{ hashFiles('**/package-lock.json') }}
23 | restore-keys: |
24 | ${{ runner.os }}-node-18-
25 | - name: Install dependencies
26 | run: npm ci
27 | - name: Run lint
28 | run: npm run lint
29 |
30 |
--------------------------------------------------------------------------------
/.github/workflows/testsuite.yml:
--------------------------------------------------------------------------------
1 | # the test suite runs the tests (headless, server+client) for multiple Meteor releases
2 | name: Test suite
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | meteorRelease:
15 | - "--release 2.7"
16 | - "--release 2.8.1"
17 | - "--release 2.15"
18 | # Latest version
19 | steps:
20 | - name: Checkout code
21 | uses: actions/checkout@v4
22 |
23 | - name: Install Node.js
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: "14.x"
27 |
28 | - name: Install Dependencies
29 | run: |
30 | curl https://install.meteor.com | /bin/sh
31 | npm i -g @zodern/mtest
32 | - name: Run Tests
33 | run: |
34 | # Fix using old versions of Meteor
35 | export NODE_TLS_REJECT_UNAUTHORIZED=0
36 |
37 | mtest --package ./ --once ${{ matrix.meteorRelease }}
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .build*
2 | .versions
3 | node_modules
4 |
5 | .idea/
6 |
--------------------------------------------------------------------------------
/.jsbeautifyrc:
--------------------------------------------------------------------------------
1 | {
2 | "js": {
3 | "allowed_file_extensions": ["js", "json", "jshintrc", "jsbeautifyrc"],
4 | "indent_size": 2,
5 | "jslint_happy": true,
6 | "indent_with_tabs": false,
7 | "brace_style": "collapse, preserve-inline",
8 | "max_preserve_newlines": 4,
9 | "end_with_newline": true,
10 | "preserve_newlines": true
11 | },
12 | "html": {
13 | "allowed_file_extensions": ["htm", "html", "xhtml", "shtml", "xml", "svg", "dust"],
14 | "indent_size": 2,
15 | "indent_with_tabs": false,
16 | "end_with_newline": true,
17 | "preserve_newlines": true
18 | },
19 | "css": {
20 | "allowed_file_extensions": ["css", "scss", "sass", "less"],
21 | "indent_size": 2,
22 | "indent_with_tabs": false,
23 | "newline_between_rules": true,
24 | "end_with_newline": true,
25 | "selector_separator_newline": true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "powermode.enabled": true,
3 | "editor.formatOnSave": true,
4 | "cSpell.enabled": true,
5 | "jshint.options": {
6 | "esversion": 6
7 | },
8 | "git.confirmSync": false,
9 | "html.format.wrapLineLength": 0,
10 | "files.eol": "\n"
11 | }
12 |
--------------------------------------------------------------------------------
/.vscode/typings/meteor.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by typings
2 | // Source: https://raw.githubusercontent.com/meteor-typings/meteor/241d0e5335025e64fc3ea064de80a026a08f3f06/1.3/header.d.ts
3 |
4 | // Generated by typings
5 | // Source: https://raw.githubusercontent.com/meteor-typings/meteor/241d0e5335025e64fc3ea064de80a026a08f3f06/1.3/main.d.ts
6 | declare module Accounts {
7 | function user(): Meteor.User;
8 |
9 | function userId(): string;
10 |
11 | function createUser(options: {
12 | username ? : string;
13 | email ? : string;
14 | password ? : string;
15 | profile ? : Object;
16 | }, callback ? : Function): string;
17 |
18 | function config(options: {
19 | sendVerificationEmail ? : boolean;
20 | forbidClientAccountCreation ? : boolean;
21 | restrictCreationByEmailDomain ? : string | Function;
22 | loginExpirationInDays ? : number;
23 | oauthSecretKey ? : string;
24 | }): void;
25 |
26 | function onLogin(func: Function): {
27 | stop: () => void
28 | };
29 |
30 | function onLoginFailure(func: Function): {
31 | stop: () => void
32 | };
33 |
34 | function loginServicesConfigured(): boolean;
35 |
36 | function onPageLoadLogin(func: Function): void;
37 | }
38 |
39 | declare module "meteor/accounts-base" {
40 | ///
41 |
42 | module Accounts {
43 | function user(): Meteor.User;
44 |
45 | function userId(): string;
46 |
47 | function createUser(options: {
48 | username ? : string;
49 | email ? : string;
50 | password ? : string;
51 | profile ? : Object;
52 | }, callback ? : Function): string;
53 |
54 | function config(options: {
55 | sendVerificationEmail ? : boolean;
56 | forbidClientAccountCreation ? : boolean;
57 | restrictCreationByEmailDomain ? : string | Function;
58 | loginExpirationInDays ? : number;
59 | oauthSecretKey ? : string;
60 | }): void;
61 |
62 | function onLogin(func: Function): {
63 | stop: () => void
64 | };
65 |
66 | function onLoginFailure(func: Function): {
67 | stop: () => void
68 | };
69 |
70 | function loginServicesConfigured(): boolean;
71 |
72 | function onPageLoadLogin(func: Function): void;
73 | }
74 | }
75 | declare module Accounts {
76 | function changePassword(oldPassword: string, newPassword: string, callback ? : Function): void;
77 |
78 | function forgotPassword(options: {
79 | email ? : string;
80 | }, callback ? : Function): void;
81 |
82 | function resetPassword(token: string, newPassword: string, callback ? : Function): void;
83 |
84 | function verifyEmail(token: string, callback ? : Function): void;
85 |
86 | function onEmailVerificationLink(callback: Function): void;
87 |
88 | function onEnrollmentLink(callback: Function): void;
89 |
90 | function onResetPasswordLink(callback: Function): void;
91 |
92 | function loggingIn(): boolean;
93 |
94 | function logout(callback ? : Function): void;
95 |
96 | function logoutOtherClients(callback ? : Function): void;
97 |
98 | var ui: {
99 | config(options: {
100 | requestPermissions ? : Object;
101 | requestOfflineToken ? : Object;
102 | forceApprovalPrompt ? : Object;
103 | passwordSignupFields ? : string;
104 | }): void;
105 | };
106 | }
107 |
108 | declare module "meteor/accounts-base" {
109 | module Accounts {
110 | function changePassword(oldPassword: string, newPassword: string, callback ? : Function): void;
111 |
112 | function forgotPassword(options: {
113 | email ? : string;
114 | }, callback ? : Function): void;
115 |
116 | function resetPassword(token: string, newPassword: string, callback ? : Function): void;
117 |
118 | function verifyEmail(token: string, callback ? : Function): void;
119 |
120 | function onEmailVerificationLink(callback: Function): void;
121 |
122 | function onEnrollmentLink(callback: Function): void;
123 |
124 | function onResetPasswordLink(callback: Function): void;
125 |
126 | function loggingIn(): boolean;
127 |
128 | function logout(callback ? : Function): void;
129 |
130 | function logoutOtherClients(callback ? : Function): void;
131 |
132 | var ui: {
133 | config(options: {
134 | requestPermissions ? : Object;
135 | requestOfflineToken ? : Object;
136 | forceApprovalPrompt ? : Object;
137 | passwordSignupFields ? : string;
138 | }): void;
139 | };
140 | }
141 | }
142 |
143 | interface EmailFields {
144 | from ? : () => string;
145 | subject ? : (user: Meteor.User) => string;
146 | text ? : (user: Meteor.User, url: string) => string;
147 | html ? : (user: Meteor.User, url: string) => string;
148 | }
149 |
150 | interface Header {
151 | [id: string]: string;
152 | }
153 |
154 | interface EmailTemplates {
155 | from: string;
156 | siteName: string;
157 | headers ? : Header;
158 | resetPassword: EmailFields;
159 | enrollAccount: EmailFields;
160 | verifyEmail: EmailFields;
161 | }
162 |
163 | declare module Accounts {
164 | var emailTemplates: EmailTemplates;
165 |
166 | function addEmail(userId: string, newEmail: string, verified ? : boolean): void;
167 |
168 | function removeEmail(userId: string, email: string): void;
169 |
170 | function onCreateUser(func: Function): void;
171 |
172 | function findUserByEmail(email: string): Object;
173 |
174 | function findUserByUsername(username: string): Object;
175 |
176 | function sendEnrollmentEmail(userId: string, email ? : string): void;
177 |
178 | function sendResetPasswordEmail(userId: string, email ? : string): void;
179 |
180 | function sendVerificationEmail(userId: string, email ? : string): void;
181 |
182 | function setUsername(userId: string, newUsername: string): void;
183 |
184 | function setPassword(userId: string, newPassword: string, options ? : {
185 | logout ? : Object;
186 | }): void;
187 |
188 | function validateNewUser(func: Function): boolean;
189 |
190 | function validateLoginAttempt(func: Function): {
191 | stop: () => void
192 | };
193 |
194 | interface IValidateLoginAttemptCbOpts {
195 | type: string;
196 | allowed: boolean;
197 | error: Meteor.Error;
198 | user: Meteor.User;
199 | connection: Meteor.Connection;
200 | methodName: string;
201 | methodArguments: any[];
202 | }
203 | }
204 |
205 | declare module "meteor/accounts-base" {
206 | ///
207 | ///
208 |
209 | interface EmailFields {
210 | from ? : () => string;
211 | subject ? : (user: Meteor.User) => string;
212 | text ? : (user: Meteor.User, url: string) => string;
213 | html ? : (user: Meteor.User, url: string) => string;
214 | }
215 |
216 | interface Header {
217 | [id: string]: string;
218 | }
219 |
220 | interface EmailTemplates {
221 | from: string;
222 | siteName: string;
223 | headers ? : Header;
224 | resetPassword: EmailFields;
225 | enrollAccount: EmailFields;
226 | verifyEmail: EmailFields;
227 | }
228 |
229 | module Accounts {
230 | var emailTemplates: EmailTemplates;
231 |
232 | function addEmail(userId: string, newEmail: string, verified ? : boolean): void;
233 |
234 | function removeEmail(userId: string, email: string): void;
235 |
236 | function onCreateUser(func: Function): void;
237 |
238 | function findUserByEmail(email: string): Object;
239 |
240 | function findUserByUsername(username: string): Object;
241 |
242 | function sendEnrollmentEmail(userId: string, email ? : string): void;
243 |
244 | function sendResetPasswordEmail(userId: string, email ? : string): void;
245 |
246 | function sendVerificationEmail(userId: string, email ? : string): void;
247 |
248 | function setUsername(userId: string, newUsername: string): void;
249 |
250 | function setPassword(userId: string, newPassword: string, options ? : {
251 | logout ? : Object;
252 | }): void;
253 |
254 | function validateNewUser(func: Function): boolean;
255 |
256 | function validateLoginAttempt(func: Function): {
257 | stop: () => void
258 | };
259 |
260 | interface IValidateLoginAttemptCbOpts {
261 | type: string;
262 | allowed: boolean;
263 | error: Meteor.Error;
264 | user: Meteor.User;
265 | connection: Meteor.Connection;
266 | methodName: string;
267 | methodArguments: any[];
268 | }
269 | }
270 | }
271 |
272 | declare module Blaze {
273 | var View: ViewStatic;
274 |
275 | interface ViewStatic {
276 | new(name ? : string, renderFunction ? : Function): View;
277 | }
278 |
279 | interface View {
280 | name: string;
281 | parentView: View;
282 | isCreated: boolean;
283 | isRendered: boolean;
284 | isDestroyed: boolean;
285 | renderCount: number;
286 | autorun(runFunc: Function): void;
287 | onViewCreated(func: Function): void;
288 | onViewReady(func: Function): void;
289 | onViewDestroyed(func: Function): void;
290 | firstNode(): Node;
291 | lastNode(): Node;
292 | template: Template;
293 | templateInstance(): TemplateInstance;
294 | }
295 | var currentView: View;
296 |
297 | function isTemplate(value: any): boolean;
298 |
299 | interface HelpersMap {
300 | [key: string]: Function;
301 | }
302 |
303 | interface EventsMap {
304 | [key: string]: Function;
305 | }
306 |
307 | var Template: TemplateStatic;
308 |
309 | interface TemplateStatic {
310 | new(viewName ? : string, renderFunction ? : Function): Template;
311 |
312 | registerHelper(name: string, func: Function): void;
313 | instance(): TemplateInstance;
314 | currentData(): any;
315 | parentData(numLevels: number): any;
316 | }
317 |
318 | interface Template {
319 | viewName: string;
320 | renderFunction: Function;
321 | constructView(): View;
322 | head: Template;
323 | find(selector: string): Template;
324 | findAll(selector: string): Template[];
325 | $: any;
326 | onCreated(cb: Function): void;
327 | onRendered(cb: Function): void;
328 | onDestroyed(cb: Function): void;
329 | created: Function;
330 | rendered: Function;
331 | destroyed: Function;
332 | helpers(helpersMap: HelpersMap): void;
333 | events(eventsMap: EventsMap): void;
334 | }
335 |
336 | var TemplateInstance: TemplateInstanceStatic;
337 |
338 | interface TemplateInstanceStatic {
339 | new(view: View): TemplateInstance;
340 | }
341 |
342 | interface TemplateInstance {
343 | $(selector: string): any;
344 | autorun(runFunc: Function): Object;
345 | data: Object;
346 | find(selector ? : string): TemplateInstance;
347 | findAll(selector: string): TemplateInstance[];
348 | firstNode: Object;
349 | lastNode: Object;
350 | subscribe(name: string, ...args: any[]): Meteor.SubscriptionHandle;
351 | subscriptionsReady(): boolean;
352 | view: Object;
353 | }
354 |
355 | function Each(argFunc: Function, contentFunc: Function, elseFunc ? : Function): View;
356 |
357 | function Unless(conditionFunc: Function, contentFunc: Function, elseFunc ? : Function): View;
358 |
359 | function If(conditionFunc: Function, contentFunc: Function, elseFunc ? : Function): View;
360 |
361 | function Let(bindings: Function, contentFunc: Function): View;
362 |
363 | function With(data: Object | Function, contentFunc: Function): View;
364 |
365 | function getData(elementOrView ? : HTMLElement | View): Object;
366 |
367 | function getView(element ? : HTMLElement): View;
368 |
369 | function remove(renderedView: View): void;
370 |
371 | function render(templateOrView: Template | View, parentNode: Node, nextNode ? : Node, parentView ? : View): View;
372 |
373 | function renderWithData(templateOrView: Template | View, data: Object | Function, parentNode: Node, nextNode ? : Node, parentView ? : View): View;
374 |
375 | function toHTML(templateOrView: Template | View): string;
376 |
377 | function toHTMLWithData(templateOrView: Template | View, data: Object | Function): string;
378 | }
379 |
380 | declare module "meteor/blaze" {
381 | ///
382 |
383 | module Blaze {
384 | var View: ViewStatic;
385 |
386 | interface ViewStatic {
387 | new(name ? : string, renderFunction ? : Function): View;
388 | }
389 |
390 | interface View {
391 | name: string;
392 | parentView: View;
393 | isCreated: boolean;
394 | isRendered: boolean;
395 | isDestroyed: boolean;
396 | renderCount: number;
397 | autorun(runFunc: Function): void;
398 | onViewCreated(func: Function): void;
399 | onViewReady(func: Function): void;
400 | onViewDestroyed(func: Function): void;
401 | firstNode(): Node;
402 | lastNode(): Node;
403 | template: Template;
404 | templateInstance(): TemplateInstance;
405 | }
406 | var currentView: View;
407 |
408 | function isTemplate(value: any): boolean;
409 |
410 | interface HelpersMap {
411 | [key: string]: Function;
412 | }
413 |
414 | interface EventsMap {
415 | [key: string]: Function;
416 | }
417 |
418 | var Template: TemplateStatic;
419 |
420 | interface TemplateStatic {
421 | new(viewName ? : string, renderFunction ? : Function): Template;
422 |
423 | registerHelper(name: string, func: Function): void;
424 | instance(): TemplateInstance;
425 | currentData(): any;
426 | parentData(numLevels: number): any;
427 | }
428 |
429 | interface Template {
430 | viewName: string;
431 | renderFunction: Function;
432 | constructView(): View;
433 | head: Template;
434 | find(selector: string): Template;
435 | findAll(selector: string): Template[];
436 | $: any;
437 | onCreated(cb: Function): void;
438 | onRendered(cb: Function): void;
439 | onDestroyed(cb: Function): void;
440 | created: Function;
441 | rendered: Function;
442 | destroyed: Function;
443 | helpers(helpersMap: HelpersMap): void;
444 | events(eventsMap: EventsMap): void;
445 | }
446 |
447 | var TemplateInstance: TemplateInstanceStatic;
448 |
449 | interface TemplateInstanceStatic {
450 | new(view: View): TemplateInstance;
451 | }
452 |
453 | interface TemplateInstance {
454 | $(selector: string): any;
455 | autorun(runFunc: Function): Object;
456 | data: Object;
457 | find(selector ? : string): TemplateInstance;
458 | findAll(selector: string): TemplateInstance[];
459 | firstNode: Object;
460 | lastNode: Object;
461 | subscribe(name: string, ...args: any[]): Meteor.SubscriptionHandle;
462 | subscriptionsReady(): boolean;
463 | view: Object;
464 | }
465 |
466 | function Each(argFunc: Function, contentFunc: Function, elseFunc ? : Function): View;
467 |
468 | function Unless(conditionFunc: Function, contentFunc: Function, elseFunc ? : Function): View;
469 |
470 | function If(conditionFunc: Function, contentFunc: Function, elseFunc ? : Function): View;
471 |
472 | function Let(bindings: Function, contentFunc: Function): View;
473 |
474 | function With(data: Object | Function, contentFunc: Function): View;
475 |
476 | function getData(elementOrView ? : HTMLElement | View): Object;
477 |
478 | function getView(element ? : HTMLElement): View;
479 |
480 | function remove(renderedView: View): void;
481 |
482 | function render(templateOrView: Template | View, parentNode: Node, nextNode ? : Node, parentView ? : View): View;
483 |
484 | function renderWithData(templateOrView: Template | View, data: Object | Function, parentNode: Node, nextNode ? : Node, parentView ? : View): View;
485 |
486 | function toHTML(templateOrView: Template | View): string;
487 |
488 | function toHTMLWithData(templateOrView: Template | View, data: Object | Function): string;
489 | }
490 | }
491 | declare module BrowserPolicy {
492 | interface framing {
493 | disallow(): void;
494 | restrictToOrigin(origin: string): void;
495 | allowAll(): void;
496 | }
497 |
498 | interface content {
499 | allowEval(): void;
500 | allowInlineStyles(): void;
501 | allowInlineScripts(): void;
502 | allowSameOriginForAll(): void;
503 | allowDataUrlForAll(): void;
504 | allowOriginForAll(origin: string): void;
505 | allowImageOrigin(origin: string): void;
506 | allowFrameOrigin(origin: string): void;
507 | allowContentTypeSniffing(): void;
508 | allowAllContentOrigin(): void;
509 | allowAllContentDataUrl(): void;
510 | allowAllContentSameOrigin(): void;
511 |
512 | disallowAll(): void;
513 | disallowInlineStyles(): void;
514 | disallowEval(): void;
515 | disallowInlineScripts(): void;
516 | disallowFont(): void;
517 | disallowObject(): void;
518 | disallowAllContent(): void;
519 | }
520 | }
521 |
522 | declare module "meteor/browser-policy" {
523 | module BrowserPolicy {
524 | interface framing {
525 | disallow(): void;
526 | restrictToOrigin(origin: string): void;
527 | allowAll(): void;
528 | }
529 |
530 | interface content {
531 | allowEval(): void;
532 | allowInlineStyles(): void;
533 | allowInlineScripts(): void;
534 | allowSameOriginForAll(): void;
535 | allowDataUrlForAll(): void;
536 | allowOriginForAll(origin: string): void;
537 | allowImageOrigin(origin: string): void;
538 | allowFrameOrigin(origin: string): void;
539 | allowContentTypeSniffing(): void;
540 | allowAllContentOrigin(): void;
541 | allowAllContentDataUrl(): void;
542 | allowAllContentSameOrigin(): void;
543 |
544 | disallowAll(): void;
545 | disallowInlineStyles(): void;
546 | disallowEval(): void;
547 | disallowInlineScripts(): void;
548 | disallowFont(): void;
549 | disallowObject(): void;
550 | disallowAllContent(): void;
551 | }
552 | }
553 | }
554 | declare module Match {
555 | var Any: any;
556 | var String: any;
557 | var Integer: any;
558 | var Boolean: any;
559 | var undefined: any;
560 | var Object: any;
561 |
562 | function Optional(pattern: any): boolean;
563 |
564 | function ObjectIncluding(dico: any): boolean;
565 |
566 | function OneOf(...patterns: any[]): any;
567 |
568 | function Where(condition: any): any;
569 |
570 | function test(value: any, pattern: any): boolean;
571 | }
572 |
573 | declare function check(value: any, pattern: any): void;
574 |
575 | declare module "meteor/check" {
576 | module Match {
577 | var Any: any;
578 | var String: any;
579 | var Integer: any;
580 | var Boolean: any;
581 | var undefined: any;
582 | var Object: any;
583 |
584 | function Optional(pattern: any): boolean;
585 |
586 | function ObjectIncluding(dico: any): boolean;
587 |
588 | function OneOf(...patterns: any[]): any;
589 |
590 | function Where(condition: any): any;
591 |
592 | function test(value: any, pattern: any): boolean;
593 | }
594 |
595 | function check(value: any, pattern: any): void;
596 | }
597 |
598 | declare module DDP {
599 | interface DDPStatic {
600 | subscribe(name: string, ...rest: any[]): Meteor.SubscriptionHandle;
601 | call(method: string, ...parameters: any[]): void;
602 | apply(method: string, ...parameters: any[]): void;
603 | methods(IMeteorMethodsDictionary: any): any;
604 | status(): DDPStatus;
605 | reconnect(): void;
606 | disconnect(): void;
607 | onReconnect(): void;
608 | }
609 |
610 | function _allSubscriptionsReady(): boolean;
611 |
612 | interface DDPStatus {
613 | connected: boolean;
614 | /**
615 | * connected,
616 | * connecting,
617 | * failed,
618 | * waiting,
619 | * offline
620 | */
621 | status: string;
622 | retryCount: number;
623 | retryTime ? : number;
624 | reason ? : string;
625 | }
626 |
627 | function connect(url: string): DDPStatic;
628 | }
629 |
630 | declare module DDPCommon {
631 | interface MethodInvocation {
632 | new(options: {}): MethodInvocation;
633 |
634 | unblock(): void;
635 |
636 | setUserId(userId: number): void;
637 | }
638 | }
639 |
640 | declare module "meteor/ddp" {
641 | ///
642 |
643 | module DDP {
644 | interface DDPStatic {
645 | subscribe(name: string, ...rest: any[]): Meteor.SubscriptionHandle;
646 | call(method: string, ...parameters: any[]): void;
647 | apply(method: string, ...parameters: any[]): void;
648 | methods(IMeteorMethodsDictionary: any): any;
649 | status(): DDPStatus;
650 | reconnect(): void;
651 | disconnect(): void;
652 | onReconnect(): void;
653 | }
654 |
655 | function _allSubscriptionsReady(): boolean;
656 |
657 | interface DDPStatus {
658 | connected: boolean;
659 | /**
660 | * connected,
661 | * connecting,
662 | * failed,
663 | * waiting,
664 | * offline
665 | */
666 | status: string;
667 | retryCount: number;
668 | retryTime ? : number;
669 | reason ? : string;
670 | }
671 |
672 | function connect(url: string): DDPStatic;
673 | }
674 |
675 | module DDPCommon {
676 | interface MethodInvocation {
677 | new(options: {}): MethodInvocation;
678 |
679 | unblock(): void;
680 |
681 | setUserId(userId: number): void;
682 | }
683 | }
684 | }
685 | interface EJSONableCustomType {
686 | clone(): EJSONableCustomType;
687 | equals(other: Object): boolean;
688 | toJSONValue(): JSONable;
689 | typeName(): string;
690 | }
691 | interface EJSONable {
692 | [key: string]: number | string | boolean | Object | number[] | string[] | Object[] | Date | Uint8Array | EJSONableCustomType;
693 | }
694 | interface JSONable {
695 | [key: string]: number | string | boolean | Object | number[] | string[] | Object[];
696 | }
697 | interface EJSON extends EJSONable {}
698 |
699 | declare module EJSON {
700 | function addType(name: string, factory: (val: JSONable) => EJSONableCustomType): void;
701 |
702 | function clone < T > (val: T): T;
703 |
704 | function equals(a: EJSON, b: EJSON, options ? : {
705 | keyOrderSensitive ? : boolean;
706 | }): boolean;
707 |
708 | function fromJSONValue(val: JSONable): any;
709 |
710 | function isBinary(x: Object): boolean;
711 | var newBinary: any;
712 |
713 | function parse(str: string): EJSON;
714 |
715 | function stringify(val: EJSON, options ? : {
716 | indent ? : boolean | number | string;
717 | canonical ? : boolean;
718 | }): string;
719 |
720 | function toJSONValue(val: EJSON): JSONable;
721 | }
722 |
723 | declare module "meteor/ejson" {
724 | interface EJSONableCustomType {
725 | clone(): EJSONableCustomType;
726 | equals(other: Object): boolean;
727 | toJSONValue(): JSONable;
728 | typeName(): string;
729 | }
730 | interface EJSONable {
731 | [key: string]: number | string | boolean | Object | number[] | string[] | Object[] | Date | Uint8Array | EJSONableCustomType;
732 | }
733 | interface JSONable {
734 | [key: string]: number | string | boolean | Object | number[] | string[] | Object[];
735 | }
736 | interface EJSON extends EJSONable {}
737 |
738 | module EJSON {
739 | function addType(name: string, factory: (val: JSONable) => EJSONableCustomType): void;
740 |
741 | function clone < T > (val: T): T;
742 |
743 | function equals(a: EJSON, b: EJSON, options ? : {
744 | keyOrderSensitive ? : boolean;
745 | }): boolean;
746 |
747 | function fromJSONValue(val: JSONable): any;
748 |
749 | function isBinary(x: Object): boolean;
750 | var newBinary: any;
751 |
752 | function parse(str: string): EJSON;
753 |
754 | function stringify(val: EJSON, options ? : {
755 | indent ? : boolean | number | string;
756 | canonical ? : boolean;
757 | }): string;
758 |
759 | function toJSONValue(val: EJSON): JSONable;
760 | }
761 | }
762 | declare module Email {
763 | function send(options: {
764 | from ? : string;
765 | to ? : string | string[];
766 | cc ? : string | string[];
767 | bcc ? : string | string[];
768 | replyTo ? : string | string[];
769 | subject ? : string;
770 | text ? : string;
771 | html ? : string;
772 | headers ? : Object;
773 | attachments ? : Object[];
774 | mailComposer ? : MailComposer;
775 | }): void;
776 | }
777 |
778 | interface MailComposerOptions {
779 | escapeSMTP: boolean;
780 | encoding: string;
781 | charset: string;
782 | keepBcc: boolean;
783 | forceEmbeddedImages: boolean;
784 | }
785 |
786 | declare var MailComposer: MailComposerStatic;
787 | interface MailComposerStatic {
788 | new(options: MailComposerOptions): MailComposer;
789 | }
790 | interface MailComposer {
791 | addHeader(name: string, value: string): void;
792 | setMessageOption(from: string, to: string, body: string, html: string): void;
793 | streamMessage(): void;
794 | pipe(stream: any /** fs.WriteStream **/ ): void;
795 | }
796 |
797 | declare module "meteor/email" {
798 | module Email {
799 | function send(options: {
800 | from ? : string;
801 | to ? : string | string[];
802 | cc ? : string | string[];
803 | bcc ? : string | string[];
804 | replyTo ? : string | string[];
805 | subject ? : string;
806 | text ? : string;
807 | html ? : string;
808 | headers ? : Object;
809 | attachments ? : Object[];
810 | mailComposer ? : MailComposer;
811 | }): void;
812 | }
813 |
814 | interface MailComposerOptions {
815 | escapeSMTP: boolean;
816 | encoding: string;
817 | charset: string;
818 | keepBcc: boolean;
819 | forceEmbeddedImages: boolean;
820 | }
821 |
822 | var MailComposer: MailComposerStatic;
823 | interface MailComposerStatic {
824 | new(options: MailComposerOptions): MailComposer;
825 | }
826 | interface MailComposer {
827 | addHeader(name: string, value: string): void;
828 | setMessageOption(from: string, to: string, body: string, html: string): void;
829 | streamMessage(): void;
830 | pipe(stream: any /** fs.WriteStream **/ ): void;
831 | }
832 | }
833 | declare module HTTP {
834 | interface HTTPRequest {
835 | content ? : string;
836 | data ? : any;
837 | query ? : string;
838 | params ? : {
839 | [id: string]: string
840 | };
841 | auth ? : string;
842 | headers ? : {
843 | [id: string]: string
844 | };
845 | timeout ? : number;
846 | followRedirects ? : boolean;
847 | }
848 |
849 | interface HTTPResponse {
850 | statusCode ? : number;
851 | headers ? : {
852 | [id: string]: string
853 | };
854 | content ? : string;
855 | data ? : any;
856 | }
857 |
858 | function call(method: string, url: string, options ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse;
859 |
860 | function del(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse;
861 |
862 | function get(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse;
863 |
864 | function post(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse;
865 |
866 | function put(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse;
867 |
868 | function call(method: string, url: string, options ? : {
869 | content ? : string;
870 | data ? : Object;
871 | query ? : string;
872 | params ? : Object;
873 | auth ? : string;
874 | headers ? : Object;
875 | timeout ? : number;
876 | followRedirects ? : boolean;
877 | npmRequestOptions ? : Object;
878 | beforeSend ? : Function;
879 | }, asyncCallback ? : Function): HTTP.HTTPResponse;
880 | }
881 |
882 | declare module "meteor/http" {
883 | module HTTP {
884 | interface HTTPRequest {
885 | content ? : string;
886 | data ? : any;
887 | query ? : string;
888 | params ? : {
889 | [id: string]: string
890 | };
891 | auth ? : string;
892 | headers ? : {
893 | [id: string]: string
894 | };
895 | timeout ? : number;
896 | followRedirects ? : boolean;
897 | }
898 |
899 | interface HTTPResponse {
900 | statusCode ? : number;
901 | headers ? : {
902 | [id: string]: string
903 | };
904 | content ? : string;
905 | data ? : any;
906 | }
907 |
908 | function call(method: string, url: string, options ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse;
909 |
910 | function del(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse;
911 |
912 | function get(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse;
913 |
914 | function post(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse;
915 |
916 | function put(url: string, callOptions ? : HTTP.HTTPRequest, asyncCallback ? : Function): HTTP.HTTPResponse;
917 |
918 | function call(method: string, url: string, options ? : {
919 | content ? : string;
920 | data ? : Object;
921 | query ? : string;
922 | params ? : Object;
923 | auth ? : string;
924 | headers ? : Object;
925 | timeout ? : number;
926 | followRedirects ? : boolean;
927 | npmRequestOptions ? : Object;
928 | beforeSend ? : Function;
929 | }, asyncCallback ? : Function): HTTP.HTTPResponse;
930 | }
931 | }
932 |
933 | declare module Meteor {
934 | /** Global props **/
935 | var isClient: boolean;
936 | var isCordova: boolean;
937 | var isServer: boolean;
938 | var release: string;
939 | var settings: {
940 | [id: string]: any
941 | };
942 | /** props **/
943 |
944 | /** User **/
945 | interface UserEmail {
946 | address: string;
947 | verified: boolean;
948 | }
949 | interface User {
950 | _id ? : string;
951 | username ? : string;
952 | emails ? : UserEmail[];
953 | createdAt ? : number;
954 | profile ? : any;
955 | services ? : any;
956 | }
957 |
958 | function user(): User;
959 |
960 | function userId(): string;
961 | var users: Mongo.Collection < User > ;
962 | /** User **/
963 |
964 | /** Error **/
965 | var Error: ErrorStatic;
966 | interface ErrorStatic {
967 | new(error: string | number, reason ? : string, details ? : string): Error;
968 | }
969 | interface Error {
970 | error: string | number;
971 | reason ? : string;
972 | details ? : string;
973 | }
974 | /** Error **/
975 |
976 | /** Method **/
977 | function methods(methods: Object): void;
978 |
979 | function call(name: string, ...args: any[]): any;
980 |
981 | function apply(name: string, args: EJSONable[], options ? : {
982 | wait ? : boolean;
983 | onResultReceived ? : Function;
984 | }, asyncCallback ? : Function): any;
985 | /** Method **/
986 |
987 | /** Url **/
988 | function absoluteUrl(path ? : string, options ? : {
989 | secure ? : boolean;
990 | replaceLocalhost ? : boolean;
991 | rootUrl ? : string;
992 | }): string;
993 | /** Url **/
994 |
995 | /** Timeout **/
996 | function setInterval(func: Function, delay: number): number;
997 |
998 | function setTimeout(func: Function, delay: number): number;
999 |
1000 | function clearInterval(id: number): void;
1001 |
1002 | function clearTimeout(id: number): void;
1003 |
1004 | function defer(func: Function): void;
1005 | /** Timeout **/
1006 |
1007 | /** utils **/
1008 | function startup(func: Function): void;
1009 |
1010 | function wrapAsync(func: Function, context ? : Object): any;
1011 | /** utils **/
1012 |
1013 | /** Pub/Sub **/
1014 | interface SubscriptionHandle {
1015 | stop(): void;
1016 | ready(): boolean;
1017 | }
1018 | interface LiveQueryHandle {
1019 | stop(): void;
1020 | }
1021 | /** Pub/Sub **/
1022 | }
1023 |
1024 | declare module "meteor/meteor" {
1025 | ///
1026 | ///
1027 |
1028 | module Meteor {
1029 | /** Global props **/
1030 | var isClient: boolean;
1031 | var isCordova: boolean;
1032 | var isServer: boolean;
1033 | var release: string;
1034 | var settings: {
1035 | [id: string]: any
1036 | };
1037 | /** props **/
1038 |
1039 | /** User **/
1040 | interface UserEmail {
1041 | address: string;
1042 | verified: boolean;
1043 | }
1044 | interface User {
1045 | _id ? : string;
1046 | username ? : string;
1047 | emails ? : UserEmail[];
1048 | createdAt ? : number;
1049 | profile ? : any;
1050 | services ? : any;
1051 | }
1052 |
1053 | function user(): User;
1054 |
1055 | function userId(): string;
1056 | var users: Mongo.Collection < User > ;
1057 | /** User **/
1058 |
1059 | /** Error **/
1060 | var Error: ErrorStatic;
1061 | interface ErrorStatic {
1062 | new(error: string | number, reason ? : string, details ? : string): Error;
1063 | }
1064 | interface Error {
1065 | error: string | number;
1066 | reason ? : string;
1067 | details ? : string;
1068 | }
1069 | /** Error **/
1070 |
1071 | /** Method **/
1072 | function methods(methods: Object): void;
1073 |
1074 | function call(name: string, ...args: any[]): any;
1075 |
1076 | function apply(name: string, args: EJSONable[], options ? : {
1077 | wait ? : boolean;
1078 | onResultReceived ? : Function;
1079 | }, asyncCallback ? : Function): any;
1080 | /** Method **/
1081 |
1082 | /** Url **/
1083 | function absoluteUrl(path ? : string, options ? : {
1084 | secure ? : boolean;
1085 | replaceLocalhost ? : boolean;
1086 | rootUrl ? : string;
1087 | }): string;
1088 | /** Url **/
1089 |
1090 | /** Timeout **/
1091 | function setInterval(func: Function, delay: number): number;
1092 |
1093 | function setTimeout(func: Function, delay: number): number;
1094 |
1095 | function clearInterval(id: number): void;
1096 |
1097 | function clearTimeout(id: number): void;
1098 |
1099 | function defer(func: Function): void;
1100 | /** Timeout **/
1101 |
1102 | /** utils **/
1103 | function startup(func: Function): void;
1104 |
1105 | function wrapAsync(func: Function, context ? : Object): any;
1106 | /** utils **/
1107 |
1108 | /** Pub/Sub **/
1109 | interface SubscriptionHandle {
1110 | stop(): void;
1111 | ready(): boolean;
1112 | }
1113 | interface LiveQueryHandle {
1114 | stop(): void;
1115 | }
1116 | /** Pub/Sub **/
1117 | }
1118 | }
1119 |
1120 | declare module Meteor {
1121 | /** Login **/
1122 | interface LoginWithExternalServiceOptions {
1123 | requestPermissions ? : string[];
1124 | requestOfflineToken ? : Boolean;
1125 | forceApprovalPrompt ? : Boolean;
1126 | loginUrlParameters ? : Object;
1127 | redirectUrl ? : string;
1128 | loginHint ? : string;
1129 | loginStyle ? : string;
1130 | }
1131 |
1132 | function loginWithMeteorDeveloperAccount(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void;
1133 |
1134 | function loginWithFacebook(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void;
1135 |
1136 | function loginWithGithub(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void;
1137 |
1138 | function loginWithGoogle(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void;
1139 |
1140 | function loginWithMeetup(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void;
1141 |
1142 | function loginWithTwitter(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void;
1143 |
1144 | function loginWithWeibo(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void;
1145 |
1146 | function loggingIn(): boolean;
1147 |
1148 | function loginWith < ExternalService > (options ? : {
1149 | requestPermissions ? : string[];
1150 | requestOfflineToken ? : boolean;
1151 | loginUrlParameters ? : Object;
1152 | userEmail ? : string;
1153 | loginStyle ? : string;
1154 | redirectUrl ? : string;
1155 | }, callback ? : Function): void;
1156 |
1157 | function loginWithPassword(user: Object | string, password: string, callback ? : Function): void;
1158 |
1159 | function logout(callback ? : Function): void;
1160 |
1161 | function logoutOtherClients(callback ? : Function): void;
1162 | /** Login **/
1163 |
1164 | /** Event **/
1165 | interface Event {
1166 | type: string;
1167 | target: HTMLElement;
1168 | currentTarget: HTMLElement;
1169 | which: number;
1170 | stopPropagation(): void;
1171 | stopImmediatePropagation(): void;
1172 | preventDefault(): void;
1173 | isPropagationStopped(): boolean;
1174 | isImmediatePropagationStopped(): boolean;
1175 | isDefaultPrevented(): boolean;
1176 | }
1177 | interface EventHandlerFunction extends Function {
1178 | (event ? : Meteor.Event, templateInstance ? : Blaze.TemplateInstance): void;
1179 | }
1180 | interface EventMap {
1181 | [id: string]: Meteor.EventHandlerFunction;
1182 | }
1183 | /** Event **/
1184 |
1185 | /** Connection **/
1186 | function reconnect(): void;
1187 |
1188 | function disconnect(): void;
1189 | /** Connection **/
1190 |
1191 | /** Status **/
1192 | function status(): DDP.DDPStatus;
1193 | /** Status **/
1194 |
1195 | /** Pub/Sub **/
1196 | function subscribe(name: string, ...args: any[]): Meteor.SubscriptionHandle;
1197 | /** Pub/Sub **/
1198 | }
1199 |
1200 | declare module "meteor/meteor" {
1201 | ///
1202 |
1203 | module Meteor {
1204 | /** Login **/
1205 | interface LoginWithExternalServiceOptions {
1206 | requestPermissions ? : string[];
1207 | requestOfflineToken ? : Boolean;
1208 | forceApprovalPrompt ? : Boolean;
1209 | loginUrlParameters ? : Object;
1210 | redirectUrl ? : string;
1211 | loginHint ? : string;
1212 | loginStyle ? : string;
1213 | }
1214 |
1215 | function loginWithMeteorDeveloperAccount(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void;
1216 |
1217 | function loginWithFacebook(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void;
1218 |
1219 | function loginWithGithub(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void;
1220 |
1221 | function loginWithGoogle(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void;
1222 |
1223 | function loginWithMeetup(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void;
1224 |
1225 | function loginWithTwitter(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void;
1226 |
1227 | function loginWithWeibo(options ? : Meteor.LoginWithExternalServiceOptions, callback ? : Function): void;
1228 |
1229 | function loggingIn(): boolean;
1230 |
1231 | function loginWith < ExternalService > (options ? : {
1232 | requestPermissions ? : string[];
1233 | requestOfflineToken ? : boolean;
1234 | loginUrlParameters ? : Object;
1235 | userEmail ? : string;
1236 | loginStyle ? : string;
1237 | redirectUrl ? : string;
1238 | }, callback ? : Function): void;
1239 |
1240 | function loginWithPassword(user: Object | string, password: string, callback ? : Function): void;
1241 |
1242 | function logout(callback ? : Function): void;
1243 |
1244 | function logoutOtherClients(callback ? : Function): void;
1245 | /** Login **/
1246 |
1247 | /** Event **/
1248 | interface Event {
1249 | type: string;
1250 | target: HTMLElement;
1251 | currentTarget: HTMLElement;
1252 | which: number;
1253 | stopPropagation(): void;
1254 | stopImmediatePropagation(): void;
1255 | preventDefault(): void;
1256 | isPropagationStopped(): boolean;
1257 | isImmediatePropagationStopped(): boolean;
1258 | isDefaultPrevented(): boolean;
1259 | }
1260 | interface EventHandlerFunction extends Function {
1261 | (event ? : Meteor.Event, templateInstance ? : Blaze.TemplateInstance): void;
1262 | }
1263 | interface EventMap {
1264 | [id: string]: Meteor.EventHandlerFunction;
1265 | }
1266 | /** Event **/
1267 |
1268 | /** Connection **/
1269 | function reconnect(): void;
1270 |
1271 | function disconnect(): void;
1272 | /** Connection **/
1273 |
1274 | /** Status **/
1275 | function status(): DDP.DDPStatus;
1276 | /** Status **/
1277 |
1278 | /** Pub/Sub **/
1279 | function subscribe(name: string, ...args: any[]): Meteor.SubscriptionHandle;
1280 | /** Pub/Sub **/
1281 | }
1282 | }
1283 | declare module Meteor {
1284 | /** Connection **/
1285 | interface Connection {
1286 | id: string;
1287 | close: Function;
1288 | onClose: Function;
1289 | clientAddress: string;
1290 | httpHeaders: Object;
1291 | }
1292 |
1293 | function onConnection(callback: Function): void;
1294 | /** Connection **/
1295 |
1296 | function publish(name: string, func: Function): void;
1297 | }
1298 |
1299 | interface Subscription {
1300 | added(collection: string, id: string, fields: Object): void;
1301 | changed(collection: string, id: string, fields: Object): void;
1302 | connection: Meteor.Connection;
1303 | error(error: Error): void;
1304 | onStop(func: Function): void;
1305 | ready(): void;
1306 | removed(collection: string, id: string): void;
1307 | stop(): void;
1308 | userId: string;
1309 | }
1310 |
1311 | declare module "meteor/meteor" {
1312 | module Meteor {
1313 | /** Connection **/
1314 | interface Connection {
1315 | id: string;
1316 | close: Function;
1317 | onClose: Function;
1318 | clientAddress: string;
1319 | httpHeaders: Object;
1320 | }
1321 |
1322 | function onConnection(callback: Function): void;
1323 | /** Connection **/
1324 |
1325 | function publish(name: string, func: Function): void;
1326 | }
1327 |
1328 | interface Subscription {
1329 | added(collection: string, id: string, fields: Object): void;
1330 | changed(collection: string, id: string, fields: Object): void;
1331 | connection: Meteor.Connection;
1332 | error(error: Error): void;
1333 | onStop(func: Function): void;
1334 | ready(): void;
1335 | removed(collection: string, id: string): void;
1336 | stop(): void;
1337 | userId: string;
1338 | }
1339 | }
1340 | declare module Mongo {
1341 | interface Selector {
1342 | [key: string]: any;
1343 | }
1344 | interface Selector extends Object {}
1345 | interface Modifier {}
1346 | interface SortSpecifier {}
1347 | interface FieldSpecifier {
1348 | [id: string]: Number;
1349 | }
1350 |
1351 | var Collection: CollectionStatic;
1352 | interface CollectionStatic {
1353 | new < T > (name: string, options ? : {
1354 | connection ? : Object;
1355 | idGeneration ? : string;
1356 | transform ? : Function;
1357 | }): Collection < T > ;
1358 | }
1359 | interface Collection < T > {
1360 | allow(options: {
1361 | insert ? : (userId: string, doc: T) => boolean;
1362 | update ? : (userId: string, doc: T, fieldNames: string[], modifier: any) => boolean;
1363 | remove ? : (userId: string, doc: T) => boolean;
1364 | fetch ? : string[];
1365 | transform ? : Function;
1366 | }): boolean;
1367 | deny(options: {
1368 | insert ? : (userId: string, doc: T) => boolean;
1369 | update ? : (userId: string, doc: T, fieldNames: string[], modifier: any) => boolean;
1370 | remove ? : (userId: string, doc: T) => boolean;
1371 | fetch ? : string[];
1372 | transform ? : Function;
1373 | }): boolean;
1374 | find(selector ? : Selector | ObjectID | string, options ? : {
1375 | sort ? : SortSpecifier;
1376 | skip ? : number;
1377 | limit ? : number;
1378 | fields ? : FieldSpecifier;
1379 | reactive ? : boolean;
1380 | transform ? : Function;
1381 | }): Cursor < T > ;
1382 | findOne(selector ? : Selector | ObjectID | string, options ? : {
1383 | sort ? : SortSpecifier;
1384 | skip ? : number;
1385 | fields ? : FieldSpecifier;
1386 | reactive ? : boolean;
1387 | transform ? : Function;
1388 | }): T;
1389 | insert(doc: T, callback ? : Function): string;
1390 | rawCollection(): any;
1391 | rawDatabase(): any;
1392 | remove(selector: Selector | ObjectID | string, callback ? : Function): number;
1393 | update(selector: Selector | ObjectID | string, modifier: Modifier, options ? : {
1394 | multi ? : boolean;
1395 | upsert ? : boolean;
1396 | }, callback ? : Function): number;
1397 | upsert(selector: Selector | ObjectID | string, modifier: Modifier, options ? : {
1398 | multi ? : boolean;
1399 | }, callback ? : Function): {
1400 | numberAffected ? : number;insertedId ? : string;
1401 | };
1402 | _ensureIndex(indexName: string, options ? : {
1403 | [key: string]: any
1404 | }): void;
1405 | }
1406 |
1407 | var Cursor: CursorStatic;
1408 | interface CursorStatic {
1409 | new < T > (): Cursor < T > ;
1410 | }
1411 | interface ObserveCallbacks {
1412 | added ? (document: Object) : void;
1413 | addedAt ? (document: Object, atIndex: number, before: Object) : void;
1414 | changed ? (newDocument: Object, oldDocument: Object) : void;
1415 | changedAt ? (newDocument: Object, oldDocument: Object, indexAt: number) : void;
1416 | removed ? (oldDocument: Object) : void;
1417 | removedAt ? (oldDocument: Object, atIndex: number) : void;
1418 | movedTo ? (document: Object, fromIndex: number, toIndex: number, before: Object) : void;
1419 | }
1420 | interface ObserveChangesCallbacks {
1421 | added ? (id: string, fields: Object) : void;
1422 | addedBefore ? (id: string, fields: Object, before: Object) : void;
1423 | changed ? (id: string, fields: Object) : void;
1424 | movedBefore ? (id: string, before: Object) : void;
1425 | removed ? (id: string) : void;
1426 | }
1427 | interface Cursor < T > {
1428 | count(): number;
1429 | fetch(): Array < T > ;
1430 | forEach(callback: < T > (doc: T, index: number, cursor: Cursor < T > ) => void, thisArg ? : any): void;
1431 | map < U > (callback: (doc: T, index: number, cursor: Cursor < T > ) => U, thisArg ? : any): Array < U > ;
1432 | observe(callbacks: ObserveCallbacks): Meteor.LiveQueryHandle;
1433 | observeChanges(callbacks: ObserveChangesCallbacks): Meteor.LiveQueryHandle;
1434 | }
1435 |
1436 | var ObjectID: ObjectIDStatic;
1437 | interface ObjectIDStatic {
1438 | new(hexString ? : string): ObjectID;
1439 | }
1440 | interface ObjectID {}
1441 | }
1442 |
1443 | declare module "meteor/mongo" {
1444 | module Mongo {
1445 | interface Selector {
1446 | [key: string]: any;
1447 | }
1448 | interface Selector extends Object {}
1449 | interface Modifier {}
1450 | interface SortSpecifier {}
1451 | interface FieldSpecifier {
1452 | [id: string]: Number;
1453 | }
1454 |
1455 | var Collection: CollectionStatic;
1456 | interface CollectionStatic {
1457 | new < T > (name: string, options ? : {
1458 | connection ? : Object;
1459 | idGeneration ? : string;
1460 | transform ? : Function;
1461 | }): Collection < T > ;
1462 | }
1463 | interface Collection < T > {
1464 | allow(options: {
1465 | insert ? : (userId: string, doc: T) => boolean;
1466 | update ? : (userId: string, doc: T, fieldNames: string[], modifier: any) => boolean;
1467 | remove ? : (userId: string, doc: T) => boolean;
1468 | fetch ? : string[];
1469 | transform ? : Function;
1470 | }): boolean;
1471 | deny(options: {
1472 | insert ? : (userId: string, doc: T) => boolean;
1473 | update ? : (userId: string, doc: T, fieldNames: string[], modifier: any) => boolean;
1474 | remove ? : (userId: string, doc: T) => boolean;
1475 | fetch ? : string[];
1476 | transform ? : Function;
1477 | }): boolean;
1478 | find(selector ? : Selector | ObjectID | string, options ? : {
1479 | sort ? : SortSpecifier;
1480 | skip ? : number;
1481 | limit ? : number;
1482 | fields ? : FieldSpecifier;
1483 | reactive ? : boolean;
1484 | transform ? : Function;
1485 | }): Cursor < T > ;
1486 | findOne(selector ? : Selector | ObjectID | string, options ? : {
1487 | sort ? : SortSpecifier;
1488 | skip ? : number;
1489 | fields ? : FieldSpecifier;
1490 | reactive ? : boolean;
1491 | transform ? : Function;
1492 | }): T;
1493 | insert(doc: T, callback ? : Function): string;
1494 | rawCollection(): any;
1495 | rawDatabase(): any;
1496 | remove(selector: Selector | ObjectID | string, callback ? : Function): number;
1497 | update(selector: Selector | ObjectID | string, modifier: Modifier, options ? : {
1498 | multi ? : boolean;
1499 | upsert ? : boolean;
1500 | }, callback ? : Function): number;
1501 | upsert(selector: Selector | ObjectID | string, modifier: Modifier, options ? : {
1502 | multi ? : boolean;
1503 | }, callback ? : Function): {
1504 | numberAffected ? : number;insertedId ? : string;
1505 | };
1506 | _ensureIndex(indexName: string, options ? : {
1507 | [key: string]: any
1508 | }): void;
1509 | }
1510 |
1511 | var Cursor: CursorStatic;
1512 | interface CursorStatic {
1513 | new < T > (): Cursor < T > ;
1514 | }
1515 | interface ObserveCallbacks {
1516 | added ? (document: Object) : void;
1517 | addedAt ? (document: Object, atIndex: number, before: Object) : void;
1518 | changed ? (newDocument: Object, oldDocument: Object) : void;
1519 | changedAt ? (newDocument: Object, oldDocument: Object, indexAt: number) : void;
1520 | removed ? (oldDocument: Object) : void;
1521 | removedAt ? (oldDocument: Object, atIndex: number) : void;
1522 | movedTo ? (document: Object, fromIndex: number, toIndex: number, before: Object) : void;
1523 | }
1524 | interface ObserveChangesCallbacks {
1525 | added ? (id: string, fields: Object) : void;
1526 | addedBefore ? (id: string, fields: Object, before: Object) : void;
1527 | changed ? (id: string, fields: Object) : void;
1528 | movedBefore ? (id: string, before: Object) : void;
1529 | removed ? (id: string) : void;
1530 | }
1531 | interface Cursor < T > {
1532 | count(): number;
1533 | fetch(): Array < T > ;
1534 | forEach(callback: < T > (doc: T, index: number, cursor: Cursor < T > ) => void, thisArg ? : any): void;
1535 | map < U > (callback: (doc: T, index: number, cursor: Cursor < T > ) => U, thisArg ? : any): Array < U > ;
1536 | observe(callbacks: ObserveCallbacks): Meteor.LiveQueryHandle;
1537 | observeChanges(callbacks: ObserveChangesCallbacks): Meteor.LiveQueryHandle;
1538 | }
1539 |
1540 | var ObjectID: ObjectIDStatic;
1541 | interface ObjectIDStatic {
1542 | new(hexString ? : string): ObjectID;
1543 | }
1544 | interface ObjectID {}
1545 | }
1546 | }
1547 | declare module Mongo {
1548 | interface AllowDenyOptions {
1549 | insert ? : (userId: string, doc: any) => boolean;
1550 | update ? : (userId: string, doc: any, fieldNames: string[], modifier: any) => boolean;
1551 | remove ? : (userId: string, doc: any) => boolean;
1552 | fetch ? : string[];
1553 | transform ? : Function;
1554 | }
1555 | }
1556 |
1557 | declare module "meteor/mongo" {
1558 | module Mongo {
1559 | interface AllowDenyOptions {
1560 | insert ? : (userId: string, doc: any) => boolean;
1561 | update ? : (userId: string, doc: any, fieldNames: string[], modifier: any) => boolean;
1562 | remove ? : (userId: string, doc: any) => boolean;
1563 | fetch ? : string[];
1564 | transform ? : Function;
1565 | }
1566 | }
1567 | }
1568 | declare module Random {
1569 | function id(numberOfChars ? : number): string;
1570 |
1571 | function secret(numberOfChars ? : number): string;
1572 |
1573 | function fraction(): number;
1574 | // @param numberOfDigits, @returns a random hex string of the given length
1575 | function hexString(numberOfDigits: number): string;
1576 | // @param array, @return a random element in array
1577 | function choice(array: any[]): string;
1578 | // @param str, @return a random char in str
1579 | function choice(str: string): string;
1580 | }
1581 |
1582 | declare module "meteor/random" {
1583 | module Random {
1584 | function id(numberOfChars ? : number): string;
1585 |
1586 | function secret(numberOfChars ? : number): string;
1587 |
1588 | function fraction(): number;
1589 | // @param numberOfDigits, @returns a random hex string of the given length
1590 | function hexString(numberOfDigits: number): string;
1591 | // @param array, @return a random element in array
1592 | function choice(array: any[]): string;
1593 | // @param str, @return a random char in str
1594 | function choice(str: string): string;
1595 | }
1596 | }
1597 | declare var ReactiveVar: ReactiveVarStatic;
1598 | interface ReactiveVarStatic {
1599 | new < T > (initialValue: T, equalsFunc ? : Function): ReactiveVar < T > ;
1600 | }
1601 | interface ReactiveVar < T > {
1602 | get(): T;
1603 | set(newValue: T): void;
1604 | }
1605 |
1606 | declare module "meteor/reactive-var" {
1607 | var ReactiveVar: ReactiveVarStatic;
1608 | interface ReactiveVarStatic {
1609 | new < T > (initialValue: T, equalsFunc ? : Function): ReactiveVar < T > ;
1610 | }
1611 | interface ReactiveVar < T > {
1612 | get(): T;
1613 | set(newValue: T): void;
1614 | }
1615 | }
1616 |
1617 | declare module Session {
1618 | function equals(key: string, value: string | number | boolean | any): boolean;
1619 |
1620 | function get(key: string): any;
1621 |
1622 | function set(key: string, value: EJSONable | any): void;
1623 |
1624 | function setDefault(key: string, value: EJSONable | any): void;
1625 | }
1626 |
1627 | declare module "meteor/session" {
1628 | ///
1629 |
1630 | module Session {
1631 | function equals(key: string, value: string | number | boolean | any): boolean;
1632 |
1633 | function get(key: string): any;
1634 |
1635 | function set(key: string, value: EJSONable | any): void;
1636 |
1637 | function setDefault(key: string, value: EJSONable | any): void;
1638 | }
1639 | }
1640 |
1641 | declare var Template: TemplateStatic;
1642 | interface TemplateStatic extends Blaze.TemplateStatic {
1643 | new(viewName ? : string, renderFunction ? : Function): Blaze.Template;
1644 | body: Blaze.Template;
1645 | [index: string]: any | Blaze.Template;
1646 | }
1647 |
1648 | declare module "meteor/templating" {
1649 | ///
1650 |
1651 | var Template: TemplateStatic;
1652 | interface TemplateStatic extends Blaze.TemplateStatic {
1653 | new(viewName ? : string, renderFunction ? : Function): Blaze.Template;
1654 | body: Blaze.Template;
1655 | [index: string]: any | Blaze.Template;
1656 | }
1657 | }
1658 | interface ILengthAble {
1659 | length: number;
1660 | }
1661 |
1662 | interface ITinytestAssertions {
1663 | ok(doc: Object): void;
1664 | expect_fail(): void;
1665 | fail(doc: Object): void;
1666 | runId(): string;
1667 | equal < T > (actual: T, expected: T, message ? : string, not ? : boolean): void;
1668 | notEqual < T > (actual: T, expected: T, message ? : string): void;
1669 | instanceOf(obj: Object, klass: Function, message ? : string): void;
1670 | notInstanceOf(obj: Object, klass: Function, message ? : string): void;
1671 | matches(actual: any, regexp: RegExp, message ? : string): void;
1672 | notMatches(actual: any, regexp: RegExp, message ? : string): void;
1673 | throws(f: Function, expected ? : string | RegExp): void;
1674 | isTrue(v: boolean, msg ? : string): void;
1675 | isFalse(v: boolean, msg ? : string): void;
1676 | isNull(v: any, msg ? : string): void;
1677 | isNotNull(v: any, msg ? : string): void;
1678 | isUndefined(v: any, msg ? : string): void;
1679 | isNotUndefined(v: any, msg ? : string): void;
1680 | isNan(v: any, msg ? : string): void;
1681 | isNotNan(v: any, msg ? : string): void;
1682 | include < T > (s: Array < T > | Object | string, value: any, msg ? : string, not ? : boolean): void;
1683 |
1684 | notInclude < T > (s: Array < T > | Object | string, value: any, msg ? : string, not ? : boolean): void;
1685 | length(obj: ILengthAble, expected_length: number, msg ? : string): void;
1686 | _stringEqual(actual: string, expected: string, msg ? : string): void;
1687 | }
1688 |
1689 | declare module Tinytest {
1690 | function add(description: string, func: (test: ITinytestAssertions) => void): void;
1691 |
1692 | function addAsync(description: string, func: (test: ITinytestAssertions) => void): void;
1693 | }
1694 |
1695 | declare module "meteor/tiny-test" {
1696 | interface ILengthAble {
1697 | length: number;
1698 | }
1699 |
1700 | interface ITinytestAssertions {
1701 | ok(doc: Object): void;
1702 | expect_fail(): void;
1703 | fail(doc: Object): void;
1704 | runId(): string;
1705 | equal < T > (actual: T, expected: T, message ? : string, not ? : boolean): void;
1706 | notEqual < T > (actual: T, expected: T, message ? : string): void;
1707 | instanceOf(obj: Object, klass: Function, message ? : string): void;
1708 | notInstanceOf(obj: Object, klass: Function, message ? : string): void;
1709 | matches(actual: any, regexp: RegExp, message ? : string): void;
1710 | notMatches(actual: any, regexp: RegExp, message ? : string): void;
1711 | throws(f: Function, expected ? : string | RegExp): void;
1712 | isTrue(v: boolean, msg ? : string): void;
1713 | isFalse(v: boolean, msg ? : string): void;
1714 | isNull(v: any, msg ? : string): void;
1715 | isNotNull(v: any, msg ? : string): void;
1716 | isUndefined(v: any, msg ? : string): void;
1717 | isNotUndefined(v: any, msg ? : string): void;
1718 | isNan(v: any, msg ? : string): void;
1719 | isNotNan(v: any, msg ? : string): void;
1720 | include < T > (s: Array < T > | Object | string, value: any, msg ? : string, not ? : boolean): void;
1721 |
1722 | notInclude < T > (s: Array < T > | Object | string, value: any, msg ? : string, not ? : boolean): void;
1723 | length(obj: ILengthAble, expected_length: number, msg ? : string): void;
1724 | _stringEqual(actual: string, expected: string, msg ? : string): void;
1725 | }
1726 |
1727 | module Tinytest {
1728 | function add(description: string, func: (test: ITinytestAssertions) => void): void;
1729 |
1730 | function addAsync(description: string, func: (test: ITinytestAssertions) => void): void;
1731 | }
1732 | }
1733 | declare module App {
1734 | function accessRule(pattern: string, options ? : {
1735 | type ? : string;
1736 | launchExternal ? : boolean;
1737 | }): void;
1738 |
1739 | function configurePlugin(id: string, config: Object): void;
1740 |
1741 | function icons(icons: Object): void;
1742 |
1743 | function info(options: {
1744 | id ? : string;
1745 | version ? : string;
1746 | name ? : string;
1747 | description ? : string;
1748 | author ? : string;
1749 | email ? : string;
1750 | website ? : string;
1751 | }): void;
1752 |
1753 | function launchScreens(launchScreens: Object): void;
1754 |
1755 | function setPreference(name: string, value: string, platform ? : string): void;
1756 | }
1757 |
1758 | declare function execFileAsync(command: string, args ? : any[], options ? : {
1759 | cwd ? : Object;
1760 | env ? : Object;
1761 | stdio ? : any[] | string;
1762 | destination ? : any;
1763 | waitForClose ? : string;
1764 | }): any;
1765 | declare function execFileSync(command: string, args ? : any[], options ? : {
1766 | cwd ? : Object;
1767 | env ? : Object;
1768 | stdio ? : any[] | string;
1769 | destination ? : any;
1770 | waitForClose ? : string;
1771 | }): String;
1772 |
1773 | declare module Assets {
1774 | function getBinary(assetPath: string, asyncCallback ? : Function): EJSON;
1775 |
1776 | function getText(assetPath: string, asyncCallback ? : Function): string;
1777 | }
1778 |
1779 | declare module Cordova {
1780 | function depends(dependencies: {
1781 | [id: string]: string
1782 | }): void;
1783 | }
1784 |
1785 | declare module Npm {
1786 | function depends(dependencies: {
1787 | [id: string]: string
1788 | }): void;
1789 |
1790 | function require(name: string): any;
1791 | }
1792 |
1793 | declare namespace Package {
1794 | function describe(options: {
1795 | summary ? : string;
1796 | version ? : string;
1797 | name ? : string;
1798 | git ? : string;
1799 | documentation ? : string;
1800 | debugOnly ? : boolean;
1801 | prodOnly ? : boolean;
1802 | testOnly ? : boolean;
1803 | }): void;
1804 |
1805 | function onTest(func: (api: PackageAPI) => void): void;
1806 |
1807 | function onUse(func: (api: PackageAPI) => void): void;
1808 |
1809 | function registerBuildPlugin(options ? : {
1810 | name ? : string;
1811 | use ? : string | string[];
1812 | sources ? : string[];
1813 | npmDependencies ? : Object;
1814 | }): void;
1815 | }
1816 |
1817 | interface PackageAPI {
1818 | new(): PackageAPI;
1819 | addAssets(filenames: string | string[], architecture: string | string[]): void;
1820 | addFiles(filenames: string | string[], architecture ? : string | string[], options ? : {
1821 | bare ? : boolean;
1822 | }): void;
1823 | export (exportedObjects: string | string[], architecture ? : string | string[], exportOptions ? : Object, testOnly ? : boolean): void;
1824 | imply(packageNames: string | string[], architecture ? : string | string[]): void;
1825 | use(packageNames: string | string[], architecture ? : string | string[], options ? : {
1826 | weak ? : boolean;
1827 | unordered ? : boolean;
1828 | }): void;
1829 | versionsFrom(meteorRelease: string | string[]): void;
1830 | }
1831 |
1832 | declare var console: Console;
1833 |
1834 | declare module "meteor/tools" {
1835 | module App {
1836 | function accessRule(pattern: string, options ? : {
1837 | type ? : string;
1838 | launchExternal ? : boolean;
1839 | }): void;
1840 |
1841 | function configurePlugin(id: string, config: Object): void;
1842 |
1843 | function icons(icons: Object): void;
1844 |
1845 | function info(options: {
1846 | id ? : string;
1847 | version ? : string;
1848 | name ? : string;
1849 | description ? : string;
1850 | author ? : string;
1851 | email ? : string;
1852 | website ? : string;
1853 | }): void;
1854 |
1855 | function launchScreens(launchScreens: Object): void;
1856 |
1857 | function setPreference(name: string, value: string, platform ? : string): void;
1858 | }
1859 |
1860 | function execFileAsync(command: string, args ? : any[], options ? : {
1861 | cwd ? : Object;
1862 | env ? : Object;
1863 | stdio ? : any[] | string;
1864 | destination ? : any;
1865 | waitForClose ? : string;
1866 | }): any;
1867 |
1868 | function execFileSync(command: string, args ? : any[], options ? : {
1869 | cwd ? : Object;
1870 | env ? : Object;
1871 | stdio ? : any[] | string;
1872 | destination ? : any;
1873 | waitForClose ? : string;
1874 | }): String;
1875 |
1876 | module Assets {
1877 | function getBinary(assetPath: string, asyncCallback ? : Function): EJSON;
1878 |
1879 | function getText(assetPath: string, asyncCallback ? : Function): string;
1880 | }
1881 |
1882 | module Cordova {
1883 | function depends(dependencies: {
1884 | [id: string]: string
1885 | }): void;
1886 | }
1887 |
1888 | module Npm {
1889 | function depends(dependencies: {
1890 | [id: string]: string
1891 | }): void;
1892 |
1893 | function require(name: string): any;
1894 | }
1895 |
1896 | namespace Package {
1897 | function describe(options: {
1898 | summary ? : string;
1899 | version ? : string;
1900 | name ? : string;
1901 | git ? : string;
1902 | documentation ? : string;
1903 | debugOnly ? : boolean;
1904 | prodOnly ? : boolean;
1905 | testOnly ? : boolean;
1906 | }): void;
1907 |
1908 | function onTest(func: (api: PackageAPI) => void): void;
1909 |
1910 | function onUse(func: (api: PackageAPI) => void): void;
1911 |
1912 | function registerBuildPlugin(options ? : {
1913 | name ? : string;
1914 | use ? : string | string[];
1915 | sources ? : string[];
1916 | npmDependencies ? : Object;
1917 | }): void;
1918 | }
1919 |
1920 | interface PackageAPI {
1921 | new(): PackageAPI;
1922 | addAssets(filenames: string | string[], architecture: string | string[]): void;
1923 | addFiles(filenames: string | string[], architecture ? : string | string[], options ? : {
1924 | bare ? : boolean;
1925 | }): void;
1926 | export (exportedObjects: string | string[], architecture ? : string | string[], exportOptions ? : Object, testOnly ? : boolean): void;
1927 | imply(packageNames: string | string[], architecture ? : string | string[]): void;
1928 | use(packageNames: string | string[], architecture ? : string | string[], options ? : {
1929 | weak ? : boolean;
1930 | unordered ? : boolean;
1931 | }): void;
1932 | versionsFrom(meteorRelease: string | string[]): void;
1933 | }
1934 |
1935 | var console: Console;
1936 | }
1937 | declare module Tracker {
1938 | function Computation(): void;
1939 | interface Computation {
1940 | firstRun: boolean;
1941 | invalidate(): void;
1942 | invalidated: boolean;
1943 | onInvalidate(callback: Function): void;
1944 | onStop(callback: Function): void;
1945 | stop(): void;
1946 | stopped: boolean;
1947 | }
1948 | var currentComputation: Computation;
1949 |
1950 | var Dependency: DependencyStatic;
1951 | interface DependencyStatic {
1952 | new(): Dependency;
1953 | }
1954 | interface Dependency {
1955 | changed(): void;
1956 | depend(fromComputation ? : Computation): boolean;
1957 | hasDependents(): boolean;
1958 | }
1959 |
1960 | var active: boolean;
1961 |
1962 | function afterFlush(callback: Function): void;
1963 |
1964 | function autorun(runFunc: (computation: Computation) => void, options ? : {
1965 | onError ? : Function;
1966 | }): Computation;
1967 |
1968 | function flush(): void;
1969 |
1970 | function nonreactive(func: Function): void;
1971 |
1972 | function onInvalidate(callback: Function): void;
1973 | }
1974 |
1975 | declare module "meteor/tracker" {
1976 | module Tracker {
1977 | function Computation(): void;
1978 | interface Computation {
1979 | firstRun: boolean;
1980 | invalidate(): void;
1981 | invalidated: boolean;
1982 | onInvalidate(callback: Function): void;
1983 | onStop(callback: Function): void;
1984 | stop(): void;
1985 | stopped: boolean;
1986 | }
1987 | var currentComputation: Computation;
1988 |
1989 | var Dependency: DependencyStatic;
1990 | interface DependencyStatic {
1991 | new(): Dependency;
1992 | }
1993 | interface Dependency {
1994 | changed(): void;
1995 | depend(fromComputation ? : Computation): boolean;
1996 | hasDependents(): boolean;
1997 | }
1998 |
1999 | var active: boolean;
2000 |
2001 | function afterFlush(callback: Function): void;
2002 |
2003 | function autorun(runFunc: (computation: Computation) => void, options ? : {
2004 | onError ? : Function;
2005 | }): Computation;
2006 |
2007 | function flush(): void;
2008 |
2009 | function nonreactive(func: Function): void;
2010 |
2011 | function onInvalidate(callback: Function): void;
2012 | }
2013 | }
--------------------------------------------------------------------------------
/Contributing.md:
--------------------------------------------------------------------------------
1 | # Publishing a new version
2 |
3 | - [ ] Pick an appropriate incremented version number v0.x.y (e.g. 0.4.0 becomes 0.4.1 or 0.5.0 depending on how much changed)
4 | - [ ] Update the `History.md` file to reflect all changes that will be in the new version. If you've been keeping it up to date (and it appears you have), this just involves inserting the above version number below vNEXT.
5 | - [ ] Update this version number in `package.js`.
6 | - [ ] git commit (but don't push yet).
7 | - [ ] Try `npm run publish`
8 | - [ ] If everything worked, `git tag v0.x.y` and push the tag (`git push origin v0.x.y`; this allows others to find the code for this version) and merge into master and push that too. (If you aren't rebasing the feature branch you may want to merge first before publishing)
9 | - [ ] If publishing didn't work, you can fix things, amend the commit as necessary, then tag and push after verifying that it went through.
10 |
11 | In order to `meteor publish`, you will need to be added as a maintainer to this package. You can see a list of the current maintainers with:
12 |
13 | ```
14 | meteor admin maintainers mizzao:user-status --list
15 | ```
16 |
17 | # Travis CI
18 |
19 | To be written.
20 |
21 | See https://travis-ci.org/Meteor-Community-Packages/meteor-user-status
22 |
23 | # Pushing a demo
24 |
25 | To be written.
26 |
27 | We host the demo at https://user-status.meteorapp.com.
28 |
29 |
--------------------------------------------------------------------------------
/History.md:
--------------------------------------------------------------------------------
1 | ## vNEXT
2 |
3 | ## v1.1.1
4 | * Add newer Meteor versions to `versionsFrom`
5 |
6 | ## v1.1.0
7 |
8 | * Bumped dependency versions
9 | * Updated tests
10 | * `Meteor.settings?.packages?.['mizzao:user-status']?.startupQuerySelector` option added to allow for custom startup selector
11 |
12 | ## v1.0.1
13 |
14 | * Bumped dependency versions
15 |
16 | ## v1.0.0
17 |
18 | * Decaffeinated!
19 | * BREAKING: User status now needs to be properly imported: `import { UserStatus } from 'meteor/mizzao:user-status'`
20 | * Added ESLint and jsbeautify
21 | * Added husky for pre-commit lint checking
22 | * Don't try to stop monitor in testing if it's not started
23 | * Upgrade demo to Meteor 1.8.1
24 | * Upgraded timesync to 0.5.1
25 | * Formatted all JS, CSS, and HTML files to jsbeautify specs
26 | * Updated all references of github.com/mizzao to github.com/Meteor-Community-Packages
27 | * Changed TravisCI to use Node 8.15.1 and added lint checks
28 | * Added Visual Studio Code settings.json
29 | * Changed all tabs to 2 spaces
30 | * Minimum meteor requirement now 1.7.0.5
31 |
32 | ## v0.6.8
33 |
34 | * Updated Coffeescript dependency to allow both v1 and v2.
35 |
36 | ## v0.6.7
37 |
38 | * Remove jQuery dependency
39 |
40 | ## v0.6.6
41 |
42 | * Declare dependent packages explicitly for Meteor 1.2.
43 |
44 | ## v0.6.5
45 |
46 | * Detect pause/resume events in Cordova. (#47, #64)
47 |
48 | ## v0.6.4
49 |
50 | * Improve consistency of the `status.online` and `status.idle` fields. (#31)
51 |
52 | ## v0.6.3
53 |
54 | * Update usage of the Mongo Collection API for Meteor 0.9.1+.
55 | * Add compatibility for the `audit-argument-checks` package.
56 |
57 | ## v0.6.2
58 |
59 | * Fix constraint syntax for the released Meteor 0.9.0.
60 |
61 | ## v0.6.1
62 |
63 | * **Updated for Meteor 0.9.**
64 |
65 | ## v0.6.0
66 |
67 | * Connections now record user agents - useful for diagnostic purposes. See the demo at http://user-status.meteor.com/.
68 | * The `lastLogin` field of documents in `Meteor.users` is **no longer a date**; it is an object with fields `date`, `ipAddr`, and `userAgent`. **Use `lastLogin.date` instead of simply `lastlogin` if you were depending on this behavior.** This provides a quick way to display connected users' IPs and UAs for administration or diagnostic purposes.
69 | * Better handling of idle/active events if TimeSync loses its computed offset temporarily due to a clock change.
70 |
71 | ## v0.5.0
72 |
73 | * All connections are tracked, including anonymous (not authenticated) ones. Idle monitoring is supported on anonymous connections, and idle state will persist across a login/logout. (#22)
74 | * The `Meteor.onConnection`, `connection.onClose`, and `Accounts.onLogin` functions are used to handle most changes in user state, except for logging out which is not directly supported in Meteor. This takes advantage of Meteor's new DDP heartbeats, and should improve issues with dangling connections caused by unclosed SockJS sockets. (#26)
75 | * Ensure that the last activity timestamp is reset when restarting the idle monitor after stopping it. (#27)
76 | * Add several unit tests for client-side idle monitoring logic.
77 |
78 | ## v0.4.1
79 |
80 | * Ensure that the latest activity timestamp updates when focusing into the app's window.
81 |
82 | ## v0.4.0
83 |
84 | * Store all dates as native `Date` objects in the database (#20). Most usage will be unaffected due to many libraries supporting the interchangeable use of `Date` objects or integer timestamps, but **some behavior may change when using operations with automatic type coercion, such as addition**.
85 |
86 | ## v0.3.5
87 |
88 | * Add some shim code for better compatibility with fast-render. (#24)
89 |
90 | ## v0.3.4
91 |
92 | * Fix an issue with properly recording the user's latest idle time across a reconnection.
93 | * Ignore actions generated while the window is blurred with `idleOnBlur` enabled.
94 |
95 | ## v0.3.3
96 |
97 | * Refactored server-side code so that it was more testable, and added multiplexing tests.
98 | * Fixed an issue where idle state would not be maintained if a connection was interrupted.
99 |
100 | ## v0.3.2
101 |
102 | * Fixed an issue where stopping the idle monitor could leave the client in an idle state.
103 |
104 | ## v0.3.1
105 |
106 | * Added multiplexing of idle status to `Meteor.users` as requested by @timhaines.
107 |
108 | ## v0.3.0
109 |
110 | * Added opt-in automatic **idle monitoring** on the client, which is reported to the server. See the demo app.
111 | * Export a single `UserStatus` variable on the server and the client, that contains all operations. **Breaks compatibility with previous usage:** the previous `UserStatus` variable is now `UserStatus.events`.
112 | * The `sessionLogin` and `sessionLogout` events have been renamed `connectionLogin` and `connectionLogout` along with the new `connectionIdle` and `connectionActive` events. **Breaks compatibility with previous usage.**
113 | * In callbacks, `sessionId` has also been renamed `connectionId` as per the Meteor change.
114 |
115 | ## v0.2.0
116 |
117 | * Exported `UserStatus` and `UserSessions` using the new API.
118 | * Moved the user status information into the `status` field instead of the `profile` field, which is user editable by default. **NOTE: this introduces a breaking change.** (#8)
119 | * Introduced a last login time for each session, and a combined time for the status field. (#8)
120 | * Added some basic tests.
121 |
122 | ## v0.1.7
123 |
124 | * Fixed a nuanced bug with the use of `upsert`.
125 |
126 | ## v0.1.6
127 |
128 | * Changed `find`/`insert`/`update` to a single upsert instead (#3, #6).
129 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Andrew Mao
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | user-status
2 | ===========
3 |
4 | ## What's this do?
5 |
6 | Keeps track of user connection data, such as IP addresses, user agents, and
7 | client-side activity, and tracks this information in `Meteor.users` as well as
8 | some other objects. This allows you to easily see users that are online, for
9 | applications such as rendering the users box below showing online users in green
10 | and idle users in orange.
11 |
12 | 
13 |
14 | For a complete example of what can be tracked, including inactivity, IP
15 | addresses, and user agents, check out a demo app at
16 | http://user-status.meteor.com, or its
17 | [source](https://github.com/Meteor-Community-Packages/meteor-user-status/tree/master/demo).
18 |
19 | ## Install
20 |
21 | Install using Meteor:
22 |
23 | ```sh
24 | $ meteor add mizzao:user-status
25 | ```
26 |
27 | Additionally, note that to read client IP addresses properly, you must set the
28 | `HTTP_FORWARDED_COUNT` environment variable for your app, and make sure that IP
29 | address headers are forwarded for any reverse proxy installed in front of the
30 | app. See the [Meteor docs on this](http://docs.meteor.com/#meteor_onconnection)
31 | for more details.
32 |
33 | ## Basic Usage - Online State
34 |
35 | This package maintains two types of status: a general user online flag in `Meteor.users`, and some additional data for each session. It uses [timesync](https://github.com/Meteor-Community-Packages/meteor-timesync) to maintain the server's time across all clients, regardless of whether they have the correct time.
36 |
37 | `Meteor.users` receives a `status` field will be updated automatically if the user logs in or logs out, closes their browser, or otherwise disconnects. A user is online if at least one connection with that `userId` is logged in. It contains the following fields:
38 |
39 | - `online`: `true` if there is at least one connection online for this user
40 | - `lastLogin`: information about the most recent login of the user, with the fields `date`, `ipAddr`, and `userAgent`.
41 | - `idle`: `true` if all connections for this user are idle. Requires idle tracking to be turned on for all connections, as below.
42 | - `lastActivity`: if the user was idle, the last time an action was observed. This field is only available when the user is online and idle. It does not maintain the user's last activity in real time or a stored value indefinitely - `lastLogin` is a coarse approximation to that. For more information, see https://github.com/Meteor-Community-Packages/meteor-user-status/issues/80.
43 |
44 | To make this available on the client, use a reactive cursor, such as by creating a publication on the server:
45 |
46 | ```javascript
47 | Meteor.publish("userStatus", function() {
48 | return Meteor.users.find({ "status.online": true }, { fields: { ... } });
49 | });
50 | ```
51 |
52 | or you can use this to do certain actions when users go online and offline.
53 |
54 | ```javascript
55 | Meteor.users.find({ "status.online": true }).observe({
56 | added: function(id) {
57 | // id just came online
58 | },
59 | removed: function(id) {
60 | // id just went offline
61 | }
62 | });
63 | ```
64 |
65 | You can use a reactive cursor to select online users in a client-side template helper:
66 |
67 | ```javascript
68 | Template.foo.usersOnline = function() {
69 | return Meteor.users.find({ "status.online": true })
70 | };
71 | ```
72 |
73 | Making this directly available on the client allows for useful template renderings of user state. For example, with something like the following you get the picture above (using bootstrap classes).
74 |
75 | ```
76 |
77 | {{username}}
78 |
79 | ```
80 |
81 | ```javascript
82 | Template.userPill.labelClass = function() {
83 | if (this.status.idle)
84 | return "label-warning"
85 | else if (this.status.online)
86 | return "label-success"
87 | else
88 | return "label-default"
89 | };
90 | ```
91 |
92 | ## Advanced Usage and Idle Tracking
93 |
94 | ### Client API
95 |
96 | On the client, the `UserStatus` object provides for seamless automatic monitoring of a client's idle state. By default, it will listen for all clicks and keypresses in `window` as signals of a user's action. It has the following functions:
97 |
98 | - `startMonitor`: a function taking an object with fields `threshold` (the amount of time before a user is counted as idle), `interval` (how often to check if a client has gone idle), and `idleOnBlur` (whether to count a window blur as a user going idle.) This function enables idle tracking on the client.
99 | - `stopMonitor`: stops the running monitor.
100 | - `pingMonitor`: if the automatic event handlers aren't catching what you need, you can manually ping the monitor to signal that a user is doing something and reset the idle monitor.
101 | - `isIdle`: a reactive variable signifying whether the user is currently idle or not.
102 | - `isMonitoring`: a reactive variable for whether the monitor is running.
103 | - `lastActivity`: a reactive variable for the last action recorded by the user (according to [server time](https://github.com/Meteor-Community-Packages/meteor-timesync)). Since this variable will be invalidated a lot and cause many recomputations, it's best only used for debugging or diagnostics (as in the demo).
104 |
105 | For an example of how the above functions are used, see the demo.
106 |
107 | ### Server API
108 |
109 | The `UserStatus.connections` (in-memory) collection contains information for all connections on the server, in the following fields:
110 |
111 | - `_id`: the connection id.
112 | - `userId`: the user id, if the connection is authenticated.
113 | - `ipAddr`: the remote address of the connection. A user logged in from different places will have one document per connection.
114 | - `userAgent`: the user agent of the connection.
115 | - `loginTime`: if authenticated, when the user logged in with this connection.
116 | - `idle`: `true` if idle monitoring is enabled on this connection and the client has gone idle.
117 |
118 | #### startupQuerySelector (Optional)
119 |
120 | On startup `meteor-user-status` automatically resets all users to `offline` and then marks each `online` as connections are reestablished.
121 |
122 | To customize this functionality you can use the `startupQuerySelector` [Meteor package option](https://docs.meteor.com/api/packagejs.html#options) like this:
123 | ```javascript
124 | "packages": {
125 | "mizzao:user-status": {
126 | "startupQuerySelector": { "$or": [{ "status.online": true }, { "status.idle": { "$exists": true } }, { "status.lastActivity": { "$exists": true } }] }
127 | }
128 | }
129 | ```
130 | The above example reduces server/database load during startup if you have a large number of users.
131 |
132 | #### Usage with collection2
133 |
134 | If your project is using `aldeed:collection2` with a schema attached to `Meteor.users`, you need to add the following items to the schema to allow modifications of the status:
135 |
136 | ```javascript
137 | import SimpleSchema from 'simpl-schema';
138 |
139 | const userSchema = new SimpleSchema({
140 | status: {
141 | type: Object,
142 | optional: true,
143 | },
144 | 'status.lastLogin': {
145 | type: Object,
146 | optional: true,
147 | },
148 | 'status.lastLogin.date': {
149 | type: Date,
150 | optional: true,
151 | },
152 | 'status.lastLogin.ipAddr': {
153 | type: String,
154 | optional: true,
155 | },
156 | 'status.lastLogin.userAgent': {
157 | type: String,
158 | optional: true,
159 | },
160 | 'status.idle': {
161 | type: Boolean,
162 | optional: true,
163 | },
164 | 'status.lastActivity': {
165 | type: Date,
166 | optional: true,
167 | },
168 | 'status.online': {
169 | type: Boolean,
170 | optional: true,
171 | },
172 | });
173 |
174 | // attaching the Schema to Meteor.users will extend it
175 | Meteor.users.attachSchema(userSchema);
176 |
177 | ```
178 |
179 | #### Events
180 |
181 | The `UserStatus.events` object is an `EventEmitter` on which you can listen for connections logging in and out. Logging out includes closing the browser; reopening the browser will trigger a new login event. The following events are supported:
182 |
183 | #### `UserStatus.events.on("connectionLogin", function(fields) { ... })`
184 |
185 | `fields` contains `userId`, `connectionId`, `ipAddr`, `userAgent`, and `loginTime`.
186 |
187 | #### `UserStatus.events.on("connectionLogout", function(fields) { ... })`
188 |
189 | `fields` contains `userId`, `connectionId`, `lastActivity`, and `logoutTime`.
190 |
191 | #### `UserStatus.events.on("connectionIdle", function(fields) { ... })`
192 |
193 | `fields` contains `userId`, `connectionId`, and `lastActivity`.
194 |
195 | #### `UserStatus.events.on("connectionActive", function(fields) { ... })`
196 |
197 | `fields` contains `userId`, `connectionId`, and `lastActivity`.
198 |
199 | Check out https://github.com/mizzao/meteor-accounts-testing for a simple accounts drop-in that you can use to test your app - this is also used in the demo.
200 |
201 | #### Startup selector
202 | By default, the startup selector for resetting user status is `{}`.
203 | If you want to change that you can set the default selector in your settings.json file:
204 |
205 | ```json
206 | {
207 | "packages": {
208 | "mizzao:user-status": {
209 | "startupQuerySelector": {
210 | // your selector here, for example:
211 | "profile.name": "admin"
212 | }
213 | }
214 | }
215 | }
216 | ```
217 |
218 | ## Testing
219 |
220 | There are some `Tinytest` unit tests that are used to test the logic in this package, but general testing with many users and connections is hard. Hence, we have set up a demo app (http://user-status.meteor.com) for testing that is also hosted as a proof of concept. If you think you've found a bug in the package, try to replicate it on the demo app and post an issue with steps to reproduce.
221 |
222 | ## Contributing
223 |
224 | See [Contributing.md](Contributing.md).
225 |
--------------------------------------------------------------------------------
/client/monitor.js:
--------------------------------------------------------------------------------
1 | /* globals window, document */
2 |
3 | import { Meteor } from 'meteor/meteor';
4 | import { TimeSync } from 'meteor/mizzao:timesync';
5 | import { Tracker } from 'meteor/tracker';
6 | /*
7 | The idle monitor watches for mouse, keyboard, and blur events,
8 | and reports idle status to the server.
9 |
10 | It uses TimeSync to report accurate time.
11 |
12 | Everything is reactive, of course!
13 | */
14 |
15 | // State variables
16 | let monitorId = null;
17 | let idle = false;
18 | let lastActivityTime = undefined;
19 |
20 | const monitorDep = new Tracker.Dependency;
21 | const idleDep = new Tracker.Dependency;
22 | const activityDep = new Tracker.Dependency;
23 |
24 | let focused = true;
25 |
26 | // These settings are internal or exported for test only
27 | export const MonitorInternals = {
28 | idleThreshold: null,
29 | idleOnBlur: false,
30 |
31 | computeState(lastActiveTime, currentTime, isWindowFocused) {
32 | const inactiveTime = currentTime - lastActiveTime;
33 | if (MonitorInternals.idleOnBlur && !isWindowFocused) {
34 | return true;
35 | }
36 | if (inactiveTime > MonitorInternals.idleThreshold) {
37 | return true;
38 | } else {
39 | return false;
40 | }
41 | },
42 |
43 | connectionChange(isConnected, wasConnected) {
44 | // We only need to do something if we reconnect and we are idle
45 | // Don't get idle status reactively, as this function only
46 | // takes care of reconnect status and doesn't care if it changes.
47 |
48 | // Note that userId does not change during a resume login, as designed by Meteor.
49 | // However, the idle state is tied to the connection and not the userId.
50 | if (isConnected && !wasConnected && idle) {
51 | return MonitorInternals.reportIdle(lastActivityTime);
52 | }
53 | },
54 |
55 | onWindowBlur() {
56 | focused = false;
57 | return monitor();
58 | },
59 |
60 | onWindowFocus() {
61 | focused = true;
62 | // Focusing should count as an action, otherwise "active" event may be
63 | // triggered at some point in the past!
64 | return monitor(true);
65 | },
66 |
67 | reportIdle(time) {
68 | return Meteor.call('user-status-idle', time);
69 | },
70 |
71 | reportActive(time) {
72 | return Meteor.call('user-status-active', time);
73 | }
74 |
75 | };
76 |
77 | const start = (settings) => {
78 | if (!TimeSync.isSynced()) {
79 | throw new Error('Can\'t start idle monitor until synced to server');
80 | }
81 | if (monitorId) {
82 | throw new Error('Idle monitor is already active. Stop it first.');
83 | }
84 |
85 | settings = settings || {};
86 |
87 | // The amount of time before a user is marked idle
88 | MonitorInternals.idleThreshold = settings.threshold || 60000;
89 |
90 | // Don't check too quickly; it doesn't matter anyway: http://stackoverflow.com/q/15871942/586086
91 | const interval = Math.max(settings.interval || 1000, 1000);
92 |
93 | // Whether blurring the window should immediately cause the user to go idle
94 | MonitorInternals.idleOnBlur = (settings.idleOnBlur != null) ? settings.idleOnBlur : false;
95 |
96 | // Set new monitoring interval
97 | monitorId = Meteor.setInterval(monitor, interval);
98 | monitorDep.changed();
99 |
100 | // Reset last activity; can't count inactivity from some arbitrary time
101 | if (lastActivityTime == null) {
102 | lastActivityTime = Tracker.nonreactive(() => TimeSync.serverTime());
103 | activityDep.changed();
104 | }
105 |
106 | monitor();
107 | };
108 |
109 | const stop = () => {
110 | if (!monitorId) {
111 | throw new Error('Idle monitor is not running.');
112 | }
113 |
114 | Meteor.clearInterval(monitorId);
115 | monitorId = null;
116 | lastActivityTime = undefined; // If monitor started again, we shouldn't re-use this time
117 | monitorDep.changed();
118 |
119 | if (idle) { // Un-set any idleness
120 | idle = false;
121 | idleDep.changed();
122 | // need to run this because the Tracker below won't re-run when monitor is off
123 | MonitorInternals.reportActive(Tracker.nonreactive(() => TimeSync.serverTime()));
124 | }
125 |
126 | };
127 |
128 | const monitor = (setAction) => {
129 | // Ignore focus/blur events when we aren't monitoring
130 | if (!monitorId) {
131 | return;
132 | }
133 |
134 | // We use setAction here to not have to call serverTime twice. Premature optimization?
135 | const currentTime = Tracker.nonreactive(() => TimeSync.serverTime());
136 | // Can't monitor if we haven't synced with server yet, or lost our sync.
137 | if (currentTime == null) {
138 | return;
139 | }
140 |
141 | // Update action as long as we're not blurred and idling on blur
142 | // We ignore actions that happen while a client is blurred, if idleOnBlur is set.
143 | if (setAction && (focused || !MonitorInternals.idleOnBlur)) {
144 | lastActivityTime = currentTime;
145 | activityDep.changed();
146 | }
147 |
148 | const newIdle = MonitorInternals.computeState(lastActivityTime, currentTime, focused);
149 |
150 | if (newIdle !== idle) {
151 | idle = newIdle;
152 | idleDep.changed();
153 | }
154 | };
155 |
156 | const touch = () => {
157 | if (!monitorId) {
158 | Meteor._debug('Cannot touch as idle monitor is not running.');
159 | return;
160 | }
161 | return monitor(true); // Check for an idle state change right now
162 | };
163 |
164 | const isIdle = () => {
165 | idleDep.depend();
166 | return idle;
167 | };
168 |
169 | const isMonitoring = () => {
170 | monitorDep.depend();
171 | return (monitorId != null);
172 | };
173 |
174 | const lastActivity = () => {
175 | if (!isMonitoring()) {
176 | return;
177 | }
178 | activityDep.depend();
179 | return lastActivityTime;
180 | };
181 |
182 | Meteor.startup(() => {
183 | // Listen for mouse and keyboard events on window
184 | // TODO other stuff - e.g. touch events?
185 | window.addEventListener('click', () => monitor(true));
186 | window.addEventListener('keydown', () => monitor(true));
187 |
188 | // catch window blur events when requested and where supported
189 | // We'll use jQuery here instead of window.blur so that other code can attach blur events:
190 | // http://stackoverflow.com/q/22415296/586086
191 | window.addEventListener('blur', MonitorInternals.onWindowBlur);
192 | window.addEventListener('focus', MonitorInternals.onWindowFocus);
193 |
194 | // Catch Cordova "pause" and "resume" events:
195 | // https://github.com/mizzao/meteor-user-status/issues/47
196 | if (Meteor.isCordova) {
197 | document.addEventListener('pause', MonitorInternals.onWindowBlur);
198 | document.addEventListener('resume', MonitorInternals.onWindowFocus);
199 | }
200 |
201 | // First check initial state if window loaded while blurred
202 | // Some browsers don't fire focus on load: http://stackoverflow.com/a/10325169/586086
203 | focused = document.hasFocus();
204 |
205 | // Report idle status whenever connection changes
206 | Tracker.autorun(() => {
207 | // Don't report idle state unless we're monitoring
208 | if (!isMonitoring()) {
209 | return;
210 | }
211 |
212 | // XXX These will buffer across a disconnection - do we want that?
213 | // The idle report will result in a duplicate message (with below)
214 | // The active report will result in a null op.
215 | if (isIdle()) {
216 | MonitorInternals.reportIdle(lastActivityTime);
217 | } else {
218 | // If we were inactive, report that we are active again to the server
219 | MonitorInternals.reportActive(lastActivityTime);
220 | }
221 | });
222 |
223 | // If we reconnect and we were idle, make sure we send that upstream
224 | let wasConnected = Meteor.status().connected;
225 | return Tracker.autorun(() => {
226 | const {
227 | connected
228 | } = Meteor.status();
229 | MonitorInternals.connectionChange(connected, wasConnected);
230 |
231 | wasConnected = connected;
232 | });
233 | });
234 |
235 | // export functions for starting and stopping idle monitor
236 | export const UserStatus = {
237 | startMonitor: start,
238 | stopMonitor: stop,
239 | pingMonitor: touch,
240 | isIdle,
241 | isMonitoring,
242 | lastActivity
243 | };
244 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | settings.json
2 |
--------------------------------------------------------------------------------
/demo/.meteor/.finished-upgraders:
--------------------------------------------------------------------------------
1 | # This file contains information which helps Meteor properly upgrade your
2 | # app when you run 'meteor update'. You should check it into version control
3 | # with your project.
4 |
5 | notices-for-0.9.0
6 | notices-for-0.9.1
7 | 0.9.4-platform-file
8 | notices-for-facebook-graph-api-2
9 | 1.2.0-standard-minifiers-package
10 | 1.2.0-meteor-platform-split
11 | 1.2.0-cordova-changes
12 | 1.2.0-breaking-changes
13 | 1.3.0-split-minifiers-package
14 | 1.4.0-remove-old-dev-bundle-link
15 | 1.4.1-add-shell-server-package
16 | 1.4.3-split-account-service-packages
17 | 1.5-add-dynamic-import-package
18 | 1.7-split-underscore-from-meteor-base
19 | 1.8.3-split-jquery-from-blaze
20 |
--------------------------------------------------------------------------------
/demo/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 |
--------------------------------------------------------------------------------
/demo/.meteor/.id:
--------------------------------------------------------------------------------
1 | # This file contains a token that is unique to your project.
2 | # Check it into your repository along with the rest of this directory.
3 | # It can be used for purposes such as:
4 | # - ensuring you don't accidentally deploy one app on top of another
5 | # - providing package authors with aggregated statistics
6 |
7 | 1s8s711noc9bq16pktns
8 |
--------------------------------------------------------------------------------
/demo/.meteor/identifier:
--------------------------------------------------------------------------------
1 | hz6wab1631fff885otl
--------------------------------------------------------------------------------
/demo/.meteor/packages:
--------------------------------------------------------------------------------
1 | # Meteor packages used by this project, one per line.
2 | # Check this file (and the other files in this directory) into your repository.
3 | #
4 | # 'meteor add' and 'meteor remove' will edit this file for you,
5 | # but you can also edit it by hand.
6 |
7 | meteor-base@1.5.1 # Packages every Meteor app needs to have
8 | mobile-experience@1.1.1 # Packages for a great mobile UX
9 | mongo@1.16.8 # The database Meteor supports right now
10 | blaze-html-templates # Compile .html files into Meteor Blaze views
11 | reactive-var@1.0.12 # Reactive variable for tracker
12 | tracker@1.3.3 # Meteor's client-side reactive programming library
13 |
14 | standard-minifier-css@1.9.2 # CSS minifier run for production mode
15 | standard-minifier-js@2.8.1 # JS minifier run for production mode
16 | es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers
17 | ecmascript@0.16.8 # Enable ECMAScript2015+ syntax in app code
18 | shell-server@0.5.0 # Server-side component of the `meteor shell` command
19 |
20 | # communitypackages:accounts-testing
21 | mizzao:timesync
22 | mizzao:user-status
23 | twbs:bootstrap
24 | dynamic-import@0.7.3
25 | underscore@1.6.0
26 | jquery
27 |
--------------------------------------------------------------------------------
/demo/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/demo/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@2.15
2 |
--------------------------------------------------------------------------------
/demo/.meteor/versions:
--------------------------------------------------------------------------------
1 | accounts-base@2.2.10
2 | allow-deny@1.1.1
3 | autoupdate@1.8.0
4 | babel-compiler@7.10.5
5 | babel-runtime@1.5.1
6 | base64@1.0.12
7 | binary-heap@1.0.11
8 | blaze@2.8.0
9 | blaze-html-templates@2.0.1
10 | blaze-tools@1.1.4
11 | boilerplate-generator@1.7.2
12 | caching-compiler@1.2.2
13 | caching-html-compiler@1.2.2
14 | callback-hook@1.5.1
15 | check@1.3.2
16 | ddp@1.4.1
17 | ddp-client@2.6.1
18 | ddp-common@1.4.0
19 | ddp-rate-limiter@1.2.1
20 | ddp-server@2.7.0
21 | diff-sequence@1.1.2
22 | dynamic-import@0.7.3
23 | ecmascript@0.16.8
24 | ecmascript-runtime@0.8.1
25 | ecmascript-runtime-client@0.12.1
26 | ecmascript-runtime-server@0.11.0
27 | ejson@1.1.3
28 | es5-shim@4.8.0
29 | fetch@0.1.4
30 | geojson-utils@1.0.11
31 | hot-code-push@1.0.4
32 | html-tools@1.1.4
33 | htmljs@1.2.0
34 | http@2.0.0
35 | id-map@1.1.1
36 | inter-process-messaging@0.1.1
37 | jquery@1.11.11
38 | launch-screen@2.0.0
39 | localstorage@1.2.0
40 | logging@1.3.3
41 | meteor@1.11.5
42 | meteor-base@1.5.1
43 | minifier-css@1.6.4
44 | minifier-js@2.7.5
45 | minimongo@1.9.3
46 | mizzao:timesync@0.5.4
47 | mizzao:user-status@1.2.0
48 | mobile-experience@1.1.1
49 | mobile-status-bar@1.1.0
50 | modern-browsers@0.1.10
51 | modules@0.20.0
52 | modules-runtime@0.13.1
53 | mongo@1.16.8
54 | mongo-decimal@0.1.3
55 | mongo-dev-server@1.1.0
56 | mongo-id@1.0.8
57 | npm-mongo@4.17.2
58 | observe-sequence@1.0.22
59 | ordered-dict@1.1.0
60 | promise@0.12.2
61 | random@1.2.1
62 | rate-limit@1.1.1
63 | react-fast-refresh@0.2.8
64 | reactive-var@1.0.12
65 | reload@1.3.1
66 | retry@1.1.0
67 | routepolicy@1.1.1
68 | shell-server@0.5.0
69 | socket-stream-client@0.5.2
70 | spacebars@1.4.1
71 | spacebars-compiler@1.3.2
72 | standard-minifier-css@1.9.2
73 | standard-minifier-js@2.8.1
74 | templating@1.4.3
75 | templating-compiler@1.4.2
76 | templating-runtime@1.6.4
77 | templating-tools@1.2.3
78 | tracker@1.3.3
79 | twbs:bootstrap@3.3.6
80 | typescript@4.9.5
81 | underscore@1.6.1
82 | url@1.3.2
83 | webapp@1.13.8
84 | webapp-hashing@1.1.1
85 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | ## Demo/test app for user-status
2 |
3 | Note the following cool features:
4 |
5 | - All clients are synced to server time using [timesync](https://github.com/mizzao/meteor-timesync), and can make local comparisons even if their actual time is way off.
6 | - Each connection maintains its own idle (or not) state.
7 | - When a client goes idle (by crossing some threshold), the last activity time is recorded for when it started being idle.
8 |
--------------------------------------------------------------------------------
/demo/client/main.css:
--------------------------------------------------------------------------------
1 | tr.bold>td {
2 | font-weight: bold;
3 | text-decoration: underline;
4 | }
5 |
--------------------------------------------------------------------------------
/demo/client/main.html:
--------------------------------------------------------------------------------
1 |
2 | User-Status Demonstration and Testing
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Instructions
13 |
14 | - Log in with any username.
15 | - Log in from other computers with the same username or open multiple tabs.
16 | These will appear as sessions/connections under the same user.
17 | - Click or keypress events reset the idle timer. The default idle threshold is 30 seconds.
18 | - Idle session information is transmitted to the server and multiplexed into the user document.
19 | - Play with the idle timer to try different client-side settings.
20 | - Click 'Resync Time' a few times to see the variance in TimeSync.
21 |
22 | {{> login}}
23 | {{> status userStatus}}
24 |
25 |
26 | {{> serverStatus }}
27 |
28 |
29 |
30 |
31 |
32 |
33 | {{#if loggedIn}}
34 | You are logged in as:
35 | {{> loginButtons}}
36 | {{else}}
37 |
43 | {{/if}}
44 |
45 |
46 |
47 | Local status:
48 |
49 |
50 |
51 |
52 | - Server Time:
53 | - {{serverTime}}
54 | - Local Offset:
55 | - {{serverOffset}} ms
56 | - RTT:
57 | - {{serverRTT}} ms
58 | - Idle:
59 | - {{isIdleText}}
60 | - Monitor on:
61 | - {{isMonitoringText}}
62 | - Last Activity:
63 | - {{lastActivity}}
64 |
65 |
66 | {{#if isMonitoring}}
67 | Idle monitoring is on.
68 |
69 | {{else}}
70 |
83 | {{/if}}
84 |
85 |
86 |
87 | All user connections on server:
88 |
89 |
90 |
91 |
92 | Username/Connection |
93 | Idle |
94 | Last Activity |
95 | IP (Last) |
96 | User Agent (Last) |
97 |
98 |
99 |
100 | {{#each anonymous}}
101 | {{> serverConnection}}
102 | {{/each}}
103 | {{#each users}}
104 |
105 | {{username}} (last login {{localeTime status.lastLogin.date}}) |
106 | {{status.idle}} |
107 | {{#with status.lastActivity}}
108 | {{> relTime}}
109 | {{else}}
110 | (active or not monitoring)
111 | {{/with}}
112 | |
113 | {{status.lastLogin.ipAddr}} |
114 | {{status.lastLogin.userAgent}} |
115 |
116 | {{#each connections}}
117 | {{> serverConnection}}
118 | {{/each}}
119 | {{/each}}
120 |
121 |
122 |
123 |
124 |
125 |
126 | {{_id}}{{#with loginTime}} (login {{this}}){{/with}} |
127 | {{idle}} |
128 | {{#with lastActivity}}
129 | {{> relTime}}
130 | {{else}}
131 | (active or not monitoring)
132 | {{/with}}
133 | |
134 | {{ipAddr}} |
135 | {{userAgent}} |
136 |
137 |
138 |
139 |
140 | {{localeTime this}}
141 |
142 | ({{relativeTime this}})
143 |
144 |
--------------------------------------------------------------------------------
/demo/client/main.js:
--------------------------------------------------------------------------------
1 | import { Handlebars } from 'meteor/blaze';
2 | import { Meteor } from 'meteor/meteor';
3 | import { TimeSync } from 'meteor/mizzao:timesync';
4 | import { Mongo } from 'meteor/mongo';
5 | import { Template } from 'meteor/templating';
6 | import { Tracker } from 'meteor/tracker';
7 | import { UserStatus } from 'meteor/mizzao:user-status';
8 | import moment from 'moment';
9 |
10 | import './main.html';
11 |
12 | const UserConnections = new Mongo.Collection('user_status_sessions');
13 |
14 | const relativeTime = (timeAgo) => {
15 | const diff = moment.utc(TimeSync.serverTime() - timeAgo);
16 | const time = diff.format('H:mm:ss');
17 | const days = +diff.format('DDD') - 1;
18 | const ago = (days ? days + 'd ' : '') + time;
19 | return `${ago} ago`;
20 | };
21 |
22 | Handlebars.registerHelper('userStatus', UserStatus);
23 | Handlebars.registerHelper('localeTime', date => date != null ? date.toLocaleString() : undefined);
24 | Handlebars.registerHelper('relativeTime', relativeTime);
25 |
26 | Template.login.helpers({
27 | loggedIn() {
28 | return Meteor.userId();
29 | }
30 | });
31 |
32 | Template.status.events = {
33 | 'submit form.start-monitor'(e, tmpl) {
34 | e.preventDefault();
35 | return UserStatus.startMonitor({
36 | threshold: tmpl.find('input[name=threshold]').valueAsNumber,
37 | interval: tmpl.find('input[name=interval]').valueAsNumber,
38 | idleOnBlur: tmpl.find('select[name=idleOnBlur]').value === 'true'
39 | });
40 | },
41 |
42 | 'click .stop-monitor'() {
43 | return UserStatus.stopMonitor();
44 | },
45 | 'click .resync'() {
46 | return TimeSync.resync();
47 | }
48 | };
49 |
50 | Template.status.helpers({
51 | lastActivity() {
52 | const lastActivity = this.lastActivity();
53 | if (lastActivity != null) {
54 | return relativeTime(lastActivity);
55 | } else {
56 | return 'undefined';
57 | }
58 | }
59 | });
60 |
61 | Template.status.helpers({
62 | serverTime() {
63 | return new Date(TimeSync.serverTime()).toLocaleString();
64 | },
65 | serverOffset: TimeSync.serverOffset,
66 | serverRTT: TimeSync.roundTripTime,
67 |
68 | // Falsy values aren't rendered in templates, so let's render them ourself
69 | isIdleText() {
70 | return this.isIdle() || 'false';
71 | },
72 | isMonitoringText() {
73 | return this.isMonitoring() || 'false';
74 | }
75 | });
76 |
77 | Template.serverStatus.helpers({
78 | anonymous() {
79 | return UserConnections.find({
80 | userId: {
81 | $exists: false
82 | }
83 | });
84 | },
85 | users() {
86 | return Meteor.users.find();
87 | },
88 | userClass() {
89 | if ((this.status != null ? this.status.idle : undefined)) {
90 | return 'warning';
91 | } else {
92 | return 'success';
93 | }
94 | },
95 | connections() {
96 | return UserConnections.find({
97 | userId: this._id
98 | });
99 | }
100 | });
101 |
102 | Template.serverConnection.helpers({
103 | connectionClass() {
104 | if (this.idle) {
105 | return 'warning';
106 | } else {
107 | return 'success';
108 | }
109 | },
110 | loginTime() {
111 | if (this.loginTime == null) {
112 | return;
113 | }
114 | return new Date(this.loginTime).toLocaleString();
115 | }
116 | });
117 |
118 | Template.login.events = {
119 | 'submit form'(e, tmpl) {
120 | e.preventDefault();
121 | const input = tmpl.find('input[name=username]');
122 | input.blur();
123 | return Meteor.insecureUserLogin(input.value, (err) => {
124 | if (err) {
125 | return console.log(err);
126 | }
127 | });
128 | }
129 | };
130 |
131 | // Start monitor as soon as we got a signal, captain!
132 | Tracker.autorun((c) => {
133 | try { // May be an error if time is not synced
134 | UserStatus.startMonitor({
135 | threshold: 30000,
136 | idleOnBlur: true
137 | });
138 | return c.stop();
139 | } catch (error) {
140 | console.error(error);
141 | }
142 | });
143 |
--------------------------------------------------------------------------------
/demo/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | DEPLOY_HOSTNAME=galaxy.meteor.com meteor deploy user-status.meteorapp.com --settings settings.json
3 |
--------------------------------------------------------------------------------
/demo/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "user-status-demo",
3 | "requires": true,
4 | "lockfileVersion": 1,
5 | "dependencies": {
6 | "@babel/code-frame": {
7 | "version": "7.10.3",
8 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz",
9 | "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==",
10 | "dev": true,
11 | "requires": {
12 | "@babel/highlight": "^7.10.3"
13 | }
14 | },
15 | "@babel/helper-environment-visitor": {
16 | "version": "7.22.20",
17 | "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
18 | "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
19 | "dev": true
20 | },
21 | "@babel/helper-hoist-variables": {
22 | "version": "7.22.5",
23 | "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
24 | "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
25 | "dev": true,
26 | "requires": {
27 | "@babel/types": "^7.22.5"
28 | },
29 | "dependencies": {
30 | "@babel/helper-validator-identifier": {
31 | "version": "7.22.20",
32 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
33 | "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
34 | "dev": true
35 | },
36 | "@babel/types": {
37 | "version": "7.23.0",
38 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
39 | "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
40 | "dev": true,
41 | "requires": {
42 | "@babel/helper-string-parser": "^7.22.5",
43 | "@babel/helper-validator-identifier": "^7.22.20",
44 | "to-fast-properties": "^2.0.0"
45 | }
46 | }
47 | }
48 | },
49 | "@babel/helper-string-parser": {
50 | "version": "7.22.5",
51 | "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
52 | "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
53 | "dev": true
54 | },
55 | "@babel/helper-validator-identifier": {
56 | "version": "7.10.3",
57 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz",
58 | "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==",
59 | "dev": true
60 | },
61 | "@babel/highlight": {
62 | "version": "7.10.3",
63 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz",
64 | "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==",
65 | "dev": true,
66 | "requires": {
67 | "@babel/helper-validator-identifier": "^7.10.3",
68 | "chalk": "^2.0.0",
69 | "js-tokens": "^4.0.0"
70 | }
71 | },
72 | "@babel/parser": {
73 | "version": "7.10.3",
74 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz",
75 | "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==",
76 | "dev": true
77 | },
78 | "@babel/runtime": {
79 | "version": "7.23.9",
80 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
81 | "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
82 | "requires": {
83 | "regenerator-runtime": "^0.14.0"
84 | }
85 | },
86 | "@babel/traverse": {
87 | "version": "7.23.2",
88 | "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
89 | "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
90 | "dev": true,
91 | "requires": {
92 | "@babel/code-frame": "^7.22.13",
93 | "@babel/generator": "^7.23.0",
94 | "@babel/helper-environment-visitor": "^7.22.20",
95 | "@babel/helper-function-name": "^7.23.0",
96 | "@babel/helper-hoist-variables": "^7.22.5",
97 | "@babel/helper-split-export-declaration": "^7.22.6",
98 | "@babel/parser": "^7.23.0",
99 | "@babel/types": "^7.23.0",
100 | "debug": "^4.1.0",
101 | "globals": "^11.1.0"
102 | },
103 | "dependencies": {
104 | "@babel/code-frame": {
105 | "version": "7.22.13",
106 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
107 | "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
108 | "dev": true,
109 | "requires": {
110 | "@babel/highlight": "^7.22.13",
111 | "chalk": "^2.4.2"
112 | }
113 | },
114 | "@babel/generator": {
115 | "version": "7.23.0",
116 | "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
117 | "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
118 | "dev": true,
119 | "requires": {
120 | "@babel/types": "^7.23.0",
121 | "@jridgewell/gen-mapping": "^0.3.2",
122 | "@jridgewell/trace-mapping": "^0.3.17",
123 | "jsesc": "^2.5.1"
124 | }
125 | },
126 | "@babel/helper-function-name": {
127 | "version": "7.23.0",
128 | "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
129 | "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
130 | "dev": true,
131 | "requires": {
132 | "@babel/template": "^7.22.15",
133 | "@babel/types": "^7.23.0"
134 | }
135 | },
136 | "@babel/helper-split-export-declaration": {
137 | "version": "7.22.6",
138 | "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
139 | "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
140 | "dev": true,
141 | "requires": {
142 | "@babel/types": "^7.22.5"
143 | }
144 | },
145 | "@babel/helper-validator-identifier": {
146 | "version": "7.22.20",
147 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
148 | "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
149 | "dev": true
150 | },
151 | "@babel/highlight": {
152 | "version": "7.22.20",
153 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
154 | "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
155 | "dev": true,
156 | "requires": {
157 | "@babel/helper-validator-identifier": "^7.22.20",
158 | "chalk": "^2.4.2",
159 | "js-tokens": "^4.0.0"
160 | }
161 | },
162 | "@babel/parser": {
163 | "version": "7.23.0",
164 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
165 | "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
166 | "dev": true
167 | },
168 | "@babel/template": {
169 | "version": "7.22.15",
170 | "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
171 | "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
172 | "dev": true,
173 | "requires": {
174 | "@babel/code-frame": "^7.22.13",
175 | "@babel/parser": "^7.22.15",
176 | "@babel/types": "^7.22.15"
177 | }
178 | },
179 | "@babel/types": {
180 | "version": "7.23.0",
181 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
182 | "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
183 | "dev": true,
184 | "requires": {
185 | "@babel/helper-string-parser": "^7.22.5",
186 | "@babel/helper-validator-identifier": "^7.22.20",
187 | "to-fast-properties": "^2.0.0"
188 | }
189 | }
190 | }
191 | },
192 | "@babel/types": {
193 | "version": "7.10.3",
194 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz",
195 | "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==",
196 | "dev": true,
197 | "requires": {
198 | "@babel/helper-validator-identifier": "^7.10.3",
199 | "lodash": "^4.17.13",
200 | "to-fast-properties": "^2.0.0"
201 | }
202 | },
203 | "@jridgewell/gen-mapping": {
204 | "version": "0.3.3",
205 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
206 | "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
207 | "dev": true,
208 | "requires": {
209 | "@jridgewell/set-array": "^1.0.1",
210 | "@jridgewell/sourcemap-codec": "^1.4.10",
211 | "@jridgewell/trace-mapping": "^0.3.9"
212 | }
213 | },
214 | "@jridgewell/resolve-uri": {
215 | "version": "3.1.1",
216 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
217 | "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
218 | "dev": true
219 | },
220 | "@jridgewell/set-array": {
221 | "version": "1.1.2",
222 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
223 | "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
224 | "dev": true
225 | },
226 | "@jridgewell/sourcemap-codec": {
227 | "version": "1.4.15",
228 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
229 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
230 | "dev": true
231 | },
232 | "@jridgewell/trace-mapping": {
233 | "version": "0.3.19",
234 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
235 | "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
236 | "dev": true,
237 | "requires": {
238 | "@jridgewell/resolve-uri": "^3.1.0",
239 | "@jridgewell/sourcemap-codec": "^1.4.14"
240 | }
241 | },
242 | "@mapbox/node-pre-gyp": {
243 | "version": "1.0.11",
244 | "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
245 | "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
246 | "requires": {
247 | "detect-libc": "^2.0.0",
248 | "https-proxy-agent": "^5.0.0",
249 | "make-dir": "^3.1.0",
250 | "node-fetch": "^2.6.7",
251 | "nopt": "^5.0.0",
252 | "npmlog": "^5.0.1",
253 | "rimraf": "^3.0.2",
254 | "semver": "^7.3.5",
255 | "tar": "^6.1.11"
256 | }
257 | },
258 | "abbrev": {
259 | "version": "1.1.1",
260 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
261 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
262 | },
263 | "agent-base": {
264 | "version": "6.0.2",
265 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
266 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
267 | "requires": {
268 | "debug": "4"
269 | }
270 | },
271 | "ansi-regex": {
272 | "version": "5.0.1",
273 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
274 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
275 | },
276 | "ansi-styles": {
277 | "version": "3.2.1",
278 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
279 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
280 | "dev": true,
281 | "requires": {
282 | "color-convert": "^1.9.0"
283 | }
284 | },
285 | "aproba": {
286 | "version": "2.0.0",
287 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
288 | "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
289 | },
290 | "are-we-there-yet": {
291 | "version": "2.0.0",
292 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
293 | "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
294 | "requires": {
295 | "delegates": "^1.0.0",
296 | "readable-stream": "^3.6.0"
297 | }
298 | },
299 | "autoprefixer": {
300 | "version": "10.4.17",
301 | "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz",
302 | "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==",
303 | "dev": true,
304 | "requires": {
305 | "browserslist": "^4.22.2",
306 | "caniuse-lite": "^1.0.30001578",
307 | "fraction.js": "^4.3.7",
308 | "normalize-range": "^0.1.2",
309 | "picocolors": "^1.0.0",
310 | "postcss-value-parser": "^4.2.0"
311 | }
312 | },
313 | "babel-eslint": {
314 | "version": "10.1.0",
315 | "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz",
316 | "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==",
317 | "dev": true,
318 | "requires": {
319 | "@babel/code-frame": "^7.0.0",
320 | "@babel/parser": "^7.7.0",
321 | "@babel/traverse": "^7.7.0",
322 | "@babel/types": "^7.7.0",
323 | "eslint-visitor-keys": "^1.0.0",
324 | "resolve": "^1.12.0"
325 | }
326 | },
327 | "balanced-match": {
328 | "version": "1.0.2",
329 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
330 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
331 | },
332 | "bcrypt": {
333 | "version": "5.1.1",
334 | "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
335 | "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
336 | "requires": {
337 | "@mapbox/node-pre-gyp": "^1.0.11",
338 | "node-addon-api": "^5.0.0"
339 | }
340 | },
341 | "brace-expansion": {
342 | "version": "1.1.11",
343 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
344 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
345 | "requires": {
346 | "balanced-match": "^1.0.0",
347 | "concat-map": "0.0.1"
348 | }
349 | },
350 | "browserslist": {
351 | "version": "4.23.0",
352 | "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
353 | "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
354 | "dev": true,
355 | "requires": {
356 | "caniuse-lite": "^1.0.30001587",
357 | "electron-to-chromium": "^1.4.668",
358 | "node-releases": "^2.0.14",
359 | "update-browserslist-db": "^1.0.13"
360 | }
361 | },
362 | "caniuse-lite": {
363 | "version": "1.0.30001591",
364 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz",
365 | "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==",
366 | "dev": true
367 | },
368 | "chalk": {
369 | "version": "2.4.2",
370 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
371 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
372 | "dev": true,
373 | "requires": {
374 | "ansi-styles": "^3.2.1",
375 | "escape-string-regexp": "^1.0.5",
376 | "supports-color": "^5.3.0"
377 | },
378 | "dependencies": {
379 | "supports-color": {
380 | "version": "5.5.0",
381 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
382 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
383 | "dev": true,
384 | "requires": {
385 | "has-flag": "^3.0.0"
386 | }
387 | }
388 | }
389 | },
390 | "chownr": {
391 | "version": "2.0.0",
392 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
393 | "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
394 | },
395 | "color-convert": {
396 | "version": "1.9.3",
397 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
398 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
399 | "dev": true,
400 | "requires": {
401 | "color-name": "1.1.3"
402 | }
403 | },
404 | "color-name": {
405 | "version": "1.1.3",
406 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
407 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
408 | "dev": true
409 | },
410 | "color-support": {
411 | "version": "1.1.3",
412 | "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
413 | "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="
414 | },
415 | "concat-map": {
416 | "version": "0.0.1",
417 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
418 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
419 | },
420 | "console-control-strings": {
421 | "version": "1.1.0",
422 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
423 | "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
424 | },
425 | "debug": {
426 | "version": "4.3.4",
427 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
428 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
429 | "requires": {
430 | "ms": "2.1.2"
431 | }
432 | },
433 | "delegates": {
434 | "version": "1.0.0",
435 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
436 | "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
437 | },
438 | "detect-libc": {
439 | "version": "2.0.2",
440 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
441 | "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="
442 | },
443 | "electron-to-chromium": {
444 | "version": "1.4.685",
445 | "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.685.tgz",
446 | "integrity": "sha512-yDYeobbTEe4TNooEzOQO6xFqg9XnAkVy2Lod1C1B2it8u47JNLYvl9nLDWBamqUakWB8Jc1hhS1uHUNYTNQdfw==",
447 | "dev": true
448 | },
449 | "emoji-regex": {
450 | "version": "8.0.0",
451 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
452 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
453 | },
454 | "escalade": {
455 | "version": "3.1.2",
456 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
457 | "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
458 | "dev": true
459 | },
460 | "escape-string-regexp": {
461 | "version": "1.0.5",
462 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
463 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
464 | "dev": true
465 | },
466 | "eslint-visitor-keys": {
467 | "version": "1.3.0",
468 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
469 | "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
470 | "dev": true
471 | },
472 | "fraction.js": {
473 | "version": "4.3.7",
474 | "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
475 | "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
476 | "dev": true
477 | },
478 | "fs-minipass": {
479 | "version": "2.1.0",
480 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
481 | "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
482 | "requires": {
483 | "minipass": "^3.0.0"
484 | },
485 | "dependencies": {
486 | "minipass": {
487 | "version": "3.3.6",
488 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
489 | "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
490 | "requires": {
491 | "yallist": "^4.0.0"
492 | }
493 | }
494 | }
495 | },
496 | "fs.realpath": {
497 | "version": "1.0.0",
498 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
499 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
500 | },
501 | "gauge": {
502 | "version": "3.0.2",
503 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
504 | "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
505 | "requires": {
506 | "aproba": "^1.0.3 || ^2.0.0",
507 | "color-support": "^1.1.2",
508 | "console-control-strings": "^1.0.0",
509 | "has-unicode": "^2.0.1",
510 | "object-assign": "^4.1.1",
511 | "signal-exit": "^3.0.0",
512 | "string-width": "^4.2.3",
513 | "strip-ansi": "^6.0.1",
514 | "wide-align": "^1.1.2"
515 | }
516 | },
517 | "glob": {
518 | "version": "7.2.3",
519 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
520 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
521 | "requires": {
522 | "fs.realpath": "^1.0.0",
523 | "inflight": "^1.0.4",
524 | "inherits": "2",
525 | "minimatch": "^3.1.1",
526 | "once": "^1.3.0",
527 | "path-is-absolute": "^1.0.0"
528 | }
529 | },
530 | "globals": {
531 | "version": "11.12.0",
532 | "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
533 | "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
534 | "dev": true
535 | },
536 | "has-flag": {
537 | "version": "3.0.0",
538 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
539 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
540 | "dev": true
541 | },
542 | "has-unicode": {
543 | "version": "2.0.1",
544 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
545 | "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
546 | },
547 | "https-proxy-agent": {
548 | "version": "5.0.1",
549 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
550 | "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
551 | "requires": {
552 | "agent-base": "6",
553 | "debug": "4"
554 | }
555 | },
556 | "inflight": {
557 | "version": "1.0.6",
558 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
559 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
560 | "requires": {
561 | "once": "^1.3.0",
562 | "wrappy": "1"
563 | }
564 | },
565 | "inherits": {
566 | "version": "2.0.4",
567 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
568 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
569 | },
570 | "is-fullwidth-code-point": {
571 | "version": "3.0.0",
572 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
573 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
574 | },
575 | "jquery": {
576 | "version": "3.7.1",
577 | "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
578 | "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
579 | },
580 | "js-tokens": {
581 | "version": "4.0.0",
582 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
583 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
584 | "dev": true
585 | },
586 | "jsesc": {
587 | "version": "2.5.2",
588 | "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
589 | "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
590 | "dev": true
591 | },
592 | "lodash": {
593 | "version": "4.17.21",
594 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
595 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
596 | "dev": true
597 | },
598 | "lru-cache": {
599 | "version": "6.0.0",
600 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
601 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
602 | "requires": {
603 | "yallist": "^4.0.0"
604 | }
605 | },
606 | "make-dir": {
607 | "version": "3.1.0",
608 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
609 | "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
610 | "requires": {
611 | "semver": "^6.0.0"
612 | },
613 | "dependencies": {
614 | "semver": {
615 | "version": "6.3.1",
616 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
617 | "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
618 | }
619 | }
620 | },
621 | "minimatch": {
622 | "version": "3.1.2",
623 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
624 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
625 | "requires": {
626 | "brace-expansion": "^1.1.7"
627 | }
628 | },
629 | "minipass": {
630 | "version": "5.0.0",
631 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
632 | "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="
633 | },
634 | "minizlib": {
635 | "version": "2.1.2",
636 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
637 | "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
638 | "requires": {
639 | "minipass": "^3.0.0",
640 | "yallist": "^4.0.0"
641 | },
642 | "dependencies": {
643 | "minipass": {
644 | "version": "3.3.6",
645 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
646 | "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
647 | "requires": {
648 | "yallist": "^4.0.0"
649 | }
650 | }
651 | }
652 | },
653 | "mkdirp": {
654 | "version": "1.0.4",
655 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
656 | "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
657 | },
658 | "moment": {
659 | "version": "2.30.1",
660 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
661 | "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="
662 | },
663 | "ms": {
664 | "version": "2.1.2",
665 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
666 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
667 | },
668 | "node-addon-api": {
669 | "version": "5.1.0",
670 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
671 | "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
672 | },
673 | "node-fetch": {
674 | "version": "2.7.0",
675 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
676 | "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
677 | "requires": {
678 | "whatwg-url": "^5.0.0"
679 | }
680 | },
681 | "node-releases": {
682 | "version": "2.0.14",
683 | "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
684 | "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
685 | "dev": true
686 | },
687 | "nopt": {
688 | "version": "5.0.0",
689 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
690 | "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
691 | "requires": {
692 | "abbrev": "1"
693 | }
694 | },
695 | "normalize-range": {
696 | "version": "0.1.2",
697 | "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
698 | "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
699 | "dev": true
700 | },
701 | "npmlog": {
702 | "version": "5.0.1",
703 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
704 | "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
705 | "requires": {
706 | "are-we-there-yet": "^2.0.0",
707 | "console-control-strings": "^1.1.0",
708 | "gauge": "^3.0.0",
709 | "set-blocking": "^2.0.0"
710 | }
711 | },
712 | "object-assign": {
713 | "version": "4.1.1",
714 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
715 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
716 | },
717 | "once": {
718 | "version": "1.4.0",
719 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
720 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
721 | "requires": {
722 | "wrappy": "1"
723 | }
724 | },
725 | "path-is-absolute": {
726 | "version": "1.0.1",
727 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
728 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
729 | },
730 | "path-parse": {
731 | "version": "1.0.7",
732 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
733 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
734 | "dev": true
735 | },
736 | "picocolors": {
737 | "version": "1.0.0",
738 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
739 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
740 | "dev": true
741 | },
742 | "postcss-value-parser": {
743 | "version": "4.2.0",
744 | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
745 | "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
746 | "dev": true
747 | },
748 | "readable-stream": {
749 | "version": "3.6.2",
750 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
751 | "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
752 | "requires": {
753 | "inherits": "^2.0.3",
754 | "string_decoder": "^1.1.1",
755 | "util-deprecate": "^1.0.1"
756 | }
757 | },
758 | "regenerator-runtime": {
759 | "version": "0.14.1",
760 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
761 | "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
762 | },
763 | "resolve": {
764 | "version": "1.17.0",
765 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
766 | "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
767 | "dev": true,
768 | "requires": {
769 | "path-parse": "^1.0.6"
770 | }
771 | },
772 | "rimraf": {
773 | "version": "3.0.2",
774 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
775 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
776 | "requires": {
777 | "glob": "^7.1.3"
778 | }
779 | },
780 | "safe-buffer": {
781 | "version": "5.2.1",
782 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
783 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
784 | },
785 | "semver": {
786 | "version": "7.5.4",
787 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
788 | "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
789 | "requires": {
790 | "lru-cache": "^6.0.0"
791 | }
792 | },
793 | "set-blocking": {
794 | "version": "2.0.0",
795 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
796 | "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
797 | },
798 | "signal-exit": {
799 | "version": "3.0.7",
800 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
801 | "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
802 | },
803 | "string-width": {
804 | "version": "4.2.3",
805 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
806 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
807 | "requires": {
808 | "emoji-regex": "^8.0.0",
809 | "is-fullwidth-code-point": "^3.0.0",
810 | "strip-ansi": "^6.0.1"
811 | }
812 | },
813 | "string_decoder": {
814 | "version": "1.3.0",
815 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
816 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
817 | "requires": {
818 | "safe-buffer": "~5.2.0"
819 | }
820 | },
821 | "strip-ansi": {
822 | "version": "6.0.1",
823 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
824 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
825 | "requires": {
826 | "ansi-regex": "^5.0.1"
827 | }
828 | },
829 | "tar": {
830 | "version": "6.2.1",
831 | "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
832 | "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
833 | "requires": {
834 | "chownr": "^2.0.0",
835 | "fs-minipass": "^2.0.0",
836 | "minipass": "^5.0.0",
837 | "minizlib": "^2.1.1",
838 | "mkdirp": "^1.0.3",
839 | "yallist": "^4.0.0"
840 | }
841 | },
842 | "to-fast-properties": {
843 | "version": "2.0.0",
844 | "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
845 | "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
846 | "dev": true
847 | },
848 | "tr46": {
849 | "version": "0.0.3",
850 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
851 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
852 | },
853 | "update-browserslist-db": {
854 | "version": "1.0.13",
855 | "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
856 | "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
857 | "dev": true,
858 | "requires": {
859 | "escalade": "^3.1.1",
860 | "picocolors": "^1.0.0"
861 | }
862 | },
863 | "util-deprecate": {
864 | "version": "1.0.2",
865 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
866 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
867 | },
868 | "webidl-conversions": {
869 | "version": "3.0.1",
870 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
871 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
872 | },
873 | "whatwg-url": {
874 | "version": "5.0.0",
875 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
876 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
877 | "requires": {
878 | "tr46": "~0.0.3",
879 | "webidl-conversions": "^3.0.0"
880 | }
881 | },
882 | "wide-align": {
883 | "version": "1.1.5",
884 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
885 | "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
886 | "requires": {
887 | "string-width": "^1.0.2 || 2 || 3 || 4"
888 | }
889 | },
890 | "wrappy": {
891 | "version": "1.0.2",
892 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
893 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
894 | },
895 | "yallist": {
896 | "version": "4.0.0",
897 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
898 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
899 | }
900 | }
901 | }
902 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "user-status-demo",
3 | "private": false,
4 | "scripts": {
5 | "lint": "./node_modules/.bin/eslint .",
6 | "test": "meteor test --once --driver-package meteortesting:mocha",
7 | "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha",
8 | "visualize": "meteor --production --extra-packages bundle-visualizer"
9 | },
10 | "dependencies": {
11 | "@babel/runtime": "^7.23.9",
12 | "bcrypt": "^5.1.1",
13 | "jquery": "^3.7.1",
14 | "moment": "^2.30.1"
15 | },
16 | "devDependencies": {
17 | "autoprefixer": "^10.4.17",
18 | "babel-eslint": "^10.1.0"
19 | },
20 | "meteor": {
21 | "mainModule": {
22 | "client": "client/main.js",
23 | "server": "server/main.js"
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/demo/packages/.gitignore:
--------------------------------------------------------------------------------
1 | /mizzao:timesync
2 | /mizzao:user-status
3 |
--------------------------------------------------------------------------------
/demo/packages/meteor-user-status/client/monitor.js:
--------------------------------------------------------------------------------
1 | ../../../../client/monitor.js
--------------------------------------------------------------------------------
/demo/packages/meteor-user-status/package.js:
--------------------------------------------------------------------------------
1 | ../../../package.js
--------------------------------------------------------------------------------
/demo/packages/meteor-user-status/server/status.js:
--------------------------------------------------------------------------------
1 | ../../../../server/status.js
--------------------------------------------------------------------------------
/demo/server/main.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { UserStatus } from 'meteor/mizzao:user-status';
3 |
4 | // Try setting this so it works on meteor.com
5 | // (https://github.com/oortcloud/unofficial-meteor-faq)
6 | process.env.HTTP_FORWARDED_COUNT = 1;
7 |
8 | Meteor.publish(null, () => [
9 | Meteor.users.find({
10 | 'status.online': true
11 | }, { // online users only
12 | fields: {
13 | status: 1,
14 | username: 1
15 | }
16 | }),
17 | UserStatus.connections.find()
18 | ]);
19 |
--------------------------------------------------------------------------------
/demo/settings-example.json:
--------------------------------------------------------------------------------
1 | {
2 | "galaxy.meteor.com": {
3 | "env": {
4 | "ROOT_URL": "https://user-status.meteorapp.com",
5 | "MONGO_URL": "mongodb://xxxxx:xxxxxx@xxxxx.mlab.com:37283/xxxxx"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docs/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meteor-Community-Packages/meteor-user-status/8cb45620af218d042e99846393ae3e5a0b973b33/docs/example.png
--------------------------------------------------------------------------------
/package.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 |
3 | Package.describe({
4 | name: 'mizzao:user-status',
5 | summary: 'User connection and idle state tracking for Meteor',
6 | version: '1.2.0',
7 | git: 'https://github.com/Meteor-Community-Packages/meteor-user-status.git'
8 | });
9 |
10 | Package.onUse((api) => {
11 | api.versionsFrom(['1.7.0.5', '2.7', '2.8.1', '2.15']);
12 |
13 | api.use('ecmascript');
14 | api.use('accounts-base');
15 | api.use('check');
16 | api.use('mongo');
17 | api.use('ddp');
18 | api.use('tracker', 'client');
19 | api.use('mizzao:timesync@0.5.4');
20 |
21 | api.export('MonitorInternals', 'client', {
22 | testOnly: true
23 | });
24 | api.export('StatusInternals', 'server', {
25 | testOnly: true
26 | });
27 |
28 | api.mainModule('client/monitor.js', 'client');
29 | api.mainModule('server/status.js', 'server');
30 |
31 | });
32 |
33 | Package.onTest((api) => {
34 | api.use('ecmascript');
35 | api.use('mizzao:user-status');
36 | api.use('mizzao:timesync@0.5.4');
37 |
38 | api.use(['accounts-base', 'accounts-password']);
39 |
40 | api.use(['random', 'tracker']);
41 |
42 | api.use('test-helpers');
43 | api.use('tinytest');
44 |
45 | api.addFiles('tests/insecure_login.js');
46 | api.addFiles('tests/setup.js');
47 | // Just some unit tests here. Use the test app otherwise.
48 | api.addFiles('tests/monitor_tests.js', 'client');
49 | api.addFiles('tests/status_tests.js', 'server');
50 |
51 | api.addFiles('tests/server_tests.js', 'server');
52 | api.addFiles('tests/client_tests.js', 'client');
53 | });
54 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "meteor-user-status",
3 | "version": "1.1.0",
4 | "repository": {
5 | "type": "git",
6 | "url": "git+https://github.com/Meteor-Community-Packages/meteor-user-status.git"
7 | },
8 | "bugs": {
9 | "url": "https://github.com/Meteor-Community-Packages/meteor-user-status/issues"
10 | },
11 | "homepage": "https://github.com/Meteor-Community-Packages/meteor-user-status#readme",
12 | "private": false,
13 | "scripts": {
14 | "lint": "./node_modules/.bin/eslint .",
15 | "test": "make test",
16 | "test-local": "meteor test-packages --once --driver-package test-in-console",
17 | "publish": "meteor npm i && npm prune --production && meteor publish && meteor npm i"
18 | },
19 | "devDependencies": {
20 | "@babel/core": "^7.23.9",
21 | "@babel/eslint-parser": "^7.23.10",
22 | "autoprefixer": "^10.4.17",
23 | "eslint": "^8.56.0",
24 | "husky": "^8.0.3",
25 | "mocha": "^10.2.0"
26 | },
27 | "husky": {
28 | "hooks": {
29 | "pre-commit": "npm run lint"
30 | }
31 | },
32 | "dependencies": {}
33 | }
34 |
--------------------------------------------------------------------------------
/server/status.js:
--------------------------------------------------------------------------------
1 | /*
2 | Apparently, the new api.export takes care of issues here. No need to attach to global namespace.
3 | See http://shiggyenterprises.wordpress.com/2013/09/09/meteor-packages-in-coffeescript-0-6-5/
4 |
5 | We may want to make UserSessions a server collection to take advantage of indices.
6 | Will implement if someone has enough online users to warrant it.
7 | */
8 | import { Accounts } from 'meteor/accounts-base';
9 | import { check, Match } from 'meteor/check';
10 | import { Meteor } from 'meteor/meteor';
11 | import { Mongo } from 'meteor/mongo';
12 | import { EventEmitter } from 'events';
13 |
14 | const UserConnections = new Mongo.Collection('user_status_sessions', {
15 | connection: null
16 | });
17 |
18 | const statusEvents = new (EventEmitter)();
19 |
20 | /*
21 | Multiplex login/logout events to status.online
22 |
23 | 'online' field is "true" if user is online, and "false" otherwise
24 |
25 | 'idle' field is tri-stated:
26 | - "true" if user is online and not idle
27 | - "false" if user is online and idle
28 | - null if user is offline
29 | */
30 | statusEvents.on('connectionLogin', (advice) => {
31 | const update = {
32 | $set: {
33 | 'status.online': true,
34 | 'status.lastLogin': {
35 | date: advice.loginTime,
36 | ipAddr: advice.ipAddr,
37 | userAgent: advice.userAgent
38 | }
39 | }
40 | };
41 |
42 | // unless ALL existing connections are idle (including this new one),
43 | // the user connection becomes active.
44 | const conns = UserConnections.find({
45 | userId: advice.userId
46 | }).fetch();
47 | if (!conns.every(c => c.idle)) {
48 | update.$set['status.idle'] = false;
49 | update.$unset = {
50 | 'status.lastActivity': null
51 | };
52 | }
53 | // in other case, idle field remains true and no update to lastActivity.
54 |
55 | Meteor.users.update(advice.userId, update);
56 | });
57 |
58 | statusEvents.on('connectionLogout', (advice) => {
59 | const conns = UserConnections.find({
60 | userId: advice.userId
61 | }).fetch();
62 | if (conns.length === 0) {
63 | // Go offline if we are the last connection for this user
64 | // This includes removing all idle information
65 | Meteor.users.update(advice.userId, {
66 | $set: {
67 | 'status.online': false
68 | },
69 | $unset: {
70 | 'status.idle': null,
71 | 'status.lastActivity': null
72 | }
73 | });
74 | } else if (conns.every(c => c.idle)) {
75 | /*
76 | All remaining connections are idle:
77 | - If the last active connection quit, then we should go idle with the most recent activity
78 |
79 | - If an idle connection quit, nothing should happen; specifically, if the
80 | most recently active idle connection quit, we shouldn't tick the value backwards.
81 | This may result in a no-op so we can be smart and skip the update.
82 | */
83 | if (advice.lastActivity != null) {
84 | return;
85 | } // The dropped connection was already idle
86 |
87 | const latestLastActivity = new Date(Math.max(...conns.map(conn => conn.lastActivity)));
88 | Meteor.users.update(advice.userId, {
89 | $set: {
90 | 'status.idle': true,
91 | 'status.lastActivity': latestLastActivity
92 | }
93 | });
94 | }
95 | });
96 |
97 | /*
98 | Multiplex idle/active events to status.idle
99 | TODO: Hopefully this is quick because it's all in memory, but we can use indices if it turns out to be slow
100 |
101 | TODO: There is a race condition when switching between tabs, leaving the user inactive while idle goes from one tab to the other.
102 | It can probably be smoothed out.
103 | */
104 | statusEvents.on('connectionIdle', (advice) => {
105 | const conns = UserConnections.find({
106 | userId: advice.userId
107 | }).fetch();
108 | if (!conns.every(c => c.idle)) {
109 | return;
110 | }
111 | // Set user to idle if all the connections are idle
112 | // This will not be the most recent idle across a disconnection, so we use max
113 |
114 | // TODO: the race happens here where everyone was idle when we looked for them but now one of them isn't.
115 | const latestLastActivity = new Date(Math.max(...conns.map(conn => conn.lastActivity)));
116 | Meteor.users.update(advice.userId, {
117 | $set: {
118 | 'status.idle': true,
119 | 'status.lastActivity': latestLastActivity
120 | }
121 | });
122 | });
123 |
124 | statusEvents.on('connectionActive', (advice) => {
125 | Meteor.users.update(advice.userId, {
126 | $set: {
127 | 'status.idle': false
128 | },
129 | $unset: {
130 | 'status.lastActivity': null
131 | }
132 | });
133 | });
134 |
135 | // Reset online status on startup (users will reconnect)
136 | const onStartup = (selector) => {
137 | if (selector == null) {
138 | selector = Meteor?.settings?.packages?.['mizzao:user-status']?.startupQuerySelector || {};
139 | }
140 | return Meteor.users.update(selector, {
141 | $set: {
142 | 'status.online': false
143 | },
144 | $unset: {
145 | 'status.idle': null,
146 | 'status.lastActivity': null
147 | }
148 | }, {
149 | multi: true
150 | });
151 | };
152 |
153 | /*
154 | Local session modification functions - also used in testing
155 | */
156 |
157 | const addSession = (connection) => {
158 | UserConnections.upsert(connection.id, {
159 | $set: {
160 | ipAddr: connection.clientAddress,
161 | userAgent: connection.httpHeaders['user-agent']
162 | }
163 | });
164 | };
165 |
166 | const loginSession = (connection, date, userId) => {
167 | UserConnections.upsert(connection.id, {
168 | $set: {
169 | userId,
170 | loginTime: date
171 | }
172 | });
173 |
174 | statusEvents.emit('connectionLogin', {
175 | userId,
176 | connectionId: connection.id,
177 | ipAddr: connection.clientAddress,
178 | userAgent: connection.httpHeaders['user-agent'],
179 | loginTime: date
180 | });
181 | };
182 |
183 | // Possibly trigger a logout event if this connection was previously associated with a user ID
184 | const tryLogoutSession = (connection, date) => {
185 | let conn;
186 | if ((conn = UserConnections.findOne({
187 | _id: connection.id,
188 | userId: {
189 | $exists: true
190 | }
191 | })) == null) {
192 | return false;
193 | }
194 |
195 | // Yes, this is actually a user logging out.
196 | UserConnections.upsert(connection.id, {
197 | $unset: {
198 | userId: null,
199 | loginTime: null
200 | }
201 | });
202 |
203 | return statusEvents.emit('connectionLogout', {
204 | userId: conn.userId,
205 | connectionId: connection.id,
206 | lastActivity: conn.lastActivity, // If this connection was idle, pass the last activity we saw
207 | logoutTime: date
208 | });
209 | };
210 |
211 | const removeSession = (connection, date) => {
212 | tryLogoutSession(connection, date);
213 | UserConnections.remove(connection.id);
214 | };
215 |
216 | const idleSession = (connection, date, userId) => {
217 | UserConnections.update(connection.id, {
218 | $set: {
219 | idle: true,
220 | lastActivity: date
221 | }
222 | });
223 |
224 | statusEvents.emit('connectionIdle', {
225 | userId,
226 | connectionId: connection.id,
227 | lastActivity: date
228 | });
229 | };
230 |
231 | const activeSession = (connection, date, userId) => {
232 | UserConnections.update(connection.id, {
233 | $set: {
234 | idle: false
235 | },
236 | $unset: {
237 | lastActivity: null
238 | }
239 | });
240 |
241 | statusEvents.emit('connectionActive', {
242 | userId,
243 | connectionId: connection.id,
244 | lastActivity: date
245 | });
246 | };
247 |
248 | /*
249 | Handlers for various client-side events
250 | */
251 | Meteor.startup(onStartup);
252 |
253 | // Opening and closing of DDP connections
254 | Meteor.onConnection((connection) => {
255 | addSession(connection);
256 |
257 | return connection.onClose(() => removeSession(connection, new Date()));
258 | });
259 |
260 | // Authentication of a DDP connection
261 | Accounts.onLogin(info => loginSession(info.connection, new Date(), info.user._id));
262 |
263 | // pub/sub trick as referenced in http://stackoverflow.com/q/10257958/586086
264 | // We used this in the past, but still need this to detect logouts on the same connection.
265 | Meteor.publish(null, function () {
266 | // Return null explicitly if this._session is not available, i.e.:
267 | // https://github.com/arunoda/meteor-fast-render/issues/41
268 | if (this._session == null) {
269 | return [];
270 | }
271 |
272 | // We're interested in logout events - re-publishes for which a past connection exists
273 | if (this.userId == null) {
274 | tryLogoutSession(this._session.connectionHandle, new Date());
275 | }
276 |
277 | return [];
278 | });
279 |
280 | // We can use the client's timestamp here because it was sent from a TimeSync
281 | // value, however we should never trust it for something security dependent.
282 | // If timestamp is not provided (probably due to a desync), use server time.
283 | Meteor.methods({
284 | 'user-status-idle'(timestamp) {
285 | check(timestamp, Match.OneOf(null, undefined, Date, Number));
286 |
287 | const date = (timestamp != null) ? new Date(timestamp) : new Date();
288 | idleSession(this.connection, date, this.userId);
289 | },
290 |
291 | 'user-status-active'(timestamp) {
292 | check(timestamp, Match.OneOf(null, undefined, Date, Number));
293 |
294 | // We only use timestamp because it's when we saw activity *on the client*
295 | // as opposed to just being notified it. It is probably more accurate even if
296 | // a dozen ms off due to the latency of sending it to the server.
297 | const date = (timestamp != null) ? new Date(timestamp) : new Date();
298 | activeSession(this.connection, date, this.userId);
299 | }
300 | });
301 |
302 | // Exported variable
303 | export const UserStatus = {
304 | connections: UserConnections,
305 | events: statusEvents
306 | };
307 |
308 | // Internal functions, exported for testing
309 | export const StatusInternals = {
310 | onStartup,
311 | addSession,
312 | removeSession,
313 | loginSession,
314 | tryLogoutSession,
315 | idleSession,
316 | activeSession,
317 | };
318 |
--------------------------------------------------------------------------------
/tests/client_tests.js:
--------------------------------------------------------------------------------
1 | /* globals navigator, Package, Tinytest */
2 |
3 | import { Meteor } from 'meteor/meteor';
4 | import { TimeSync } from 'meteor/mizzao:timesync';
5 | import { InsecureLogin } from './insecure_login';
6 |
7 | // The maximum tolerance we expect in client-server tests
8 | // TODO why must this be so large?
9 | const timeTol = 1000;
10 | let loginTime = null;
11 | let idleTime = null;
12 |
13 | // Monitor tests will wait for timesync, so we don't need to here.
14 | Tinytest.addAsync('status - login', (test, next) => InsecureLogin.ready(() => {
15 | test.ok();
16 | loginTime = new Date(TimeSync.serverTime());
17 | return next();
18 | }));
19 |
20 | // Check that initialization is empty
21 | Tinytest.addAsync('status - online recorded on server', (test, next) => Meteor.call('grabStatus', function (err, res) {
22 | test.isUndefined(err);
23 | test.length(res, 1);
24 |
25 | const user = res[0];
26 | test.equal(user._id, Meteor.userId());
27 | test.equal(user.status.online, true);
28 |
29 | test.isTrue(Math.abs(user.status.lastLogin.date - loginTime) < timeTol);
30 |
31 | // TODO: user-agent doesn't seem to match up in phantomjs for some reason
32 | if (Package['test-in-console'] == null) {
33 | test.equal(user.status.lastLogin.userAgent, navigator.userAgent);
34 | }
35 |
36 | test.equal(user.status.idle, false);
37 | test.isFalse(user.status.lastActivity != null);
38 |
39 | return next();
40 | }));
41 |
42 | Tinytest.addAsync('status - session recorded on server', (test, next) => Meteor.call('grabSessions', function (err, res) {
43 | test.isUndefined(err);
44 | test.length(res, 1);
45 |
46 | const doc = res[0];
47 | test.equal(doc.userId, Meteor.userId());
48 | test.isTrue(doc.ipAddr != null);
49 | test.isTrue(Math.abs(doc.loginTime - loginTime) < timeTol);
50 |
51 | // This shit doesn't seem to work properly in PhantomJS on Travis
52 | if (Package['test-in-console'] == null) {
53 | test.equal(doc.userAgent, navigator.userAgent);
54 | }
55 |
56 | test.isFalse(doc.idle != null); // connection record, not user
57 | test.isFalse(doc.lastActivity != null);
58 |
59 | return next();
60 | }));
61 |
62 | Tinytest.addAsync('status - online recorded on client', (test, next) => {
63 | test.equal(Meteor.user().status.online, true);
64 | return next();
65 | });
66 |
67 | Tinytest.addAsync('status - idle report to server', (test, next) => {
68 | const now = TimeSync.serverTime();
69 | idleTime = new Date(now);
70 |
71 | return Meteor.call('user-status-idle', now, (err) => {
72 | test.isUndefined(err);
73 |
74 | // Testing grabStatus should be sufficient to ensure that sessions work
75 | return Meteor.call('grabStatus', function (err, res) {
76 | test.isUndefined(err);
77 | test.length(res, 1);
78 |
79 | const user = res[0];
80 | test.equal(user._id, Meteor.userId());
81 | test.equal(user.status.online, true);
82 | test.equal(user.status.idle, true);
83 | test.isTrue(user.status.lastLogin != null);
84 | // This should be the exact date we sent to the server
85 | test.equal(user.status.lastActivity, idleTime);
86 |
87 | return next();
88 | });
89 | });
90 | });
91 |
92 | Tinytest.addAsync('status - active report to server', (test, next) => {
93 | const now = TimeSync.serverTime();
94 |
95 | return Meteor.call('user-status-active', now, (err) => {
96 | test.isUndefined(err);
97 |
98 | return Meteor.call('grabStatus', function (err, res) {
99 | test.isUndefined(err);
100 | test.length(res, 1);
101 |
102 | const user = res[0];
103 | test.equal(user._id, Meteor.userId());
104 | test.equal(user.status.online, true);
105 | test.isTrue(user.status.lastLogin != null);
106 |
107 | test.equal(user.status.idle, false);
108 | test.isFalse(user.status.lastActivity != null);
109 |
110 | return next();
111 | });
112 | });
113 | });
114 |
115 | Tinytest.addAsync('status - idle report with no timestamp', (test, next) => {
116 | const now = TimeSync.serverTime();
117 | idleTime = new Date(now);
118 |
119 | return Meteor.call('user-status-idle', undefined, (err) => {
120 | test.isUndefined(err);
121 |
122 | return Meteor.call('grabStatus', function (err, res) {
123 | test.isUndefined(err);
124 | test.length(res, 1);
125 |
126 | const user = res[0];
127 | test.equal(user._id, Meteor.userId());
128 | test.equal(user.status.online, true);
129 | test.equal(user.status.idle, true);
130 | test.isTrue(user.status.lastLogin != null);
131 | // This will be approximate
132 | test.isTrue(Math.abs(user.status.lastActivity - idleTime) < timeTol);
133 |
134 | return next();
135 | });
136 | });
137 | });
138 |
139 | Tinytest.addAsync('status - active report with no timestamp', (test, next) => Meteor.call('user-status-active', undefined, (err) => {
140 | test.isUndefined(err);
141 |
142 | return Meteor.call('grabStatus', function (err, res) {
143 | test.isUndefined(err);
144 | test.length(res, 1);
145 |
146 | const user = res[0];
147 | test.equal(user._id, Meteor.userId());
148 | test.equal(user.status.online, true);
149 | test.isTrue(user.status.lastLogin != null);
150 |
151 | test.equal(user.status.idle, false);
152 | test.isFalse(user.status.lastActivity != null);
153 |
154 | return next();
155 | });
156 | }));
157 |
158 | Tinytest.addAsync('status - logout', (test, next) => Meteor.logout((err) => {
159 | test.isUndefined(err);
160 | return next();
161 | }));
162 |
163 | Tinytest.addAsync('status - offline recorded on server', (test, next) => Meteor.call('grabStatus', function (err, res) {
164 | test.isUndefined(err);
165 | test.length(res, 1);
166 |
167 | const user = res[0];
168 | test.isTrue(user._id != null);
169 | test.equal(user.status.online, false);
170 | // logintime is still maintained
171 | test.isTrue(user.status.lastLogin != null);
172 |
173 | test.isFalse(user.status.idle != null);
174 | test.isFalse(user.status.lastActivity != null);
175 |
176 | return next();
177 | }));
178 |
179 | Tinytest.addAsync('status - session userId deleted on server', (test, next) => Meteor.call('grabSessions', function (err, res) {
180 | test.isUndefined(err);
181 | test.length(res, 1);
182 |
183 | const doc = res[0];
184 | test.isFalse(doc.userId != null);
185 | test.isTrue(doc.ipAddr != null);
186 | test.isFalse(doc.loginTime != null);
187 |
188 | test.isFalse(doc.idle); // === false
189 | test.isFalse(doc.lastActivity != null);
190 |
191 | return next();
192 | }));
193 |
--------------------------------------------------------------------------------
/tests/insecure_login.js:
--------------------------------------------------------------------------------
1 | import { Accounts } from 'meteor/accounts-base';
2 | import { Meteor } from 'meteor/meteor';
3 |
4 | /*
5 | * Created by https://github.com/matb33 for testing packages that make user of userIds
6 | * Original file https://github.com/matb33/meteor-collection-hooks/blob/master/tests/insecure_login.js
7 | */
8 | export const InsecureLogin = {
9 | queue: [],
10 | ran: false,
11 | ready: (callback) => {
12 | InsecureLogin.queue.push(callback);
13 | if (InsecureLogin.ran) InsecureLogin.unwind();
14 | },
15 | run: () => {
16 | InsecureLogin.ran = true;
17 | InsecureLogin.unwind();
18 | },
19 | unwind: () => {
20 | for (const callback of InsecureLogin.queue) {
21 | callback();
22 | }
23 | InsecureLogin.queue = [];
24 | }
25 | };
26 |
27 | if (Meteor.isClient) {
28 | Accounts.callLoginMethod({
29 | methodArguments: [{
30 | username: 'InsecureLogin'
31 | }],
32 | userCallback: (err) => {
33 | if (err) throw err;
34 | console.info('Insecure login successful!');
35 | InsecureLogin.run();
36 | }
37 | });
38 | } else {
39 | InsecureLogin.run();
40 | }
41 |
42 | if (Meteor.isServer) {
43 | if (!Meteor.users.find({
44 | 'username': 'InsecureLogin'
45 | }).count()) {
46 | Accounts.createUser({
47 | username: 'InsecureLogin',
48 | email: 'test@test.com',
49 | password: 'password',
50 | profile: {
51 | name: 'InsecureLogin'
52 | }
53 | });
54 | }
55 |
56 | Accounts.registerLoginHandler((options) => {
57 | if (!options.username) return;
58 |
59 | var user = Meteor.users.findOne({
60 | 'username': options.username
61 | });
62 | if (!user) return;
63 |
64 | return {
65 | userId: user._id
66 | };
67 | });
68 | }
69 |
--------------------------------------------------------------------------------
/tests/monitor_tests.js:
--------------------------------------------------------------------------------
1 | /* globals Tinytest */
2 | import { Meteor } from 'meteor/meteor';
3 | import { TimeSync } from 'meteor/mizzao:timesync';
4 | import { Tracker } from 'meteor/tracker';
5 | import { MonitorInternals, UserStatus } from '../client/monitor';
6 | import { getCleanupWrapper } from './setup';
7 |
8 | const tolMs = 100;
9 | const reportedEvents = [];
10 |
11 | // Stub out reporting methods for testing
12 | MonitorInternals.reportIdle = time => reportedEvents.push({
13 | status: 'idle',
14 | time
15 | });
16 |
17 | MonitorInternals.reportActive = time => reportedEvents.push({
18 | status: 'active',
19 | time
20 | });
21 |
22 | // Test function wrapper that cleans up reported events array
23 | const withCleanup = getCleanupWrapper({
24 | before() {
25 | MonitorInternals.onWindowFocus();
26 | MonitorInternals.idleThreshold = null;
27 | MonitorInternals.idleOnBlur = false;
28 | // http://stackoverflow.com/a/1232046/586086
29 | return reportedEvents.length = 0;
30 | },
31 |
32 | after() {
33 | if (UserStatus.isMonitoring()) {
34 | try {
35 | return UserStatus.stopMonitor();
36 | } catch (error) {
37 | console.error(error);
38 | }
39 | }
40 | }
41 | });
42 |
43 | // This is a 2x2 test of all possible cases
44 |
45 | Tinytest.add('monitor - idleOnBlur and window blurred', (test) => {
46 | MonitorInternals.idleThreshold = 60000;
47 | MonitorInternals.idleOnBlur = true;
48 |
49 | const activity = Date.now();
50 | const newTime = activity + 120000;
51 |
52 | test.equal(MonitorInternals.computeState(activity, activity, false), true);
53 | // Should not change if we go idle
54 | return test.equal(MonitorInternals.computeState(activity, newTime, false), true);
55 | });
56 |
57 | Tinytest.add('monitor - idleOnBlur and window focused', (test) => {
58 | MonitorInternals.idleThreshold = 60000;
59 | MonitorInternals.idleOnBlur = true;
60 |
61 | const activity = Date.now();
62 | const newTime = activity + 120000;
63 |
64 | test.equal(MonitorInternals.computeState(activity, activity, true), false);
65 | // Should change if we go idle
66 | return test.equal(MonitorInternals.computeState(activity, newTime, true), true);
67 | });
68 |
69 | Tinytest.add('monitor - idle below threshold', (test) => {
70 | MonitorInternals.idleThreshold = 60000;
71 | MonitorInternals.idleOnBlur = false;
72 |
73 | const activity = Date.now();
74 | test.equal(MonitorInternals.computeState(activity, activity, true), false);
75 | // Shouldn't change if we go out of focus
76 | return test.equal(MonitorInternals.computeState(activity, activity, false), false);
77 | });
78 |
79 | Tinytest.add('monitor - idle above threshold', (test) => {
80 | MonitorInternals.idleThreshold = 60000;
81 | MonitorInternals.idleOnBlur = false;
82 |
83 | const activity = Date.now();
84 | const newTime = activity + 120000;
85 |
86 | test.equal(MonitorInternals.computeState(activity, newTime, true), true);
87 | // Shouldn't change if we go out of focus
88 | return test.equal(MonitorInternals.computeState(activity, newTime, false), true);
89 | });
90 |
91 | // We need to wait for TimeSync to run or errors will ensue.
92 | Tinytest.addAsync('monitor - wait for timesync', (test, next) => Tracker.autorun((c) => {
93 | if (TimeSync.isSynced()) {
94 | c.stop();
95 | return next();
96 | }
97 | }));
98 |
99 | Tinytest.add('monitor - start with default settings', withCleanup((test) => {
100 | UserStatus.startMonitor();
101 |
102 | test.equal(UserStatus.isMonitoring(), true);
103 | test.isTrue(UserStatus.lastActivity());
104 |
105 | test.equal(MonitorInternals.idleThreshold, 60000);
106 | return test.equal(MonitorInternals.idleOnBlur, false);
107 | }));
108 |
109 | Tinytest.add('monitor - start with window focused, and idleOnBlur = false', withCleanup((test) => {
110 | UserStatus.startMonitor({
111 | threshold: 30000,
112 | idleOnBlur: false
113 | });
114 |
115 | const timestamp = Tracker.nonreactive(() => TimeSync.serverTime());
116 |
117 | test.equal(MonitorInternals.idleThreshold, 30000);
118 | test.equal(MonitorInternals.idleOnBlur, false);
119 |
120 | Tracker.flush();
121 |
122 | test.equal(UserStatus.isIdle(), false);
123 | test.isTrue(Math.abs(UserStatus.lastActivity() - timestamp) < tolMs);
124 | test.length(reportedEvents, 1);
125 | test.equal(reportedEvents[0] != null ? reportedEvents[0].status : undefined, 'active');
126 | return test.isTrue(Math.abs((reportedEvents[0] != null ? reportedEvents[0].time : undefined) - timestamp) < tolMs);
127 | }));
128 |
129 | Tinytest.add('monitor - start with window focused, and idleOnBlur = true', withCleanup((test) => {
130 | UserStatus.startMonitor({
131 | threshold: 30000,
132 | idleOnBlur: true
133 | });
134 |
135 | const timestamp = Tracker.nonreactive(() => TimeSync.serverTime());
136 |
137 | test.equal(MonitorInternals.idleThreshold, 30000);
138 | test.equal(MonitorInternals.idleOnBlur, true);
139 |
140 | Tracker.flush();
141 |
142 | test.equal(UserStatus.isIdle(), false);
143 | test.isTrue(Math.abs(UserStatus.lastActivity() - timestamp) < tolMs);
144 | test.length(reportedEvents, 1);
145 | test.equal(reportedEvents[0] != null ? reportedEvents[0].status : undefined, 'active');
146 | return test.isTrue(Math.abs((reportedEvents[0] != null ? reportedEvents[0].time : undefined) - timestamp) < tolMs);
147 | }));
148 |
149 | Tinytest.add('monitor - start with window blurred, and idleOnBlur = false', withCleanup((test) => {
150 | MonitorInternals.onWindowBlur();
151 |
152 | UserStatus.startMonitor({
153 | threshold: 30000,
154 | idleOnBlur: false
155 | });
156 |
157 | const timestamp = Tracker.nonreactive(() => TimeSync.serverTime());
158 |
159 | test.equal(MonitorInternals.idleThreshold, 30000);
160 | test.equal(MonitorInternals.idleOnBlur, false);
161 |
162 | Tracker.flush();
163 |
164 | test.equal(UserStatus.isIdle(), false);
165 | test.isTrue(Math.abs(UserStatus.lastActivity() - timestamp) < tolMs);
166 | test.length(reportedEvents, 1);
167 | test.equal(reportedEvents[0] != null ? reportedEvents[0].status : undefined, 'active');
168 | return test.isTrue(Math.abs((reportedEvents[0] != null ? reportedEvents[0].time : undefined) - timestamp) < tolMs);
169 | }));
170 |
171 | Tinytest.add('monitor - start with window blurred, and idleOnBlur = true', withCleanup((test) => {
172 | MonitorInternals.onWindowBlur();
173 |
174 | UserStatus.startMonitor({
175 | threshold: 30000,
176 | idleOnBlur: true
177 | });
178 |
179 | const timestamp = Tracker.nonreactive(() => TimeSync.serverTime());
180 |
181 | test.equal(MonitorInternals.idleThreshold, 30000);
182 | test.equal(MonitorInternals.idleOnBlur, true);
183 |
184 | Tracker.flush();
185 |
186 | test.equal(UserStatus.isIdle(), true);
187 | test.isTrue(Math.abs(UserStatus.lastActivity() - timestamp) < tolMs);
188 | test.length(reportedEvents, 1);
189 | test.equal(reportedEvents[0] != null ? reportedEvents[0].status : undefined, 'idle');
190 | return test.isTrue(Math.abs((reportedEvents[0] != null ? reportedEvents[0].time : undefined) - timestamp) < tolMs);
191 | }));
192 |
193 | Tinytest.add('monitor - ignore actions when window is blurred with idleOnBlur = true', withCleanup((test) => {
194 |
195 | UserStatus.startMonitor({
196 | idleOnBlur: true
197 | });
198 | Tracker.flush();
199 |
200 | const startTime = UserStatus.lastActivity();
201 |
202 | test.length(reportedEvents, 1);
203 | test.equal(reportedEvents[0] != null ? reportedEvents[0].status : undefined, 'active');
204 | test.isTrue(Math.abs((reportedEvents[0] != null ? reportedEvents[0].time : undefined) - startTime) < tolMs);
205 |
206 | MonitorInternals.onWindowBlur();
207 | Tracker.flush();
208 |
209 | test.length(reportedEvents, 2);
210 | test.equal(reportedEvents[1] != null ? reportedEvents[1].status : undefined, 'idle');
211 |
212 | UserStatus.pingMonitor();
213 |
214 | // Shouldn't have changed
215 | test.length(reportedEvents, 2);
216 | return test.equal(UserStatus.lastActivity(), startTime);
217 | }));
218 |
219 | Tinytest.add('monitor - stopping while idle unsets idle state', withCleanup((test) => {
220 |
221 | UserStatus.startMonitor({
222 | idleOnBlur: true
223 | });
224 | Tracker.flush();
225 |
226 | test.length(reportedEvents, 1);
227 | test.equal(reportedEvents[0] != null ? reportedEvents[0].status : undefined, 'active');
228 |
229 | const startTime = UserStatus.lastActivity();
230 |
231 | // Simulate going idle immediately using blur
232 | MonitorInternals.onWindowBlur();
233 | Tracker.flush();
234 |
235 | test.length(reportedEvents, 2);
236 | test.equal(reportedEvents[1] != null ? reportedEvents[1].status : undefined, 'idle');
237 |
238 | test.equal(UserStatus.isIdle(), true);
239 | test.equal(UserStatus.lastActivity(), startTime);
240 |
241 | UserStatus.stopMonitor();
242 | Tracker.flush();
243 |
244 | test.length(reportedEvents, 3);
245 | test.equal(reportedEvents[2] != null ? reportedEvents[2].status : undefined, 'active');
246 |
247 | test.equal(UserStatus.isIdle(), false);
248 | return test.equal(UserStatus.lastActivity(), undefined);
249 | }));
250 |
251 | Tinytest.addAsync('monitor - stopping and restarting grabs new start time', withCleanup((test, next) => {
252 |
253 | UserStatus.startMonitor({
254 | idleOnBlur: true
255 | });
256 | Tracker.flush();
257 |
258 | const start1 = UserStatus.lastActivity();
259 |
260 | UserStatus.stopMonitor();
261 | Tracker.flush();
262 |
263 | return Meteor.setTimeout(() => {
264 | UserStatus.startMonitor({
265 | idleOnBlur: true
266 | });
267 | Tracker.flush();
268 |
269 | const start2 = UserStatus.lastActivity();
270 |
271 | test.isTrue(start2 > start1);
272 | return next();
273 | }, 50);
274 | }));
275 |
276 | Tinytest.add('monitor - idle state reported across a disconnect', withCleanup((test) => {
277 |
278 | UserStatus.startMonitor({
279 | idleOnBlur: true
280 | });
281 | Tracker.flush();
282 |
283 | test.length(reportedEvents, 1);
284 | test.equal(reportedEvents[0] != null ? reportedEvents[0].status : undefined, 'active');
285 |
286 | // Simulate going idle immediately using blur
287 | MonitorInternals.onWindowBlur();
288 | Tracker.flush();
289 |
290 | test.length(reportedEvents, 2);
291 | test.equal(reportedEvents[1] != null ? reportedEvents[1].status : undefined, 'idle');
292 |
293 | MonitorInternals.connectionChange(false, true);
294 | MonitorInternals.connectionChange(true, false);
295 |
296 | test.length(reportedEvents, 3);
297 | return test.equal(reportedEvents[2] != null ? reportedEvents[2].status : undefined, 'idle');
298 | }));
299 |
--------------------------------------------------------------------------------
/tests/server_tests.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { UserStatus } from '../server/status';
3 | import { TEST_IP, TEST_userId } from './setup';
4 |
5 | /*
6 | Manual tests to do:
7 |
8 | logged out -> logged in
9 | logged in -> logged out
10 | logged in -> close session -> reopen
11 | logged in -> connection timeout
12 | */
13 |
14 | // Publish status to client
15 | Meteor.publish(null, () => Meteor.users.find({}, {
16 | fields: {
17 | status: 1
18 | }
19 | }));
20 |
21 | Meteor.methods({
22 | 'grabStatus'() {
23 | return Meteor.users.find({
24 | _id: {
25 | $ne: TEST_userId
26 | }
27 | }, {
28 | fields: {
29 | status: 1
30 | }
31 | }).fetch();
32 | },
33 | 'grabSessions'() {
34 | // Only grab sessions not generated by server-side tests.
35 | return UserStatus.connections.find({
36 | $and: [{
37 | userId: {
38 | $ne: TEST_userId
39 | }
40 | },
41 | {
42 | ipAddr: {
43 | $ne: TEST_IP
44 | }
45 | }
46 | ]
47 | }).fetch();
48 | }
49 | });
50 |
--------------------------------------------------------------------------------
/tests/setup.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-useless-catch */
2 | import { Meteor } from 'meteor/meteor';
3 |
4 | export const TEST_username = 'status_test';
5 | export let TEST_userId = '';
6 | export const TEST_IP = '255.255.255.0';
7 |
8 | if (Meteor.isServer) {
9 | const testUserExists = Meteor.users.findOne({
10 | username: TEST_username
11 | });
12 |
13 | if (!testUserExists) {
14 | TEST_userId = Meteor.users.insert({
15 | username: TEST_username
16 | });
17 | console.log('Inserted test user id: ', TEST_userId);
18 | } else {
19 | TEST_userId = testUserExists._id;
20 | }
21 | }
22 |
23 | // Get a wrapper that runs a before and after function wrapping some test function.
24 | export const getCleanupWrapper = function (settings) {
25 | const { before } = settings;
26 | const { after } = settings;
27 | // Take a function...
28 | return fn => // Return a function that, when called, executes the hooks around the function.
29 | (function () {
30 | const next = arguments[1];
31 | if (typeof before === 'function') {
32 | before();
33 | }
34 |
35 | if (next == null) {
36 | // Synchronous version - Tinytest.add
37 | try {
38 | return fn.apply(this, arguments);
39 | } catch (error) {
40 | throw error;
41 | } finally {
42 | if (typeof after === 'function') {
43 | after();
44 | }
45 | }
46 | } else {
47 | // Asynchronous version - Tinytest.addAsync
48 | const hookedNext = function () {
49 | if (typeof after === 'function') {
50 | after();
51 | }
52 | return next();
53 | };
54 | return fn.call(this, arguments[0], hookedNext);
55 | }
56 | });
57 | };
58 |
--------------------------------------------------------------------------------
/tests/status_tests.js:
--------------------------------------------------------------------------------
1 | /* globals Tinytest */
2 | import { Meteor } from 'meteor/meteor';
3 | import { Random } from 'meteor/random';
4 | import { Tracker } from 'meteor/tracker';
5 | import { StatusInternals, UserStatus } from '../server/status';
6 | import { getCleanupWrapper, TEST_IP, TEST_userId } from './setup';
7 |
8 | let lastLoginAdvice = null;
9 | let lastLogoutAdvice = null;
10 | let lastIdleAdvice = null;
11 | let lastActiveAdvice = null;
12 |
13 | // Record events for tests
14 | UserStatus.events.on('connectionLogin', advice => lastLoginAdvice = advice);
15 | UserStatus.events.on('connectionLogout', advice => lastLogoutAdvice = advice);
16 | UserStatus.events.on('connectionIdle', advice => lastIdleAdvice = advice);
17 | UserStatus.events.on('connectionActive', advice => lastActiveAdvice = advice);
18 |
19 | const TEST_UA = 'old-ass browser';
20 |
21 | // Make sure repeated calls to this return different values
22 | const delayedDate = () => Meteor.wrapAsync(cb => Meteor.setTimeout((() => cb(undefined, new Date())), 1))();
23 |
24 | const randomConnection = () => ({
25 | id: Random.id(),
26 | clientAddress: TEST_IP,
27 |
28 | httpHeaders: {
29 | 'user-agent': TEST_UA
30 | }
31 | });
32 |
33 | // Delete the entire status field and sessions after each test
34 | const withCleanup = getCleanupWrapper({
35 | after() {
36 | lastLoginAdvice = null;
37 | lastLogoutAdvice = null;
38 | lastIdleAdvice = null;
39 | lastActiveAdvice = null;
40 |
41 | Meteor.users.update(TEST_userId, {
42 | $unset: {
43 | status: null
44 | }
45 | });
46 | UserStatus.connections.remove({
47 | $or: [{
48 | userId: TEST_userId
49 | },
50 | {
51 | ipAddr: TEST_IP
52 | }
53 | ]
54 | });
55 |
56 | return Tracker.flush();
57 | }
58 | });
59 |
60 | // Clean up before we add any tests just in case some crap left over from before
61 | withCleanup(function () {});
62 |
63 | Tinytest.add('status - adding anonymous session', withCleanup((test) => {
64 | const conn = randomConnection();
65 |
66 | StatusInternals.addSession(conn);
67 |
68 | const doc = UserStatus.connections.findOne(conn.id);
69 |
70 | test.isTrue(doc != null);
71 | test.equal(doc._id, conn.id);
72 | test.equal(doc.ipAddr, TEST_IP);
73 | test.equal(doc.userAgent, TEST_UA);
74 | test.isFalse(doc.userId);
75 | return test.isFalse(doc.loginTime);
76 | }));
77 |
78 | Tinytest.add('status - adding and removing anonymous session', withCleanup((test) => {
79 | const conn = randomConnection();
80 |
81 | StatusInternals.addSession(conn);
82 | StatusInternals.removeSession(conn, delayedDate());
83 |
84 | return test.isFalse(UserStatus.connections.findOne(conn.id));
85 | }));
86 |
87 | Tinytest.add('status - adding one authenticated session', withCleanup((test) => {
88 | const conn = randomConnection();
89 | const ts = delayedDate();
90 |
91 | StatusInternals.addSession(conn);
92 | StatusInternals.loginSession(conn, ts, TEST_userId);
93 |
94 | const doc = UserStatus.connections.findOne(conn.id);
95 | const user = Meteor.users.findOne(TEST_userId);
96 |
97 | test.isTrue(doc != null);
98 | test.equal(doc._id, conn.id);
99 | test.equal(doc.userId, TEST_userId);
100 | test.equal(doc.loginTime, ts);
101 | test.equal(doc.ipAddr, TEST_IP);
102 | test.equal(doc.userAgent, TEST_UA);
103 |
104 | test.equal(lastLoginAdvice.userId, TEST_userId);
105 | test.equal(lastLoginAdvice.connectionId, conn.id);
106 | test.equal(lastLoginAdvice.loginTime, ts);
107 | test.equal(lastLoginAdvice.ipAddr, TEST_IP);
108 | test.equal(lastLoginAdvice.userAgent, TEST_UA);
109 |
110 | test.equal(user.status.online, true);
111 | test.equal(user.status.idle, false);
112 | test.equal(user.status.lastLogin.date, ts);
113 | return test.equal(user.status.lastLogin.userAgent, TEST_UA);
114 | }));
115 |
116 | Tinytest.add('status - adding and removing one authenticated session', withCleanup((test) => {
117 | const conn = randomConnection();
118 | const ts = delayedDate();
119 |
120 | StatusInternals.addSession(conn);
121 | StatusInternals.loginSession(conn, ts, TEST_userId);
122 |
123 | const logoutTime = delayedDate();
124 | StatusInternals.removeSession(conn, logoutTime);
125 |
126 | const doc = UserStatus.connections.findOne(conn.id);
127 | const user = Meteor.users.findOne(TEST_userId);
128 |
129 | test.isFalse(doc != null);
130 |
131 | test.equal(lastLogoutAdvice.userId, TEST_userId);
132 | test.equal(lastLogoutAdvice.connectionId, conn.id);
133 | test.equal(lastLogoutAdvice.logoutTime, logoutTime);
134 | test.isFalse(lastLogoutAdvice.lastActivity != null);
135 |
136 | test.equal(user.status.online, false);
137 | test.isFalse(user.status.idle != null);
138 | return test.equal(user.status.lastLogin.date, ts);
139 | }));
140 |
141 | Tinytest.add('status - logout and then close one authenticated session', withCleanup((test) => {
142 | const conn = randomConnection();
143 | const ts = delayedDate();
144 |
145 | StatusInternals.addSession(conn);
146 | StatusInternals.loginSession(conn, ts, TEST_userId);
147 |
148 | const logoutTime = delayedDate();
149 | StatusInternals.tryLogoutSession(conn, logoutTime);
150 |
151 | test.equal(lastLogoutAdvice.userId, TEST_userId);
152 | test.equal(lastLogoutAdvice.connectionId, conn.id);
153 | test.equal(lastLogoutAdvice.logoutTime, logoutTime);
154 | test.isFalse(lastLogoutAdvice.lastActivity != null);
155 |
156 | lastLogoutAdvice = null;
157 | // After logging out, the user closes the browser, which triggers a close callback
158 | // However, the event should not be emitted again
159 | const closeTime = delayedDate();
160 | StatusInternals.removeSession(conn, closeTime);
161 |
162 | const doc = UserStatus.connections.findOne(conn.id);
163 | const user = Meteor.users.findOne(TEST_userId);
164 |
165 | test.isFalse(doc != null);
166 | test.isFalse(lastLogoutAdvice != null);
167 |
168 | test.equal(user.status.online, false);
169 | test.isFalse(user.status.idle != null);
170 | return test.equal(user.status.lastLogin.date, ts);
171 | }));
172 |
173 | Tinytest.add('status - idling one authenticated session', withCleanup((test) => {
174 | const conn = randomConnection();
175 | const ts = delayedDate();
176 |
177 | StatusInternals.addSession(conn);
178 | StatusInternals.loginSession(conn, ts, TEST_userId);
179 |
180 | const idleTime = delayedDate();
181 |
182 | StatusInternals.idleSession(conn, idleTime, TEST_userId);
183 |
184 | const doc = UserStatus.connections.findOne(conn.id);
185 | const user = Meteor.users.findOne(TEST_userId);
186 |
187 | test.isTrue(doc != null);
188 | test.equal(doc._id, conn.id);
189 | test.equal(doc.userId, TEST_userId);
190 | test.equal(doc.loginTime, ts);
191 | test.equal(doc.ipAddr, TEST_IP);
192 | test.equal(doc.userAgent, TEST_UA);
193 | test.equal(doc.idle, true);
194 | test.equal(doc.lastActivity, idleTime);
195 |
196 | test.equal(lastIdleAdvice.userId, TEST_userId);
197 | test.equal(lastIdleAdvice.connectionId, conn.id);
198 | test.equal(lastIdleAdvice.lastActivity, idleTime);
199 |
200 | test.equal(user.status.online, true);
201 | test.equal(user.status.lastLogin.date, ts);
202 | test.equal(user.status.idle, true);
203 | return test.equal(user.status.lastActivity, idleTime);
204 | }));
205 |
206 | Tinytest.add('status - idling and reactivating one authenticated session', withCleanup((test) => {
207 | const conn = randomConnection();
208 | const ts = delayedDate();
209 |
210 | StatusInternals.addSession(conn);
211 | StatusInternals.loginSession(conn, ts, TEST_userId);
212 |
213 | const idleTime = delayedDate();
214 | StatusInternals.idleSession(conn, idleTime, TEST_userId);
215 | const activeTime = delayedDate();
216 | StatusInternals.activeSession(conn, activeTime, TEST_userId);
217 |
218 | const doc = UserStatus.connections.findOne(conn.id);
219 | const user = Meteor.users.findOne(TEST_userId);
220 |
221 | test.isTrue(doc != null);
222 | test.equal(doc._id, conn.id);
223 | test.equal(doc.userId, TEST_userId);
224 | test.equal(doc.loginTime, ts);
225 | test.equal(doc.ipAddr, TEST_IP);
226 | test.equal(doc.userAgent, TEST_UA);
227 | test.equal(doc.idle, false);
228 | test.isFalse(doc.lastActivity != null);
229 |
230 | test.equal(lastActiveAdvice.userId, TEST_userId);
231 | test.equal(lastActiveAdvice.connectionId, conn.id);
232 | test.equal(lastActiveAdvice.lastActivity, activeTime);
233 |
234 | test.equal(user.status.online, true);
235 | test.equal(user.status.lastLogin.date, ts);
236 | test.equal(user.status.idle, false);
237 | return test.isFalse(user.status.lastActivity != null);
238 | }));
239 |
240 | Tinytest.add('status - idling and removing one authenticated session', withCleanup((test) => {
241 | const conn = randomConnection();
242 | const ts = delayedDate();
243 |
244 | StatusInternals.addSession(conn);
245 | StatusInternals.loginSession(conn, ts, TEST_userId);
246 | const idleTime = delayedDate();
247 | StatusInternals.idleSession(conn, idleTime, TEST_userId);
248 | const logoutTime = delayedDate();
249 | StatusInternals.removeSession(conn, logoutTime);
250 |
251 | const doc = UserStatus.connections.findOne(conn.id);
252 | const user = Meteor.users.findOne(TEST_userId);
253 |
254 | test.isFalse(doc != null);
255 |
256 | test.equal(lastLogoutAdvice.userId, TEST_userId);
257 | test.equal(lastLogoutAdvice.connectionId, conn.id);
258 | test.equal(lastLogoutAdvice.logoutTime, logoutTime);
259 | test.equal(lastLogoutAdvice.lastActivity, idleTime);
260 |
261 | test.equal(user.status.online, false);
262 | return test.equal(user.status.lastLogin.date, ts);
263 | }));
264 |
265 | Tinytest.add('status - idling and reconnecting one authenticated session', withCleanup((test) => {
266 | const conn = randomConnection();
267 | const ts = delayedDate();
268 |
269 | StatusInternals.addSession(conn);
270 | StatusInternals.loginSession(conn, ts, TEST_userId);
271 | const idleTime = delayedDate();
272 | StatusInternals.idleSession(conn, idleTime, TEST_userId);
273 |
274 | // Session reconnects but was idle
275 |
276 | const discTime = delayedDate();
277 | StatusInternals.removeSession(conn, discTime);
278 |
279 | const reconn = randomConnection();
280 | const reconnTime = delayedDate();
281 |
282 | StatusInternals.addSession(reconn);
283 | StatusInternals.loginSession(reconn, reconnTime, TEST_userId);
284 | StatusInternals.idleSession(reconn, idleTime, TEST_userId);
285 |
286 | const doc = UserStatus.connections.findOne(reconn.id);
287 | const user = Meteor.users.findOne(TEST_userId);
288 |
289 | test.isTrue(doc != null);
290 | test.equal(doc._id, reconn.id);
291 | test.equal(doc.userId, TEST_userId);
292 | test.equal(doc.loginTime, reconnTime);
293 | test.equal(doc.ipAddr, TEST_IP);
294 | test.equal(doc.userAgent, TEST_UA);
295 | test.equal(doc.idle, true);
296 | test.equal(doc.lastActivity, idleTime);
297 |
298 | test.equal(user.status.online, true);
299 | test.equal(user.status.lastLogin.date, reconnTime);
300 | test.equal(user.status.idle, true);
301 | return test.equal(user.status.lastActivity, idleTime);
302 | }));
303 |
304 | Tinytest.add('multiplex - two online sessions', withCleanup((test) => {
305 | const conn = randomConnection();
306 |
307 | const conn2 = randomConnection();
308 |
309 | const ts = delayedDate();
310 | const ts2 = delayedDate();
311 |
312 | StatusInternals.addSession(conn);
313 | StatusInternals.loginSession(conn, ts, TEST_userId);
314 |
315 | StatusInternals.addSession(conn2);
316 | StatusInternals.loginSession(conn2, ts2, TEST_userId);
317 |
318 | const user = Meteor.users.findOne(TEST_userId);
319 |
320 | test.equal(user.status.online, true);
321 | return test.equal(user.status.lastLogin.date, ts2);
322 | }));
323 |
324 | Tinytest.add('multiplex - two online sessions with one going offline', withCleanup((test) => {
325 | let user;
326 | const conn = randomConnection();
327 |
328 | const conn2 = randomConnection();
329 |
330 | const ts = delayedDate();
331 | const ts2 = delayedDate();
332 |
333 | StatusInternals.addSession(conn);
334 | StatusInternals.loginSession(conn, ts, TEST_userId);
335 |
336 | StatusInternals.addSession(conn2);
337 | StatusInternals.loginSession(conn2, ts2, TEST_userId);
338 |
339 | StatusInternals.removeSession(conn, delayedDate(),
340 |
341 | (user = Meteor.users.findOne(TEST_userId)));
342 |
343 | test.equal(user.status.online, true);
344 | return test.equal(user.status.lastLogin.date, ts2);
345 | }));
346 |
347 | Tinytest.add('multiplex - two online sessions to offline', withCleanup((test) => {
348 | const conn = randomConnection();
349 |
350 | const conn2 = randomConnection();
351 |
352 | const ts = delayedDate();
353 | const ts2 = delayedDate();
354 |
355 | StatusInternals.addSession(conn);
356 | StatusInternals.loginSession(conn, ts, TEST_userId);
357 |
358 | StatusInternals.addSession(conn2);
359 | StatusInternals.loginSession(conn2, ts2, TEST_userId);
360 |
361 | StatusInternals.removeSession(conn, delayedDate());
362 | StatusInternals.removeSession(conn2, delayedDate());
363 |
364 | const user = Meteor.users.findOne(TEST_userId);
365 |
366 | test.equal(user.status.online, false);
367 | return test.equal(user.status.lastLogin.date, ts2);
368 | }));
369 |
370 | Tinytest.add('multiplex - idling one of two online sessions', withCleanup((test) => {
371 | const conn = randomConnection();
372 |
373 | const conn2 = randomConnection();
374 |
375 | const ts = delayedDate();
376 | const ts2 = delayedDate();
377 |
378 | StatusInternals.addSession(conn);
379 | StatusInternals.loginSession(conn, ts, TEST_userId);
380 |
381 | StatusInternals.addSession(conn2);
382 | StatusInternals.loginSession(conn2, ts2, TEST_userId);
383 |
384 | const idle1 = delayedDate();
385 | StatusInternals.idleSession(conn, idle1, TEST_userId);
386 |
387 | const user = Meteor.users.findOne(TEST_userId);
388 |
389 | test.equal(user.status.online, true);
390 | test.equal(user.status.lastLogin.date, ts2);
391 | return test.equal(user.status.idle, false);
392 | }));
393 |
394 | Tinytest.add('multiplex - idling two online sessions', withCleanup((test) => {
395 | const conn = randomConnection();
396 |
397 | const conn2 = randomConnection();
398 |
399 | const ts = delayedDate();
400 | const ts2 = delayedDate();
401 |
402 | StatusInternals.addSession(conn);
403 | StatusInternals.loginSession(conn, ts, TEST_userId);
404 |
405 | StatusInternals.addSession(conn2);
406 | StatusInternals.loginSession(conn2, ts2, TEST_userId);
407 |
408 | const idle1 = delayedDate();
409 | const idle2 = delayedDate();
410 | StatusInternals.idleSession(conn, idle1, TEST_userId);
411 | StatusInternals.idleSession(conn2, idle2, TEST_userId);
412 |
413 | const user = Meteor.users.findOne(TEST_userId);
414 |
415 | test.equal(user.status.online, true);
416 | test.equal(user.status.lastLogin.date, ts2);
417 | test.equal(user.status.idle, true);
418 | return test.equal(user.status.lastActivity, idle2);
419 | }));
420 |
421 | Tinytest.add('multiplex - idling two then reactivating one session', withCleanup((test) => {
422 | const conn = randomConnection();
423 |
424 | const conn2 = randomConnection();
425 |
426 | const ts = delayedDate();
427 | const ts2 = delayedDate();
428 |
429 | StatusInternals.addSession(conn);
430 | StatusInternals.loginSession(conn, ts, TEST_userId);
431 |
432 | StatusInternals.addSession(conn2);
433 | StatusInternals.loginSession(conn2, ts2, TEST_userId);
434 |
435 | const idle1 = delayedDate();
436 | const idle2 = delayedDate();
437 | StatusInternals.idleSession(conn, idle1, TEST_userId);
438 | StatusInternals.idleSession(conn2, idle2, TEST_userId);
439 |
440 | StatusInternals.activeSession(conn, delayedDate(), TEST_userId);
441 |
442 | const user = Meteor.users.findOne(TEST_userId);
443 |
444 | test.equal(user.status.online, true);
445 | test.equal(user.status.lastLogin.date, ts2);
446 | test.equal(user.status.idle, false);
447 | return test.isFalse(user.status.lastActivity != null);
448 | }));
449 |
450 | Tinytest.add('multiplex - logging in while an existing session is idle', withCleanup((test) => {
451 | const conn = randomConnection();
452 |
453 | const conn2 = randomConnection();
454 |
455 | const ts = delayedDate();
456 | const ts2 = delayedDate();
457 |
458 | StatusInternals.addSession(conn);
459 | StatusInternals.loginSession(conn, ts, TEST_userId);
460 |
461 | const idle1 = delayedDate();
462 | StatusInternals.idleSession(conn, idle1, TEST_userId);
463 |
464 | StatusInternals.addSession(conn2);
465 | StatusInternals.loginSession(conn2, ts2, TEST_userId);
466 |
467 | const user = Meteor.users.findOne(TEST_userId);
468 |
469 | test.equal(user.status.online, true);
470 | test.equal(user.status.lastLogin.date, ts2);
471 | test.equal(user.status.idle, false);
472 | return test.isFalse(user.status.lastActivity != null);
473 | }));
474 |
475 | Tinytest.add('multiplex - simulate tab switch', withCleanup((test) => {
476 | const conn = randomConnection();
477 |
478 | const conn2 = randomConnection();
479 |
480 | const ts = delayedDate();
481 | const ts2 = delayedDate();
482 |
483 | // open first tab then becomes idle
484 | StatusInternals.addSession(conn);
485 | StatusInternals.loginSession(conn, ts, TEST_userId);
486 |
487 | const idle1 = delayedDate();
488 | StatusInternals.idleSession(conn, idle1, TEST_userId);
489 |
490 | // open second tab then becomes idle
491 | StatusInternals.addSession(conn2);
492 | StatusInternals.loginSession(conn2, ts2, TEST_userId);
493 | const idle2 = delayedDate();
494 | StatusInternals.idleSession(conn2, idle2, TEST_userId);
495 |
496 | // go back to first tab
497 | StatusInternals.activeSession(conn, delayedDate(), TEST_userId);
498 |
499 | const user = Meteor.users.findOne(TEST_userId);
500 |
501 | test.equal(user.status.online, true);
502 | test.equal(user.status.lastLogin.date, ts2);
503 | test.equal(user.status.idle, false);
504 | return test.isFalse(user.status.lastActivity != null);
505 | }));
506 |
507 | // Test for idling one session across a disconnection; not most recent idle time
508 | Tinytest.add('multiplex - disconnection and reconnection while idle', withCleanup((test) => {
509 | const conn = randomConnection();
510 |
511 | const conn2 = randomConnection();
512 |
513 | const ts = delayedDate();
514 | const ts2 = delayedDate();
515 |
516 | StatusInternals.addSession(conn);
517 | StatusInternals.loginSession(conn, ts, TEST_userId);
518 |
519 | StatusInternals.addSession(conn2);
520 | StatusInternals.loginSession(conn2, ts2, TEST_userId);
521 |
522 | const idle1 = delayedDate();
523 | StatusInternals.idleSession(conn, idle1, TEST_userId);
524 | const idle2 = delayedDate();
525 | StatusInternals.idleSession(conn2, idle2, TEST_userId);
526 |
527 | // Second session, which connected later, reconnects but remains idle
528 | StatusInternals.removeSession(conn2, delayedDate(), TEST_userId);
529 |
530 | let user = Meteor.users.findOne(TEST_userId);
531 |
532 | test.equal(user.status.online, true);
533 | test.equal(user.status.lastLogin.date, ts2);
534 | test.equal(user.status.idle, true);
535 | test.equal(user.status.lastActivity, idle2);
536 |
537 | const reconn2 = randomConnection();
538 |
539 | const ts3 = delayedDate();
540 | StatusInternals.addSession(reconn2);
541 | StatusInternals.loginSession(reconn2, ts3, TEST_userId);
542 |
543 | StatusInternals.idleSession(reconn2, idle2, TEST_userId);
544 |
545 | user = Meteor.users.findOne(TEST_userId);
546 |
547 | test.equal(user.status.online, true);
548 | test.equal(user.status.lastLogin.date, ts3);
549 | test.equal(user.status.idle, true);
550 | return test.equal(user.status.lastActivity, idle2);
551 | }));
552 |
553 | Tinytest.add('status - user online set to false on startup', withCleanup((test) => {
554 | // special argument to onStartup prevents this from affecting client tests
555 | StatusInternals.onStartup(TEST_userId);
556 |
557 | const userAfterStartup = Meteor.users.findOne(TEST_userId);
558 | // Check reset status
559 | test.equal(userAfterStartup.status.online, false);
560 | test.equal(userAfterStartup.status.idle, undefined);
561 | test.equal(userAfterStartup.status.lastActivity, undefined);
562 |
563 | // Make user come online, then restart the server
564 | const conn = randomConnection();
565 | const ts = delayedDate();
566 |
567 | StatusInternals.addSession(conn);
568 | StatusInternals.loginSession(conn, ts, TEST_userId);
569 |
570 | const userAfterLogin = Meteor.users.findOne(TEST_userId);
571 |
572 | test.equal(userAfterLogin.status.online, true);
573 | test.isFalse(userAfterLogin.status.idle);
574 |
575 | StatusInternals.onStartup(TEST_userId);
576 |
577 | const userAfterRestart = Meteor.users.findOne(TEST_userId);
578 | // Check reset status again
579 | test.equal(userAfterRestart.status.online, false);
580 | test.equal(userAfterRestart.status.idle, undefined);
581 | return test.equal(userAfterRestart.status.lastActivity, undefined);
582 | }));
583 |
--------------------------------------------------------------------------------