@openstax/ts-utils 1.46.0 → 1.48.0

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 (45) hide show
  1. package/dist/cjs/misc/hashValue.js +2 -2
  2. package/dist/cjs/misc/jwks.d.ts +5 -2
  3. package/dist/cjs/services/apiGateway/index.js +1 -2
  4. package/dist/cjs/services/assignmentsGateway/index.d.ts +26 -0
  5. package/dist/cjs/services/assignmentsGateway/index.js +31 -0
  6. package/dist/cjs/services/authProvider/index.js +3 -6
  7. package/dist/cjs/services/authProvider/utils/userSubrequest.js +2 -5
  8. package/dist/cjs/services/fileServer/localFileServer.js +2 -3
  9. package/dist/cjs/services/fileServer/s3FileServer.js +2 -2
  10. package/dist/cjs/services/httpMessage/verifier.d.ts +2 -2
  11. package/dist/cjs/services/keyStore/index.d.ts +3 -6
  12. package/dist/cjs/services/keyStore/index.js +20 -9
  13. package/dist/cjs/services/launchParams/verifier.d.ts +2 -2
  14. package/dist/cjs/services/lrsGateway/addStatementDefaultFields.js +1 -2
  15. package/dist/cjs/services/lrsGateway/file-system.js +2 -2
  16. package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
  17. package/dist/esm/misc/hashValue.js +2 -2
  18. package/dist/esm/misc/jwks.d.ts +5 -2
  19. package/dist/esm/services/apiGateway/index.js +1 -2
  20. package/dist/esm/services/assignmentsGateway/index.d.ts +26 -0
  21. package/dist/esm/services/assignmentsGateway/index.js +27 -0
  22. package/dist/esm/services/authProvider/index.js +3 -3
  23. package/dist/esm/services/authProvider/utils/userSubrequest.js +2 -2
  24. package/dist/esm/services/fileServer/localFileServer.js +2 -3
  25. package/dist/esm/services/fileServer/s3FileServer.js +2 -2
  26. package/dist/esm/services/httpMessage/verifier.d.ts +2 -2
  27. package/dist/esm/services/keyStore/index.d.ts +3 -6
  28. package/dist/esm/services/keyStore/index.js +20 -9
  29. package/dist/esm/services/launchParams/verifier.d.ts +2 -2
  30. package/dist/esm/services/lrsGateway/addStatementDefaultFields.js +1 -2
  31. package/dist/esm/services/lrsGateway/file-system.js +2 -2
  32. package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
  33. package/package.json +16 -15
  34. package/script/bin/delete-stack.bash +2 -2
  35. package/script/bin/deploy.bash +14 -3
  36. package/script/bin/destroy-deployment.bash +4 -4
  37. package/script/bin/get-deployed-environments.bash +1 -1
  38. package/script/bin/get-env-param.bash +4 -4
  39. package/script/bin/init-constants-script.bash +1 -1
  40. package/script/bin/init-params-script.bash +1 -1
  41. package/script/bin/update-utils.bash +3 -3
  42. package/script/bin/upload-pager-duty-endpoints.bash +3 -3
  43. package/script/bin/upload-params.bash +5 -5
  44. package/script/bin-entry.bash +1 -1
  45. package/script/build.bash +4 -4
