├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bower.json ├── lib └── credentials-polyfill.js ├── package.json └── tests ├── bower.json ├── components ├── agent-controller.js ├── agent.html ├── main-controller.js ├── main.html ├── main.js ├── repo-controller.js ├── repo-legacy-controller.js ├── repo-legacy.html └── repo.html ├── config.js ├── index.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "amd": true, 4 | "browser": true 5 | }, 6 | "rules": { 7 | "indent": ["error", 2, {"outerIIFEBody": 0}] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.sw[nop] 3 | *~ 4 | .project 5 | .settings 6 | TAGS 7 | bower_components 8 | node_modules 9 | coverage 10 | node_modules 11 | npm-debug.log 12 | reports 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # credentials-polyfill ChangeLog 2 | 3 | ## 1.1.6 - 2017-06-23 4 | 5 | ### Fixed 6 | - Fix IdentityCredential prototype. 7 | 8 | ## 1.1.5 - 2017-05-31 9 | 10 | ### Changed 11 | - Add package.json file. 12 | 13 | ## 1.1.4 - 2017-04-17 14 | 15 | ### Fixed 16 | - Remove window focus channel abort feature. 17 | 18 | ## 1.1.3 - 2017-04-17 19 | 20 | ### Fixed 21 | - Always attempt channel abort on context closing; MS Edge 22 | cannot read `context.handle.closed` which should be 23 | true when called regardless. 24 | 25 | ## 1.1.2 - 2017-04-14 26 | 27 | ### Fixed 28 | - Catch errors if backdrop CSS is unsupported. 29 | 30 | ## 1.1.1 - 2017-04-10 31 | 32 | ### Fixed 33 | - Fix for legacy `register` API. 34 | 35 | ## 1.1.0 - 2017-04-05 36 | 37 | ### Added 38 | - Add `requestPermission` to API. 39 | - Use `respondWith` for polyfill events; expose IdentityCredentialRegistration. 40 | 41 | ## 1.0.1 - 2017-02-26 42 | 43 | ### Fixed 44 | - Fix dialog polyfilling, opening, and canceling bugs. 45 | 46 | ## 1.0.0 - 2017-02-22 47 | 48 | ### Changed 49 | - Use iframe by default and simplify dialog. 50 | 51 | ## 0.10.1 - 2016-12-12 52 | 53 | ### Added 54 | - Add `enableRegistration` flag. 55 | - Allow error text to be passed through channel. 56 | 57 | ## 0.10.0 - 2016-07-21 58 | 59 | ### Added 60 | - **BREAKING**: Add experimental support for IE11. This change requires 61 | dropping backwards compatibility support that was in version 0.8.x. IE11 62 | is supported via use of an iframe, as IE11 does not support using 63 | postMessage with cross-domain windows. 64 | 65 | - See git history for changes. 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | New BSD License (3-clause) 2 | Copyright (c) 2015-2016, Digital Bazaar, Inc. 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of Digital Bazaar, Inc. nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL DIGITAL BAZAAR BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Identity Credentials Browser API 2 | 3 | A browser polyfill that provides the Identity Credentials Browser API, which 4 | supports: 5 | 6 | * Registration of decentralized identifiers (DIDs) 7 | * Storing credentials 8 | * Getting credentials 9 | 10 | This polyfill works in conjunction with [authorization.io](https://github.com/digitalbazaar/authorization.io). A 11 | demo on authorization.io is [here](https://authorization.io). 12 | 13 | The API it provides is meant to eventually extend the [Credential Management API][]. 14 | 15 | # Documentation 16 | 17 | This API enables a developer to write Web applications that can create new 18 | DIDs for an entity, get credentials, and store credentials through the browser. 19 | The API is outlined below, separated by different actors in the system: 20 | 21 | APIs called by credential issuers: 22 | * *navigator.credentials.store(* **credential** *)* 23 | 24 | APIs called by credential consumers: 25 | * *navigator.credentials.get(* **options** *)* 26 | 27 | APIs called by credential repository (previously known as identity providers): 28 | * *IdentityCredentialRegistration.register(* **options** *)* 29 | * *navigator.credentials.getPendingOperation(* **options** *)* 30 | * *CredentialOperation.complete(* **result** *)* 31 | 32 | ## Registering a new decentralized identity 33 | 34 | The *IdentityCredentialRegistration* object can be used to register a new 35 | decentralized identifier and link it to the entity's credential repository. 36 | 37 | The object can be instantiated with the following options: 38 | 39 | * **options** (**required** *object*) 40 | * **repository** (*string*) - A URL identifier for the credential repository 41 | (previously known as identity provider) that should be associated with the 42 | newly created decentralized identifier. 43 | * **name** (*string*) - a friendly, human-meaningful identifier, such as 44 | an email address, that can be shown in UIs to help the user make identity 45 | selections. 46 | 47 | Once instantiated, the object's *register()* method can be called. The call 48 | returns a *Promise* that resolves to the document associated with the 49 | registered DID. 50 | 51 | Example: 52 | 53 | ```javascript 54 | var registration = new IdentityCredentialRegistration({ 55 | repository: 'did:d1d1d1d1-d1d1-d1d1-d1d1-d1d1d1d1d1d1', 56 | name: 'person@example.org' 57 | }); 58 | 59 | registration.register().then(function(didDocument) { 60 | // ... 61 | }); 62 | ``` 63 | 64 | The example above will result in an a JSON-LD document that looks like 65 | the following: 66 | 67 | ```jsonld 68 | { 69 | "@context": "https://w3id.org/identity/v1", 70 | "id": "did:59cf8ba9-70f6-456e-aa7f-6e898c3a3e5f", 71 | "idp": "did:d1d1d1d1-d1d1-d1d1-d1d1-d1d1d1d1d1d1", 72 | "accessControl": { 73 | "writePermission": [{ 74 | "id": "did:59cf8ba9-70f6-456e-aa7f-6e898c3a3e5f/keys/1", 75 | "type": "CryptographicKey" 76 | }, { 77 | "id": "did:d1d1d1d1-d1d1-d1d1-d1d1-d1d1d1d1d1d1", 78 | "type": "Identity" 79 | }] 80 | }, 81 | "publicKey": [{ 82 | "id": "did:59cf8ba9-70f6-456e-aa7f-6e898c3a3e5f/keys/1", 83 | "type": "CryptographicKey", 84 | "owner": "did:59cf8ba9-70f6-456e-aa7f-6e898c3a3e5f", 85 | "publicKeyPem": "-----BEGIN PUBLIC KEY-----\r\nMIIBI...AQAB\r\n-----END PUBLIC KEY-----\r\n" 86 | }], 87 | "signature": { 88 | "type": "LinkedDataSignature2015", 89 | "created": "2015-07-02T16:54:27Z", 90 | "creator": "did:59cf8ba9-70f6-456e-aa7f-6e898c3a3e5f/keys/1", 91 | "signatureValue": "JukNUu...I0g==" 92 | } 93 | } 94 | ``` 95 | 96 | ## Registering a new public key for an existing decentralized identity 97 | 98 | The *IdentityCredentialRegistration* object can be used to register a new 99 | public key on a new device/browser for an existing decentralized identifier, 100 | provided that credential repository has sufficient authority to update 101 | the entity's decentralized identifier document. 102 | 103 | The object should be instantiated using these options: 104 | 105 | * **options** (**required** *object*) 106 | * **repository** (*string*) - A URL identifier for the credential repository 107 | (previously known as identity provider) that should be associated with the 108 | newly created decentralized identifier. 109 | * **name** (*string*) - a friendly, human-meaningful identifier, such as 110 | an email address, that can be shown in UIs to help the user make identity 111 | selections. 112 | * **id** (*string*) - the decentralized identifier to use. 113 | 114 | Once instantiated, the object's 115 | *addEventListener(* **event** *,* **listener** *)* method can be 116 | called using `registerIdentityCredential` for `event` and a function that 117 | will receive a `RegisterIdentityCredentialEvent` object as a parameter. Once 118 | an event listener has been added, a call to *register()* on the object 119 | should be made. Once the user has approved the registration operation, the 120 | event listener will be called with a `RegisterIdentityCredentialEvent`, which 121 | includes: 122 | 123 | * **publicKey** (*PublicKey*) - An object including `owner` and `publicKeyPem`. 124 | * **respondWith(** **Promise** *identity* **)** - Called from an event 125 | listener to respond with the registered identity information. 126 | 127 | The *register()* call returns a *Promise* that resolves to the document 128 | associated with the registered DID. 129 | 130 | Example: 131 | 132 | ```javascript 133 | var registration = new IdentityCredentialRegistration({ 134 | repository: 'did:d1d1d1d1-d1d1-d1d1-d1d1-d1d1d1d1d1d1', 135 | name: 'person@example.org' 136 | }); 137 | 138 | registration.addEventListener('registerIdentityCredential', function(event) { 139 | var publicKey = event.publicKey; 140 | 141 | // async register public key with `publicKey.owner`'s' DID document; 142 | // response returns public key with its new ID 143 | event.respondWith($http.post('/some-register-endpoint', publicKey) 144 | .then(function(response) { 145 | return { 146 | '@context': 'https://w3id.org/identity/v1', 147 | id: publicKey.owner, 148 | publicKey: response.data 149 | }; 150 | })); 151 | }); 152 | 153 | registration.register().then(function(didDocument) { 154 | // ... 155 | }).catch(function(err) { 156 | // ... handle error case 157 | }) 158 | ``` 159 | 160 | The example above will result in an a JSON-LD document that looks like 161 | the following: 162 | 163 | ```jsonld 164 | { 165 | "@context": "https://w3id.org/identity/v1", 166 | "id": "did:59cf8ba9-70f6-456e-aa7f-6e898c3a3e5f", 167 | "idp": "did:d1d1d1d1-d1d1-d1d1-d1d1-d1d1d1d1d1d1", 168 | "accessControl": { 169 | "writePermission": [{ 170 | "id": "did:59cf8ba9-70f6-456e-aa7f-6e898c3a3e5f/keys/1", 171 | "type": "CryptographicKey" 172 | }, { 173 | "id": "did:d1d1d1d1-d1d1-d1d1-d1d1-d1d1d1d1d1d1", 174 | "type": "Identity" 175 | }] 176 | }, 177 | "publicKey": [{ 178 | "id": "did:59cf8ba9-70f6-456e-aa7f-6e898c3a3e5f/keys/1", 179 | "type": "CryptographicKey", 180 | "owner": "did:59cf8ba9-70f6-456e-aa7f-6e898c3a3e5f", 181 | "publicKeyPem": "-----BEGIN PUBLIC KEY-----\r\nMIIBI...AQAB\r\n-----END PUBLIC KEY-----\r\n" 182 | }], 183 | "signature": { 184 | "type": "LinkedDataSignature2015", 185 | "created": "2015-07-02T16:54:27Z", 186 | "creator": "did:59cf8ba9-70f6-456e-aa7f-6e898c3a3e5f/keys/1", 187 | "signatureValue": "JukNUu...I0g==" 188 | } 189 | } 190 | ``` 191 | 192 | ## Storing a Credential 193 | 194 | The *navigator.credentials.store(* **credential** *)* call can be 195 | used to store a set of attributes about an entity, backed by credentials, 196 | at an entity's credential repository. 197 | 198 | The call takes the following arguments: 199 | 200 | * **credential** (**required** *IdentityCredential*) - An IdentityCredential 201 | containing a JSON-LD document that contains at least one valid 202 | *credential* entry. 203 | 204 | The call returns a *Promise* that resolves to an IdentityCredential containing 205 | a JSON-LD document that contains the credentials that were stored. 206 | 207 | ```javascript 208 | navigator.credentials.store(new IdentityCredential({ 209 | "@context": "https://w3id.org/identity/v1", 210 | "id": "did:04054703-8c94-46a3-bae7-7ffd07c0c962", 211 | "credential": [{ 212 | "@graph": { 213 | "@context": "https://w3id.org/identity/v1", 214 | "id": "https://issuer.example.com/creds/1", 215 | "type": ["Credential", "EmailCredential"], 216 | "claim": { 217 | "id": "did:04054703-8c94-46a3-bae7-7ffd07c0c962", 218 | "email": "test@example.com" 219 | }, 220 | "signature": { 221 | "type": "LinkedDataSignature2015", 222 | "created": "2015-07-02T17:41:39Z", 223 | "creator": "https://issuer.example.com/keys/1", 224 | "signatureValue": "Tyd5S0A...nx33Yg==" 225 | } 226 | } 227 | }] 228 | })).then(function(credential) { 229 | // ... 230 | }); 231 | ``` 232 | 233 | The example above will result in an IdentityCredential containing a JSON-LD 234 | document that looks like the one that was passed via the *credential* 235 | parameter. Optionally, the recipient of the credentials may choose to not 236 | store some of the credentials and can notify the issuer that those credentials 237 | were not stored by omitting them from the response. 238 | 239 | ## Getting a Credential 240 | 241 | The *navigator.credentials.get(* **options** *)* call can be used to 242 | request a set of properties about an entity that are backed by 243 | credentials from an entity's credential repository. 244 | 245 | The call takes the following arguments: 246 | 247 | * **options** (**required** *object*) 248 | * **identity** (**required** *object*) 249 | * **query** (**required** *object*) - A JSON-LD document that is a 250 | "query by example". The query consists of the attributes associated with 251 | an entity that the credential consumer would like to see. 252 | 253 | The call returns a *Promise* that resolves to an IdentityCredential containing 254 | a JSON-LD document that contains the credentials that were retrieved. 255 | 256 | ```javascript 257 | navigator.credentials.get({ 258 | identity: { 259 | query: { 260 | '@context': 'https://w3id.org/identity/v1', 261 | id: '', 262 | email: '' 263 | } 264 | } 265 | }).then(function(credential) { 266 | if(credential === null) { 267 | // no credential found/selected 268 | // ... 269 | } 270 | // get JSON-LD identity document from credential 271 | var identity = credential.identity; 272 | // ... 273 | }); 274 | ``` 275 | 276 | The example above will eventually result in the following JSON-LD document: 277 | 278 | ```jsonld 279 | { 280 | "@context": "https://w3id.org/identity/v1", 281 | "id": "did:04054703-8c94-46a3-bae7-7ffd07c0c962", 282 | "type": "Identity", 283 | "credential": [{ 284 | "@graph": { 285 | "@context": "https://w3id.org/identity/v1", 286 | "id": "urn:credential-1", 287 | "type": ["Credential", "CryptographicKeyCredential"], 288 | "claim": { 289 | "id": "did:04054703-8c94-46a3-bae7-7ffd07c0c962", 290 | "email": "test@example.com", 291 | "publicKey": { 292 | "id": "did:04054703-8c94-46a3-bae7-7ffd07c0c962/keys/1", 293 | "publicKeyPem": "-----BEGIN PUBLIC KEY-----\r\nMIIBIj...IDAQAB\r\n-----END PUBLIC KEY-----\r\n" 294 | } 295 | }, 296 | "signature": { 297 | "type": "LinkedDataSignature2015", 298 | "created": "2015-07-02T17:45:21Z", 299 | "creator": "https://authorization.dev:33443/idp/keys/1", 300 | "signatureValue": "S33Qcs...zWDqQQ==" 301 | } 302 | } 303 | }], 304 | "signature": { 305 | "type": "LinkedDataSignature2015", 306 | "created": "2015-07-02T17:46:04Z", 307 | "creator": "did:04054703-8c94-46a3-bae7-7ffd07c0c962/keys/1", 308 | "signatureValue": "LpoVj...LP2A==" 309 | } 310 | } 311 | ``` 312 | 313 | ## Getting a Pending Credential Operation 314 | 315 | The `getPendingOperation` method is only used by credential repositories to 316 | complete a pending `get` or `store` credentials operation once authorization 317 | has been provided by the entity. 318 | 319 | The call takes no arguments. It returns a *Promise* that resolves to a 320 | *CredentialOperation*. A *CredentialOperation* has the following properties: 321 | 322 | * **name** (*string*) - The name of the pending operation (ie: `get` or `store`). 323 | * **options** (*object*) 324 | * **query** (*object*) - Present if the operation name is `get`. The query 325 | passed to `navigator.credentials.get`. 326 | * **store** (*object*) - Present if operation name is `store`. Contains the 327 | identity document contained in the `IdentityCredential` passed to 328 | `navigator.credentials.store`. 329 | * **identity** (*object*) - The entity's signed identity for the device they 330 | are using, including a CryptographicKeyCredential. 331 | * **registerKey** (*boolean*) - True if an attempt should be made to register 332 | the entity's public key (provided in the CryptographicKeyCredential) with 333 | its decentralized identity. This will only be possible if the entity has 334 | granted permission to their identity provider to write new keys to their 335 | decentralized identity. 336 | 337 | The credential repository can now help the entity to fulfill the credentials 338 | query or ask it to accept the storage request. Once the credential repository 339 | has completed the operation, it must call `complete` on the 340 | *CredentialOperation* instance, passing the result of the operation. This 341 | call will cause the browser to navigate away from the credential repository 342 | with the result. 343 | 344 | **Note: `getPendingOperation` may be changed to use [MessageChannels](https://html.spec.whatwg.org/multipage/comms.html#message-channels). ** 345 | 346 | ```javascript 347 | navigator.credentials.getPendingOperation().then(function(operation) { 348 | // ... 349 | 350 | // operation now complete 351 | operation.complete(result); 352 | }); 353 | ``` 354 | 355 | Source 356 | ------ 357 | 358 | The source code for the JavaScript implementation is available at: 359 | 360 | https://github.com/digitalbazaar/credentials-polyfill 361 | 362 | 363 | [Credential Management API]: https://w3c.github.io/webappsec-credential-management/ 364 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "credentials-polyfill", 3 | "moduleType": [ 4 | "amd" 5 | ], 6 | "description": "A polyfill for the Identity Credentials API", 7 | "authors": [ 8 | "The Open Payments Foundation" 9 | ], 10 | "license": "BSD", 11 | "main": [ 12 | "lib/credentials-polyfill.js" 13 | ], 14 | "dependencies": { 15 | "es6-promise": "^2.0.0" 16 | }, 17 | "ignore": [ 18 | "node_modules", 19 | "bower_components", 20 | "tests" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /lib/credentials-polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Credentials API Polyfill. 3 | * 4 | * A polyfill for the Credentials API is two parts: 5 | * 6 | * 1. A JavaScript library to be served along side a Web application that uses 7 | * the API. 8 | * 9 | * 2. A "Credential Agent" which is implemented as a Web application that is 10 | * served from a community run, independent origin. 11 | */ 12 | /* global dialogPolyfill */ 13 | (function() { 14 | 15 | //////////////// DISCOVER LOCAL CONTEXT TO INSTALL POLYFILL ON //////////////// 16 | 17 | var local; 18 | if(typeof global !== 'undefined') { 19 | local = global; 20 | } else if(typeof window !== 'undefined') { 21 | local = window; 22 | } else { 23 | try { 24 | local = Function('return this')(); 25 | } catch(e) { 26 | throw new Error( 27 | 'credentials-polyfill failed to install because the global object is ' + 28 | 'unavailable in this environment.'); 29 | } 30 | } 31 | 32 | /////////////////// IF API PRESENT, DO NOT INSTALL POLYFILL /////////////////// 33 | if('navigator' in local && 'credentials' in local.navigator && 34 | typeof local.IdentityCredential !== 'undefined') { 35 | return; 36 | } 37 | 38 | ////////////////////// DEFINE AND INSTALL PUBLIC API ////////////////////////// 39 | 40 | if(!('navigator' in local)) { 41 | local.navigator = {}; 42 | } 43 | 44 | /////////////////////// PUBLIC CREDENTIAL CONTAINER API /////////////////////// 45 | 46 | var CredentialsContainer; 47 | var _credentialsContainer; 48 | var _nativeCredentialsContainer; 49 | if('credentials' in local.navigator) { 50 | // native instance exists, prepare to modify it 51 | CredentialsContainer = local.CredentialsContainer; 52 | _nativeCredentialsContainer = local.navigator.credentials; 53 | _credentialsContainer = _nativeCredentialsContainer; 54 | } else { 55 | // no native support, create local instance 56 | CredentialsContainer = function() { 57 | if(!(this instanceof CredentialsContainer)) { 58 | return new CredentialsContainer(); 59 | } 60 | }; 61 | _credentialsContainer = new CredentialsContainer(); 62 | local.navigator.credentials = _credentialsContainer; 63 | } 64 | 65 | /** 66 | * Gets a Credential from the container. 67 | * 68 | * @param options the options to use. 69 | * identity the IdentityCredentialRequestOptions: 70 | * query the query-by-example object that should be filled out by 71 | * the credential repository. It is an object that includes a 72 | * JSON-LD context and a number of properties that should be 73 | * included in the response by the repository. 74 | * [enableRegistration] if true, the user interface will show 75 | * a credential repository registration option that, when chosen, 76 | * will cause the Promise to be rejected with a 77 | * `NotRegisteredError`. 78 | * [agentUrl] the Credential Agent URL to use to proxy the request. 79 | * The default is `https://authorization.io/agent`. 80 | * 81 | * @return a Promise that resolves to the result of the query. 82 | */ 83 | _credentialsContainer.get = function(options) { 84 | var legacy = false; 85 | options = options || {}; 86 | if('query' in options) { 87 | // backwards compatibility; assume query is an 'identity' credential query 88 | options = {identity: options}; 89 | legacy = true; 90 | } 91 | if(!('identity' in options)) { 92 | if(_nativeCredentialsContainer) { 93 | return _nativeCredentialsContainer.apply( 94 | _nativeCredentialsContainer, arguments); 95 | } 96 | throw new Error('Could not get credentials; only "identity" ' + 97 | 'credential queries are supported.'); 98 | } 99 | if(!options.identity.query) { 100 | throw new Error('Could not get credentials; no query provided.'); 101 | } 102 | if('enableRegistration' in options.identity && 103 | typeof options.identity.enableRegistration !== 'boolean') { 104 | throw new TypeError('enableRegistration must be a boolean.'); 105 | } 106 | var agentUrl = options.identity.agentUrl || 'https://authorization.io/agent'; 107 | agentUrl = _updateQueryStringParameter(agentUrl, 'op', 'get'); 108 | agentUrl = _updateQueryStringParameter(agentUrl, 'route', 'params'); 109 | agentUrl = _updateQueryStringParameter( 110 | agentUrl, 'origin', window.location.origin); 111 | return Flow.start(agentUrl, 'get', { 112 | query: options.identity.query, 113 | enableRegistration: !!options.identity.enableRegistration 114 | }).then(function(credential) { 115 | if(!credential) { 116 | return credential; 117 | } 118 | return legacy ? credential.identity : credential; 119 | }); 120 | }; 121 | 122 | /** 123 | * Stores a Credential in the container. 124 | * 125 | * @param credential the Credential to store. 126 | * @param options the options to use. 127 | * [agentUrl] the agent URL to use to proxy the request. The 128 | * default is `https://authorization.io/agent`. 129 | * 130 | * @return a Promise that resolves to a storage acknowledgement. 131 | */ 132 | _credentialsContainer.store = function(credential, options) { 133 | if(!credential) { 134 | throw new Error('Could not store credential; no credential provided.'); 135 | } 136 | var legacy = false; 137 | if(!(credential instanceof Credential)) { 138 | // backwards compatibility; assume `credential` is a JSON-LD identity 139 | credential = new IdentityCredential(credential); 140 | legacy = true; 141 | } 142 | if(!(credential instanceof IdentityCredential)) { 143 | if(_nativeCredentialsContainer) { 144 | return _nativeCredentialsContainer.apply( 145 | _nativeCredentialsContainer, arguments); 146 | } 147 | throw new Error('Could not store credential; only ' + 148 | 'IdentityCredentials are supported.'); 149 | } 150 | var agentUrl = options.agentUrl || 'https://authorization.io/agent'; 151 | agentUrl = _updateQueryStringParameter(agentUrl, 'op', 'store'); 152 | agentUrl = _updateQueryStringParameter(agentUrl, 'route', 'params'); 153 | agentUrl = _updateQueryStringParameter( 154 | agentUrl, 'origin', window.location.origin); 155 | return Flow.start(agentUrl, 'store', credential.identity) 156 | .then(function(credential) { 157 | if(!credential) { 158 | return credential; 159 | } 160 | return legacy ? credential.identity : credential; 161 | }); 162 | }; 163 | 164 | /** 165 | * Gets a pending operation on the container. This is called by a third party 166 | * website, called a Credential Repository, that provides a remote 167 | * implementation of a CredentialsContainer. 168 | * 169 | * @param options the options to use. 170 | * [agentUrl] the agent URL to use to get the pending operation. The 171 | * default is `https://authorization.io/agent`. 172 | * 173 | * @return a Promise that resolves to the CredentialOperation. 174 | */ 175 | _credentialsContainer.getPendingOperation = function(options) { 176 | options = options || {}; 177 | var agentUrl = options.agentUrl || 'https://authorization.io/agent'; 178 | agentUrl = _updateQueryStringParameter(agentUrl, 'route', 'params'); 179 | agentUrl = _updateQueryStringParameter( 180 | agentUrl, 'origin', window.location.origin); 181 | return Flow.resume(agentUrl, options).then(function(message) { 182 | var operation = new CredentialOperation(); 183 | operation.name = message.type.split('.')[0]; 184 | operation.options = message.data.options; 185 | return operation; 186 | }); 187 | }; 188 | 189 | /////////////////////// PUBLIC CREDENTIAL OPERATION API /////////////////////// 190 | 191 | /** 192 | * Creates a new pending CredentialOperation. 193 | */ 194 | function CredentialOperation() { 195 | if(!(this instanceof CredentialOperation)) { 196 | return new CredentialOperation(); 197 | } 198 | } 199 | 200 | /** 201 | * Completes this pending CredentialOperation by transmitting a result. 202 | * 203 | * @param result the result to resolve the pending Promise to. 204 | * @param options the options to use. 205 | * [agentUrl] the Credential Agent URL to use to send the 206 | * result. The default is `https://authorization.io/agent`. 207 | */ 208 | CredentialOperation.prototype.complete = function(result, options) { 209 | options = options || {}; 210 | var agentUrl = options.agentUrl || 'https://authorization.io/agent'; 211 | agentUrl = _updateQueryStringParameter(agentUrl, 'op', this.name); 212 | agentUrl = _updateQueryStringParameter(agentUrl, 'route', 'result'); 213 | agentUrl = _updateQueryStringParameter( 214 | agentUrl, 'origin', window.location.origin); 215 | return Flow.end(agentUrl, this.name, result, options); 216 | }; 217 | 218 | //////////////////////////// PUBLIC CREDENTIAL API //////////////////////////// 219 | 220 | var Credential; 221 | if('Credential' in local) { 222 | // use native Credential 223 | Credential = local.Credential; 224 | } else { 225 | /** 226 | * Creates a new Credential. 227 | * 228 | * @param id the identifier for this Credential. 229 | * @param type the type for this Credential. 230 | */ 231 | Credential = function(id, type) { 232 | if(!(this instanceof Credential)) { 233 | return new Credential(id, type); 234 | } 235 | this.id = id; 236 | this.type = type; 237 | }; 238 | local.Credential = Credential; 239 | } 240 | 241 | /////////////////////// PUBLIC IDENTITY CREDENTIAL API //////////////////////// 242 | 243 | /** 244 | * Creates a new IdentityCredential instance. 245 | * 246 | * @param identity the JSON-LD identity for this credential. 247 | */ 248 | function IdentityCredential(identity) { 249 | if(!(this instanceof IdentityCredential)) { 250 | return new IdentityCredential(identity); 251 | } 252 | this.id = identity.id; 253 | this.type = 'identity'; 254 | this.identity = identity; 255 | } 256 | IdentityCredential.prototype = Object.create( 257 | Credential.prototype, { 258 | id: {writable: true}, 259 | type: {writable: true} 260 | }); 261 | IdentityCredential.prototype.constructor = IdentityCredential; 262 | local.IdentityCredential = IdentityCredential; 263 | 264 | /** 265 | * ** DEPRECATED, use `IdentityCredentialRegistration` instead **. 266 | * 267 | * Registers a new decentralized identifier. 268 | * 269 | * @param options the options for the request. 270 | * repo the decentralized identifier (DID) for the Credential 271 | * Repository to register for the new decentralized identifier. 272 | * idp **deprecated** - use `repo` instead. 273 | * [name] a friendly, human-meaningful identifier, such as 274 | * an email address, that can be shown in UIs to help the user 275 | * make identity selections. 276 | * [agentUrl] the agent URL to use to service the request. The 277 | * default is `https://authorization.io/register`. 278 | * 279 | * @return a Promise that resolves to the resulting DID Document. 280 | */ 281 | IdentityCredential.register = function(options) { 282 | if(!options) { 283 | throw new Error( 284 | 'Could not register DID; credential repository information not ' + 285 | ' provided.'); 286 | } 287 | if('id' in options) { 288 | throw new Error('`options.id` must not be specified.'); 289 | } 290 | 291 | // backwards compatibility for deprecated `idp` option 292 | var opts = {}; 293 | for(var key in options) { 294 | opts[key] = options[key]; 295 | } 296 | var repo = options.repo || options.idp; 297 | if(!repo) { 298 | throw new Error( 299 | 'Could not register DID; credential repository\'s ID was not provided.'); 300 | } 301 | opts.repository = repo; 302 | var registration = new IdentityCredentialRegistration(opts); 303 | opts = {}; 304 | if('agentUrl' in options) { 305 | opts.agentUrl = options.agentUrl; 306 | } 307 | return registration.register(opts); 308 | }; 309 | 310 | /** 311 | * Requests permissions associated with IdentityCredentials. 312 | * 313 | * @param permissions the permisison identifier (a string) or an array of 314 | * permission identifiers to request for the current origin. 315 | * @param options the registration options. 316 | * [agentUrl] the agent URL to use to service the request. The 317 | * default is `https://authorization.io/agent`. 318 | * 319 | * @return a Promise that resolves to 'default', denied', or 'granted'. 320 | */ 321 | IdentityCredential.requestPermission = function(permissions, options) { 322 | if(!Array.isArray(permissions)) { 323 | permissions = [permissions]; 324 | } 325 | for(var i = 0; i < permissions.length; ++i) { 326 | if(typeof permissions[i] !== 'string') { 327 | throw new TypeError( 328 | '`permissions` must be a string or an array of strings.'); 329 | } 330 | } 331 | 332 | var agentUrl = options.agentUrl || 'https://authorization.io/agent'; 333 | agentUrl = _updateQueryStringParameter(agentUrl, 'op', 'requestPermission'); 334 | agentUrl = _updateQueryStringParameter(agentUrl, 'route', 'params'); 335 | agentUrl = _updateQueryStringParameter( 336 | agentUrl, 'origin', window.location.origin); 337 | return Flow.start(agentUrl, 'requestPermission', permissions); 338 | }; 339 | 340 | /** 341 | * Creates a new decentralized identifier registration object. It can be 342 | * used to register a new decentralized identifier or add a keypair to an 343 | * existing decentralized identifier for the current user. 344 | * 345 | * @param options the registration options. 346 | * repository the decentralized identifier (DID) for the Credential 347 | * Repository to register for the new decentralized identifier. 348 | * name a friendly, human-meaningful identifier, such as 349 | * an email address, that can be shown in UIs to help the user 350 | * make identity selections. 351 | * [id] the decentralized identifier to use. 352 | */ 353 | function IdentityCredentialRegistration(options) { 354 | if(!(this instanceof IdentityCredentialRegistration)) { 355 | return new IdentityCredentialRegistration(options); 356 | } 357 | if(!options || typeof options !== 'object') { 358 | throw new TypeError('`options` must be a non-empty object.'); 359 | } 360 | if(typeof options.repository !== 'string') { 361 | throw new TypeError('`options.repository` must be a string.'); 362 | } 363 | if(typeof options.name !== 'string') { 364 | throw new TypeError('`options.name` must be a string.'); 365 | } 366 | this.repository = options.repository; 367 | this.name = options.name; 368 | this.id = options.id || null; 369 | this._emitter = new EventEmitter(); 370 | } 371 | local.IdentityCredentialRegistration = IdentityCredentialRegistration; 372 | 373 | /** 374 | * Adds an event listener. 375 | * 376 | * @param event the name of the event to listen for. 377 | * @param listener the function to call when emitting the event. 378 | */ 379 | IdentityCredentialRegistration.prototype.addEventListener = function( 380 | event, listener) { 381 | return this._emitter.addEventListener(event, listener); 382 | }; 383 | 384 | /** 385 | * Removes an event listener. 386 | * 387 | * @param event the name of the event to remove the listener for. 388 | * @param listener the function to remove. 389 | */ 390 | IdentityCredentialRegistration.prototype.removeEventListener = function( 391 | event, listener) { 392 | return this._emitter.removeEventListener(event, listener); 393 | }; 394 | 395 | /** 396 | * Attempts to perform decentralized identifier registration. If `id` was 397 | * given during construction, then a `register` event will be emitted that must 398 | * be handled or the registration will fail. 399 | * 400 | * @param options the registration options. 401 | * [agentUrl] the agent URL to use to service the request. The 402 | * default is `https://authorization.io/register`. 403 | * 404 | * @return a Promise that resolves to the resulting DID Document. 405 | */ 406 | IdentityCredentialRegistration.prototype.register = function(options) { 407 | var agentUrl = options.agentUrl || 'https://authorization.io/register'; 408 | agentUrl = _updateQueryStringParameter(agentUrl, 'op', 'registerDid'); 409 | agentUrl = _updateQueryStringParameter(agentUrl, 'route', 'params'); 410 | agentUrl = _updateQueryStringParameter( 411 | agentUrl, 'origin', window.location.origin); 412 | var request = { 413 | '@context': 'https://w3id.org/identity/v1', 414 | // TODO: change to `idp` to `credentialRepository` 415 | idp: this.repository, 416 | name: this.name 417 | }; 418 | if(this.id) { 419 | request.id = this.id; 420 | } 421 | return Flow.start(agentUrl, 'registerDid', request, this._emitter); 422 | }; 423 | 424 | /////////////////////////////////// STYLE ///////////////////////////////////// 425 | 426 | (function() { 427 | var style = document.createElement('style'); 428 | // WebKit hack :( 429 | style.appendChild(document.createTextNode('')); 430 | document.head.appendChild(style); 431 | try { 432 | style.sheet.insertRule( 433 | 'dialog.credentials-polyfill-dialog::backdrop {background: transparent}', 434 | 0); 435 | } catch(e) {} 436 | })(); 437 | 438 | ////////////////////////////// PRIVATE FLOW API /////////////////////////////// 439 | 440 | /** 441 | * Flow for `credentials.get` and `credentials.store`: 442 | * - return Promise 443 | * ===OPEN IFRAME TO CREDENTIAL AGENT=== 444 | * - send `params` request to opener (postMessage) 445 | * - receive `params` from opener (postMessage) 446 | * ===OPEN IFRAME TO REPO=== 447 | * - credentials.getPendingOperation 448 | * - send `params` request to opener (postMessage) 449 | * - return CredentialOperation 450 | * - create `result` 451 | * - CredentialOperation.complete() 452 | * - send `result` to opener (postMessage) 453 | * ===CLOSE REPO IFRAME=== 454 | * - send `result` to opener (postMessage) 455 | * ===CLOSE CREDENTIAL AGENT IFRAME=== 456 | * - receive `result` (postMessage) 457 | * - resolve Promise 458 | * 459 | * Flow for `IdentityCredentialRegistration.register` for a new DID: 460 | * - return Promise 461 | * ===OPEN IFRAME TO CREDENTIAL AGENT=== 462 | * - send `params` request to opener (postMessage) 463 | * - receive `params` from opener (postMessage) 464 | * - do DID registration 465 | * - send `result` to opener (postMessage) 466 | * ===CLOSE CREDENTIAL AGENT IFRAME=== 467 | * - receive `result` (postMessage) 468 | * - resolve Promise 469 | * 470 | * Flow for `IdentityCredentialRegistration.register` for an existing DID: 471 | * - return Promise 472 | * ===OPEN IFRAME TO CREDENTIAL AGENT URL=== 473 | * - send `params` request to opener (postMessage) 474 | * - receive `params` from opener (postMessage) 475 | * - send `event` to opener (postMessage) 476 | * ===ORIGIN=== 477 | * - receive `event` (postMessage) 478 | * - process event (i.e. perform DDO update) 479 | * - send `continue` (postMessage) 480 | * ===CREDENTIAL AGENT IFRAME=== 481 | * - receive `continue` from opener (postMessage) 482 | * - send `result` to opener (postMessage) 483 | * ===CLOSE FLOW WINDOW=== 484 | * - receive `result` (postMessage) 485 | * - resolve Promise 486 | * 487 | * Flow for `IdentityCredentialRegistration.requestPermission`: 488 | * - return Promise 489 | * ===OPEN IFRAME TO CREDENTIAL AGENT=== 490 | * - send `params` request to opener (postMessage) 491 | * - receive `params` from opener (postMessage) 492 | * - get user consent to grant permission 493 | * - send `result` to opener (postMessage) 494 | * ===CLOSE CREDENTIAL AGENT IFRAME=== 495 | * - receive `result` (postMessage) 496 | * - resolve Promise 497 | * 498 | * TODO: Can Flow/Router be further refactored to be more Promise-like? 499 | */ 500 | var Flow = {}; 501 | 502 | /** 503 | * Starts a credentials flow in a new window. 504 | * 505 | * @param url the Credential Agent URL to use. 506 | * @param op the name of the operation the flow is for. 507 | * @param params the parameters for the flow. 508 | * @param [emitter] the event emitter to use w/the channel. 509 | * 510 | * @return a Promise that resolves to the result of the flow. 511 | */ 512 | Flow.start = function(url, op, params, emitter) { 513 | // start flow in new browsing context 514 | var context = new BrowsingContext(url); 515 | var channel = new Channel(context, emitter); 516 | // when context is canceled, channel must be aborted 517 | context.onCancel = abortChannel; 518 | function abortChannel() { 519 | context.onCancel = null; 520 | channel.abort(); 521 | } 522 | 523 | // serve params for message based on API function name 524 | return channel.serve(op + '.params', params).then(function() { 525 | // receive result 526 | return channel.receive(op + '.result'); 527 | }).catch(function(err) { 528 | if(err instanceof ChannelAbortError) { 529 | var data; 530 | if(op === 'requestPermission') { 531 | data = 'default'; 532 | } else { 533 | data = null; 534 | } 535 | return {type: op + '.abort', data: data}; 536 | } 537 | // ensure context is closed on error 538 | context.close(); 539 | throw err; 540 | }).then(function(message) { 541 | context.close(); 542 | if(op === 'registerDid' || op === 'requestPermission') { 543 | return message.data; 544 | } 545 | if(!message.data) { 546 | return null; 547 | } 548 | return new IdentityCredential(message.data); 549 | }); 550 | }; 551 | 552 | /** 553 | * Resumes an existing flow. This call is used to contact the Credential Agent 554 | * to request the parameters for the current flow. 555 | * 556 | * @param url the Credential Agent URL to use. 557 | * @param [options] the options to use. 558 | * 559 | * @return a Promise that resolves to the resulting channel message containing 560 | * its type and the flow parameters. 561 | */ 562 | Flow.resume = function(url, options) { 563 | // communicate with parent 564 | var context = new BrowsingContext(url, {handle: window.parent}); 565 | var channel = new Channel(context); 566 | return channel.request(['get.params', 'store.params']); 567 | }; 568 | 569 | /** 570 | * Ends an existing flow. This call is used to contact the Credential Agent 571 | * and send the result of the flow. 572 | * 573 | * @param url the Credential Agent URL to use. 574 | * @param op the name of the operation the flow is for. 575 | * @param result the result of the flow. 576 | * @param [options] the options to use. 577 | */ 578 | Flow.end = function(url, op, result, options) { 579 | // communicate with parent 580 | var context = new BrowsingContext(url, {handle: window.parent}); 581 | var channel = new Channel(context); 582 | channel.send(op + '.result', result); 583 | }; 584 | 585 | ////////////// PRIVATE API CALLED BY CREDENTIAL AGENT HELPER API ////////////// 586 | navigator.credentials._Router = Router; 587 | 588 | /** 589 | * Creates a new Router for use by the Credential Agent. The Credential Agent 590 | * uses a Router to send or receive either the parameters or the result of a 591 | * remote API call. 592 | * 593 | * @param origin the origin to route communicate to/from. 594 | * @param [options] the options to use. 595 | * [handle] the handle for the browsing context to communicate with. 596 | */ 597 | function Router(origin, options) { 598 | if(!(this instanceof Router)) { 599 | return new Router(origin, options); 600 | } 601 | options = options || {}; 602 | if('handle' in options) { 603 | if(!options.handle) { 604 | throw new Error('Invalid browser context handle.'); 605 | } 606 | } 607 | this.channel = new Channel(new BrowsingContext( 608 | origin, {handle: options.handle || window.opener || window.top})); 609 | } 610 | 611 | /** 612 | * Receives a request from the other end of the Channel and sends a response. 613 | * 614 | * @param type the type of request to serve, eg: . 615 | * @param response the response data to serve. 616 | * 617 | * @return a Promise that resolves once the response has been served. 618 | */ 619 | Router.prototype.serve = function(type, response) { 620 | return this.channel.serve(type, response); 621 | }; 622 | 623 | /** 624 | * Called by the Credential Agent to request the parameters or the result from 625 | * a remote API operation. 626 | * 627 | * This call will notify its `opener browsing context` that it is ready to 628 | * receive either the parameters or the result from the remote operation. It 629 | * then returns a Promise that will resolve when the information has been 630 | * received. 631 | * 632 | * @param op the name of the specific API operation. 633 | * @param subject either `params` or `result`. 634 | * 635 | * @return a Promise that resolves to the received remote operation information. 636 | */ 637 | Router.prototype.request = function(op, subject) { 638 | var self = this; 639 | var expect = op + '.' + subject; 640 | return self.channel.request(expect).then(function(message) { 641 | var split = message.type.split('.'); 642 | return { 643 | origin: self.channel.origin, 644 | type: message.type, 645 | op: split[0], 646 | route: split[1], 647 | data: message.data 648 | }; 649 | }); 650 | }; 651 | 652 | /** 653 | * Called by the Credential Agent send the parameters or the result for a 654 | * remote API operation. 655 | * 656 | * For example, this call will send the result of the remote operation to the 657 | * `opener browsing context`. The `opener browsing context` is expected to 658 | * then resolve the Promise returned from the pending operation to the result. 659 | * 660 | * @param op the name of the API operation. 661 | * @param subject either `params` or `result`. 662 | * @param data the parameters or result to send. 663 | */ 664 | Router.prototype.send = function(op, subject, data) { 665 | this.channel.send(op + '.' + subject, data); 666 | }; 667 | 668 | /** 669 | * Called by the Credential Agent to emit an event during a remote API 670 | * operation. 671 | * 672 | * This call will send the event to the `opener browsing context`. The 673 | * `opener browsing context` is expected to then send a `continue` message 674 | * with an optional response message. 675 | * 676 | * @param name the name of the event. 677 | * @param event the event data. 678 | */ 679 | Router.prototype.emit = function(name, event) { 680 | this.channel.send('event', { 681 | name: name, 682 | event: event 683 | }); 684 | }; 685 | 686 | /** 687 | * Called by the Credential Agent receive the result of a remote API operation. 688 | * 689 | * This call will return a Promise that will resolve to a message wrapping 690 | * the result of the remote operation received from the other end of this 691 | * Router's channel. 692 | * 693 | * @param type the expected type(s) of message, eg: `get.params`/`get.result` 694 | * or [`get.params`, `store.params`]. 695 | * 696 | * @return a Promise that resolves to the received message. 697 | */ 698 | Router.prototype.receive = function(type) { 699 | return this.channel.receive(type); 700 | }; 701 | 702 | //////////////////////// PRIVATE BROWSING CONTEXT API ///////////////////////// 703 | 704 | /** 705 | * Creates a browsing context that can be communicated with using a 706 | * cross-origin channel. The channel will only operate if the browsing 707 | * context's origin matches that of the given `url`. 708 | * 709 | * @param [url] the URL for the browsing context; any communication channels 710 | * that use the browsing context will be bound to this URL's origin. 711 | * @param [options] the options to use: 712 | * [handle] a handle to an existing browsing context. 713 | * [iframe] `true` to open the browsing context in an iframe, `false` 714 | * to open a separate window (default: `true`). 715 | */ 716 | function BrowsingContext(url, options) { 717 | var self = this; 718 | if(!(self instanceof BrowsingContext)) { 719 | return new BrowsingContext(url, options); 720 | } 721 | 722 | options = options || {}; 723 | var useIframe = options.iframe !== false; 724 | // IE11 detected, use iframe because postMessage is broken on IE, it 725 | // can't communicate cross-domain with other windows, only with iframes 726 | if(navigator.userAgent.indexOf('MSIE') !== -1 || 727 | navigator.appVersion.indexOf('Trident/') > 0) { 728 | useIframe = true; 729 | } 730 | 731 | if('handle' in options) { 732 | if(!options.handle) { 733 | throw new Error('Invalid browser context handle.'); 734 | } 735 | self.handle = options.handle; 736 | // noop 737 | self.show = function() {}; 738 | } else if(useIframe) { 739 | showIframe(); 740 | } else { 741 | // any other browser, create new window 742 | var width = options.width || 800; 743 | var height = options.height || 600; 744 | self.handle = window.open(url, '_blank', 745 | 'left=' + ((screen.width - width) / 2) + 746 | ',top=' + ((screen.height - height) / 2) + 747 | ',width=' + width + 748 | ',height=' + height + 749 | ',resizeable,scrollbars'); 750 | self.close = function() { 751 | self.handle.close(); 752 | }; 753 | // noop 754 | self.show = function() {}; 755 | } 756 | 757 | self.origin = parseOrigin(url); 758 | 759 | function parseOrigin(url) { 760 | // `URL` API not supported on IE, use DOM to parse URL 761 | var parser = document.createElement('a'); 762 | parser.href = url; 763 | var origin = (parser.protocol || window.location.protocol) + '//'; 764 | if(parser.host) { 765 | // use hostname when using default ports 766 | // (IE adds always adds port to `parser.host`) 767 | if((parser.protocol === 'http:' && parser.port === '80') || 768 | (parser.protocol === 'https:' && parser.port === '443')) { 769 | origin += parser.hostname; 770 | } else { 771 | origin += parser.host; 772 | } 773 | } else { 774 | origin += window.location.host; 775 | } 776 | return origin; 777 | } 778 | 779 | function showIframe() { 780 | // create a top-level dialog overlay 781 | var dialog = document.createElement('dialog'); 782 | applyStyle(dialog, { 783 | position: 'fixed', 784 | top: 0, 785 | right: 0, 786 | bottom: 0, 787 | left: 0, 788 | width: 'auto', 789 | height: 'auto', 790 | display: 'block', 791 | margin: 0, 792 | padding: 0, 793 | border: 'none', 794 | background: 'transparent', 795 | color: 'black', 796 | 'box-sizing': 'border-box', 797 | overflow: 'hidden', 798 | visibility: 'hidden', 799 | 'z-index': 1000000 800 | }); 801 | dialog.className = 'credentials-polyfill-dialog'; 802 | 803 | // create iframe 804 | var iframe = document.createElement('iframe'); 805 | iframe.src = url; 806 | iframe.scrolling = 'no'; 807 | applyStyle(iframe, { 808 | position: 'absolute', 809 | top: 0, 810 | left: 0, 811 | border: 'none', 812 | background: 'transparent', 813 | width: '100vw', 814 | height: '100vh', 815 | overflow: 'hidden', 816 | visibility: 'hidden' 817 | }); 818 | 819 | // enable showing the iframe 820 | var shown = false; 821 | self.show = function() { 822 | if(!shown) { 823 | dialog.style.visibility = iframe.style.visibility = 'visible'; 824 | } 825 | shown = true; 826 | }; 827 | 828 | // assemble dialog 829 | dialog.appendChild(iframe); 830 | 831 | // handle cancel (user pressed escape) 832 | dialog.addEventListener('cancel', function(e) { 833 | e.preventDefault(); 834 | self.close(); 835 | if(self.onCancel) { 836 | self.onCancel(); 837 | } 838 | }); 839 | 840 | // attach to DOM 841 | document.body.appendChild(dialog); 842 | self.handle = iframe.contentWindow; 843 | self.close = function() { 844 | if(dialog) { 845 | if(dialog.close) { 846 | try { 847 | dialog.close(); 848 | } catch(e) { 849 | console.error(e); 850 | } 851 | } 852 | dialog.parentNode.removeChild(dialog); 853 | dialog = null; 854 | } 855 | }; 856 | 857 | // register dialog if necessary 858 | if(!dialog.showModal) { 859 | if(typeof require === 'function' && 860 | typeof dialogPolyfill === 'undefined') { 861 | try { 862 | dialogPolyfill = require('dialog-polyfill'); 863 | } catch(e) {} 864 | } 865 | if(typeof dialogPolyfill !== 'undefined') { 866 | dialogPolyfill.registerDialog(dialog); 867 | } 868 | } 869 | dialog.showModal(); 870 | } 871 | 872 | function applyStyle(element, style) { 873 | for(var name in style) { 874 | element.style[name] = style[name]; 875 | } 876 | } 877 | } 878 | 879 | ///////////////////////////// PRIVATE CHANNEL API ///////////////////////////// 880 | 881 | /** 882 | * Creates a new cross-origin communication Channel that is bound to another 883 | * browsing context. 884 | * 885 | * @param context the BrowsingContext to bind the Channel to. 886 | * @param emitter the event emitter to use for any channel events. 887 | */ 888 | function Channel(context, emitter) { 889 | if(!(this instanceof Channel)) { 890 | return new Channel(context); 891 | } 892 | this.context = context; 893 | this.end = context.handle; 894 | this.origin = context.origin; 895 | this._abort = null; 896 | this._emitter = emitter || new EventEmitter(); 897 | } 898 | 899 | /** 900 | * Receives a request from the other end of the Channel and sends a response. 901 | * 902 | * @param type the type of request to serve, eg: . 903 | * @param response the response data to serve. 904 | * 905 | * @return a Promise that resolves once the response has been served. 906 | */ 907 | Channel.prototype.serve = function(type, response) { 908 | var self = this; 909 | return self.receive(['request', type]).then(function() { 910 | self.send(type, response); 911 | }); 912 | }; 913 | 914 | /** 915 | * Requests a response from the other end of the Channel. 916 | * 917 | * @param type the types of response to expect, eg: . 918 | * 919 | * @return a Promise that resolves to the response. 920 | */ 921 | Channel.prototype.request = function(type) { 922 | var request = type; 923 | var response = type; 924 | if(Array.isArray(type)) { 925 | request = 'request'; 926 | } 927 | return this.send(request, null).receive(response); 928 | }; 929 | 930 | /** 931 | * Sends a message to the other end. 932 | * 933 | * @param type the type of message, eg: . 934 | * @param data the data for the message. 935 | */ 936 | Channel.prototype.send = function(type, data) { 937 | var message = {type: type, data: data}; 938 | this.end.postMessage(message, this.origin); 939 | return this; 940 | }; 941 | 942 | /** 943 | * Receives a message from the other end. 944 | * 945 | * @param type the expected type(s) of message, eg: `get.params`/`get.result` 946 | * or [`get.params`, `store.params`]. 947 | * 948 | * @return a Promise that resolves to the received message. 949 | */ 950 | Channel.prototype.receive = function(type) { 951 | var self = this; 952 | if(!Array.isArray(type)) { 953 | type = [type]; 954 | } 955 | return new Promise(function(resolve, reject) { 956 | // TODO: add timeout 957 | window.addEventListener('message', listener); 958 | function listener(e) { 959 | // ignore messages that aren't from the other end 960 | // TODO: is this check sufficient to prevent bugs/abuse? 961 | if(e.source === self.end && e.origin === self.origin) { 962 | window.removeEventListener('message', listener); 963 | self._abort = null; 964 | self.context.show(); 965 | // TODO: this validation could use some clean up, could be more 966 | // robust, and potentially be broken into separate functions 967 | 968 | // validate message 969 | if(typeof e.data === 'object' && 'data' in e.data) { 970 | if(type.indexOf(e.data.type) !== -1) { 971 | return resolve(e.data); 972 | } 973 | if(e.data.type.split('.')[1] === 'error' && 974 | typeof e.data.data === 'object' && 975 | typeof e.data.data.message === 'string') { 976 | return reject(new Error(e.data.data.message)); 977 | } 978 | if(e.data.type === 'event' && 979 | typeof e.data.data === 'object' && 980 | typeof e.data.data.name === 'string' && 981 | typeof e.data.data.event === 'object') { 982 | if(e.data.data.name !== 'registerIdentityCredential') { 983 | return reject( 984 | new Error('Credential protocol error; unknown event.')); 985 | } 986 | // emit event, add message listener again, and trigger continue 987 | return self.emit( 988 | new RegisterIdentityCredentialEvent(e.data.data.event.publicKey)) 989 | .catch(function(err) { 990 | // listen for channel message and send `error` 991 | window.addEventListener('message', listener); 992 | self.send('error', {name: err.name, message: err.message}); 993 | return new Error('Error in event handler.'); 994 | }).then(function(response) { 995 | if(!(response instanceof Error)) { 996 | // listen for channel message and send `continue` 997 | window.addEventListener('message', listener); 998 | self.send('continue', response || null); 999 | } 1000 | }).catch(function(err) { 1001 | // failure to `send`, reject to handle error 1002 | window.removeEventListener('message', listener); 1003 | reject(err); 1004 | }); 1005 | } 1006 | } 1007 | reject(new Error('Credential protocol error.')); 1008 | } 1009 | } 1010 | // make receive abortable 1011 | self._abort = function() { 1012 | window.removeEventListener('message', listener); 1013 | self._abort = null; 1014 | reject(new ChannelAbortError('Credential protocol aborted.')); 1015 | }; 1016 | }); 1017 | }; 1018 | 1019 | /** 1020 | * Aborts the current receive operation, if any. 1021 | */ 1022 | Channel.prototype.abort = function() { 1023 | if(this._abort) { 1024 | this._abort(); 1025 | } 1026 | return this; 1027 | }; 1028 | 1029 | /** 1030 | * Emits an event from the other end. 1031 | * 1032 | * @param event the event to emit. 1033 | */ 1034 | Channel.prototype.emit = function(event) { 1035 | return this._emitter.emit(event); 1036 | }; 1037 | 1038 | function ChannelAbortError(message) { 1039 | this.name = 'ChannelAbortError'; 1040 | this.message = message; 1041 | this.stack = (new Error()).stack; 1042 | } 1043 | ChannelAbortError.prototype = Object.create(Error.prototype); 1044 | ChannelAbortError.prototype.constructor = ChannelAbortError; 1045 | 1046 | ///////////////////////// PRIVATE EVENT EMITTER API /////////////////////////// 1047 | 1048 | function EventEmitter() { 1049 | this.listener = null; 1050 | } 1051 | 1052 | /** 1053 | * Adds an event listener. 1054 | * 1055 | * @param event the name of the event to listen for. 1056 | * @param listener the function to call when emitting the event. 1057 | */ 1058 | EventEmitter.prototype.addEventListener = function(event, listener) { 1059 | if(typeof event !== 'string') { 1060 | throw new TypeError('`event` must be a function.'); 1061 | } 1062 | if(typeof listener !== 'function') { 1063 | throw new TypeError('`listener` must be a function.'); 1064 | } 1065 | if(event !== 'registerIdentityCredential') { 1066 | // event does not exist, ignore 1067 | return; 1068 | } 1069 | if(this.listener !== null) { 1070 | throw new Error('Event listener already registered; only one permitted.'); 1071 | } 1072 | this.listener = listener; 1073 | }; 1074 | 1075 | /** 1076 | * Removes an event listener. 1077 | * 1078 | * @param event the name of the event to remove the listener for. 1079 | * @param listener the function to remove. 1080 | */ 1081 | EventEmitter.prototype.removeEventListener = function(event, listener) { 1082 | if(typeof event !== 'string') { 1083 | throw new TypeError('`event` must be a function.'); 1084 | } 1085 | if(typeof listener !== 'function') { 1086 | throw new TypeError('`listener` must be a function.'); 1087 | } 1088 | if(event !== 'registerIdentityCredential') { 1089 | // event does not exist, ignore 1090 | return; 1091 | } 1092 | if(listener === this.listener) { 1093 | this.listener = null; 1094 | } 1095 | }; 1096 | 1097 | /** 1098 | * Emits an event from the other end. 1099 | * 1100 | * @param event the event to emit. 1101 | */ 1102 | EventEmitter.prototype.emit = function(event) { 1103 | if(this.listener) { 1104 | this.listener(event); 1105 | } 1106 | return Promise.resolve(event._waitUntil); 1107 | }; 1108 | 1109 | /** 1110 | * Creates a new RegisterIdentityCredentialEvent. 1111 | * 1112 | * @param publicKey the publicKey associated with the device that the 1113 | * IdentityCredential is to be registered on. 1114 | * 1115 | * @return the event to pass to emit. 1116 | */ 1117 | function RegisterIdentityCredentialEvent(publicKey) { 1118 | var self = this; 1119 | self.publicKey = publicKey; 1120 | // causes event handling to pause until the Promise resolves with an 1121 | // appropriate response based on the type of event 1122 | self.respondWith = function(promise) { 1123 | if(self._waitUntil) { 1124 | throw new Error('`respondWith` already called.'); 1125 | } 1126 | self._waitUntil = Promise.resolve(promise); 1127 | }; 1128 | } 1129 | 1130 | /////////////////////////// PRIVATE HELPER FUNCTIONS ////////////////////////// 1131 | 1132 | /** 1133 | * Update a query parameter in a URL. 1134 | * 1135 | * From: http://stackoverflow.com/questions/5999118/add-or-update-query-string-parameter#answer-6021027 1136 | * 1137 | * @param uri the base URI to use. 1138 | * @param key the query parameter to add or modify. 1139 | * @param value the value of the query parameter. 1140 | * 1141 | * @return the modified URI. 1142 | */ 1143 | function _updateQueryStringParameter(uri, key, value) { 1144 | key = encodeURIComponent(key); 1145 | value = encodeURIComponent(value); 1146 | var re = new RegExp('([?&])' + key + '=.*?(&|$)', 'i'); 1147 | var separator = uri.indexOf('?') !== -1 ? '&' : '?'; 1148 | if(uri.match(re)) { 1149 | return uri.replace(re, '$1' + key + '=' + value + '$2'); 1150 | } 1151 | return uri + separator + key + '=' + value; 1152 | } 1153 | 1154 | })(); 1155 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "credentials-polyfill", 3 | "version": "1.1.7-0", 4 | "description": "A browser polyfill that provides the Identity Credentials Browser API.", 5 | "main": "lib/credentials-polyfill.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/Spec-Ops/credentials-polyfill.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/Spec-Ops/credentials-polyfill/issues" 12 | }, 13 | "homepage": "https://github.com/Spec-Ops/credentials-polyfill#readme" 14 | } 15 | -------------------------------------------------------------------------------- /tests/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "credentials-polyfill-test", 3 | "version": "0.0.1-dev", 4 | "description": "Test for a polyfill for the Identity Credentials API", 5 | "authors": [ 6 | "The Open Payments Foundation" 7 | ], 8 | "license": "BSD", 9 | "dependencies": { 10 | "es6-promise": "^2.0.0", 11 | "bedrock-angular": "^1.3.0" 12 | }, 13 | "resolutions": { 14 | "angular": "1.3.20" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/components/agent-controller.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Credential Agent Controller. 3 | * 4 | * Copyright (c) 2015-2016 Digital Bazaar, Inc. All rights reserved. 5 | * 6 | * @author Dave Longley 7 | */ 8 | define(['angular', 'credentials-polyfill'], function(angular) { 9 | 10 | 'use strict'; 11 | 12 | var Router = navigator.credentials._Router; 13 | 14 | /* @ngInject */ 15 | function factory($location, $scope) { 16 | var self = this; 17 | 18 | self.registerDid = function() { 19 | console.log('registering DID...'); 20 | var router = new Router(query.origin); 21 | router.send('registerDid', 'result', { 22 | '@context': 'https://w3id.org/identity/v1', 23 | id: 'did:example-1234', 24 | publicKey: { 25 | id: 'did:example-1234/keys/1', 26 | owner: 'did:example-1234', 27 | publicKeyPem: '-----BEGIN PUBLIC KEY-----...' 28 | } 29 | }); 30 | }; 31 | 32 | var query = $location.search(); 33 | handleOperation(query.op, query.route, query.origin); 34 | 35 | function handleOperation(op, route, origin) { 36 | // < 0.8.x 37 | if(window.frameElement) { 38 | // handle legacy iframe proxy 39 | var agent = {origin: window.location.origin, handle: window.top}; 40 | var repo = {origin: origin, handle: window.parent}; 41 | var order; 42 | if(route === 'params') { 43 | // proxy from agent -> repo 44 | console.log('proxy params from credential agent to repo...'); 45 | order = [agent, repo]; 46 | } else { 47 | // proxy from repo -> agent 48 | console.log('proxy result from repo to credential agent...'); 49 | order = [repo, agent]; 50 | } 51 | var router = new Router(order[0].origin, {handle: order[0].handle}); 52 | return router.request(route).then(function(message) { 53 | router = new Router(order[1].origin, {handle: order[1].handle}); 54 | var split = message.type.split('.'); 55 | router.send(split[0], split[1], message.data); 56 | }); 57 | } 58 | 59 | // >= 0.8.x 60 | 61 | // request params from RP 62 | var rpRouter = new Router(origin); 63 | return rpRouter.request(query.op, 'params').then(function(message) { 64 | if(op === 'registerDid') { 65 | self.register = true; 66 | $scope.$apply(); 67 | return; 68 | } 69 | 70 | // display repo in iframe 71 | self.repo = window.location.origin + '/' + query.repo; 72 | self.showRepo = true; 73 | $scope.$apply(); 74 | 75 | // get iframe handle 76 | var iframe = angular.element('iframe[name="repo"]')[0]; 77 | var repoHandle = iframe.contentWindow; 78 | 79 | // wrap params to allow additional agent info to be sent 80 | var params = {}; 81 | if(op === 'get') { 82 | params.options = message.data; 83 | } else { 84 | params.options = {}; 85 | params.options.store = message.data; 86 | } 87 | 88 | /* use once 0.7.x is no longer supported 89 | // serve params to repo 90 | var repoRouter = new Router(window.location.origin, {handle: repoHandle}); 91 | repoRouter.serve(op + '.params', params).then(function() { 92 | // receive result from repo 93 | repoRouter.receive(op + '.result'); 94 | }).then(function(result) { 95 | // send result to RP 96 | console.log('credential agent sending to RP...'); 97 | rpRouter.send(op, 'result', result); 98 | });*/ 99 | 100 | // the code path includes legacy support, remove once no longer supported 101 | serveParams().then(function() { 102 | return receiveResult(); 103 | }).then(function(result) { 104 | console.log('credential agent sending to RP...'); 105 | rpRouter.send(op, 'result', result); 106 | }); 107 | 108 | function serveParams() { 109 | // will either receive request from the repo (>= 0.8.x) or from 110 | // the iframe proxy (< 0.8.x) 111 | return new Promise(function(resolve, reject) { 112 | // TODO: add timeout 113 | window.addEventListener('message', listener); 114 | function listener(e) { 115 | if(typeof e.data === 'object' && 'data' in e.data && 116 | e.data.type === 'request') { 117 | if(e.source === repoHandle && 118 | e.origin === window.location.origin) { 119 | return resolve(e); 120 | } 121 | // assume request is from iframe proxy 122 | if(e.origin === window.location.origin) { 123 | return resolve(e); 124 | } 125 | } 126 | reject(new Error('Credential protocol error.')); 127 | } 128 | }).then(function(e) { 129 | e.source.postMessage( 130 | {type: op + '.params', data: params}, 131 | e.origin); 132 | }); 133 | } 134 | 135 | function receiveResult() { 136 | // will either receive result from the repo (>= 0.8.x) or from 137 | // the iframe proxy (< 0.8.x) 138 | return new Promise(function(resolve, reject) { 139 | // TODO: add timeout 140 | window.addEventListener('message', listener); 141 | function listener(e) { 142 | if(typeof e.data === 'object' && 'data' in e.data && 143 | e.data.type === op + '.result') { 144 | if(e.source === repoHandle && 145 | e.origin === window.location.origin) { 146 | return resolve(e.data); 147 | } 148 | // assume result is from iframe proxy 149 | if(e.origin === window.location.origin) { 150 | return resolve(e.data); 151 | } 152 | } 153 | reject(new Error('Credential protocol error.')); 154 | } 155 | }); 156 | } 157 | }); 158 | } 159 | } 160 | 161 | return {AgentController: factory}; 162 | 163 | }); 164 | -------------------------------------------------------------------------------- /tests/components/agent.html: -------------------------------------------------------------------------------- 1 |
2 | 4 |
5 | 8 |
9 |
10 | -------------------------------------------------------------------------------- /tests/components/main-controller.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Main Controller. 3 | * 4 | * Copyright (c) 2015 The Open Payments Foundation. All rights reserved. 5 | * 6 | * @author Dave Longley 7 | */ 8 | define(['credentials-polyfill'], function() { 9 | 10 | 'use strict'; 11 | 12 | /* @ngInject */ 13 | function factory() { 14 | var self = this; 15 | 16 | self.registerDid = function(version) { 17 | console.log('credentials.registerDid'); 18 | var repo = '?repo=repo' + (version === '0.7.x' ? '-legacy' : ''); 19 | IdentityCredential.register({ 20 | idp: 'did:test-1234', 21 | agentUrl: '/agent' + repo 22 | }).then(function(result) { 23 | console.log('credentials.registerDid result', result); 24 | }); 25 | }; 26 | 27 | self.get = function(version) { 28 | console.log('credentials.get'); 29 | var repo = '?repo=repo' + (version === '0.7.x' ? '-legacy' : ''); 30 | navigator.credentials.get({ 31 | identity: { 32 | query: {foo: ''}, 33 | agentUrl: '/agent' + repo 34 | } 35 | }).then(function(result) { 36 | console.log('credentials.get result', result); 37 | }); 38 | }; 39 | 40 | self.store = function(version) { 41 | console.log('credentials.store'); 42 | var repo = '?repo=repo' + (version === '0.7.x' ? '-legacy' : ''); 43 | navigator.credentials.store( 44 | new IdentityCredential({id: 'did:test-1234', foo: 'bar'}), 45 | {agentUrl: '/agent' + repo}).then(function(result) { 46 | console.log('credentials.store result', result); 47 | }); 48 | }; 49 | } 50 | 51 | return {MainController: factory}; 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /tests/components/main.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | Test version >= 0.8.x 5 |

6 | 7 | 8 | 9 | 10 |
11 | 12 |

13 | Test version < 0.8.x 14 |

15 | 16 | 17 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /tests/components/main.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Example component module. 3 | * 4 | * Copyright (c) 2015-2016 Digital Bazaar, Inc. All rights reserved. 5 | * 6 | * @author Omar Malik 7 | * @author Dave Longley 8 | */ 9 | define([ 10 | 'angular', 11 | './main-controller', 12 | './agent-controller', 13 | './repo-controller', 14 | './repo-legacy-controller' 15 | ], function( 16 | angular, mainController, agentController, repoController, repoLegacyController) { 17 | 18 | 'use strict'; 19 | 20 | var module = angular.module('credentials-polyfill.test', ['ngRoute']); 21 | 22 | module.controller(mainController); 23 | module.controller(agentController); 24 | module.controller(repoController); 25 | module.controller(repoLegacyController); 26 | 27 | /* @ngInject */ 28 | module.config(function($routeProvider) { 29 | $routeProvider 30 | .when('/', { 31 | templateUrl: requirejs.toUrl('credentials-polyfill-test/main.html') 32 | }); 33 | $routeProvider 34 | .when('/agent', { 35 | templateUrl: requirejs.toUrl('credentials-polyfill-test/agent.html') 36 | }); 37 | $routeProvider 38 | .when('/repo', { 39 | templateUrl: requirejs.toUrl('credentials-polyfill-test/repo.html') 40 | }); 41 | $routeProvider 42 | .when('/repo-legacy', { 43 | templateUrl: requirejs.toUrl('credentials-polyfill-test/repo-legacy.html') 44 | }); 45 | }); 46 | 47 | return module.name; 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /tests/components/repo-controller.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Repo Controller. 3 | * 4 | * Copyright (c) 2015-2016 Digital Bazaar, Inc. All rights reserved. 5 | * 6 | * @author Dave Longley 7 | */ 8 | define(['credentials-polyfill'], function() { 9 | 10 | 'use strict'; 11 | 12 | /* @ngInject */ 13 | function factory($scope, $location) { 14 | var self = this; 15 | 16 | var operation; 17 | 18 | console.log('Repo receiving params...'); 19 | 20 | navigator.credentials.getPendingOperation({ 21 | agentUrl: '/agent?repo=repo' 22 | }).then(function(op) { 23 | operation = op; 24 | self.op = op.name; 25 | self.params = op.options; 26 | $scope.$apply(); 27 | }); 28 | 29 | self.complete = function() { 30 | operation.complete({id: 'did:test-1234', foo: 'bar'}, { 31 | agentUrl: '/agent?repo=repo' 32 | }); 33 | }; 34 | } 35 | 36 | return {RepoController: factory}; 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /tests/components/repo-legacy-controller.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Repo Legacy Controller. 3 | * 4 | * Copyright (c) 2015-2016 Digital Bazaar, Inc. All rights reserved. 5 | * 6 | * @author Dave Longley 7 | */ 8 | define(['credentials-polyfill'], function() { 9 | 10 | 'use strict'; 11 | 12 | /* @ngInject */ 13 | function factory($scope, $location) { 14 | var self = this; 15 | 16 | var operation; 17 | 18 | console.log('Repo receiving params...'); 19 | 20 | navigator.credentials.getPendingOperation({ 21 | agentUrl: '/agent?repo=repo-legacy', 22 | version: '0.7.x' 23 | }).then(function(op) { 24 | operation = op; 25 | self.op = op.name; 26 | self.params = op.options; 27 | $scope.$apply(); 28 | }); 29 | 30 | self.complete = function() { 31 | operation.complete({id: 'did:test-1234', foo: 'bar'}, { 32 | agentUrl: '/agent?repo=repo-legacy', 33 | version: '0.7.x' 34 | }); 35 | }; 36 | } 37 | 38 | return {RepoLegacyController: factory}; 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /tests/components/repo-legacy.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
Operation: {{ctrl.op}}
4 |
Params: {{ctrl.params|json}}
5 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /tests/components/repo.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
Operation: {{ctrl.op}}
4 |
Params: {{ctrl.params|json}}
5 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /tests/config.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Credentials polyfill test configuration. 3 | * 4 | * Copyright (c) 2015 The Open Payments Foundation. All rights reserved. 5 | * 6 | * @author Omar Malik 7 | * @author Dave Longley 8 | */ 9 | var config = require('bedrock').config; 10 | var fs = require('fs'); 11 | var path = require('path'); 12 | 13 | config.server.port = 18444; 14 | config.server.httpPort = 18081; 15 | if(config.server.port !== 443) { 16 | config.server.host += ':' + config.server.port; 17 | } 18 | config.server.baseUri = 'https://' + config.server.host; 19 | 20 | // add pseudo bower packages 21 | var rootPath = path.join(__dirname, '..'); 22 | config.requirejs.bower.packages.push({ 23 | path: rootPath, 24 | manifest: JSON.parse(fs.readFileSync( 25 | path.join(rootPath, 'bower.json'), {encoding: 'utf8'})) 26 | }); 27 | config.requirejs.bower.packages.push({ 28 | path: path.join(__dirname, 'components'), 29 | manifest: { 30 | name: 'credentials-polyfill-test', 31 | moduleType: 'amd', 32 | main: './main.js', 33 | dependencies: { 34 | angular: '~1.3.0' 35 | } 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Credentials polyfill test application. 3 | * 4 | * Copyright (c) 2015 The Open Payments Foundation. All rights reserved. 5 | * 6 | * @author Omar Malik 7 | * @author Dave Longley 8 | */ 9 | var bedrock = require('bedrock'); 10 | 11 | // modules 12 | require('bedrock-express'); 13 | 14 | // frontend configuration 15 | require('bedrock-requirejs'); 16 | require('bedrock-server'); 17 | require('bedrock-views'); 18 | require('./config.js'); 19 | 20 | bedrock.start(); 21 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "credentials-polyfill-test", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "start": "node index.js", 6 | "postinstall": "bower install && node index.js compile-less" 7 | }, 8 | "dependencies": { 9 | "bedrock": "^1.0.4", 10 | "bedrock-express": "^1.4.0", 11 | "bedrock-server": "^1.0.2", 12 | "bedrock-views": "^1.2.0", 13 | "bedrock-requirejs": "^1.0.0", 14 | "bower": "~1.5.2" 15 | }, 16 | "engines": { 17 | "node": ">=0.10.0" 18 | }, 19 | "directories": {}, 20 | "main": "index.js" 21 | } 22 | --------------------------------------------------------------------------------