{ "info": { "author": "Simon Willison", "author_email": "", "bugtrack_url": null, "classifiers": [], "description": "# datasette-auth-github\n\n[![PyPI](https://img.shields.io/pypi/v/datasette-auth-github.svg)](https://pypi.org/project/datasette-auth-github/)\n[![CircleCI](https://circleci.com/gh/simonw/datasette-auth-github.svg?style=svg)](https://circleci.com/gh/simonw/datasette-auth-github)\n[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-auth-github/blob/master/LICENSE)\n\nDatasette plugin and ASGI middleware that authenticates users against GitHub.\n\nThis requires [Datasette 0.29](https://datasette.readthedocs.io/en/stable/changelog.html#v0-29) or later.\n\n## Setup instructions\n\n* Install the plugin - `pip install datasette-auth-github`\n* Create a GitHub OAuth app: https://github.com/settings/applications/new\n* Set the Authorization callback URL to `http://127.0.0.1:8001/-/auth-callback`\n* Create a `metadata.json` file with the following structure:\n\n```json\n{\n \"title\": \"datasette-auth-github demo\",\n \"plugins\": {\n \"datasette-auth-github\": {\n \"client_id\": {\"$env\": \"GITHUB_CLIENT_ID\"},\n \"client_secret\": {\"$env\": \"GITHUB_CLIENT_SECRET\"}\n }\n }\n}\n```\nNow you can start Datasette like this, passing in the secrets as environment variables:\n\n $ GITHUB_CLIENT_ID=XXX GITHUB_CLIENT_SECRET=YYY datasette \\\n fixtures.db -m metadata.json\n\nNote that hard-coding secrets in `metadata.json` is a bad idea as they will be visible to anyone who can navigate to `/-/metadata`. Instead, we use a new mechanism for [adding secret plugin configuration options](https://datasette.readthedocs.io/en/latest/plugins.html#secret-configuration-values).\n\nBy default, the plugin will require users to sign in before they can interact with Datasette - but it will allow in anyone with a GitHub account.\n\nIf you want anonymous users to be able to view Datasette without signing in, you can add the `\"require_auth\": false` setting to your configuration:\n\n```json\n{\n \"plugins\": {\n \"datasette-auth-github\": {\n \"client_id\": ...,\n \"require_auth\": false\n }\n }\n}\n```\n\n## Automatic log in\n\nAssuming you are requiring authentication (the default) Datasette will redirect users to GitHub to sign in. If the user has previously authenticated with GitHub they will be redirected back again automatically, providing an instant sign-on experience.\n\nIf you would rather they saw a \"You are logged out\" screen with a button first, you can change this behaviour by adding the \"disable_auto_login\" setting to your configuration:\n\n```json\n{\n \"plugins\": {\n \"datasette-auth-github\": {\n \"client_id\": \"...\",\n \"client_secret\": \"...\",\n \"disable_auto_login\": true\n }\n }\n}\n```\n\n## Restricting access to specific users\n\nBy default the plugin will allow any GitHub user to log in. You can restrict allowed users to a specific list using the `allow_users` configuration option:\n\n```json\n{\n \"plugins\": {\n \"datasette-auth-github\": {\n \"client_id\": \"...\",\n \"client_secret\": \"...\",\n \"allow_users\": [\"simonw\"]\n }\n }\n}\n```\nYou can list one or more GitHub usernames here.\n\n## Restricting access to specific GitHub organizations or teams\n\nYou can also restrict access to users who are members of a specific GitHub organization:\n\n```json\n{\n \"plugins\": {\n \"datasette-auth-github\": {\n \"client_id\": \"...\",\n \"client_secret\": \"...\",\n \"allow_orgs\": [\"datasette-project\"]\n }\n }\n}\n```\n\nIf your organization is [arranged into teams](https://help.github.com/en/articles/organizing-members-into-teams) you can restrict access to a specific team like this:\n\n```json\n{\n \"plugins\": {\n \"datasette-auth-github\": {\n \"client_id\": \"...\",\n \"client_secret\": \"...\",\n \"allow_teams\": [\"your-organization/engineering\"]\n }\n }\n}\n```\n\n## Using this with the 'datasette publish' command\n\n`allow_orgs`, `allow_users` and `allow_teams` can both be single strings rather than lists. This means you can publish data and configure the plugin entirely from the command-line like so:\n\n $ datasette publish nowv1 fixtures.db \\\n --alias datasette-auth-demo \\\n --install=datasette-auth-github \\\n --plugin-secret datasette-auth-github client_id 86e397f7fd7a54d26a3a \\\n --plugin-secret datasette-auth-github client_secret ... \\\n --plugin-secret datasette-auth-github allow_user simonw\n\n## Cookie expiration\n\nThe cookies set by this plugin default to expiring after an hour. Users with expired cookies will be automatically redirected back through GitHub to log in, so they are unlikely to notice that their cookies have expired.\n\nYou can change the cookie expiration policy in seconds using the `cookie_ttl` setting. Here's how to increase that timeout to 24 hours:\n\n```json\n{\n \"plugins\": {\n \"datasette-auth-github\": {\n \"client_id\": \"...\",\n \"client_secret\": \"...\",\n \"cookie_ttl\": 86400\n }\n }\n}\n```\n\n## Forced cookie expiration\n\nIf you are using GitHub organizations or teams with this plugin, you need to be aware that users may continue to hold valid cookies even after they have been removed from a team or organization - generally for up to an hour unless you have changed the `cookie_ttl`.\n\nIf you need to revoke access to your instance immediately, you can do so using the `cookie_version` setting. Simply modify your metadata to add a new value for `cookie_version` and restart or redeploy your Datasette instance:\n\n```json\n{\n \"plugins\": {\n \"datasette-auth-github\": {\n \"client_id\": \"...\",\n \"client_secret\": \"...\",\n \"cookie_version\": 2\n }\n }\n}\n```\n\nAll existing cookies will be invalidated. Users who are still members of the organization or team will be able to sign in again - and in fact may be signed in automatically. Users who have been removed from the organization or team will lose access to the Datasette instance.\n\n## Using this as ASGI middleware without Datasette\n\nWhile `datasette-auth-github` is designed to be used as a [Datasette plugin](https://datasette.readthedocs.io/en/stable/plugins.html), it can also be used as regular ASGI middleware to add GitHub authentication to any ASGI application.\n\nHere's how to do that:\n\n```python\nfrom datasette_auth_github import GitHubAuth\nfrom your_asgi_app import asgi_app\n\n\napp = GitHubAuth(\n asgi_app,\n client_id=\"github_client_id\",\n client_secret=\"github_client_secret\",\n require_auth=True, # Defaults to False\n # Other options:\n # cookie_ttl=24 * 60 * 60,\n # disable_auto_login=True,\n # allow_users=[\"simonw\"],\n # allow_orgs=[\"my-org\"],\n # allow_teams=[\"my-org/engineering\"],\n)\n```\n\nThe keyword arguments work in the same way as the Datasette plugin settings documented above.\n\nThere's one key difference: when used as a plugin, `require_auth` defaults to True. If you are wrapping your own application using the middleware the default behaviour is to allow anonymous access - you need to explicitly set the `require_auth=True` keyword argument to change this behaviour.\n\nOnce wrapped in this way, your application will redirect users to GitHub to authenticate if they are not yet signed in. Authentication is recorded using a signed cookie.\n\nThe middleware adds a new `\"auth\"` key to the scope containing details of the signed-in user, which is then passed to your application. The contents of the `scope[\"auth\"]` key will look like this:\n\n```json\n{\n \"id\": \"1234 (their GitHub user ID)\",\n \"name\": \"Their Display Name\",\n \"username\": \"their-github-username\",\n \"email\": \"their-github@email-address.com\",\n \"ts\": 1562602415\n}\n```\nThe `\"ts\"` value is an integer `time.time()` timestamp representing when the user last signed in.\n\nIf the user is not signed in (and you are not using required authentication) the `\"auth\"` scope key will be set to `None`.\n\n### cacheable_prefixes\n\nBy default, the middleware marks all returned responses as `cache-control: private`. This is to ensure that content which was meant to be private to an individual user is not accidentally stored and re-transmitted by any intermediary proxying caches.\n\nThis means even static JavaScript and CSS assets will not be cached by the user's browser, which can have a negative impact on performance.\n\nYou can specify path prefixes that should NOT be marked as `cache-control: private` using the `cacheable_prefixes` constructor argument:\n\n```python\napp = GitHubAuth(\n asgi_app,\n client_id=\"github_client_id\",\n client_secret=\"github_client_secret\",\n require_auth=True,\n cacheable_prefixes=[\"/static/\"],\n)\n```\n\nNow any files within the `/static/` directory will not have the `cache-control: private` header added by the middleware.\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/simonw/datasette-auth-github", "keywords": "", "license": "Apache License, Version 2.0", "maintainer": "", "maintainer_email": "", "name": "datasette-auth-github", "package_url": "https://pypi.org/project/datasette-auth-github/", "platform": "", "project_url": "https://pypi.org/project/datasette-auth-github/", "project_urls": { "Homepage": "https://github.com/simonw/datasette-auth-github" }, "release_url": "https://pypi.org/project/datasette-auth-github/0.11/", "requires_dist": [ "datasette ; extra == 'test'", "pytest ; extra == 'test'", "pytest-asyncio ; extra == 'test'", "asgiref (~=3.1.2) ; extra == 'test'" ], "requires_python": "", "summary": "Datasette plugin and ASGI middleware that authenticates users against GitHub", "version": "0.11" }, "last_serial": 5972203, "releases": { "0.1": [ { "comment_text": "", "digests": { "md5": "7f8ab1938e8234ccdb55bcbb37c1c2dd", "sha256": "4c339327eab1899294b89f6d18290842285a6d189ad25a9b88a72d24d98178bc" }, "downloads": -1, "filename": "datasette_auth_github-0.1-py3-none-any.whl", "has_sig": false, "md5_digest": "7f8ab1938e8234ccdb55bcbb37c1c2dd", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 8419, "upload_time": "2019-07-04T15:07:19", "url": "https://files.pythonhosted.org/packages/b7/26/153364b7b4cad8b94ab2bc3af8230a42d6ffba28bada534a0beb15c18f3d/datasette_auth_github-0.1-py3-none-any.whl" } ], "0.1.1": [ { "comment_text": "", "digests": { "md5": "cae0a76832e3c83737fa0ed9ae9b7fe1", "sha256": "aedb4f70c41150197ae48f6a1ba5911ca1891cea0abed99759ac454c483f82c9" }, "downloads": -1, "filename": "datasette_auth_github-0.1.1-py3-none-any.whl", "has_sig": false, "md5_digest": "cae0a76832e3c83737fa0ed9ae9b7fe1", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 8445, "upload_time": "2019-07-05T15:59:45", "url": "https://files.pythonhosted.org/packages/6a/56/5f3d0e091721a6d7eebbb275ac732b05d84998b5d8b443bd96f035570ada/datasette_auth_github-0.1.1-py3-none-any.whl" } ], "0.10": [ { "comment_text": "", "digests": { "md5": "4f537323b52b8e11167730c421ecc72b", "sha256": "f49f7c3cfb02ccdef5c38ca9fcd845e8c786d440f70f4e892a6370bb0178079d" }, "downloads": -1, "filename": "datasette_auth_github-0.10-py3-none-any.whl", "has_sig": false, "md5_digest": "4f537323b52b8e11167730c421ecc72b", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 14749, "upload_time": "2019-10-07T15:40:11", "url": "https://files.pythonhosted.org/packages/2c/3b/c10aea2612d656ef17b152f3caf42f9885cee7e5ec6db6c092893796bac4/datasette_auth_github-0.10-py3-none-any.whl" } ], "0.11": [ { "comment_text": "", "digests": { "md5": "3c89b370052afa9d46b0b2e74ed7f2a9", "sha256": "35f0dfb431bd529675c363712520bbcd3f5af749f7f659ff543c49dc6d74e135" }, "downloads": -1, "filename": "datasette_auth_github-0.11-py3-none-any.whl", "has_sig": false, "md5_digest": "3c89b370052afa9d46b0b2e74ed7f2a9", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 14805, "upload_time": "2019-10-14T16:06:05", "url": "https://files.pythonhosted.org/packages/29/64/8e25bd06587e245421ab199eb98c8d0e6b6d8de02c9d0787890d502af831/datasette_auth_github-0.11-py3-none-any.whl" } ], "0.2": [ { "comment_text": "", "digests": { "md5": "aa7ea3f8ce24d6f9ef850db61a5ac4c5", "sha256": "f144586eca25a415230aabde51852150e0542fd8db28e2a0c0c2a7987f074cce" }, "downloads": -1, "filename": "datasette_auth_github-0.2-py3-none-any.whl", "has_sig": false, "md5_digest": "aa7ea3f8ce24d6f9ef850db61a5ac4c5", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 9781, "upload_time": "2019-07-06T17:12:25", "url": "https://files.pythonhosted.org/packages/9d/24/9e9b59debc4b57e9b0c108b2085bfde2c50f5b21eaa2580708d1d334f86c/datasette_auth_github-0.2-py3-none-any.whl" } ], "0.3": [ { "comment_text": "", "digests": { "md5": "4147868f655efade5ff45af02c783881", "sha256": "f5469a98c9b57cdd0c0b85084558c65cfdababbf12ce9fdaf038ac1d92616cd5" }, "downloads": -1, "filename": "datasette_auth_github-0.3-py3-none-any.whl", "has_sig": false, "md5_digest": "4147868f655efade5ff45af02c783881", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 9781, "upload_time": "2019-07-06T17:15:45", "url": "https://files.pythonhosted.org/packages/36/9d/25cc3e9677feb69d3cc365c9bb0de926f8ef7661ad0bd133621fbd7d1204/datasette_auth_github-0.3-py3-none-any.whl" } ], "0.3.1": [ { "comment_text": "", "digests": { "md5": "250ef0494393fd5dee82ec5768a43e2c", "sha256": "516bd7731d2c4d3a2285d4d8a9a05a72c64500f565dd4bf93deba6bb2f1d6daa" }, "downloads": -1, "filename": "datasette_auth_github-0.3.1-py3-none-any.whl", "has_sig": false, "md5_digest": "250ef0494393fd5dee82ec5768a43e2c", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 9834, "upload_time": "2019-07-06T17:27:52", "url": "https://files.pythonhosted.org/packages/63/7c/51e68c7f87cd2d4bf6fd7a2bc94116da0af4a66167500d39aeda88a64bf7/datasette_auth_github-0.3.1-py3-none-any.whl" } ], "0.3.2": [ { "comment_text": "", "digests": { "md5": "f7576e2cd9bddfe8de3bad514ea736b4", "sha256": "e97ca5b8bc2a2ca9f8d23bd712cc53b93bb7c48cb29bd385113b39c5723b2828" }, "downloads": -1, "filename": "datasette_auth_github-0.3.2-py3-none-any.whl", "has_sig": false, "md5_digest": "f7576e2cd9bddfe8de3bad514ea736b4", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 10317, "upload_time": "2019-07-06T20:19:28", "url": "https://files.pythonhosted.org/packages/d9/81/9690f2130a221b4dce7a419fd47069e0506ca9bd1b6653323c3bb0ecb2d9/datasette_auth_github-0.3.2-py3-none-any.whl" } ], "0.4": [ { "comment_text": "", "digests": { "md5": "a944632220887065ed4db444818a3ad6", "sha256": "0b1951d37ebfe395054b8d854e57b96981eaa30a0883dbb551bb747d83b1e6cd" }, "downloads": -1, "filename": "datasette_auth_github-0.4-py3-none-any.whl", "has_sig": false, "md5_digest": "a944632220887065ed4db444818a3ad6", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 10511, "upload_time": "2019-07-06T22:03:30", "url": "https://files.pythonhosted.org/packages/b4/42/8addf89a9848d44f54fa2f21d70a49c15802aa5f818ce41b25c15c6b77ca/datasette_auth_github-0.4-py3-none-any.whl" } ], "0.5": [ { "comment_text": "", "digests": { "md5": "481a9f9bfd237395344ddc48dec85c7c", "sha256": "ca4363f532395611651514c3d27b10cb4cfd98d5f9108e5a4b60b3cf8caa328f" }, "downloads": -1, "filename": "datasette_auth_github-0.5-py3-none-any.whl", "has_sig": false, "md5_digest": "481a9f9bfd237395344ddc48dec85c7c", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 11829, "upload_time": "2019-07-07T02:36:05", "url": "https://files.pythonhosted.org/packages/d4/5d/5403c5b4114ce3c3fca8f2de64e770cea08732a9eb40623fff890e079430/datasette_auth_github-0.5-py3-none-any.whl" } ], "0.6": [ { "comment_text": "", "digests": { "md5": "fca37f651f9d7e34a9e0a0505750a0af", "sha256": "c641767018501f4a049e9af1b1a51881cc91440098b5e4b2490195cc86dcfa46" }, "downloads": -1, "filename": "datasette_auth_github-0.6-py3-none-any.whl", "has_sig": false, "md5_digest": "fca37f651f9d7e34a9e0a0505750a0af", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 12800, "upload_time": "2019-07-07T19:42:31", "url": "https://files.pythonhosted.org/packages/6d/02/5b7cdce229f940149f96e2e636ddf573cc9a6893101bafeb27c74632b41e/datasette_auth_github-0.6-py3-none-any.whl" } ], "0.6.1": [ { "comment_text": "", "digests": { "md5": "4a0bd6d7095b8ed6dd53b690a1f71539", "sha256": "634e908e5827ca44e54c980631af5d590def8aacff9045f9b2ed92f3c4f5058f" }, "downloads": -1, "filename": "datasette_auth_github-0.6.1-py3-none-any.whl", "has_sig": false, "md5_digest": "4a0bd6d7095b8ed6dd53b690a1f71539", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 12815, "upload_time": "2019-07-07T20:39:38", "url": "https://files.pythonhosted.org/packages/31/ec/11a58f8e00e4e089ca2f89fc20a93327fefbd7c322a49a5da0e1086ec0fd/datasette_auth_github-0.6.1-py3-none-any.whl" } ], "0.6.2": [ { "comment_text": "", "digests": { "md5": "fde3a8501dbb6b5959f737ff4b3c19b5", "sha256": "4e512d10df05d01e1b6a967e2ff7b1236bbecde3d5690e5a7ef61794a7e9bb15" }, "downloads": -1, "filename": "datasette_auth_github-0.6.2-py3-none-any.whl", "has_sig": false, "md5_digest": "fde3a8501dbb6b5959f737ff4b3c19b5", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 12819, "upload_time": "2019-07-08T03:48:17", "url": "https://files.pythonhosted.org/packages/41/62/d3ff4fcee8e075d648f61544824b294032c3720ca1f886eec526630a5d4e/datasette_auth_github-0.6.2-py3-none-any.whl" } ], "0.6.3": [ { "comment_text": "", "digests": { "md5": "10e7e2e394027fd28c46a369ef896db7", "sha256": "b96ca819f2a6209d6f494892ae5b704bdf1e8657f7189520350c48b0e88446c1" }, "downloads": -1, "filename": "datasette_auth_github-0.6.3-py3-none-any.whl", "has_sig": false, "md5_digest": "10e7e2e394027fd28c46a369ef896db7", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 13092, "upload_time": "2019-07-08T16:51:59", "url": "https://files.pythonhosted.org/packages/b5/ad/2fccbfcb874102adc429568359ff7a62c0710a5ab51c89b6395d09cfe1b5/datasette_auth_github-0.6.3-py3-none-any.whl" } ], "0.7": [ { "comment_text": "", "digests": { "md5": "09ab94c2a6610c21388116a0692f7f57", "sha256": "5d2de409d3f20aa0d5cfedaf13cba2f22bc0d310cb092d7d9f9fdf1c3b3aba7c" }, "downloads": -1, "filename": "datasette_auth_github-0.7-py3-none-any.whl", "has_sig": false, "md5_digest": "09ab94c2a6610c21388116a0692f7f57", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 13475, "upload_time": "2019-07-11T15:06:49", "url": "https://files.pythonhosted.org/packages/fa/51/0cd19d04283a9fe097e65c142ffc160a4f9565a3c02548d29afea3985e68/datasette_auth_github-0.7-py3-none-any.whl" } ], "0.8": [ { "comment_text": "", "digests": { "md5": "92360921eaa9f33e0c37c26e6b07f301", "sha256": "5e2de495fc38a3ca6c5d3011a110a44d483875a5f730d5bbd61ee40baccac858" }, "downloads": -1, "filename": "datasette_auth_github-0.8-py3-none-any.whl", "has_sig": false, "md5_digest": "92360921eaa9f33e0c37c26e6b07f301", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 13792, "upload_time": "2019-07-13T18:40:37", "url": "https://files.pythonhosted.org/packages/c5/16/92da4e4002111d7d0366fdce6612407a5eb0ad9fe8b0ce48878aeda329d0/datasette_auth_github-0.8-py3-none-any.whl" } ], "0.9": [ { "comment_text": "", "digests": { "md5": "543b06202792a3312fbc731a91bcfb4e", "sha256": "5c8141662f2005f91eff353ea0f775e9adc860acf3ec0b33fcabe90a79475cb6" }, "downloads": -1, "filename": "datasette_auth_github-0.9-py3-none-any.whl", "has_sig": false, "md5_digest": "543b06202792a3312fbc731a91bcfb4e", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 14326, "upload_time": "2019-07-14T00:41:25", "url": "https://files.pythonhosted.org/packages/ed/f4/3195c80d74100ba2d1413615a406faa04d0a38a6d3c062b8cbca14886095/datasette_auth_github-0.9-py3-none-any.whl" } ], "0.9.1": [ { "comment_text": "", "digests": { "md5": "f3f9d41c72a93ad5bcc43c251d8099dc", "sha256": "55e89bb59e693c3d46185f4ef8fe41cef5b3c095aeeab25e6dd27a7bc0d8d0b3" }, "downloads": -1, "filename": "datasette_auth_github-0.9.1-py3-none-any.whl", "has_sig": false, "md5_digest": "f3f9d41c72a93ad5bcc43c251d8099dc", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 14353, "upload_time": "2019-07-14T00:59:28", "url": "https://files.pythonhosted.org/packages/64/07/98534b1b92e9ad6fd8e96d05d7e469e6cbb84b4a94144c4f750ecc92c94b/datasette_auth_github-0.9.1-py3-none-any.whl" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "3c89b370052afa9d46b0b2e74ed7f2a9", "sha256": "35f0dfb431bd529675c363712520bbcd3f5af749f7f659ff543c49dc6d74e135" }, "downloads": -1, "filename": "datasette_auth_github-0.11-py3-none-any.whl", "has_sig": false, "md5_digest": "3c89b370052afa9d46b0b2e74ed7f2a9", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 14805, "upload_time": "2019-10-14T16:06:05", "url": "https://files.pythonhosted.org/packages/29/64/8e25bd06587e245421ab199eb98c8d0e6b6d8de02c9d0787890d502af831/datasette_auth_github-0.11-py3-none-any.whl" } ] }