@@ -1,4 +1,4 @@
1
- import { createHash } from 'crypto';
1
+ import { sha1 } from 'js-sha1';
2
2
  /**
3
3
  * creates a string hash of lots of different kinds of things.
4
4
  *
@@ -10,5 +10,5 @@ export const hashValue = (value) => {
10
10
  const allKeys = new Set();
11
11
  JSON.stringify(value, (k, v) => (allKeys.add(k), v));
12
12
  const strValue = (_a = JSON.stringify(value, Array.from(allKeys).sort())) !== null && _a !== void 0 ? _a : 'undefined';
13
- return createHash('sha1').update(strValue).digest('hex');
13
+ return sha1(strValue);
14
14
  };
@@ -1,7 +1,10 @@
1
+ import type { webcrypto } from 'crypto';
1
2
  import { JwksClient } from 'jwks-rsa';
2
- import type { JWK } from 'node-jose';
3
+ export type PublicJsonWebKey = webcrypto.JsonWebKey & {
4
+ kid: string;
5
+ };
3
6
  export type JwksFetcher = (uri: string) => Promise<{
4
- keys: JWK.RawKey[];
7
+ keys: PublicJsonWebKey[];
5
8
  }>;
6
9
  export declare const getJwksClient: (jwksUri: string, fetcher?: JwksFetcher) => JwksClient;
7
10
  export declare const getJwksKey: (iss: string, kid?: string, fetcher?: JwksFetcher) => Promise<import("jwks-rsa").SigningKey>;
@@ -1,6 +1,5 @@
1
1
  import * as pathToRegexp from 'path-to-regexp';
2
2
  import queryString from 'query-string';
3
- import { v4 as uuid } from 'uuid';
4
3
  import { resolveConfigValue } from '../../config/index.js';
5
4
  import { SessionExpiredError, UnauthorizedError } from '../../errors/index.js';
6
5
  import { fetchStatusRetry } from '../../fetch/fetchStatusRetry.js';
@@ -32,7 +31,7 @@ const makeRouteClient = (initializer, config, route, app) => {
32
31
  const url = await renderUrl({ params, query });
33
32
  const body = payload ? JSON.stringify(payload) : undefined;
34
33
  const baseOptions = merge((await ((_a = app === null || app === void 0 ? void 0 : app.authProvider) === null || _a === void 0 ? void 0 : _a.getAuthorizedFetchConfig())) || {}, fetchConfig || {});
35
- const requestId = uuid();
34
+ const requestId = globalThis.crypto.randomUUID();
36
35
  const requestLogger = (_b = app === null || app === void 0 ? void 0 : app.logger) === null || _b === void 0 ? void 0 : _b.createSubContext();
37
36
  requestLogger === null || requestLogger === void 0 ? void 0 : requestLogger.setContext({ requestId, url, timeStamp: new Date().getTime() });
38
37
  const fetcher = fetchStatusRetry(fetch, { retries: 1, status: [502], logger: requestLogger });
@@ -0,0 +1,26 @@
1
+ import { ConfigProviderForConfig } from '../../config/index.js';
2
+ import { GenericFetch } from '../../fetch/index.js';
3
+ import { HttpMessageSigner } from '../httpMessage/signer.js';
4
+ export type Config = {
5
+ assignmentsBase: string;
6
+ };
7
+ export type RawMinMaxScore = {
8
+ raw: number;
9
+ min: number;
10
+ max: number;
11
+ };
12
+ interface Initializer<C> {
13
+ configSpace?: C;
14
+ fetch: GenericFetch;
15
+ httpMessageSigner: HttpMessageSigner;
16
+ }
17
+ export declare const assignmentsGateway: <C extends string = "assignments">(initializer: Initializer<C>) => (configProvider: { [_key in C]: ConfigProviderForConfig<Config>; }) => {
18
+ completeAttempt: (launchToken: string, variant?: {
19
+ score: RawMinMaxScore;
20
+ } | {
21
+ pendingScore: true;
22
+ }) => Promise<Record<string, never>>;
23
+ scoreAttempt: (launchToken: string, score: RawMinMaxScore) => Promise<Record<string, never>>;
24
+ };
25
+ export type AssignmentsGateway = ReturnType<ReturnType<typeof assignmentsGateway>>;
26
+ export {};
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+ import { resolveConfigValue } from '../../config/index.js';
3
+ import { ifDefined } from '../../guards/index.js';
4
+ import { once } from '../../misc/helpers.js';
5
+ import { METHOD } from '../../routing/index.js';
6
+ const emptySchema = z.object({});
7
+ export const assignmentsGateway = (initializer) => (configProvider) => {
8
+ const config = configProvider[ifDefined(initializer.configSpace, 'assignments')];
9
+ const assignmentsBase = once(async () => (await resolveConfigValue(config.assignmentsBase)).replace(/\/+$/, ''));
10
+ const request = async (path, body, expectedStatus, schema, extraHeaders) => {
11
+ const url = `${await assignmentsBase()}${path}`;
12
+ const bodyStr = JSON.stringify(body);
13
+ const signedHeaders = await initializer.httpMessageSigner.signRequest(METHOD.POST, url, bodyStr);
14
+ const response = await initializer.fetch(url, {
15
+ method: METHOD.POST,
16
+ headers: { 'Content-Type': 'application/json', ...extraHeaders, ...signedHeaders },
17
+ body: bodyStr,
18
+ });
19
+ if (response.status !== expectedStatus) {
20
+ throw new Error(`assignments returned ${response.status}: ${await response.text()}`);
21
+ }
22
+ return schema.parse(await response.json());
23
+ };
24
+ const completeAttempt = (launchToken, variant) => request('/api/v0/integrations/complete-attempt', { launchToken, ...(variant !== null && variant !== void 0 ? variant : {}) }, 201, emptySchema);
25
+ const scoreAttempt = (launchToken, score) => request('/api/v0/integrations/score-attempt', { launchToken, score }, 201, emptySchema);
26
+ return { completeAttempt, scoreAttempt };
27
+ };
@@ -1,4 +1,4 @@
1
- import cookie from 'cookie';
1
+ import { parse as parseCookie, serialize as serializeCookie } from 'cookie';
2
2
  import { tuple } from '../../misc/helpers.js';
3
3
  import { getHeader } from '../../routing/helpers.js';
4
4
  export const stubAuthProvider = (user) => {
@@ -15,12 +15,12 @@ export const getAuthTokenOrCookie = (request, cookieName, queryKey = 'auth') =>
15
15
  var _a, _b;
16
16
  const authParam = request.queryStringParameters ? request.queryStringParameters[queryKey] : undefined;
17
17
  const authHeader = getHeader(request.headers, 'authorization');
18
- const cookieValue = cookie.parse((_b = (_a = request.cookies) === null || _a === void 0 ? void 0 : _a.join('; ')) !== null && _b !== void 0 ? _b : '')[cookieName];
18
+ const cookieValue = parseCookie((_b = (_a = request.cookies) === null || _a === void 0 ? void 0 : _a.join('; ')) !== null && _b !== void 0 ? _b : '')[cookieName];
19
19
  return typeof authParam === 'string'
20
20
  ? tuple(authParam, { Authorization: `Bearer ${authParam}` })
21
21
  : authHeader && authHeader.length >= 8 && authHeader.startsWith('Bearer ')
22
22
  ? tuple(authHeader.slice(7), { Authorization: authHeader })
23
23
  : cookieValue
24
- ? tuple(cookieValue, { cookie: cookie.serialize(cookieName, cookieValue) })
24
+ ? tuple(cookieValue, { cookie: serializeCookie(cookieName, cookieValue) })
25
25
  : tuple(null, {});
26
26
  };
@@ -1,6 +1,6 @@
1
- import cookie from 'cookie';
1
+ import { serialize as serializeCookie } from 'cookie';
2
2
  export const loadUserData = (fetch, accountsBase, cookieName, token) => {
3
- const headers = { cookie: cookie.serialize(cookieName, token) };
3
+ const headers = { cookie: serializeCookie(cookieName, token) };
4
4
  // this returns `{"error_id":null}` when the token is invalid
5
5
  return fetch(accountsBase.replace(/\/+$/, '') + '/api/user', { headers })
6
6
  .then(response => response.json())
@@ -5,7 +5,6 @@ import https from 'https';
5
5
  import path from 'path';
6
6
  import { pipeline } from 'stream/promises';
7
7
  import Busboy from 'busboy';
8
- import { v4 as uuid } from 'uuid';
9
8
  import { assertString } from '../../assertions/index.js';
10
9
  import { resolveConfigValue } from '../../config/index.js';
11
10
  import { ifDefined } from '../../guards/index.js';
@@ -39,7 +38,7 @@ const handleUpload = (req, res, uploadDir) => {
39
38
  });
40
39
  busboy.on('file', (_name, file, info) => {
41
40
  originalName = info.filename;
42
- tempPath = path.join(uploadDir, uuid());
41
+ tempPath = path.join(uploadDir, crypto.randomUUID());
43
42
  file.pipe(fs.createWriteStream(tempPath));
44
43
  });
45
44
  busboy.on('finish', async () => {
@@ -133,7 +132,7 @@ export const localFileServer = (initializer) => (configProvider) => {
133
132
  return source;
134
133
  };
135
134
  const getSignedFileUploadConfig = async () => {
136
- const prefix = 'uploads/' + uuid();
135
+ const prefix = 'uploads/' + crypto.randomUUID();
137
136
  return {
138
137
  url: `https://${await host}:${await port}/`,
139
138
  payload: {
@@ -1,9 +1,9 @@
1
1
  /* spell-checker: ignore presigner */
