@openstax/ts-utils 1.1.26 → 1.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/{assertions.d.ts → cjs/assertions.d.ts} +0 -0
  2. package/dist/{assertions.js → cjs/assertions.js} +0 -0
  3. package/dist/{aws → cjs/aws}/securityTokenService.d.ts +0 -0
  4. package/dist/{aws → cjs/aws}/securityTokenService.js +0 -0
  5. package/dist/{aws → cjs/aws}/ssmService.d.ts +0 -0
  6. package/dist/{aws → cjs/aws}/ssmService.js +0 -0
  7. package/dist/{config.d.ts → cjs/config.d.ts} +0 -0
  8. package/dist/{config.js → cjs/config.js} +0 -0
  9. package/dist/{errors.d.ts → cjs/errors.d.ts} +0 -0
  10. package/dist/{errors.js → cjs/errors.js} +0 -0
  11. package/dist/{fetch.d.ts → cjs/fetch.d.ts} +0 -0
  12. package/dist/{fetch.js → cjs/fetch.js} +0 -0
  13. package/dist/{guards.d.ts → cjs/guards.d.ts} +0 -0
  14. package/dist/{guards.js → cjs/guards.js} +0 -0
  15. package/dist/{index.d.ts → cjs/index.d.ts} +0 -0
  16. package/dist/{index.js → cjs/index.js} +0 -0
  17. package/dist/{middleware.d.ts → cjs/middleware.d.ts} +0 -0
  18. package/dist/{middleware.js → cjs/middleware.js} +0 -0
  19. package/dist/{pagination.d.ts → cjs/pagination.d.ts} +0 -0
  20. package/dist/{pagination.js → cjs/pagination.js} +0 -0
  21. package/dist/{profile.d.ts → cjs/profile.d.ts} +0 -0
  22. package/dist/{profile.js → cjs/profile.js} +0 -0
  23. package/dist/{routing.d.ts → cjs/routing.d.ts} +0 -0
  24. package/dist/{routing.js → cjs/routing.js} +0 -0
  25. package/dist/{services → cjs/services}/apiGateway/index.d.ts +0 -0
  26. package/dist/{services → cjs/services}/apiGateway/index.js +0 -0
  27. package/dist/{services → cjs/services}/authProvider/browser.d.ts +0 -0
  28. package/dist/{services → cjs/services}/authProvider/browser.js +0 -0
  29. package/dist/{services → cjs/services}/authProvider/decryption.d.ts +0 -0
  30. package/dist/{services → cjs/services}/authProvider/decryption.js +0 -0
  31. package/dist/{services → cjs/services}/authProvider/index.d.ts +0 -0
  32. package/dist/{services → cjs/services}/authProvider/index.js +0 -0
  33. package/dist/{services → cjs/services}/authProvider/subrequest.d.ts +0 -0
  34. package/dist/{services → cjs/services}/authProvider/subrequest.js +0 -0
  35. package/dist/{services → cjs/services}/authProvider/utils/embeddedAuthProvider.d.ts +0 -0
  36. package/dist/{services → cjs/services}/authProvider/utils/embeddedAuthProvider.js +0 -0
  37. package/dist/{services → cjs/services}/exercisesGateway/index.d.ts +0 -0
  38. package/dist/{services → cjs/services}/exercisesGateway/index.js +0 -0
  39. package/dist/{services → cjs/services}/lrsGateway/attempt-utils.d.ts +0 -0
  40. package/dist/{services → cjs/services}/lrsGateway/attempt-utils.js +0 -0
  41. package/dist/{services → cjs/services}/lrsGateway/file-system.d.ts +0 -0
  42. package/dist/{services → cjs/services}/lrsGateway/file-system.js +0 -0
  43. package/dist/{services → cjs/services}/lrsGateway/index.d.ts +0 -0
  44. package/dist/{services → cjs/services}/lrsGateway/index.js +0 -0
  45. package/dist/{services → cjs/services}/searchProvider/index.d.ts +0 -0
  46. package/dist/{services → cjs/services}/searchProvider/index.js +0 -0
  47. package/dist/{services → cjs/services}/searchProvider/memorySearchTheBadWay.d.ts +0 -0
  48. package/dist/{services → cjs/services}/searchProvider/memorySearchTheBadWay.js +0 -0
  49. package/dist/{services → cjs/services}/versionedDocumentStore/dynamodb.d.ts +0 -0
  50. package/dist/{services → cjs/services}/versionedDocumentStore/dynamodb.js +0 -0
  51. package/dist/{services → cjs/services}/versionedDocumentStore/file-system.d.ts +0 -0
  52. package/dist/{services → cjs/services}/versionedDocumentStore/file-system.js +0 -0
  53. package/dist/{services → cjs/services}/versionedDocumentStore/index.d.ts +0 -0
  54. package/dist/{services → cjs/services}/versionedDocumentStore/index.js +0 -0
  55. package/dist/cjs/tsconfig.withoutspecs.cjs.tsbuildinfo +1 -0
  56. package/dist/{types.d.ts → cjs/types.d.ts} +0 -0
  57. package/dist/{types.js → cjs/types.js} +0 -0
  58. package/dist/esm/assertions.d.ts +9 -0
  59. package/dist/esm/assertions.js +90 -0
  60. package/dist/esm/aws/securityTokenService.d.ts +2 -0
  61. package/dist/esm/aws/securityTokenService.js +3 -0
  62. package/dist/esm/aws/ssmService.d.ts +2 -0
  63. package/dist/esm/aws/ssmService.js +3 -0
  64. package/dist/esm/config.d.ts +27 -0
  65. package/dist/esm/config.js +127 -0
  66. package/dist/esm/errors.d.ts +12 -0
  67. package/dist/esm/errors.js +26 -0
  68. package/dist/esm/fetch.d.ts +64 -0
  69. package/dist/esm/fetch.js +46 -0
  70. package/dist/esm/guards.d.ts +6 -0
  71. package/dist/esm/guards.js +29 -0
  72. package/dist/esm/index.d.ts +29 -0
  73. package/dist/esm/index.js +210 -0
  74. package/dist/esm/middleware.d.ts +9 -0
  75. package/dist/esm/middleware.js +34 -0
  76. package/dist/esm/pagination.d.ts +63 -0
  77. package/dist/esm/pagination.js +77 -0
  78. package/dist/esm/profile.d.ts +59 -0
  79. package/dist/esm/profile.js +191 -0
  80. package/dist/esm/routing.d.ts +107 -0
  81. package/dist/esm/routing.js +208 -0
  82. package/dist/esm/services/apiGateway/index.d.ts +55 -0
  83. package/dist/esm/services/apiGateway/index.js +51 -0
  84. package/dist/esm/services/authProvider/browser.d.ts +61 -0
  85. package/dist/esm/services/authProvider/browser.js +119 -0
  86. package/dist/esm/services/authProvider/decryption.d.ts +16 -0
  87. package/dist/esm/services/authProvider/decryption.js +61 -0
  88. package/dist/esm/services/authProvider/index.d.ts +42 -0
  89. package/dist/esm/services/authProvider/index.js +15 -0
  90. package/dist/esm/services/authProvider/subrequest.d.ts +16 -0
  91. package/dist/esm/services/authProvider/subrequest.js +36 -0
  92. package/dist/esm/services/authProvider/utils/embeddedAuthProvider.d.ts +20 -0
  93. package/dist/esm/services/authProvider/utils/embeddedAuthProvider.js +30 -0
  94. package/dist/esm/services/exercisesGateway/index.d.ts +74 -0
  95. package/dist/esm/services/exercisesGateway/index.js +69 -0
  96. package/dist/esm/services/lrsGateway/attempt-utils.d.ts +62 -0
  97. package/dist/esm/services/lrsGateway/attempt-utils.js +251 -0
  98. package/dist/esm/services/lrsGateway/file-system.d.ts +15 -0
  99. package/dist/esm/services/lrsGateway/file-system.js +96 -0
  100. package/dist/esm/services/lrsGateway/index.d.ts +110 -0
  101. package/dist/esm/services/lrsGateway/index.js +87 -0
  102. package/dist/esm/services/searchProvider/index.d.ts +19 -0
  103. package/dist/esm/services/searchProvider/index.js +1 -0
  104. package/dist/esm/services/searchProvider/memorySearchTheBadWay.d.ts +12 -0
  105. package/dist/esm/services/searchProvider/memorySearchTheBadWay.js +53 -0
  106. package/dist/esm/services/versionedDocumentStore/dynamodb.d.ts +23 -0
  107. package/dist/esm/services/versionedDocumentStore/dynamodb.js +147 -0
  108. package/dist/esm/services/versionedDocumentStore/file-system.d.ts +25 -0
  109. package/dist/esm/services/versionedDocumentStore/file-system.js +81 -0
  110. package/dist/esm/services/versionedDocumentStore/index.d.ts +23 -0
  111. package/dist/esm/services/versionedDocumentStore/index.js +1 -0
  112. package/dist/esm/tsconfig.withoutspecs.esm.tsbuildinfo +1 -0
  113. package/dist/esm/types.d.ts +6 -0
  114. package/dist/esm/types.js +1 -0
  115. package/package.json +25 -8
  116. package/dist/tsconfig.tsbuildinfo +0 -1
  117. package/dist/tsconfig.withoutspecs.tsbuildinfo +0 -1
