Skip to content

Commit

Permalink
create a js library for fasten-sources.
Browse files Browse the repository at this point in the history
  • Loading branch information
AnalogJ committed May 30, 2024
1 parent dcda6d6 commit 803ad22
Show file tree
Hide file tree
Showing 14 changed files with 321 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@
# vendor/

.DS_Store

js/dist
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ deps:
serve-backend: deps
cd testutils && go run oauth_cli.go

build-js:
tygo generate

publish-js: build-js
tsc
npm publish

test:
go test ./...
1 change: 1 addition & 0 deletions definitions/models/lighthouse_endpoint_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
//
// Similar in functionality to https://build.fhir.org/ig/HL7/smart-app-launch/conformance.html#example-request
// /apis/fhir/.well-known/smart-configuration
// TODO: refactor & rename LighthouseSourceDefinition to LighthouseEndpointDefinition
type LighthouseSourceDefinition struct {
BrandId string `json:"brand_id,omitempty" yaml:"-" validate:"omitempty,uuid"`
PortalId string `json:"portal_id,omitempty" yaml:"-" validate:"omitempty,uuid"`
Expand Down
1 change: 1 addition & 0 deletions js/.yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npmRegistryServer: "http:https://registry.npmjs.org"
20 changes: 20 additions & 0 deletions js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@fastenhealth/fasten-sources-js",
"version": "0.6.0",
"description": "",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"/dist"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Jason Kulatunga <[email protected]>",
"dependencies": {
"@panva/oauth4webapi": "1.2.0"
},
"publishConfig": {
"access": "public"
}
}
50 changes: 50 additions & 0 deletions js/src/connect/authorization-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as Oauth from '@panva/oauth4webapi';
import {uuidV4} from '../utils/uuid';
import {SourceState} from '../models/source-state';
import {LighthouseEndpointDefinition} from '../models/lighthouse';

export async function generateSourceAuthorizeUrl(lighthouseSource: LighthouseEndpointDefinition, reconnectSourceId?: string): Promise<{url: URL, sourceState: SourceState}> {
const state = uuidV4()
let sourceStateInfo = new SourceState()
sourceStateInfo.state = state
sourceStateInfo.endpoint_id = lighthouseSource.id
sourceStateInfo.portal_id = lighthouseSource.portal_id
sourceStateInfo.brand_id = lighthouseSource.brand_id
if(reconnectSourceId){
//if the source already exists, and we want to re-connect it (because of an expiration), we need to pass the existing source id
sourceStateInfo.reconnect_source_id = reconnectSourceId
}

// generate the authorization url
const authorizationUrl = new URL(lighthouseSource.authorization_endpoint);
authorizationUrl.searchParams.set('redirect_uri', lighthouseSource.redirect_uri);
authorizationUrl.searchParams.set('response_type', lighthouseSource.response_types_supported[0]);
authorizationUrl.searchParams.set('response_mode', lighthouseSource.response_modes_supported[0]);
authorizationUrl.searchParams.set('state', state);
authorizationUrl.searchParams.set('client_id', lighthouseSource.client_id);
if(lighthouseSource.scopes_supported && lighthouseSource.scopes_supported.length){
authorizationUrl.searchParams.set('scope', lighthouseSource.scopes_supported.join(' '));
} else {
authorizationUrl.searchParams.set('scope', '');
}
if (lighthouseSource.aud) {
authorizationUrl.searchParams.set('aud', lighthouseSource.aud);
}

//this is for providers that support CORS and PKCE (public client auth)
if(!lighthouseSource.confidential || (lighthouseSource.code_challenge_methods_supported || []).length > 0){
// https://github.com/panva/oauth4webapi/blob/8eba19eac408bdec5c1fe8abac2710c50bfadcc3/examples/public.ts
const codeVerifier = Oauth.generateRandomCodeVerifier();
const codeChallenge = await Oauth.calculatePKCECodeChallenge(codeVerifier);
const codeChallengeMethod = lighthouseSource.code_challenge_methods_supported?.[0] || 'S256'

sourceStateInfo.code_verifier = codeVerifier
sourceStateInfo.code_challenge = codeChallenge
sourceStateInfo.code_challenge_method = codeChallengeMethod

authorizationUrl.searchParams.set('code_challenge', codeChallenge);
authorizationUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
}

return {url: authorizationUrl, sourceState: sourceStateInfo}
}
5 changes: 5 additions & 0 deletions js/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export {generateSourceAuthorizeUrl} from './connect/authorization-url'
export {LighthouseSourceMetadata} from './models/lighthouse'
export {PatientAccessEndpoint, PatientAccessBrand, PatientAccessPortal} from './models/patient-access-brands'
export {SourceState} from './models/source-state'
export {uuidV4} from './utils/uuid'
27 changes: 27 additions & 0 deletions js/src/models/lighthouse/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {PatientAccessEndpoint} from '../patient-access-brands';

export interface LighthouseEndpointDefinition extends PatientAccessEndpoint {
brand_id: string
portal_id: string
// endpoint_id = embedded PatientAccessEndpoint.id

scopes_supported: string[]
grant_types_supported: string[]
response_types_supported: string[]
response_modes_supported: string[]
code_challenge_methods_supported: string[]

confidential: boolean
dynamic_client_registration_mode: string
cors_relay_required: boolean

issuer: string
aud: string


platform_type: string
client_id: string
redirect_uri: string


}
155 changes: 155 additions & 0 deletions js/src/models/patient-access-brands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Code generated by tygo. DO NOT EDIT.

//////////
// source: patient_access_brand.go

