package sherpats const libTS = `export const defaultBaseURL = BASEURL // NOTE: code below is shared between github.com/mjl-/sherpaweb and github.com/mjl-/sherpats. // KEEP IN SYNC. export const supportedSherpaVersion = 1 export interface Section { Name: string Docs: string Functions: Function[] Sections: Section[] Structs: Struct[] Ints: Ints[] Strings: Strings[] Version: string // only for top-level section SherpaVersion: number // only for top-level section SherpadocVersion: number // only for top-level section } export interface Function { Name: string Docs: string Params: Arg[] Returns: Arg[] } export interface Arg { Name: string Typewords: string[] } export interface Struct { Name: string Docs: string Fields: Field[] } export interface Field { Name: string Docs: string Typewords: string[] } export interface Ints { Name: string Docs: string Values: { Name: string Value: number Docs: string }[] | null } export interface Strings { Name: string Docs: string Values: { Name: string Value: string Docs: string }[] | null } export type NamedType = Struct | Strings | Ints export type TypenameMap = { [k: string]: NamedType } // verifyArg typechecks "v" against "typewords", returning a new (possibly modified) value for JSON-encoding. // toJS indicate if the data is coming into JS. If so, timestamps are turned into JS Dates. Otherwise, JS Dates are turned into strings. // allowUnknownKeys configures whether unknown keys in structs are allowed. // types are the named types of the API. export const verifyArg = (path: string, v: any, typewords: string[], toJS: boolean, allowUnknownKeys: boolean, types: TypenameMap, opts: ClientOptions): any => { return new verifier(types, toJS, allowUnknownKeys, opts).verify(path, v, typewords) } export const parse = (name: string, v: any): any => verifyArg(name, v, [name], true, false, types, defaultOptions) class verifier { constructor(private types: TypenameMap, private toJS: boolean, private allowUnknownKeys: boolean, private opts: ClientOptions) { } verify(path: string, v: any, typewords: string[]): any { typewords = typewords.slice(0) const ww = typewords.shift() const error = (msg: string) => { if (path != '') { msg = path + ': ' + msg } throw new Error(msg) } if (typeof ww !== 'string') { error('bad typewords') return // should not be necessary, typescript doesn't see error always throws an exception? } const w: string = ww const ensure = (ok: boolean, expect: string): any => { if (!ok) { error('got ' + JSON.stringify(v) + ', expected ' + expect) } return v } switch (w) { case 'nullable': if (v === null || v === undefined && this.opts.nullableOptional) { return v } return this.verify(path, v, typewords) case '[]': if (v === null && this.opts.slicesNullable || v === undefined && this.opts.slicesNullable && this.opts.nullableOptional) { return v } ensure(Array.isArray(v), "array") return v.map((e: any, i: number) => this.verify(path + '[' + i + ']', e, typewords)) case '{}': if (v === null && this.opts.mapsNullable || v === undefined && this.opts.mapsNullable && this.opts.nullableOptional) { return v } ensure(v !== null || typeof v === 'object', "object") const r: any = {} for (const k in v) { r[k] = this.verify(path + '.' + k, v[k], typewords) } return r } ensure(typewords.length == 0, "empty typewords") const t = typeof v switch (w) { case 'any': return v case 'bool': ensure(t === 'boolean', 'bool') return v case 'int8': case 'uint8': case 'int16': case 'uint16': case 'int32': case 'uint32': case 'int64': case 'uint64': ensure(t === 'number' && Number.isInteger(v), 'integer') return v case 'float32': case 'float64': ensure(t === 'number', 'float') return v case 'int64s': case 'uint64s': ensure(t === 'number' && Number.isInteger(v) || t === 'string', 'integer fitting in float without precision loss, or string') return '' + v case 'string': ensure(t === 'string', 'string') return v case 'timestamp': if (this.toJS) { ensure(t === 'string', 'string, with timestamp') const d = new Date(v) if (d instanceof Date && !isNaN(d.getTime())) { return d } error('invalid date ' + v) } else { ensure(t === 'object' && v !== null, 'non-null object') ensure(v.__proto__ === Date.prototype, 'Date') return v.toISOString() } } // We're left with named types. const nt = this.types[w] if (!nt) { error('unknown type ' + w) } if (v === null) { error('bad value ' + v + ' for named type ' + w) } if (structTypes[nt.Name]) { const t = nt as Struct if (typeof v !== 'object') { error('bad value ' + v + ' for struct ' + w) } const r: any = {} for (const f of t.Fields) { r[f.Name] = this.verify(path + '.' + f.Name, v[f.Name], f.Typewords) } // If going to JSON also verify no unknown fields are present. if (!this.allowUnknownKeys) { const known: { [key: string]: boolean } = {} for (const f of t.Fields) { known[f.Name] = true } Object.keys(v).forEach((k) => { if (!known[k]) { error('unknown key ' + k + ' for struct ' + w) } }) } return r } else if (stringsTypes[nt.Name]) { const t = nt as Strings if (typeof v !== 'string') { error('mistyped value ' + v + ' for named strings ' + t.Name) } if (!t.Values || t.Values.length === 0) { return v } for (const sv of t.Values) { if (sv.Value === v) { return v } } error('unknown value ' + v + ' for named strings ' + t.Name) } else if (intsTypes[nt.Name]) { const t = nt as Ints if (typeof v !== 'number' || !Number.isInteger(v)) { error('mistyped value ' + v + ' for named ints ' + t.Name) } if (!t.Values || t.Values.length === 0) { return v } for (const sv of t.Values) { if (sv.Value === v) { return v } } error('unknown value ' + v + ' for named ints ' + t.Name) } else { throw new Error('unexpected named type ' + nt) } } } export interface ClientOptions { baseURL?: string aborter?: {abort?: () => void} timeoutMsec?: number skipParamCheck?: boolean skipReturnCheck?: boolean slicesNullable?: boolean mapsNullable?: boolean nullableOptional?: boolean csrfHeader?: string login?: (reason: string) => Promise } export interface AuthState { token?: string // For csrf request header. loginPromise?: Promise // To let multiple API calls wait for a single login attempt, not each opening a login popup. } const _sherpaCall = async (baseURL: string, authState: AuthState, options: ClientOptions, paramTypes: string[][], returnTypes: string[][], name: string, params: any[]): Promise => { if (!options.skipParamCheck) { if (params.length !== paramTypes.length) { return Promise.reject({ message: 'wrong number of parameters in sherpa call, saw ' + params.length + ' != expected ' + paramTypes.length }) } params = params.map((v: any, index: number) => verifyArg('params[' + index + ']', v, paramTypes[index], false, false, types, options)) } const simulate = async (json: string) => { const config = JSON.parse(json || 'null') || {} const waitMinMsec = config.waitMinMsec || 0 const waitMaxMsec = config.waitMaxMsec || 0 const wait = Math.random() * (waitMaxMsec - waitMinMsec) const failRate = config.failRate || 0 return new Promise((resolve, reject) => { if (options.aborter) { options.aborter.abort = () => { reject({ message: 'call to ' + name + ' aborted by user', code: 'sherpa:aborted' }) reject = resolve = () => { } } } setTimeout(() => { const r = Math.random() if (r < failRate) { reject({ message: 'injected failure on ' + name, code: 'server:injected' }) } else { resolve() } reject = resolve = () => { } }, waitMinMsec + wait) }) } // Only simulate when there is a debug string. Otherwise it would always interfere // with setting options.aborter. let json: string = '' try { json = window.localStorage.getItem('sherpats-debug') || '' } catch (err) {} if (json) { await simulate(json) } const fn = (resolve: (v: any) => void, reject: (v: any) => void) => { let resolve1 = (v: any) => { resolve(v) resolve1 = () => { } reject1 = () => { } } let reject1 = (v: { code: string, message: string }) => { if ((v.code === 'user:noAuth' || v.code === 'user:badAuth') && options.login) { const login = options.login if (!authState.loginPromise) { authState.loginPromise = new Promise((aresolve, areject) => { login(v.code === 'user:badAuth' ? (v.message || '') : '') .then((token) => { authState.token = token authState.loginPromise = undefined aresolve() }, (err: any) => { authState.loginPromise = undefined areject(err) }) }) } authState.loginPromise .then(() => { fn(resolve, reject) }, (err: any) => { reject(err) }) return } reject(v) resolve1 = () => { } reject1 = () => { } } const url = baseURL + name const req = new window.XMLHttpRequest() if (options.aborter) { options.aborter.abort = () => { req.abort() reject1({ code: 'sherpa:aborted', message: 'request aborted' }) } } req.open('POST', url, true) if (options.csrfHeader && authState.token) { req.setRequestHeader(options.csrfHeader, authState.token) } if (options.timeoutMsec) { req.timeout = options.timeoutMsec } req.onload = () => { if (req.status !== 200) { if (req.status === 404) { reject1({ code: 'sherpa:badFunction', message: 'function does not exist' }) } else { reject1({ code: 'sherpa:http', message: 'error calling function, HTTP status: ' + req.status }) } return } let resp: any try { resp = JSON.parse(req.responseText) } catch (err) { reject1({ code: 'sherpa:badResponse', message: 'bad JSON from server' }) return } if (resp && resp.error) { const err = resp.error reject1({ code: err.code, message: err.message }) return } else if (!resp || !resp.hasOwnProperty('result')) { reject1({ code: 'sherpa:badResponse', message: "invalid sherpa response object, missing 'result'" }) return } if (options.skipReturnCheck) { resolve1(resp.result) return } let result = resp.result try { if (returnTypes.length === 0) { if (result) { throw new Error('function ' + name + ' returned a value while prototype says it returns "void"') } } else if (returnTypes.length === 1) { result = verifyArg('result', result, returnTypes[0], true, true, types, options) } else { if (result.length != returnTypes.length) { throw new Error('wrong number of values returned by ' + name + ', saw ' + result.length + ' != expected ' + returnTypes.length) } result = result.map((v: any, index: number) => verifyArg('result[' + index + ']', v, returnTypes[index], true, true, types, options)) } } catch (err) { let errmsg = 'bad types' if (err instanceof Error) { errmsg = err.message } reject1({ code: 'sherpa:badTypes', message: errmsg }) } resolve1(result) } req.onerror = () => { reject1({ code: 'sherpa:connection', message: 'connection failed' }) } req.ontimeout = () => { reject1({ code: 'sherpa:timeout', message: 'request timeout' }) } req.setRequestHeader('Content-Type', 'application/json') try { req.send(JSON.stringify({ params: params })) } catch (err) { reject1({ code: 'sherpa:badData', message: 'cannot marshal to JSON' }) } } return await new Promise(fn) } `