@@ -0,0 +1,107 @@
1
+ import { Track } from './profile';
2
+ export declare type QueryParams = Record<string, string | undefined | string[] | null>;
3
+ export declare type RouteParams = {
4
+ [key: string]: string;
5
+ };
6
+ export declare type AnyRoute<R> = R extends Route<infer N, infer P, infer Sa, infer Sr, infer Ri, infer Ro> ? Route<N, P, Sa, Sr, Ri, Ro> : never;
7
+ export declare type AnySpecificRoute<R, Sa, Ri, Ro> = R extends Route<infer N, infer P, Sa, infer Sr, Ri, Ro> & infer E ? Route<N, P, Sa, Sr, Ri, Ro> & E : never;
8
+ export declare type OutputForRoute<R> = R extends Route<any, any, any, any, any, infer Ro> ? Ro : never;
9
+ export declare type ParamsForRoute<R> = R extends Route<any, infer P, any, any, any, any> ? P : never;
10
+ export declare type RequestServicesForRoute<R> = R extends Route<any, any, any, infer Sr, any, any> ? Sr : never;
11
+ export declare type ParamsForRouteOrEmpty<R> = ParamsForRoute<R> extends undefined ? {} : Exclude<ParamsForRoute<R>, undefined>;
12
+ export declare type RouteMatchRecord<R> = R extends AnyRoute<R> ? {
13
+ route: R;
14
+ params: ParamsForRoute<R>;
15
+ } : never;
16
+ export declare type PayloadForRoute<R> = RequestServicesForRoute<R> extends {
17
+ payload: any;
18
+ } ? RequestServicesForRoute<R>['payload'] : undefined;
19
+ declare type RequestServiceProvider<Sa, Sr, Ri> = (app: Sa) => <R>(middleware: {
20
+ request: Ri;
21
+ profile: Track;
22
+ }, match: RouteMatchRecord<R>) => Sr;
23
+ declare type RouteHandler<P, Sr, Ro> = (params: P, request: Sr) => Ro;
24
+ declare type Route<N extends string, P extends RouteParams | undefined, Sa, Sr, Ri, Ro> = (Sr extends undefined ? {
25
+ requestServiceProvider?: RequestServiceProvider<Sa, Sr, Ri> | undefined;
26
+ } : {
27
+ requestServiceProvider: RequestServiceProvider<Sa, Sr, Ri>;
28
+ }) & {
29
+ name: N;
30
+ path: string;
31
+ handler: (params: P, request: Sr) => Ro;
32
+ };
33
+ declare type CreateRouteConfig<Sa, Sr, Ri, N extends string, Ex> = (Sr extends undefined ? {
34
+ requestServiceProvider?: RequestServiceProvider<Sa, Sr, Ri> | undefined;
35
+ } : {
36
+ requestServiceProvider: RequestServiceProvider<Sa, Sr, Ri>;
37
+ }) & {
38
+ name: N;
39
+ path: string;
40
+ } & Ex;
41
+ export interface CreateRoute<Sa, Ri, Ex> {
42
+ <N extends string, Ro, Sr extends unknown | undefined = undefined, P extends RouteParams | undefined = undefined>(config: CreateRouteConfig<Sa, Sr, Ri, N, Ex> & {
43
+ handler: RouteHandler<P, Sr, Ro>;
44
+ }): Route<N, P, Sa, Sr, Ri, Ro> & Ex;
45
+ <N extends string, Ro, Sr extends unknown | undefined, P extends RouteParams | undefined = undefined>(config: CreateRouteConfig<Sa, Sr, Ri, N, Ex>, handler: RouteHandler<P, Sr, Ro>): Route<N, P, Sa, Sr, Ri, Ro> & Ex;
46
+ }
47
+ export declare const makeCreateRoute: <Sa, Ri, Ex = {}>() => CreateRoute<Sa, Ri, Ex>;
48
+ export declare const makeRenderRouteUrl: <Ru extends {
49
+ path: string;
50
+ }>() => <R extends Ru>(route: R, params: ParamsForRoute<R>, query?: QueryParams) => string;
51
+ export declare const renderAnyRouteUrl: <R extends any>(route: R, params: ParamsForRoute<R>, query?: QueryParams) => string;
52
+ declare type RequestPathExtractor<Ri> = (request: Ri) => string;
53
+ declare type RequestRouteMatcher<Ri, R> = (request: Ri, route: R) => boolean;
54
+ declare type RequestResponder<Sa, Ri, Ro> = {
55
+ (services: Sa): (request: Ri) => Ro | undefined;
56
+ <RoF>(services: Sa, responseMiddleware: (app: Sa) => (response: Ro | undefined, request: {
57
+ request: Ri;
58
+ profile: Track;
59
+ }) => RoF): (request: Ri) => RoF;
60
+ };
61
+ export declare const makeGetRequestResponder: <Sa, Ru, Ri, Ro>() => ({ routes, pathExtractor, routeMatcher, errorHandler }: {
62
+ routes: () => AnySpecificRoute<Ru, Sa, Ri, Ro>[];
63
+ pathExtractor: RequestPathExtractor<Ri>;
64
+ routeMatcher?: RequestRouteMatcher<Ri, AnySpecificRoute<Ru, Sa, Ri, Ro>> | undefined;
65
+ errorHandler?: ((e: Error) => Ro) | undefined;
66
+ }) => RequestResponder<Sa, Ri, Ro>;
67
+ export declare type HttpHeaders = {
68
+ [key: string]: string | undefined | string[];
69
+ };
70
+ export declare type JsonCompatibleValue = string | number | null | undefined | boolean;
71
+ export declare type JsonCompatibleArray = Array<JsonCompatibleValue | JsonCompatibleStruct | JsonCompatibleStruct>;
72
+ export declare type JsonCompatibleStruct = {
73
+ [key: string]: JsonCompatibleStruct | JsonCompatibleValue | JsonCompatibleArray;
74
+ };
75
+ export declare type ApiResponse<S extends number, T> = {
76
+ isBase64Encoded?: boolean;
77
+ statusCode: S;
78
+ data: T;
79
+ body: string;
80
+ headers?: {
81
+ [key: string]: string;
82
+ };
83
+ };
84
+ export declare const apiJsonResponse: <S extends number, T extends JsonCompatibleStruct>(statusCode: S, data: T, headers?: HttpHeaders | undefined) => ApiResponse<S, T>;
85
+ export declare const apiTextResponse: <S extends number>(statusCode: S, data: string, headers?: HttpHeaders | undefined) => ApiResponse<S, string>;
86
+ export declare const apiHtmlResponse: <S extends number>(statusCode: S, data: string, headers?: HttpHeaders | undefined) => ApiResponse<S, string>;
87
+ export declare enum METHOD {
88
+ GET = "GET",
89
+ HEAD = "HEAD",
90
+ POST = "POST",
91
+ PUT = "PUT",
92
+ PATCH = "PATCH",
93
+ DELETE = "DELETE",
94
+ OPTIONS = "OPTIONS"
95
+ }
96
+ export declare const getHeader: (headers: HttpHeaders, name: string) => string | undefined;
97
+ export declare const getRequestBody: (request: {
98
+ headers: HttpHeaders;
99
+ body?: string | undefined;
100
+ }) => any;
101
+ export declare const unsafePayloadValidator: <T>() => (input: any) => input is T;
102
+ export declare const requestPayloadProvider: <T>(validator: (input: any) => input is T) => () => <M extends {
103
+ request: Parameters<typeof getRequestBody>[0];
104
+ }>(requestServices: M) => M & {
105
+ payload: T;
106
+ };
107
+ export {};
@@ -0,0 +1,208 @@
1
+ import * as pathToRegexp from 'path-to-regexp';
2
+ import queryString from 'query-string';
3
+ import { assertErrorInstanceOf } from './assertions';
4
+ import { InvalidRequestError } from './errors';
5
+ import { isPlainObject } from './guards';
6
+ import { createProfile } from './profile';
7
+ import { mapFind, memoize } from '.';
8
+ /*
9
+ * route definition helper. the only required params of the route are the name, path, and handler. other params
10
+ * can be added to the type and then later used in the routeMatcher. when defining the `createRoute` method, only
11
+ * the request input format is defined, the result format is derived from the routes.
12
+ *
13
+ * eg:
14
+ * export const createRoute = makeCreateRoute<AppServices, ApiRouteRequest, {
15
+ * method: METHOD;
16
+ * }>();
17
+ *
18
+ * eg when defining requestServiceProvider in line, the types have a hard time, it helps to put in another argument:
19
+ * export const exampleRoute = createRoute({name: 'exampleRoute', method: METHOD.GET, path: '/api/example/:key',
20
+ * requestServiceProvider: requestServiceProvider({
21
+ * cookieAuthMiddleware,
22
+ * documentStoreMiddleware,
23
+ * }},
24
+ * async(params: {key: string}, services) => {
25
+ * const result = await services.myDocumentStore.getItem(params.key);
26
+ *
27
+ * if (!result) {
28
+ * throw new NotFoundError('requested item not found');
29
+ * }
30
+ *
31
+ * return apiJsonResponse(200, result);
32
+ * }
33
+ * );
34
+ *
35
+ * eg when using a pre-existing provider variable the types work better:
36
+ * export const exampleRoute = createRoute({name: 'exampleRoute', method: METHOD.GET, path: '/api/example/:key',
37
+ * requestServiceProvider,
38
+ * handler: async(params: {key: string}, services) => {
39
+ * const result = await services.myDocumentStore.getItem(params.key);
40
+ *
41
+ * if (!result) {
42
+ * throw new NotFoundError('requested item not found');
43
+ * }
44
+ *
45
+ * return apiJsonResponse(200, result);
46
+ * }
47
+ * });
48
+ */
49
+ export const makeCreateRoute = () => (...args) => {
50
+ return (args.length === 1
51
+ ? args[0]
52
+ : { ...args[0], handler: args[1] });
53
+ };
54
+ /* begin reverse routing utils */
55
+ export const makeRenderRouteUrl = () => (route, params, query = {}) => {
56
+ const getPathForParams = pathToRegexp.compile(route.path, { encode: encodeURIComponent });
57
+ const search = queryString.stringify(query);
58
+ const path = getPathForParams(params) + (search ? `?${search}` : '');
59
+ return path;
60
+ };
61
+ export const renderAnyRouteUrl = makeRenderRouteUrl();
62
+ const bindRoute = (services, appBinder, pathExtractor, matcher) => (route) => {
63
+ const getParamsFromPath = pathToRegexp.match(route.path, { decode: decodeURIComponent });
64
+ const boundServiceProvider = route.requestServiceProvider && appBinder(services, route.requestServiceProvider);
65
+ return (request, profile) => {
66
+ const path = pathExtractor(request);
67
+ const match = getParamsFromPath(path);
68
+ if ((!matcher || matcher(request, route)) && match) {
69
+ return profile.track(route.name, routeProfile => () => route.handler(match.params, boundServiceProvider ? boundServiceProvider({ request, profile: routeProfile }, { route, params: match.params }) : undefined));
70
+ }
71
+ };
72
+ };
73
+ /*
74
+ * here among other things we're specifying a generic response format that the response and error handling middleware can use,
75
+ * if any routes have responses that don't adhere to this it'll complain about it.
76
+ *
77
+ * eg:
78
+ * export const getRequestResponder = makeGetRequestResponder<AppServices, TRoutes, ApiRouteRequest, Promise<ApiRouteResponse>>()({
79
+ * routes: apiRoutes, // the route definitions
80
+ * pathExtractor, // how to get the path out of the request format
81
+ * routeMatcher, // logic for matching route (if there is any in addition to the path matching)
82
+ * errorHandler, // any special error handling
83
+ * });
84
+ *
85
+ * eg an lambda entrypoint:
86
+ * export const handler: (request: APIGatewayProxyEventV2) => Promise<ApiRouteResponse> =
87
+ * getRequestResponder(
88
+ * lambdaServices, // the AppServices for this entrypoint
89
+ * lambdaMiddleware // environment specific response middleware (like cors)
90
+ * );
91
+ */
92
+ export const makeGetRequestResponder = () => ({ routes, pathExtractor, routeMatcher, errorHandler }) => (services, responseMiddleware) => {
93
+ const appBinderImpl = (app, middleware) => middleware(app, appBinder);
94
+ const appBinder = memoize(appBinderImpl);
95
+ const boundRoutes = routes().map(bindRoute(services, appBinder, pathExtractor, routeMatcher));
96
+ const boundResponseMiddleware = responseMiddleware ? responseMiddleware(services) : undefined;
97
+ // *note* this opaque promise guard is less generic than i hoped so
98
+ // i'm leaving it here instead of the guards file.
99
+ //
100
+ // its less than ideal because it enforces that the handlers return
101
+ // the same type as the parent promise, usually a handler can be either a
102
+ // promise or a non-promise value and the promise figures it out, but those
103
+ // types are getting complicated quickly here.
104
+ const isPromise = (thing) => thing instanceof Promise;
105
+ return (request) => {
106
+ const { end, ...profile } = createProfile(new Date().toISOString()).start();
107
+ try {
108
+ const executor = mapFind(boundRoutes, (route) => route(request, profile));
109
+ if (executor) {
110
+ const result = boundResponseMiddleware ?
111
+ boundResponseMiddleware(executor(), { request, profile }) : executor();
112
+ if (isPromise(result) && errorHandler) {
113
+ const errorHandlerWithMiddleware = (e) => boundResponseMiddleware ?
114
+ boundResponseMiddleware(errorHandler(e), { request, profile }) : errorHandler(e);
115
+ return result.catch(errorHandlerWithMiddleware);
116
+ }
117
+ else {
118
+ return result;
119
+ }
120
+ }
121
+ else if (boundResponseMiddleware) {
122
+ return boundResponseMiddleware(undefined, { request, profile });
123
+ }
124
+ }
125
+ catch (e) {
126
+ if (errorHandler && e instanceof Error) {
127
+ return boundResponseMiddleware ? boundResponseMiddleware(errorHandler(e), { request, profile }) : errorHandler(e);
128
+ }
129
+ throw e;
130
+ }
131
+ return undefined;
132
+ };
133
+ };
134
+ export const apiJsonResponse = (statusCode, data, headers) => ({ statusCode, data, body: JSON.stringify(data), headers: { ...headers, 'content-type': 'application/json' } });
135
+ export const apiTextResponse = (statusCode, data, headers) => ({ statusCode, data, body: data, headers: { ...headers, 'content-type': 'text/plain' } });
136
+ export const apiHtmlResponse = (statusCode, data, headers) => ({ statusCode, data, body: data, headers: { ...headers, 'content-type': 'text/html' } });
137
+ export var METHOD;
138
+ (function (METHOD) {
139
+ METHOD["GET"] = "GET";
140
+ METHOD["HEAD"] = "HEAD";
141
+ METHOD["POST"] = "POST";
142
+ METHOD["PUT"] = "PUT";
143
+ METHOD["PATCH"] = "PATCH";
144
+ METHOD["DELETE"] = "DELETE";
145
+ METHOD["OPTIONS"] = "OPTIONS";
146
+ })(METHOD || (METHOD = {}));
147
+ /* utils and middleware for loading request payload (must follow this pattern for `PayloadForRoute` to work) */
148
+ // use this to support case insensitive header keys
149
+ // note if there are multiple headers of the same value, this only returns the first value
150
+ export const getHeader = (headers, name) => {
151
+ const key = Object.keys(headers).find(header => header.toLowerCase() === name.toLowerCase());
152
+ const value = key ? headers[key] : undefined;
153
+ return value instanceof Array
154
+ ? value[0]
155
+ : value;
156
+ };
157
+ export const getRequestBody = (request) => {
158
+ if (getHeader(request.headers, 'content-type') !== 'application/json') {
159
+ throw new InvalidRequestError('unknown content type: ' + getHeader(request.headers, 'content-type'));
160
+ }
161
+ if (!request.body) {
162
+ return {};
163
+ }
164
+ try {
165
+ return JSON.parse(request.body);
166
+ }
167
+ catch (error) {
168
+ // Since the body is provided by the user, invalid JSON in the body is an invalid request
169
+ // We return the message which tells them why the JSON is invalid, but no backtrace
170
+ throw new InvalidRequestError(assertErrorInstanceOf(error, SyntaxError).message);
171
+ }
172
+ };
173
+ // stub validator because writing validators is annoying
174
+ export const unsafePayloadValidator = () => (input) => {
175
+ return isPlainObject(input) && Object.keys(input).length > 0;
176
+ };
177
+ /*
178
+ * the given validator is a guard, which provides the correct type this helper loads the body, runs the validator, throws if it isn't valid, or returns it as
179
+ * the correct type if it is valid.
180
+ *
181
+ * this accomplishes a few things:
182
+ * - establishes type of payload for route body logic
183
+ * - validates the payload for route logic
184
+ * - establishes type of payload for client logic calling this route
185
+ *
186
+ * eg:
187
+ * export const exampleRoute = createRoute({name: 'exampleRoute', method: METHOD.POST, path: '/example/:id',
188
+ * requestServiceProvider: composeServiceMiddleware(
189
+ * requestServiceProvider, // previously compiled middleware can be re-composed if you have something to add
190
+ * requestPayloadProvider(validatePayload)
191
+ * )},
192
+ * async(params: {id: string}, services) => {
193
+ * const result = await services.myDocumentStore.putItem({
194
+ * ...services.payload,
195
+ * id: params.id,
196
+ * });
197
+ * return apiJsonResponse(201, result);
198
+ * }
199
+ * );
200
+ * */
201
+ export const requestPayloadProvider = (validator) => () => (requestServices) => {
202
+ const payload = getRequestBody(requestServices.request);
203
+ // for more precise error messages, throw your own InvalidRequestError from your validator function
204
+ if (!validator(payload)) {
205
+ throw new InvalidRequestError();
206
+ }
207
+ return { ...requestServices, payload };
208
+ };
@@ -0,0 +1,55 @@
1
+ import { ConfigProviderForConfig } from '../../config';
2
+ import { ConfigForFetch, GenericFetch, Response } from '../../fetch';
3
+ import { AnyRoute, ApiResponse, OutputForRoute, ParamsForRoute, PayloadForRoute, QueryParams } from '../../routing';
4
+ import { UnwrapPromise } from '../../types';
5
+ declare type TResponsePayload<R> = R extends ApiResponse<any, infer P> ? P : never;
6
+ declare type TResponseStatus<R> = R extends ApiResponse<infer S, any> ? S : never;
7
+ declare type RouteClient<R> = {
8
+ (config: {
9
+ fetchConfig?: any;
10
+ query?: QueryParams;
11
+ } & (ParamsForRoute<R> extends undefined ? {} : {
12
+ params: ParamsForRoute<R>;
13
+ }) & (PayloadForRoute<R> extends undefined ? {} : {
14
+ payload: PayloadForRoute<R>;
15
+ })): Promise<UnsafeApiClientResponse<UnwrapPromise<OutputForRoute<R>>>>;
16
+ renderUrl: (config: {
17
+ query?: QueryParams;
18
+ } & (ParamsForRoute<R> extends undefined ? {} : {
19
+ params: ParamsForRoute<R>;
20
+ })) => Promise<string>;
21
+ };
22
+ interface AcceptStatus<Ro> {
23
+ <S extends TResponseStatus<Ro>[]>(...args: S): ApiClientResponse<Extract<Ro, Record<'statusCode', S[number]>>>;
24
+ <S extends number[]>(...args: S): ApiClientResponse<any>;
25
+ }
26
+ declare type UnsafeApiClientResponse<Ro> = {
27
+ load: () => Promise<any>;
28
+ status: number;
29
+ acceptStatus: AcceptStatus<Ro>;
30
+ };
31
+ declare type ApiClientResponse<Ro> = Ro extends any ? {
32
+ status: TResponseStatus<Ro>;
33
+ load: () => Promise<TResponsePayload<Ro>>;
34
+ } : never;
35
+ declare type MapRoutesToClient<Ru> = [Ru] extends [AnyRoute<Ru>] ? {
36
+ [N in Ru['name']]: RouteClient<Extract<Ru, Record<'name', N>>>;
37
+ } : never;
38
+ declare type MapRoutesToConfig<Ru> = [Ru] extends [AnyRoute<Ru>] ? {
39
+ [N in Ru['name']]: {
40
+ path: string;
41
+ method: string;
42
+ };
43
+ } : never;
44
+ export declare const loadResponse: (response: Response) => () => Promise<any>;
45
+ interface MakeApiGateway<F> {
46
+ <Ru>(config: ConfigProviderForConfig<{
47
+ apiBase: string;
48
+ }>, routes: MapRoutesToConfig<Ru>, authProvider?: {
49
+ getAuthorizedFetchConfig: () => Promise<ConfigForFetch<F>>;
50
+ }): MapRoutesToClient<Ru>;
51
+ }
52
+ export declare const createApiGateway: <F extends GenericFetch<import("../../fetch").FetchConfig, Response>>(initializer: {
53
+ fetch: F;
54
+ }) => MakeApiGateway<F>;
55
+ export {};
@@ -0,0 +1,51 @@
1
+ import * as pathToRegexp from 'path-to-regexp';
2
+ import queryString from 'query-string';
3
+ import { merge } from '../..';
4
+ import { resolveConfigValue } from '../../config';
5
+ export const loadResponse = (response) => () => {
6
+ const [contentType] = (response.headers.get('content-type') || '').split(';');
7
+ switch (contentType) {
8
+ case 'text/plain':
9
+ return response.text();
10
+ case 'application/json':
11
+ return response.json();
12
+ default:
13
+ throw new Error(`unknown content type ${contentType}`);
14
+ }
15
+ };
16
+ const makeRouteClient = (initializer, config, route, authProvider) => {
17
+ const renderUrl = async ({ params, query }) => {
18
+ const apiBase = await resolveConfigValue(config.apiBase);
19
+ const getPathForParams = pathToRegexp.compile(route.path, { encode: encodeURIComponent });
20
+ const search = query && queryString.stringify(query);
21
+ return apiBase.replace(/\/+$/, '') + getPathForParams(params || {}) + (search ? `?${search}` : '');
22
+ };
23
+ const routeClient = async ({ params, payload, query, fetchConfig }) => {
24
+ const url = await renderUrl({ params, query });
25
+ const body = payload ? JSON.stringify(payload) : undefined;
26
+ const baseOptions = merge((await (authProvider === null || authProvider === void 0 ? void 0 : authProvider.getAuthorizedFetchConfig())) || {}, fetchConfig || {});
27
+ return initializer.fetch(url, merge(baseOptions, {
28
+ method: route.method,
29
+ body,
30
+ headers: {
31
+ ...fetchConfig === null || fetchConfig === void 0 ? void 0 : fetchConfig.headers,
32
+ ...(body ? { 'content-type': 'application/json' } : {}),
33
+ }
34
+ })).then(response => ({
35
+ status: response.status,
36
+ acceptStatus: (...status) => {
37
+ if (!status.includes(response.status)) {
38
+ throw new Error('unexpected response from api');
39
+ }
40
+ return { status: response.status, load: loadResponse(response) };
41
+ },
42
+ load: loadResponse(response),
43
+ }));
44
+ };
45
+ routeClient.renderUrl = renderUrl;
46
+ return routeClient;
47
+ };
48
+ export const createApiGateway = (initializer) => (config, routes, authProvider) => {
49
+ return Object.fromEntries(Object.entries(routes)
50
+ .map(([key, routeConfig]) => ([key, makeRouteClient(initializer, config, routeConfig, authProvider)])));
51
+ };
@@ -0,0 +1,61 @@
1
+ import { ConfigProviderForConfig } from '../../config';
2
+ import { FetchConfig, GenericFetch } from '../../fetch';
3
+ import { User } from '.';
4
+ declare type Config = {
5
+ accountsUrl: string;
6
+ };
7
+ interface Initializer<C> {
8
+ configSpace?: C;
9
+ window: Window;
10
+ }
11
+ export declare type EventHandler = (e: {
12
+ data: any;
13
+ origin: string;
14
+ source: Pick<Window, 'postMessage'>;
15
+ }) => void;
16
+ export interface Window {
17
+ fetch: GenericFetch;
18
+ top: {} | null;
19
+ parent: Pick<Window, 'postMessage'> | null;
20
+ location: {
21
+ search: string;
22
+ };
23
+ document: {
24
+ referrer: string;
25
+ };
26
+ postMessage: (data: any, origin: string) => void;
27
+ addEventListener: (event: 'message', callback: EventHandler) => void;
28
+ removeEventListener: (event: 'message', callback: EventHandler) => void;
29
+ }
30
+ export declare const browserAuthProvider: <C extends string = "auth">({ window, configSpace }: Initializer<C>) => (configProvider: { [key in C]: {
31
+ accountsUrl: import("../../config").ConfigValueProvider<string>;
32
+ }; }) => {
33
+ /**
34
+ * adds auth parameters to the url. this is only safe to use when using javascript to navigate
35
+ * within the current window, eg `window.location = 'https://my.otherservice.com';` anchors
36
+ * should use getAuthorizedLinkUrl for their href.
37
+ *
38
+ * result unreliable unless `getUser` is resolved first.
39
+ */
40
+ getAuthorizedUrl: (urlString: string) => string;
41
+ /**
42
+ * all link href-s must be rendered with auth tokens so that they work when opened in a new tab
43
+ *
44
+ * result unreliable unless `getUser` is resolved first.
45
+ */
46
+ getAuthorizedLinkUrl: (urlString: string) => string;
47
+ /**
48
+ * gets an authorized url for an iframe src. sets params on the url and saves its
49
+ * origin to trust releasing user identity to it
50
+ */
51
+ getAuthorizedEmbedUrl: (urlString: string) => string;
52
+ /**
53
+ * gets second argument for `fetch` that has authentication token or cookie
54
+ */
55
+ getAuthorizedFetchConfig: () => Promise<FetchConfig>;
56
+ /**
57
+ * loads current user identity. does not reflect changes in identity after being called the first time.
58
+ */
59
+ getUser: () => Promise<User | undefined>;
60
+ };
61
+ export {};
@@ -0,0 +1,119 @@
1
+ import { once } from '../..';
2
+ import { resolveConfigValue } from '../../config';
3
+ import { ifDefined } from '../../guards';
4
+ import { embeddedAuthProvider, PostMessageTypes } from './utils/embeddedAuthProvider';
5
+ export const browserAuthProvider = ({ window, configSpace }) => (configProvider) => {
6
+ const config = configProvider[ifDefined(configSpace, 'auth')];
7
+ const accountsUrl = once(() => resolveConfigValue(config.accountsUrl));
8
+ const queryString = window.location.search;
9
+ const queryKey = 'auth';
10
+ const authQuery = new URLSearchParams(queryString).get(queryKey);
11
+ const referrer = window.document.referrer ? new URL(window.document.referrer) : undefined;
12
+ const isEmbedded = window.top && window.top !== window;
13
+ const trustedParent = isEmbedded && referrer && referrer.hostname.match(/^(openstax\.org|((.*)(\.openstax\.org|local|localhost)))$/) ? referrer : undefined;
14
+ const { embeddedQueryValue, getAuthorizedEmbedUrl } = embeddedAuthProvider(() => getUserData(), { queryKey, window });
15
+ let userData = {
16
+ token: [null, embeddedQueryValue].includes(authQuery) ? null : authQuery
17
+ };
18
+ const getAuthorizedLinkUrl = (urlString) => {
19
+ const url = new URL(urlString);
20
+ if (userData.token) {
21
+ url.searchParams.set(queryKey, userData.token);
22
+ }
23
+ return url.href;
24
+ };
25
+ const getAuthorizedUrl = (urlString) => {
26
+ const url = new URL(urlString);
27
+ if (authQuery) {
28
+ url.searchParams.set(queryKey, authQuery);
29
+ }
30
+ return url.href;
31
+ };
32
+ // *note* that this does not actually prevent cookies from being sent on same-origin
33
+ // requests, i'm not sure if its possible to stop browsers from sending cookies in
34
+ // that case
35
+ const getAuthorizedFetchConfigFromData = (data) => {
36
+ const { token } = data;
37
+ return token ? {
38
+ headers: { Authorization: `Bearer ${token}` },
39
+ } : {
40
+ credentials: 'include',
41
+ };
42
+ };
43
+ const getAuthorizedFetchConfig = async () => {
44
+ return getAuthorizedFetchConfigFromData(userData.token ? userData : await getUserData());
45
+ };
46
+ /*
47
+ * requests user identity from parent window via postMessage
48
+ */
49
+ const getParentWindowUser = () => new Promise((resolve, reject) => {
50
+ if (!window.parent || !trustedParent) {
51
+ return reject(new Error('parent window is undefined or not trusted'));
52
+ }
53
+ const handler = (event) => {
54
+ if (event.data.type === PostMessageTypes.ReceiveUser && event.origin === trustedParent.origin) {
55
+ clearTimeout(timeout);
56
+ window.removeEventListener('message', handler);
57
+ resolve(event.data.userData);
58
+ }
59
+ };
60
+ window.addEventListener('message', handler);
61
+ window.parent.postMessage({ type: PostMessageTypes.RequestUser }, trustedParent.origin);
62
+ const timeout = setTimeout(() => {
63
+ window.removeEventListener('message', handler);
64
+ reject(new Error('loading user identity timed out'));
65
+ }, 100);
66
+ });
67
+ /*
68
+ * requests user identity from accounts api using given token or cookie
69
+ */
70
+ const getFetchUser = async () => {
71
+ const response = await window.fetch((await accountsUrl()).replace(/\/+$/, '') + '/accounts/api/user', getAuthorizedFetchConfigFromData(userData));
72
+ if (response.status === 200) {
73
+ return { ...userData, user: await response.json() };
74
+ }
75
+ if (response.status === 403) {
76
+ return { ...userData, user: undefined };
77
+ }
78
+ const message = await response.text();
79
+ throw new Error(`Error response from Accounts ${response.status}: ${message}`);
80
+ };
81
+ const getUserData = once(async () => {
82
+ userData = authQuery === embeddedQueryValue
83
+ ? await getParentWindowUser()
84
+ : await getFetchUser();
85
+ return userData;
86
+ });
87
+ const getUser = async () => {
88
+ return (await getUserData()).user;
89
+ };
90
+ return {
91
+ /**
92
+ * adds auth parameters to the url. this is only safe to use when using javascript to navigate
93
+ * within the current window, eg `window.location = 'https://my.otherservice.com';` anchors
94
+ * should use getAuthorizedLinkUrl for their href.
95
+ *
96
+ * result unreliable unless `getUser` is resolved first.
97
+ */
98
+ getAuthorizedUrl,
99
+ /**
100
+ * all link href-s must be rendered with auth tokens so that they work when opened in a new tab
101
+ *
102
+ * result unreliable unless `getUser` is resolved first.
103
+ */
104
+ getAuthorizedLinkUrl,
105
+ /**
106
+ * gets an authorized url for an iframe src. sets params on the url and saves its
107
+ * origin to trust releasing user identity to it
108
+ */
109
+ getAuthorizedEmbedUrl,
110
+ /**
111
+ * gets second argument for `fetch` that has authentication token or cookie
112
+ */
113
+ getAuthorizedFetchConfig,
114
+ /**
115
+ * loads current user identity. does not reflect changes in identity after being called the first time.
116
+ */
117
+ getUser
118
+ };
119
+ };
@@ -0,0 +1,16 @@
1
+ import { ConfigProviderForConfig } from '../../config';
2
+ import { CookieAuthProvider } from '.';
3
+ declare type Config = {
4
+ cookieName: string;
5
+ encryptionPrivateKey: string;
6
+ signaturePublicKey: string;
7
+ };
8
+ interface Initializer<C> {
9
+ configSpace?: C;
10
+ }
11
+ export declare const decryptionAuthProvider: <C extends string = "decryption">(initializer: Initializer<C>) => (configProvider: { [key in C]: {
12
+ cookieName: import("../../config").ConfigValueProvider<string>;
13
+ encryptionPrivateKey: import("../../config").ConfigValueProvider<string>;
14
+ signaturePublicKey: import("../../config").ConfigValueProvider<string>;
15
+ }; }) => CookieAuthProvider;
16
+ export {};