/**
* TODO: generate via reflection
*/
export interface PatientAccessBrand {
/**
* Fasten UUID for the brand - id should be a unique identifier for the brand. It is globally unique and should be a UUID
*/
id: string;
/**
* List of identifiers for the organization, e.g., external system, etc NPI, etc
* Identifiers SHOULD include a platform identifier, so we know where this entry came from, but not required
*/
identifiers?: any /* datatypes.Identifier */[];
/**
* RFC3339 Date and time the organization was last updated - Timestamp should be the last updated datetime for the data from this source, not the current date
*/
last_updated: string;
/**
* Primary name for the organization to display on a card, e.g., “General Hospital”
* Note this is not used within the app, only the Portal name should be used.
*/
name: string;
/**
* URL for the organization’s primary website. note this is distinct from a patient portal, described under “Patient Access Details” below
*/
brand_website?: string;
/**
* URL for the organization’s logo, which will be displayed on a card, Note this is a fallback logo, the primary logo will always be the Portal logo
*/
logo?: string;
/**
* List of alternate names for the organization, e.g., “GH”, “General”, “GH Hospital”
*/
aliases?: string[];
/**
* List of locations for the organization
* These should be the locations where the organization has a physical presence, e.g., a hospital or clinic"
*/
locations?: any /* datatypes.Address */[];
/**
* Patient Access Details
* These must be references to Patient Access Portal resource Ids
*/
portal_ids: string[];
/**
* list of brand ids that were merged together to creat this brand
*/
brand_ids?: string[];
}

//////////
// source: patient_access_endpoint.go

/**
* TODO: generate via reflection
*/
export interface PatientAccessEndpoint {
/**
* Fasten UUID for the endpoint
*/
id: string;
/**
* List of identifiers for the endpoint, e.g., “GH1234”
*/
identifiers?: any /* datatypes.Identifier */[];
/**
* RFC3339 Date and time the endpoint was last updated
*/
last_updated: string;
/**
* Status of the endpoint, e.g., “active” - http:https://terminology.hl7.org/CodeSystem/endpoint-status
*/
status: string;
/**
* Connection type for the endpoint, e.g., “hl7-fhir-rest” - http:https://terminology.hl7.org/CodeSystem/endpoint-connection-type
*/
connection_type: string;
/**
* Platform type for the endpoint, e.g., “epic”, "cerner"
*/
platform_type: string;
/**
* URL for the endpoint, must have trailing slash
*/
url: string;
/**
* oauth endpoints
*/
authorization_endpoint?: string;
token_endpoint?: string;
introspection_endpoint?: string;
userinfo_endpoint?: string;
/**
* optional - required when Dynamic Client Registration mode is set
*/
registration_endpoint?: string;
/**
* Fasten custom configuration
*/
fhir_version?: string;
smart_configuration_url?: string;
fhir_capabilities_url?: string;
/**
* Software info
*/
software_name?: string;
software_version?: string;
software_release_date?: string;
}

//////////
// source: patient_access_portal.go

/**
* TODO: generate via reflection
*/
export interface PatientAccessPortal {
/**
* Fasten UUID for the portal
*/
id: string;
/**
* List of identifiers for the organization, e.g., “GH1234”
*/
identifiers?: any /* datatypes.Identifier */[];
/**
* RFC3339 date & time of the last update to the patient portal’s information
*/
last_updated: string;
/**
* Name of the patient portal, e.g., “MyChart”
*/
name: string;
/**
* URL for the patient portal’s logo, which will be displayed on a card
*/
logo?: string;
/**
* URL for the patient portal, where patients can manage accounts with this provider.
*/
portal_website?: string;
/**
* Description of the patient portal, e.g., “Manage your health information with General Hospital”
*/
description?: string;
/**
* List of endpoint IDs for the patient portal. This is used to associate the patient portal with the endpoints that are used to access it.
*/
endpoint_ids: string[];
}
15 changes: 15 additions & 0 deletions js/src/models/source-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export class SourceState {
state: string

endpoint_id: string
portal_id: string
brand_id: string

reconnect_source_id?: string //used to reconnect a source

code_verifier?: string
code_challenge_method?: string
code_challenge?: string
hidden: boolean
redirect_uri?: string
}
7 changes: 7 additions & 0 deletions js/src/utils/uuid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

export function uuidV4(){
// @ts-ignore
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
12 changes: 12 additions & 0 deletions js/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"moduleResolution":"node",
"module": "es2020",
"target": "es2020",
"declaration": true,
"outDir": "./dist"
},
"include": [
"src/**/*"
]
}
8 changes: 8 additions & 0 deletions js/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


"@panva/[email protected]":
version "1.2.0"
resolved "https://registry.npmjs.org/@panva/oauth4webapi/-/oauth4webapi-1.2.0.tgz#f0bce0da0953dd8f8c091af77d559fddf44f1b0f"
integrity sha512-OmwAE3fzlSJsA0zzCWA/ob7Nwb7nwzku8vbAjgmnkkVYbpyokBsaVrrzog9cM0RMytXexpNNcbQbMF/UNa71mg==
12 changes: 12 additions & 0 deletions tygo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
packages:
- path: "github.com/fastenhealth/fasten-sources/pkg/models/catalog"
output_path: "js/src/models/patient-access-brands/"
exclude_files:
- "catalog_query_options.go"
#TODO: BROKEN, see: https://github.com/gzuidhof/tygo/issues/54
# - path: "github.com/fastenhealth/fasten-sources/definitions/models"
# output_path: "js/models/lighthouse/"
# flavor: "json"



0 comments on commit 803ad22

Please sign in to comment.