2
+ import { randomUUID } from 'crypto';
2
3
  import path from 'path';
3
4
  import { CopyObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
4
5
  import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
5
6
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
6
- import { v4 as uuid } from 'uuid';
7
7
  import { assertDefined } from '../../assertions/index.js';
8
8
  import { resolveConfigValue } from '../../config/index.js';
9
9
  import { ifDefined } from '../../guards/index.js';
@@ -57,7 +57,7 @@ export const s3FileServer = (initializer) => (configProvider) => {
57
57
  * https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html
58
58
  */
59
59
  const getSignedFileUploadConfig = async () => {
60
- const prefix = 'uploads/' + uuid();
60
+ const prefix = 'uploads/' + randomUUID();
61
61
  const bucket = (await bucketName());
62
62
  const Conditions = [
63
63
  { acl: 'private' },
@@ -1,7 +1,7 @@
1
1
  import { APIGatewayProxyEventV2 } from 'aws-lambda';
2
2
  import { VerifyConfig } from 'http-message-signatures';
3
- import { JWK } from 'node-jose';
4
3
  import { ConfigProviderForConfig } from '../../config/index.js';
4
+ import { type PublicJsonWebKey } from '../../misc/jwks.js';
5
5
  type Config = {
6
6
  apiHost: string;
7
7
  bypassSignatureVerification: string;
@@ -9,7 +9,7 @@ type Config = {
9
9
  interface Initializer<C> {
10
10
  configSpace?: C;
11
11
  fetcher?: (uri: string) => Promise<{
12
- keys: JWK.RawKey[];
12
+ keys: PublicJsonWebKey[];
13
13
  }>;
14
14
  }
15
15
  export type SignatureAgentVerifier = (signatureAgent: string) => boolean | Promise<boolean>;
@@ -1,5 +1,5 @@
1
- import { JWK } from 'node-jose';
2
1
  import { ConfigProviderForConfig } from '../../config/index.js';
2
+ import type { PublicJsonWebKey } from '../../misc/jwks.js';
3
3
  type Config = {
4
4
  privateKey: string;
5
5
  };
@@ -8,12 +8,9 @@ export type PrivateKey = {
8
8
  pem: string;
9
9
  };
10
10
  export declare const createKeyStore: (config: ConfigProviderForConfig<Config>) => {
11
- getPrivateKey: () => Promise<{
12
- kid: string;
13
- pem: string;
14
- }>;
11
+ getPrivateKey: () => Promise<PrivateKey>;
15
12
  jwks: () => Promise<{
16
- keys: JWK.RawKey[];
13
+ keys: PublicJsonWebKey[];
17
14
  }>;
18
15
  };
19
16
  export type KeyStore = ReturnType<typeof createKeyStore>;
@@ -1,23 +1,34 @@
1
- import { JWK } from 'node-jose';
1
+ import { createHash, createPublicKey } from 'crypto';
2
2
  import { resolveConfigValue } from '../../config/index.js';
3
3
  import { once } from '../../misc/helpers.js';
4
+ // RFC 7638 required members per key type, in lexicographic order
5
+ const thumbprintMembers = {
6
+ RSA: ['e', 'kty', 'n'],
7
+ EC: ['crv', 'kty', 'x', 'y'],
8
+ OKP: ['crv', 'kty', 'x'],
9
+ oct: ['k', 'kty'],
10
+ };
11
+ const jwkThumbprint = (jwk) => {
12
+ const members = thumbprintMembers[jwk.kty];
13
+ const canonical = Object.fromEntries(members.map((m) => [m, jwk[m]]));
14
+ return createHash('sha256').update(JSON.stringify(canonical)).digest('base64url');
15
+ };
4
16
  export const createKeyStore = (config) => {
5
17
  const store = once(async () => {
6
18
  const pem = await resolveConfigValue(config.privateKey);
7
- const keyStore = JWK.createKeyStore();
8
- const { kid } = await keyStore.add(pem, 'pem');
9
- const keys = [{ kid, pem }];
10
- return { keyStore, keys };
19
+ const publicJwk = createPublicKey(pem).export({ format: 'jwk' });
20
+ const kid = jwkThumbprint(publicJwk);
21
+ return { pem, publicJwk, kid };
11
22
  });
12
23
  return {
13
24
  // this method only supports one key for now (always returns the first key)
14
25
  getPrivateKey: async () => {
15
- const { keys } = await store();
16
- return keys[0];
26
+ const { pem, kid } = await store();
27
+ return { kid, pem };
17
28
  },
18
29
  jwks: async () => {
19
- const { keyStore } = await store();
20
- return keyStore.toJSON(false);
30
+ const { publicJwk, kid } = await store();
31
+ return { keys: [{ ...publicJwk, kid }] };
21
32
  },
22
33
  };
23
34
  };
@@ -1,6 +1,6 @@
1
1
  import jwt from 'jsonwebtoken';
2
- import type { JWK } from 'node-jose';
3
2
  import { ConfigProviderForConfig } from '../../config/index.js';
3
+ import { type PublicJsonWebKey } from '../../misc/jwks.js';
4
4
  type Config = {
5
5
  trustedDomain: string;
6
6
  bypassSignatureVerification: string;
@@ -8,7 +8,7 @@ type Config = {
8
8
  interface Initializer<C> {
9
9
  configSpace?: C;
10
10
  fetcher?: (uri: string) => Promise<{
11
- keys: JWK.RawKey[];
11
+ keys: PublicJsonWebKey[];
12
12
  }>;
13
13
  }
14
14
  /**
@@ -1,8 +1,7 @@
1
1
  import formatISO from 'date-fns/formatISO';
2
- import { v4 as uuid } from 'uuid';
3
2
  import { formatAgent } from './attempt-utils.js';
4
3
  export const addStatementDefaultFields = (statement, user) => ({
5
- id: uuid(),
4
+ id: globalThis.crypto.randomUUID(),
6
5
  actor: formatAgent(user.uuid),
7
6
  timestamp: formatISO(new Date()),
8
7
  ...statement,
@@ -1,7 +1,7 @@
1
+ import { randomUUID } from 'crypto';
1
2
  import * as fsModule from 'fs';
2
3
  import path from 'path';
3
4
  import formatISO from 'date-fns/formatISO';
4
- import { v4 as uuid } from 'uuid';
5
5
  import { assertDefined } from '../../assertions/index.js';
6
6
  import { resolveConfigValue } from '../../config/index.js';
7
7
  import { UnauthorizedError } from '../../errors/index.js';
@@ -69,7 +69,7 @@ export const fileSystemLrsGateway = (initializer) => (configProvider) => ({ auth
69
69
  : assertDefined(await authProvider.getUser(), new UnauthorizedError);
70
70
  const statementsWithDefaults = statements.map(statement => ({
71
71
  ...statement,
72
- id: uuid(),
72
+ id: randomUUID(),
73
73
  actor: formatAgent(author.uuid),
74
74
  timestamp: formatISO(new Date()),
75
75
  }));