├── docs ├── .gitkeep └── index.md ├── downloaded └── .gitkeep ├── src ├── global.d.ts ├── checks │ ├── add │ │ ├── index.ts │ │ └── addPin.ts │ ├── edit │ │ ├── index.ts │ │ └── replacePin.ts │ ├── delete │ │ ├── index.ts │ │ ├── deleteNewPin.ts │ │ └── deleteAllPins.ts │ ├── auth │ │ ├── index.ts │ │ ├── checkInvalidBearerToken.ts │ │ └── checkEmptyBearerToken.ts │ ├── get │ │ ├── index.ts │ │ ├── getAllPins.ts │ │ ├── matchPin.ts │ │ └── testPagination.ts │ └── index.ts ├── guards │ ├── index.ts │ ├── isError.ts │ └── isResponse.ts ├── cli │ ├── options │ │ ├── index.ts │ │ ├── debug.ts │ │ ├── verbose.ts │ │ └── serviceAndToken.ts │ └── index.ts ├── utils │ ├── getHostnameFromUrl.ts │ ├── waitForDate.ts │ ├── sleep.ts │ ├── knownFailureStatusPin.ts │ ├── getOldestPinCreateDate.ts │ ├── markdownLinkToTextLabel.ts │ ├── getQueue.ts │ ├── getInlineCid.ts │ ├── fetchSafe │ │ ├── getTextAndJson.ts │ │ └── getText.ts │ ├── getIpfsClient.ts │ ├── stringifyHeaders.ts │ ├── report.ts │ ├── gitHash.ts │ ├── pinTracker.ts │ ├── getRequestid.ts │ ├── constants.ts │ ├── getJoiSchema.ts │ └── logs.ts ├── output │ ├── getSuccessIcon.ts │ ├── linkToCommit.ts │ ├── linkToNpm.ts │ ├── linkToGithubRepo.ts │ ├── complianceCheckHeader.ts │ ├── formatter.ts │ ├── getErrorsMarkdown.ts │ ├── getExpectationsMarkdown.ts │ ├── joiValidationAsMarkdown.ts │ ├── writeJsonResults.ts │ ├── linkToHeading.ts │ ├── getReportEntry.ts │ ├── getHeader.ts │ └── reporting.ts ├── expectations │ └── index.ts ├── clientFromServiceAndTokenPair.ts ├── index.ts ├── types.d.ts ├── middleware │ └── requestReponseLogger.ts └── ApiCall.ts ├── .tool-versions ├── .gitignore ├── LICENSE ├── tsconfig.json ├── .github ├── dependabot.yml ├── workflows │ ├── semantic-pull-request.yml │ ├── stale.yml │ ├── generated-pr.yml │ ├── on-schedule-weekly-tuesday.yml │ ├── on-main-PR.yml │ ├── js-test-and-release.yml │ ├── on-schedule-weekly-saturday.yml │ ├── on-main-push.yml │ ├── build-all-platforms.yml │ ├── codeql-analysis.yml │ ├── automerge.yml │ ├── publish-reports.yml │ ├── check-compliance.yml │ └── build-reports.yml └── CODEOWNERS ├── .aegir.js ├── .env-copy ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── package.json └── CHANGELOG.md /docs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /downloaded/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'oas2joi'; 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | direnv 2.30.3 2 | nodejs 18.16.0 3 | -------------------------------------------------------------------------------- /src/checks/add/index.ts: -------------------------------------------------------------------------------- 1 | export * from './addPin.js' 2 | -------------------------------------------------------------------------------- /src/checks/edit/index.ts: -------------------------------------------------------------------------------- 1 | export * from './replacePin.js' 2 | -------------------------------------------------------------------------------- /src/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './isResponse.js' 2 | export * from './isError.js' 3 | -------------------------------------------------------------------------------- /src/checks/delete/index.ts: -------------------------------------------------------------------------------- 1 | export * from './deleteAllPins.js' 2 | export * from './deleteNewPin.js' 3 | -------------------------------------------------------------------------------- /src/checks/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './checkEmptyBearerToken.js' 2 | export * from './checkInvalidBearerToken.js' 3 | -------------------------------------------------------------------------------- /src/checks/get/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getAllPins.js' 2 | export * from './testPagination.js' 3 | export * from './matchPin.js' 4 | -------------------------------------------------------------------------------- /src/cli/options/index.ts: -------------------------------------------------------------------------------- 1 | export * from './serviceAndToken.js' 2 | export * from './verbose.js' 3 | export * from './debug.js' 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | .docs 5 | .coverage 6 | node_modules 7 | package-lock.json 8 | yarn.lock 9 | .vscode 10 | .env 11 | -------------------------------------------------------------------------------- /src/guards/isError.ts: -------------------------------------------------------------------------------- 1 | const isError = (potentialError: Error | unknown): potentialError is Error => potentialError instanceof Error 2 | 3 | export { isError } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This project is dual licensed under MIT and Apache-2.0. 2 | 3 | MIT: https://www.opensource.org/licenses/mit 4 | Apache-2.0: https://www.apache.org/licenses/license-2.0 5 | -------------------------------------------------------------------------------- /src/utils/getHostnameFromUrl.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | 3 | const getHostnameFromUrl = (url: string): string => new URL(url).hostname 4 | 5 | export { getHostnameFromUrl } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "aegir/src/config/tsconfig.aegir.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | }, 6 | "include": [ 7 | "src", 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /src/output/getSuccessIcon.ts: -------------------------------------------------------------------------------- 1 | import { Icons } from '../utils/constants.js' 2 | 3 | const getSuccessIcon = (success: boolean): string => success ? Icons.SUCCESS : Icons.FAILURE 4 | 5 | export { getSuccessIcon } 6 | -------------------------------------------------------------------------------- /src/checks/index.ts: -------------------------------------------------------------------------------- 1 | // export * from './Check' 2 | export * from './add/index.js' 3 | export * from './auth/index.js' 4 | export * from './delete/index.js' 5 | export * from './edit/index.js' 6 | export * from './get/index.js' 7 | -------------------------------------------------------------------------------- /src/output/linkToCommit.ts: -------------------------------------------------------------------------------- 1 | import { sourceRepoUrl } from '../utils/constants.js' 2 | 3 | const linkToCommit = (revision: string): string => `[${revision}](${sourceRepoUrl}/commit/${revision})` 4 | 5 | export { linkToCommit } 6 | -------------------------------------------------------------------------------- /src/output/linkToNpm.ts: -------------------------------------------------------------------------------- 1 | import { packageName } from '../utils/constants.js' 2 | 3 | const linkToNpm = (version = packageName): string => `[${version}](https://www.npmjs.com/package/${packageName}/v/${version})` 4 | 5 | export { linkToNpm } 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 20 9 | commit-message: 10 | prefix: "deps" 11 | prefix-development: "deps(dev)" 12 | -------------------------------------------------------------------------------- /src/output/linkToGithubRepo.ts: -------------------------------------------------------------------------------- 1 | import { sourceRepoUrl } from '../utils/constants.js' 2 | 3 | const linkToGithubRepo = (displayText: string, pathSuffix?: string): string => `[${displayText}](${sourceRepoUrl}${pathSuffix != null ? `/${pathSuffix}` : ''})` 4 | 5 | export { linkToGithubRepo } 6 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Semantic PR 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | uses: pl-strflt/.github/.github/workflows/reusable-semantic-pull-request.yml@v0.3 13 | -------------------------------------------------------------------------------- /src/utils/waitForDate.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from './sleep.js' 2 | 3 | const waitForDate = async (date: Date): Promise => { 4 | const now = new Date() 5 | const delta = date.getTime() - now.getTime() 6 | 7 | // add 2 seconds as a buffer 8 | await sleep(delta + 2000) 9 | } 10 | export { waitForDate } 11 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close Stale Issues 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-stale-issue.yml@v1 15 | -------------------------------------------------------------------------------- /.github/workflows/generated-pr.yml: -------------------------------------------------------------------------------- 1 | name: Close Generated PRs 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-generated-pr.yml@v1 15 | -------------------------------------------------------------------------------- /src/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | import { logger } from './logs.js' 2 | 3 | const sleep = async (delay: number): Promise => { 4 | logger.debug(`starting sleep for ${delay / 1000} seconds...`) 5 | await new Promise((resolve) => setTimeout(() => { 6 | logger.debug('done waiting') 7 | resolve() 8 | }, delay)) 9 | } 10 | 11 | export { sleep } 12 | -------------------------------------------------------------------------------- /src/cli/options/debug.ts: -------------------------------------------------------------------------------- 1 | const debug = { 2 | alias: 'd', 3 | boolean: true, 4 | description: 'Whether you want debug output or not', 5 | nargs: 0, 6 | requiresArg: false, 7 | default: false, 8 | coerce: (debug: boolean) => { 9 | if (debug != null) { 10 | return Boolean(debug) 11 | } 12 | return false 13 | } 14 | } 15 | 16 | export { debug } 17 | -------------------------------------------------------------------------------- /src/cli/options/verbose.ts: -------------------------------------------------------------------------------- 1 | const verbose = { 2 | alias: 'v', 3 | boolean: true, 4 | description: 'Whether you want verbose output or not', 5 | nargs: 0, 6 | requiresArg: false, 7 | default: false, 8 | coerce: (verbosity: boolean) => { 9 | if (verbosity != null) { 10 | return Boolean(verbosity) 11 | } 12 | return false 13 | } 14 | } 15 | 16 | export { verbose } 17 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | 3 | # These owners will be the default owners for everything in 4 | # the repo. Unless a later match takes precedence, 5 | # these owners will be requested for review when someone 6 | # opens a pull request. 7 | # All GUI Teams: @ipfs-shipyard/ipfs-gui @ipfs-shipyard/gui @ipfs/gui-dev @ipfs/wg-gui-ux 8 | * @ipfs-shipyard/gui 9 | -------------------------------------------------------------------------------- /src/output/complianceCheckHeader.ts: -------------------------------------------------------------------------------- 1 | import { Icons } from '../utils/constants.js' 2 | 3 | interface ComplianceCheckHeaderProps { 4 | title: string 5 | successful: boolean 6 | } 7 | const complianceCheckHeader = ({ title, successful }: ComplianceCheckHeaderProps): string => `${title} - ${successful ? `${Icons.SUCCESS} SUCCESS` : `${Icons.FAILURE} FAILED`}` 8 | 9 | export { complianceCheckHeader } 10 | -------------------------------------------------------------------------------- /src/utils/knownFailureStatusPin.ts: -------------------------------------------------------------------------------- 1 | import { Status } from '@ipfs-shipyard/pinning-service-client' 2 | import type { PinStatus } from '@ipfs-shipyard/pinning-service-client' 3 | 4 | const knownFailureStatusPin: PinStatus = { 5 | requestid: 'N/A', 6 | created: new Date(), 7 | status: Status.Failed, 8 | pin: { 9 | cid: 'fake' 10 | }, 11 | delegates: new Set() 12 | } 13 | 14 | export { knownFailureStatusPin } 15 | -------------------------------------------------------------------------------- /src/utils/getOldestPinCreateDate.ts: -------------------------------------------------------------------------------- 1 | import type { PinStatus } from '@ipfs-shipyard/pinning-service-client' 2 | 3 | const getOldestPinCreateDate = (pins: Set): Date => { 4 | let oldestCreateDate = new Date() 5 | pins.forEach((pin) => { 6 | if (pin.created < oldestCreateDate) { 7 | oldestCreateDate = pin.created 8 | } 9 | }) 10 | return oldestCreateDate 11 | } 12 | 13 | export { getOldestPinCreateDate } 14 | -------------------------------------------------------------------------------- /src/guards/isResponse.ts: -------------------------------------------------------------------------------- 1 | const isResponse = (potentialResponse: Response | unknown): potentialResponse is Response => { 2 | const { ok, body, url, redirected, status, statusText, headers } = potentialResponse as Response 3 | 4 | return ok != null && 5 | body != null && 6 | url != null && 7 | redirected != null && 8 | status != null && 9 | statusText != null && 10 | headers != null 11 | } 12 | 13 | export { isResponse } 14 | -------------------------------------------------------------------------------- /.aegir.js: -------------------------------------------------------------------------------- 1 | 2 | /** @type {import('aegir').PartialOptions} */ 3 | export default { 4 | docs: { 5 | publish: true, 6 | entryPoint: './docs' 7 | }, 8 | tsRepo: true, 9 | build: { 10 | types: true, 11 | config: { 12 | format: 'esm', 13 | platform: 'node', 14 | external: ['electron', '#ansi-styles', 'yargs/yargs', '#supports-color'] 15 | }, 16 | bundlesizeMax: '44KB', 17 | 18 | }, 19 | test: { 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/on-schedule-weekly-tuesday.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 2 | name: On every Tuesday 3 | 4 | on: 5 | workflow_dispatch: 6 | schedule: 7 | - cron: '30 12 * * 2' # Run at 12:30 on every Tuesday 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | uses: ./.github/workflows/codeql-analysis.yml 13 | -------------------------------------------------------------------------------- /.env-copy: -------------------------------------------------------------------------------- 1 | PINATA_API_ENDPOINT=https://api.pinata.cloud/psa 2 | PINATA_API_TOKEN= 3 | 4 | WEB3_API_ENDPOINT=https://api.web3.storage 5 | WEB3_API_TOKEN= 6 | 7 | ESTUARY_API_ENDPOINT=https://api.estuary.tech/pinning 8 | ESTUARY_API_TOKEN= 9 | 10 | NFT_API_ENDPOINT=https://nft.storage/api 11 | NFT_API_TOKEN= 12 | 13 | FOREVERLAND_API_ENDPOINT=https://api.4everland.dev 14 | FOREVERLAND_API_TOKEN= 15 | 16 | SCALEWAY_API_ENDPOINT=https://pl-waw.ipfs.labs.scw.cloud/ 17 | SCALEWAY_API_TOKEN= 18 | 19 | NODE_ENV=development 20 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 2 | 3 | http://www.apache.org/licenses/LICENSE-2.0 4 | 5 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 6 | -------------------------------------------------------------------------------- /src/utils/markdownLinkToTextLabel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @example 3 | * var foo = '[Report History](https://github.com/ipfs-shipyard/pinning-service-compliance/commits/gh-pages/api.pinata.cloud.md)' 4 | * var bar = markdownLinkToTextLabel(foo) 5 | * // bar = 'Report History: https://github.com/ipfs-shipyard/pinning-service-compliance/commits/gh-pages/api.pinata.cloud.md' 6 | */ 7 | const markdownLinkToTextLabel = (markdownLink: string): string => markdownLink.replace(/\[([^\]]+)\]\((.+)\)/, '$1: $2') 8 | 9 | export { markdownLinkToTextLabel } 10 | -------------------------------------------------------------------------------- /src/utils/getQueue.ts: -------------------------------------------------------------------------------- 1 | import PQueue from 'p-queue' 2 | 3 | type QueueInstance = InstanceType 4 | const queues = new Map() 5 | 6 | const getQueue = (endpointUrl: string, options: ConstructorParameters[0] = { concurrency: 1, intervalCap: 1, interval: 1000 }): PQueue => { 7 | if (queues.has(endpointUrl)) { 8 | return queues.get(endpointUrl) as PQueue 9 | } 10 | const newQueueLimiter = new PQueue(options) 11 | 12 | queues.set(endpointUrl, newQueueLimiter) 13 | 14 | return newQueueLimiter 15 | } 16 | 17 | export { getQueue } 18 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs' 2 | import { hideBin } from 'yargs/helpers' 3 | import { serviceAndToken, verbose, debug } from './options/index.js' 4 | import type { ServiceAndTokenPair } from '../types.js' 5 | 6 | const cli = yargs(hideBin(process.argv)).options({ 7 | serviceAndToken, 8 | verbose, 9 | debug 10 | }) 11 | 12 | /** 13 | * yargs typings can be extremely naive and incorrect 14 | */ 15 | const argv = cli.argv as { 16 | [x: string]: unknown 17 | serviceAndToken: ServiceAndTokenPair[] 18 | verbose: boolean 19 | debug: boolean 20 | _: Array 21 | $0: string 22 | } 23 | 24 | export { cli, argv } 25 | -------------------------------------------------------------------------------- /src/output/formatter.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { marked } from 'marked' 3 | import { markedTerminal } from 'marked-terminal' 4 | import type { TerminalRendererOptions } from 'marked-terminal' 5 | 6 | const getFormatter = (options: TerminalRendererOptions) => async (markdown: string) => { 7 | // @ts-expect-error types do not overlap 8 | marked.use(markedTerminal(options)) 9 | 10 | return marked.parse(markdown) 11 | } 12 | 13 | const red = getFormatter({ paragraph: chalk.red }) 14 | const regular = getFormatter({ paragraph: chalk.reset }) 15 | const bold = getFormatter({ paragraph: chalk.bold }) 16 | 17 | export { getFormatter, red, regular, bold } 18 | -------------------------------------------------------------------------------- /src/output/getErrorsMarkdown.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | import { Icons } from '../utils/constants.js' 3 | 4 | const errorToMarkdown = (error: Error): string => { 5 | let errorOutput = '' 6 | if (error.stack != null) { 7 | errorOutput = ` 8 | ${Icons.ERROR} ${error.stack}` 9 | } else if (error.name != null && error.message != null) { 10 | errorOutput = `${Icons.ERROR} ${error.name} - ${error.message}` 11 | } else { 12 | errorOutput = `${Icons.ERROR} ${inspect(error)}` 13 | } 14 | return errorOutput 15 | } 16 | const getErrorsMarkdown = (errors: Error[]): string => errors.map(errorToMarkdown).join('\n') 17 | 18 | export { getErrorsMarkdown } 19 | -------------------------------------------------------------------------------- /.github/workflows/on-main-PR.yml: -------------------------------------------------------------------------------- 1 | name: On PR to main branch 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | 10 | analyze: 11 | uses: ./.github/workflows/codeql-analysis.yml 12 | 13 | build: 14 | uses: ./.github/workflows/build-all-platforms.yml 15 | 16 | # see https://docs.github.com/en/actions/using-workflows/reusing-workflows#calling-a-reusable-workflow 17 | build_reports: 18 | needs: [build] 19 | uses: ./.github/workflows/build-reports.yml 20 | secrets: inherit 21 | 22 | automerge: 23 | needs: [build_reports, build] 24 | uses: ./.github/workflows/automerge.yml 25 | secrets: inherit 26 | -------------------------------------------------------------------------------- /src/checks/auth/checkInvalidBearerToken.ts: -------------------------------------------------------------------------------- 1 | import { ApiCall } from '../../ApiCall.js' 2 | import { responseCode } from '../../expectations/index.js' 3 | import { getJoiSchema } from '../../utils/getJoiSchema.js' 4 | import type { ServiceAndTokenPair } from '../../types.js' 5 | 6 | const checkInvalidBearerToken = async (pair: ServiceAndTokenPair): Promise => { 7 | const schema = await getJoiSchema('Failure') 8 | 9 | await new ApiCall({ 10 | pair: [pair[0], 'purposefullyInvalid'], 11 | fn: async (client) => client.pinsGet({}), 12 | schema, 13 | title: 'Request with invalid token' 14 | }) 15 | .expect(responseCode(401)) 16 | .runExpectations() 17 | } 18 | 19 | export { checkInvalidBearerToken } 20 | -------------------------------------------------------------------------------- /src/utils/getInlineCid.ts: -------------------------------------------------------------------------------- 1 | import { CID, bytes } from 'multiformats' 2 | import { code, encode } from 'multiformats/codecs/raw' 3 | import { sha256 } from 'multiformats/hashes/sha2' 4 | import { logger } from './logs.js' 5 | 6 | const { fromString } = bytes 7 | const getInlineCid = async (value: string = process.hrtime().toString()): Promise => { 8 | const inlineUint8Array = fromString(value) 9 | try { 10 | const bytes = encode(inlineUint8Array) 11 | const hash = await sha256.digest(bytes) 12 | const cid = CID.create(1, code, hash) 13 | return cid.toString() 14 | } catch (err) { 15 | logger.error('Problem creating an inline CID', err) 16 | throw err 17 | } 18 | } 19 | 20 | export { getInlineCid } 21 | -------------------------------------------------------------------------------- /src/checks/auth/checkEmptyBearerToken.ts: -------------------------------------------------------------------------------- 1 | import { ApiCall } from '../../ApiCall.js' 2 | import { responseCode } from '../../expectations/index.js' 3 | import { getJoiSchema } from '../../utils/getJoiSchema.js' 4 | import type { ServiceAndTokenPair } from '../../types.js' 5 | 6 | const checkEmptyBearerToken = async (pair: ServiceAndTokenPair): Promise => { 7 | const schema = await getJoiSchema('Failure') 8 | 9 | const apiCall = new ApiCall({ 10 | pair: [pair[0], undefined], 11 | fn: async (client) => client.pinsGet({}), 12 | schema, 13 | title: 'Request with no authentication token' 14 | }) 15 | 16 | apiCall.expect(responseCode(401)) 17 | 18 | await apiCall.runExpectations() 19 | } 20 | 21 | export { checkEmptyBearerToken } 22 | -------------------------------------------------------------------------------- /src/cli/options/serviceAndToken.ts: -------------------------------------------------------------------------------- 1 | import type { ServiceAndTokenPair } from '../../types.js' 2 | 3 | const serviceAndToken = { 4 | alias: 's', 5 | description: 'An ipfs pinning service endpoint and the secret token for that endpoint', 6 | nargs: 2, 7 | requiresArg: true, 8 | coerce: (pinningServicePairs: string[]): ServiceAndTokenPair[] => { 9 | const coercedPairs: ServiceAndTokenPair[] = [] 10 | if (pinningServicePairs == null) { 11 | return coercedPairs 12 | } 13 | for (let i = 0; i < pinningServicePairs.length; i += 2) { 14 | const [service, key] = pinningServicePairs.slice(i, i + 2) 15 | coercedPairs.push([service, key]) 16 | } 17 | return coercedPairs 18 | } 19 | } 20 | 21 | export { serviceAndToken } 22 | -------------------------------------------------------------------------------- /src/output/getExpectationsMarkdown.ts: -------------------------------------------------------------------------------- 1 | import { Icons } from '../utils/constants.js' 2 | import type { ComplianceCheckDetails, PinsApiResponseTypes } from '../types.js' 3 | 4 | const getExpectationsMarkdown = (details: ComplianceCheckDetails): string => { 5 | let checks = 0 6 | let successes = 0 7 | const lineItems = details.expectationResults.map(({ title, success, error }) => { 8 | checks++ 9 | if (success) { 10 | successes++ 11 | return `${Icons.SUCCESS} ${title} (success)` 12 | } 13 | return `${Icons.FAILURE} ${title} (failure)` 14 | }) 15 | 16 | return `### Expectations (${successes}/${checks} successful) 17 | 18 | ${lineItems.join('\n\n ')} 19 | ` 20 | } 21 | 22 | export { getExpectationsMarkdown } 23 | -------------------------------------------------------------------------------- /src/checks/get/getAllPins.ts: -------------------------------------------------------------------------------- 1 | import { ApiCall } from '../../ApiCall.js' 2 | import { responseCode, responseOk } from '../../expectations/index.js' 3 | import { allPinStatuses } from '../../utils/constants.js' 4 | import { getJoiSchema } from '../../utils/getJoiSchema.js' 5 | import type { ServiceAndTokenPair } from '../../types.js' 6 | 7 | const getAllPins = async (pair: ServiceAndTokenPair): Promise => { 8 | const schema = await getJoiSchema('PinResults') 9 | 10 | await new ApiCall({ 11 | pair, 12 | title: 'List pin objects (GET /pins) in all states', 13 | fn: async (client) => client.pinsGet({ status: allPinStatuses }), 14 | schema 15 | }).expect(responseOk()) 16 | .expect(responseCode(200)) 17 | .runExpectations() 18 | } 19 | 20 | export { getAllPins } 21 | -------------------------------------------------------------------------------- /src/output/joiValidationAsMarkdown.ts: -------------------------------------------------------------------------------- 1 | import type { ValidationError, ValidationResult } from '@hapi/joi' 2 | 3 | const joiValidationAsMarkdown = (validationResult: ValidationResult | null): string => { 4 | let joiValidationFailures: string = 'No failures' 5 | if (validationResult?.errors != null || validationResult?.error != null) { 6 | // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 7 | const errors = (validationResult.errors ?? validationResult.error) as ValidationError 8 | joiValidationFailures = '' 9 | 10 | errors.details.forEach((errorItem) => { 11 | joiValidationFailures = `${joiValidationFailures} 12 | * ${errorItem.message} 13 | ` 14 | }) 15 | } 16 | return joiValidationFailures 17 | } 18 | export { joiValidationAsMarkdown } 19 | -------------------------------------------------------------------------------- /src/utils/fetchSafe/getTextAndJson.ts: -------------------------------------------------------------------------------- 1 | import { getText } from './getText.js' 2 | import type { ApiCall } from '../../ApiCall.js' 3 | import type { PinsApiResponseTypes } from '../../types.js' 4 | 5 | const getTextAndJson = async (response: ApiCall['response']): Promise<{ text: string | null, json: T | null, errors: Error[] }> => { 6 | const errors: Error[] = [] 7 | let text: string | null = null 8 | let json: T | null = null 9 | 10 | try { 11 | text = await getText(response) 12 | if (text != null) { 13 | try { 14 | json = JSON.parse(text) as T 15 | } catch (err) { 16 | errors.push(err as Error) 17 | } 18 | } 19 | } catch (err) { 20 | errors.push(err as Error) 21 | } 22 | 23 | return { 24 | text, 25 | json, 26 | errors 27 | } 28 | } 29 | 30 | export { getTextAndJson } 31 | -------------------------------------------------------------------------------- /.github/workflows/js-test-and-release.yml: -------------------------------------------------------------------------------- 1 | name: test & maybe release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: write 12 | id-token: write 13 | packages: write 14 | pull-requests: write 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | js-test-and-release: 22 | uses: ipdxco/unified-github-workflows/.github/workflows/js-test-and-release.yml@v1.0 23 | secrets: 24 | DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} 25 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | UCI_GITHUB_TOKEN: ${{ secrets.UCI_GITHUB_TOKEN }} 28 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 29 | -------------------------------------------------------------------------------- /src/expectations/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApiCallExpectation } from '../ApiCall.js' 2 | import type { PinsApiResponseTypes } from '../types.js' 3 | 4 | const addMsg = (msg?: string): string => `${msg != null ? `: ${msg}` : ''}` 5 | 6 | const responseOk = (msg?: string): ApiCallExpectation => ({ 7 | title: `Response is ok${addMsg(msg)}`, 8 | fn: ({ responseContext }) => responseContext.response.ok 9 | }) 10 | 11 | const resultNotNull = (msg?: string): ApiCallExpectation => ({ 12 | title: `Result is not null${addMsg(msg)}`, 13 | fn: ({ result }) => result != null 14 | }) 15 | 16 | const responseCode = (code: number, msg?: string): ApiCallExpectation => ({ 17 | title: `Response code is ${code}${addMsg(msg)}`, 18 | fn: ({ responseContext }) => responseContext.response.status === code 19 | }) 20 | 21 | export { 22 | responseOk, 23 | resultNotNull, 24 | responseCode 25 | } 26 | -------------------------------------------------------------------------------- /src/checks/add/addPin.ts: -------------------------------------------------------------------------------- 1 | import { Status } from '@ipfs-shipyard/pinning-service-client' 2 | import { ApiCall } from '../../ApiCall.js' 3 | import { getInlineCid } from '../../utils/getInlineCid.js' 4 | import { getJoiSchema } from '../../utils/getJoiSchema.js' 5 | import type { ServiceAndTokenPair } from '../../types.js' 6 | 7 | const addPin = async (pair: ServiceAndTokenPair): Promise => { 8 | const cid = await getInlineCid() 9 | const schema = await getJoiSchema('PinStatus') 10 | 11 | const apiCall = new ApiCall({ 12 | pair, 13 | fn: async (client) => client.pinsPost({ pin: { cid } }), 14 | schema, 15 | title: `Pins post of CID '${cid}'` 16 | }) 17 | 18 | apiCall.expect({ 19 | title: `Pinning status is either ${Status.Queued}, ${Status.Pinning}, or ${Status.Pinned}`, 20 | fn: ({ result }) => result !== null && result.status !== Status.Failed && [Status.Queued, Status.Pinning, Status.Pinned].includes(result.status) 21 | }) 22 | 23 | await apiCall.runExpectations() 24 | } 25 | 26 | export { addPin } 27 | -------------------------------------------------------------------------------- /src/output/writeJsonResults.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'fs/promises' 2 | import { join } from 'path' 3 | import { docsDir } from '../utils/constants.js' 4 | import { getHostnameFromUrl } from '../utils/getHostnameFromUrl.js' 5 | import { logger } from '../utils/logs.js' 6 | import { globalReport } from '../utils/report.js' 7 | import type { ServiceAndTokenPair } from '../types.js' 8 | 9 | const writeJsonResults = async (pair: ServiceAndTokenPair): Promise => { 10 | const { passed, failed } = globalReport 11 | const total = passed + failed 12 | const success = passed === total 13 | 14 | const hostname = getHostnameFromUrl(pair[0]) 15 | 16 | const jsonResultsFile = join(docsDir, `${hostname}.json`) 17 | 18 | const stringifiedJSON = JSON.stringify({ total, passed, failed, success }, null, 2) 19 | 20 | try { 21 | await writeFile(jsonResultsFile, stringifiedJSON) 22 | } catch (err) { 23 | logger.error(`Could not write JSON results to ${jsonResultsFile}. JSON is:\n ${stringifiedJSON}`) 24 | logger.error(err) 25 | } 26 | } 27 | 28 | export { writeJsonResults } 29 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/clientFromServiceAndTokenPair.ts: -------------------------------------------------------------------------------- 1 | import { RemotePinningServiceClient, Configuration } from '@ipfs-shipyard/pinning-service-client' 2 | import { requestResponseLogger, type RequestResponseLoggerOptions } from './middleware/requestReponseLogger.js' 3 | import type { ServiceAndTokenPair } from './types.js' 4 | 5 | function clientFromServiceAndTokenPair ([endpointUrl, accessToken]: ServiceAndTokenPair, middleWareOptions?: Omit): RemotePinningServiceClient { 6 | const requestResponseLoggerOptions: RequestResponseLoggerOptions = { ...middleWareOptions, pair: [endpointUrl, accessToken] } 7 | 8 | const config = new Configuration({ 9 | endpointUrl, 10 | accessToken, 11 | fetchApi: async (url: RequestInfo | URL, init?: RequestInit) => { 12 | return fetch(url, { 13 | ...init, 14 | signal: init?.signal ?? AbortSignal.timeout(60000) 15 | }) 16 | }, 17 | middleware: [ 18 | requestResponseLogger(requestResponseLoggerOptions) 19 | ] 20 | }) 21 | 22 | return new RemotePinningServiceClient(config) 23 | } 24 | 25 | export { clientFromServiceAndTokenPair } 26 | -------------------------------------------------------------------------------- /src/utils/getIpfsClient.ts: -------------------------------------------------------------------------------- 1 | import { createNode } from 'ipfsd-ctl' 2 | import { path } from 'kubo' 3 | import { create, type KuboRPCClient } from 'kubo-rpc-client' 4 | import { logger } from './logs.js' 5 | 6 | interface GetIpfsClientResponse { 7 | client: KuboRPCClient 8 | /** 9 | * A function that should be called when you're done using the client so cleanup can be performed. 10 | */ 11 | done(): Promise 12 | } 13 | const getIpfsClient = async (): Promise => { 14 | try { 15 | logger.debug('Attempting to use ipfsd-ctl to create a client') 16 | const ipfsd = await createNode({ 17 | type: 'kubo', 18 | rpc: create, 19 | bin: path(), 20 | test: true, 21 | disposable: true 22 | }) 23 | const client = ipfsd.api 24 | 25 | if (await client.isOnline()) { 26 | return { 27 | client, 28 | done: async () => { await ipfsd.stop() } 29 | } 30 | } 31 | throw new Error('Client is not online') 32 | } catch (err) { 33 | logger.error('Could not get ipfs client', err) 34 | throw err 35 | } 36 | } 37 | 38 | export { getIpfsClient } 39 | -------------------------------------------------------------------------------- /src/utils/stringifyHeaders.ts: -------------------------------------------------------------------------------- 1 | const sanitizeHeaders = (key: string, val: string): { key: string, val: string } => { 2 | if (/authorization/i.test(key)) { 3 | // Authorization: 4 | val = `${val.split(' ')[0]} REDACTED` 5 | } 6 | return { key, val } 7 | } 8 | const stringifyHeaders = (headers?: Headers | Record | string[][]): string => { 9 | const headerObj: Record = {} 10 | if (headers != null) { 11 | if (headers.forEach != null) { 12 | const actualHeaders = headers as Headers 13 | actualHeaders.forEach((val, key) => { 14 | const sanitized = sanitizeHeaders(key, val) 15 | headerObj[sanitized.key] = sanitized.val 16 | }) 17 | } else { 18 | const headersInit = headers as Record 19 | Object.keys(headersInit).forEach((key) => { 20 | const val = headersInit[key] 21 | const sanitized = sanitizeHeaders(key, val) 22 | headerObj[sanitized.key] = sanitized.val 23 | }) 24 | } 25 | } 26 | return JSON.stringify(headerObj, null, 2) 27 | } 28 | 29 | export { stringifyHeaders } 30 | -------------------------------------------------------------------------------- /.github/workflows/on-schedule-weekly-saturday.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 2 | name: On every Saturday 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | type: 8 | description: 'Emulate a push manually: Only do this if a manual report update is required!' 9 | required: false 10 | default: 'nothing' 11 | type: choice 12 | options: 13 | - schedule 14 | - nothing 15 | schedule: 16 | - cron: "0 0 * * 6" # Run at 00:00 on every Saturday 17 | 18 | jobs: 19 | # see https://docs.github.com/en/actions/using-workflows/reusing-workflows#calling-a-reusable-workflow 20 | build_reports: 21 | name: Build Compliance Reports 22 | uses: ./.github/workflows/build-reports.yml 23 | secrets: inherit 24 | 25 | publish_reports: 26 | name: Publish Compliance Reports 27 | needs: [build_reports] 28 | uses: ./.github/workflows/publish-reports.yml 29 | secrets: inherit 30 | with: 31 | type: ${{ github.event.inputs.type || github.event_name }} 32 | -------------------------------------------------------------------------------- /src/utils/report.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Eventually, this will house all the logic that is currently scattered throughout src/output/*.ts 3 | */ 4 | class Report { 5 | apiCallCount = 0 6 | totalExpectationsCount = 0 7 | runExpectationsCallCount = 0 8 | failed = 0 9 | passed = 0 10 | 11 | incrementApiCallsCount (): void { 12 | this.apiCallCount++ 13 | } 14 | 15 | incrementTotalExpectationsCount (): void { 16 | this.totalExpectationsCount++ 17 | } 18 | 19 | incrementRunExpectationsCallCount (): void { 20 | this.runExpectationsCallCount++ 21 | } 22 | 23 | incrementFailedExpectationsCount (): void { 24 | this.failed++ 25 | } 26 | 27 | incrementPassedExpectationsCount (): void { 28 | this.passed++ 29 | } 30 | 31 | toString (): string { 32 | return `The total counts for this run are: 33 | Total Expectations ${this.totalExpectationsCount} 34 | Total ApiCall instances ${this.apiCallCount} 35 | Total FirstClass ApiCalls ${this.runExpectationsCallCount} 36 | ` 37 | } 38 | } 39 | 40 | /** 41 | * Singleton report per run. 42 | */ 43 | const globalReport = new Report() 44 | 45 | export { Report, globalReport } 46 | -------------------------------------------------------------------------------- /src/utils/gitHash.ts: -------------------------------------------------------------------------------- 1 | import git from 'git-rev' 2 | import type { Revision } from '../types.js' 3 | 4 | /** 5 | * Provides the git hash of HEAD by default, in short format. 6 | * 7 | * This function will not work when called from npx, because the npm package doesn't come with the git repo. Please 8 | * catch any errors and display npm package version instead. 9 | * 10 | * @param {number} [fromHead=0] - How many commits from the head you want the hash of 11 | * 12 | * @returns {Revision} The hash of the requested commit 13 | */ 14 | const gitHash = async (fromHead = 0): Promise => { 15 | return new Promise((resolve, reject) => { 16 | try { 17 | if (fromHead === 0) { 18 | git.short((commitHash) => { resolve(commitHash) }) 19 | } else { 20 | // @see https://www.npmjs.com/package/git-rev#logfunction-array--- 21 | git.log((log) => { 22 | try { 23 | resolve(log[fromHead][0].slice(0, 7)) 24 | } catch (err: any) { 25 | reject(err as Error) 26 | } 27 | }) 28 | } 29 | } catch (err: any) { 30 | reject(err as Error) 31 | } 32 | }) 33 | } 34 | 35 | export { gitHash } 36 | -------------------------------------------------------------------------------- /src/output/linkToHeading.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a markdown link to a heading on the same page (hash link) 3 | * 4 | * This function attempts to create a link that matches the generated links occurring automatically when github renders a markdown document. 5 | * 6 | * @example 7 | * https://github.com/ipfs-shipyard/pinning-service-compliance/blob/f19db8c708b9a9ef72437db2510eaa45840d382c/nft.storage.md#can-create-and-then-delete-a-new-pin----success 8 | * 9 | * @param text - The text to use for the link 10 | * @param headerText - The text of the header we want to link to 11 | * 12 | * @returns {string} A Markdown link to the header in the form of [text](converted_headerText) 13 | */ 14 | const linkToHeading = (text: string, headerText = text): string => { 15 | const link = headerText 16 | .replace(/['=()/\\:,]/g, '') // remove invalid characters first 17 | .replace(/[🟢]/gu, '') // remove invalid characters first 18 | .replace(/[❌]/gu, '') // remove invalid characters first 19 | .replace(/[^a-zA-Z0-9]/g, '-') // replace any remaining non-alphanumeric characters with hyphens 20 | .toLowerCase() 21 | 22 | return `[${text}](#${link})` 23 | } 24 | 25 | export { linkToHeading } 26 | -------------------------------------------------------------------------------- /.github/workflows/on-main-push.yml: -------------------------------------------------------------------------------- 1 | name: On push to main branch 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | workflow_dispatch: 8 | inputs: 9 | type: 10 | description: 'Emulate a push manually: Only do this if semantic-release fails!' 11 | required: false 12 | default: 'nothing' 13 | type: choice 14 | options: 15 | - push 16 | - nothing 17 | 18 | jobs: 19 | 20 | analyze: 21 | uses: ./.github/workflows/codeql-analysis.yml 22 | 23 | build: 24 | uses: ./.github/workflows/build-all-platforms.yml 25 | 26 | release: 27 | needs: [build] 28 | uses: ./.github/workflows/js-test-and-release.yml 29 | secrets: inherit 30 | 31 | # see https://docs.github.com/en/actions/using-workflows/reusing-workflows#calling-a-reusable-workflow 32 | build_reports: 33 | name: Build Compliance Reports 34 | uses: ./.github/workflows/build-reports.yml 35 | secrets: inherit 36 | 37 | publish_reports: 38 | name: Publish Compliance Reports 39 | needs: [build_reports] 40 | uses: ./.github/workflows/publish-reports.yml 41 | secrets: inherit 42 | with: 43 | type: ${{ github.event.inputs.type || github.event_name }} 44 | 45 | -------------------------------------------------------------------------------- /.github/workflows/build-all-platforms.yml: -------------------------------------------------------------------------------- 1 | name: Build all platforms 2 | on: 3 | # see https://docs.github.com/en/actions/using-workflows/reusing-workflows#creating-a-reusable-workflow 4 | workflow_call: 5 | workflow_dispatch: 6 | inputs: 7 | type: 8 | description: 'Emulate either schedule, push, or pull_request' 9 | required: true 10 | default: 'schedule' 11 | type: choice 12 | options: 13 | - schedule 14 | - push 15 | - pull_request 16 | 17 | jobs: 18 | check: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: 20 25 | - uses: ipfs/aegir/actions/cache-node-modules@master 26 | - run: npm run --if-present lint 27 | - run: npm run --if-present dep-check 28 | 29 | build: 30 | runs-on: ${{ matrix.os }} 31 | strategy: 32 | matrix: 33 | os: [windows-latest, ubuntu-latest, macos-latest] 34 | fail-fast: false 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: actions/setup-node@v4 38 | with: 39 | node-version: 20 40 | - uses: ipfs/aegir/actions/cache-node-modules@master 41 | - run: npm run --if-present build 42 | -------------------------------------------------------------------------------- /src/checks/delete/deleteNewPin.ts: -------------------------------------------------------------------------------- 1 | import { ApiCall } from '../../ApiCall.js' 2 | import { responseCode, responseOk, resultNotNull } from '../../expectations/index.js' 3 | import { getInlineCid } from '../../utils/getInlineCid.js' 4 | import { getRequestid } from '../../utils/getRequestid.js' 5 | import type { ServiceAndTokenPair } from '../../types.js' 6 | 7 | const deleteNewPin = async (pair: ServiceAndTokenPair): Promise => { 8 | const cid = await getInlineCid() 9 | const createNewPinApiCall = new ApiCall({ 10 | pair, 11 | title: 'Can create and then delete a new pin', 12 | fn: async (client) => client.pinsPost({ pin: { cid } }) 13 | }) 14 | .expect(resultNotNull()) 15 | .expect(responseCode(202)) 16 | 17 | new ApiCall({ 18 | parent: createNewPinApiCall, 19 | pair, 20 | fn: async (client) => { 21 | const pin = await createNewPinApiCall.request 22 | const requestid = getRequestid(pin, createNewPinApiCall) 23 | await client.pinsRequestidDelete({ requestid }) 24 | }, 25 | title: 'The newly created pin can be immediately deleted' 26 | }) 27 | .expect(responseOk()) 28 | .expect(responseCode(202, 'The Pin was deleted successfully')) 29 | 30 | await createNewPinApiCall.runExpectations() 31 | } 32 | 33 | export { deleteNewPin } 34 | -------------------------------------------------------------------------------- /src/utils/fetchSafe/getText.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../logs.js' 2 | import { sleep } from '../sleep.js' 3 | import type { ApiCall } from '../../ApiCall.js' 4 | import type { PinsApiResponseTypes } from '../../types.js' 5 | 6 | const TIMEOUT_SECONDS = 5 7 | const handleLargeRequests = async (): Promise => sleep(TIMEOUT_SECONDS * 1000).then(() => { throw new Error(`Attempted to get text from response but it wasn't available within ${TIMEOUT_SECONDS} seconds.`) }) 8 | 9 | const getText = async (response: ApiCall['response']): Promise => { 10 | const actualTextPromise = new Promise((resolve, reject) => { 11 | response.clone().text().then((result) => { 12 | try { 13 | const obj = JSON.parse(result) 14 | const text = JSON.stringify(obj, null, 2) 15 | resolve(text) 16 | } catch { 17 | resolve(result) 18 | } 19 | }, (error: any) => { 20 | reject(error as Error) 21 | }) 22 | }) 23 | 24 | return Promise.race([handleLargeRequests(), actualTextPromise]).then( 25 | (result) => result as string, 26 | (error) => { 27 | logger.debug('Unexpected error when extracting text() from request') 28 | logger.debug(error) 29 | throw error 30 | }) 31 | } 32 | 33 | export { getText } 34 | -------------------------------------------------------------------------------- /src/utils/pinTracker.ts: -------------------------------------------------------------------------------- 1 | import { clientFromServiceAndTokenPair } from '../clientFromServiceAndTokenPair.js' 2 | import { allPinStatuses } from './constants.js' 3 | import type { ServiceAndTokenPair } from '../types.js' 4 | 5 | /** 6 | * Keep track of pins created by the compliance checker so we do not change/manipulate existing account pins. 7 | * 8 | * @see https://github.com/ipfs-shipyard/pinning-service-compliance/issues/118#issuecomment-1159050013 9 | */ 10 | class PinTracker extends Set { 11 | originalPinCount: number 12 | constructor (count: number) { 13 | super() 14 | this.originalPinCount = count 15 | } 16 | } 17 | 18 | const pinTrackerMap = new Map() 19 | 20 | const getPinTracker = async (pair: ServiceAndTokenPair): Promise => { 21 | const pairAsKey = pair.join('') 22 | let pinTracker: PinTracker | undefined = pinTrackerMap.get(pairAsKey) 23 | let count: number 24 | if (pinTracker == null) { 25 | const client = clientFromServiceAndTokenPair(pair) 26 | try { 27 | const pinResults = await client.pinsGet({ status: allPinStatuses }) 28 | count = pinResults.count 29 | } catch { 30 | count = 0 31 | } 32 | pinTracker = new PinTracker(count) 33 | pinTrackerMap.set(pairAsKey, pinTracker) 34 | } 35 | 36 | return pinTracker 37 | } 38 | 39 | export { PinTracker, getPinTracker } 40 | -------------------------------------------------------------------------------- /src/utils/getRequestid.ts: -------------------------------------------------------------------------------- 1 | import type { ApiCall } from '../ApiCall.js' 2 | import type { PinResults, PinStatus } from '@ipfs-shipyard/pinning-service-client' 3 | 4 | const getRequestid = (pin: PinStatus | null, apiCall: ApiCall | ApiCall): string => { 5 | if (pin == null) { 6 | return 'null' 7 | } 8 | let { requestid, pin: { cid } } = pin 9 | const pinResultsJson = apiCall.json as PinResults | null 10 | const pinStatusJson = apiCall.json as PinStatus | null 11 | if (requestid == null) { 12 | apiCall.logger.debug('Implementing workaround to get "requestid" from raw response') 13 | if (pinResultsJson?.results != null) { 14 | /** 15 | * This workaround is needed because web3.storage is returning requestid as 'requestId' as of 2022-04-12 16 | */ 17 | const rawPinStatus = Array.from(pinResultsJson.results).find((pinStatus) => pinStatus.pin.cid === cid) 18 | requestid = (rawPinStatus as PinStatus & { requestId: string }).requestId 19 | } else if (pinStatusJson?.pin != null) { 20 | if (pinStatusJson.pin.cid === cid) { 21 | requestid = (pinStatusJson as PinStatus & { requestId: string }).requestId 22 | } else { 23 | throw new Error(`CID for provided pin and raw apiCall results do not match! '${cid}' vs. '${pinStatusJson.pin.cid}'`) 24 | } 25 | } 26 | } 27 | return requestid 28 | } 29 | export { getRequestid } 30 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { resolve, dirname } from 'path' 2 | import { fileURLToPath } from 'url' 3 | import { Status } from '@ipfs-shipyard/pinning-service-client' 4 | 5 | const _filename = fileURLToPath(import.meta.url) 6 | const _dirname = dirname(_filename) 7 | 8 | const allPinStatuses = new Set([Status.Failed, Status.Pinned, Status.Pinning, Status.Queued]) 9 | 10 | const specVersion = 'v1.0.0' 11 | const specFile = 'ipfs-pinning-service.yaml' 12 | const specLocation = `https://raw.githubusercontent.com/ipfs/pinning-services-api-spec/${specVersion}/${specFile}` 13 | 14 | const downloadDir = resolve(_dirname, '..', '..', 'downloaded') 15 | const generatedDir = resolve(_dirname, '..', '..', 'generated') 16 | const docsDir = resolve(_dirname, '..', '..', 'docs') 17 | 18 | const publishedReportsUrl = 'https://ipfs-shipyard.github.io/pinning-service-compliance' 19 | const sourceRepoUrl = 'https://github.com/ipfs-shipyard/pinning-service-compliance' 20 | 21 | enum Icons { 22 | SUCCESS = '🟢', 23 | FAILURE = '❌', 24 | ERROR = '⚠️' 25 | } 26 | 27 | const packageName = process.env.npm_package_name ?? 'pinning-service-compliance' 28 | const packageVersion = process.env.npm_package_version ?? 'unknown' 29 | 30 | export { 31 | allPinStatuses, 32 | docsDir, 33 | downloadDir, 34 | generatedDir, 35 | specFile, 36 | specLocation, 37 | specVersion, 38 | Icons, 39 | publishedReportsUrl, 40 | sourceRepoUrl, 41 | packageName, 42 | packageVersion 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 2 | name: "CodeQL" 3 | 4 | on: 5 | workflow_call: 6 | workflow_dispatch: 7 | inputs: 8 | type: 9 | description: 'Emulate either schedule, push, or pull_request' 10 | required: true 11 | default: 'schedule' 12 | type: choice 13 | options: 14 | - schedule 15 | - push 16 | - pull_request 17 | 18 | jobs: 19 | analyze: 20 | name: Analyze 21 | runs-on: ubuntu-latest 22 | 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | language: [ 'javascript' ] 27 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 28 | # Learn more: 29 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | 35 | # Initializes the CodeQL tools for scanning. 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@v3 38 | with: 39 | languages: ${{ matrix.language }} 40 | 41 | - name: Autobuild 42 | uses: github/codeql-action/autobuild@v3 43 | 44 | - name: Perform CodeQL Analysis 45 | uses: github/codeql-action/analyze@v3 46 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Pinning Service Compliance Reports 4 | 5 | ## Latest reports 6 | 7 | Periodically tested: 8 | 9 | * [Crust](./pin.crustcode.com.md) 10 | * [Estuary](./api.estuary.tech.md) 11 | * [Filebase](./api.filebase.io.md) 12 | * [Pinata](./api.pinata.cloud.md) 13 | * [web3.storage](./api.web3.storage.md) 14 | * [nft.storage](./nft.storage.md) 15 | * [4EVERLAND](./api.4everland.dev.md) 16 | * [Scaleway](./fr-par.ipfs.labs.scw.cloud.md) 17 | 18 | Want to add your service to the list? [Open an issue](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/new). 19 | 20 | 21 | ## About 22 | 23 | ### What is Pinning Service Compliance? 24 | 25 | It’s a test suite to help our pinning service providers, and IPFS implementers who use those providers, see which services are correctly implementing the [IPFS Pinning Service API Spec](https://ipfs.github.io/pinning-services-api-spec/). Our primary goal is to ensure that all of the pinning service providers are consistent, and users can depend on the functionality they expect. 26 | 27 | The Pinning Service Compliance project originated from [pinning-services-api-spec/issues/64](https://github.com/ipfs/pinning-services-api-spec/issues/64). 28 | 29 | ### How to run the compliance checker against my own pinning service? 30 | 31 | ***Disclaimer***: It is recommended to use an `auth_token` separate from your production/live services. The compliance checks will do their best not to corrupt any existing pins you have, but consistent tests without consistent data is challenging. 32 | 33 | [pinning-service-compliance](https://www.npmjs.com/package/@ipfs-shipyard/pinning-service-compliance) package is available on NPM: 34 | 35 | ```bash 36 | npx @ipfs-shipyard/pinning-service-compliance -s 37 | ``` 38 | 39 | ### Bugs? Suggestions? 40 | 41 | Sources and issues: [ipfs-shipyard/pinning-service-compliance](https://github.com/ipfs-shipyard/pinning-service-compliance) 42 | -------------------------------------------------------------------------------- /src/output/getReportEntry.ts: -------------------------------------------------------------------------------- 1 | import { stringifyHeaders } from '../utils/stringifyHeaders.js' 2 | import { complianceCheckHeader } from './complianceCheckHeader.js' 3 | import { getErrorsMarkdown } from './getErrorsMarkdown.js' 4 | import { getExpectationsMarkdown } from './getExpectationsMarkdown.js' 5 | import { joiValidationAsMarkdown } from './joiValidationAsMarkdown.js' 6 | import type { ComplianceCheckDetails, PinsApiResponseTypes } from '../types.js' 7 | 8 | const getReportEntry = (details: ComplianceCheckDetails): string => { 9 | const { request, response, title, url, method, validationResult, result: clientParsedResult, successful, errors } = details 10 | const joiValidationMarkdown = joiValidationAsMarkdown(validationResult) 11 | const reportEntry = `## ${complianceCheckHeader({ title, successful })} 12 | 13 | ${getExpectationsMarkdown(details)} 14 | 15 | ${ 16 | errors.length > 0 17 | ? `### Errors during run 18 | ${getErrorsMarkdown(errors)}` 19 | : '' 20 | } 21 | 22 | ${ 23 | joiValidationMarkdown !== 'No failures' 24 | ? `#### Response object doesn't match expected schema: 25 | ${joiValidationMarkdown} 26 | ` 27 | : '' 28 | } 29 | ### Details 30 | 31 | #### Request 32 | \`\`\` 33 | ${method} ${url} 34 | \`\`\` 35 | ##### Headers 36 | \`\`\`json 37 | ${stringifyHeaders(request.headers)} 38 | \`\`\` 39 | ##### Body 40 | \`\`\`json 41 | ${request.body} 42 | \`\`\` 43 | 44 | #### Response 45 | \`\`\` 46 | ${response.status} ${response.statusText} 47 | \`\`\` 48 | ##### Headers 49 | \`\`\`json 50 | ${stringifyHeaders(response.headers)} 51 | \`\`\` 52 | ##### Body 53 | \`\`\`json 54 | ${response.body} 55 | \`\`\` 56 | 57 | ##### Body (as JSON) 58 | \`\`\`json 59 | ${JSON.stringify(response.json, null, 2)} 60 | \`\`\` 61 | ##### Body (parsed by [pinning-service-client](https://www.npmjs.com/package/@ipfs-shipyard/pinning-service-client)) 62 | \`\`\`json 63 | ${JSON.stringify(clientParsedResult, null, 2)} 64 | \`\`\` 65 | ` 66 | 67 | return reportEntry 68 | } 69 | 70 | export { getReportEntry } 71 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | # Automatically merge pull requests opened by web3-bot, as soon as (and only if) all tests pass. 2 | # This reduces the friction associated with updating with our workflows. 3 | 4 | name: Automerge 5 | on: 6 | # see https://docs.github.com/en/actions/using-workflows/reusing-workflows#creating-a-reusable-workflow 7 | workflow_call: 8 | 9 | jobs: 10 | automerge-check: 11 | if: github.event.pull_request.user.login == 'web3-bot' 12 | runs-on: ubuntu-latest 13 | outputs: 14 | status: ${{ steps.should-automerge.outputs.status }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Check if we should automerge 20 | id: should-automerge 21 | run: | 22 | for commit in $(git rev-list --first-parent origin/${{ github.event.pull_request.base.ref }}..${{ github.event.pull_request.head.sha }}); do 23 | committer=$(git show --format=$'%ce' -s $commit) 24 | echo "Committer: $committer" 25 | if [[ "$committer" != "web3-bot@users.noreply.github.com" ]]; then 26 | echo "Commit $commit wasn't committed by web3-bot, but by $committer." 27 | echo "::set-output name=status::false" 28 | exit 29 | fi 30 | done 31 | echo "::set-output name=status::true" 32 | automerge: 33 | needs: automerge-check 34 | runs-on: ubuntu-latest 35 | # The check for the user is redundant here, as this job depends on the automerge-check job, 36 | # but it prevents this job from spinning up, just to be skipped shortly after. 37 | if: github.event.pull_request.user.login == 'web3-bot' && needs.automerge-check.outputs.status == 'true' 38 | steps: 39 | - name: Wait on tests 40 | uses: lewagon/wait-on-check-action@e2558238c09778af25867eb5de5a3ce4bbae3dcd # v1.1.1 41 | with: 42 | ref: ${{ github.event.pull_request.head.sha }} 43 | repo-token: ${{ secrets.GITHUB_TOKEN }} 44 | wait-interval: 10 45 | running-workflow-name: 'automerge' # the name of this job 46 | - name: Merge PR 47 | uses: pascalgn/automerge-action@22948e0bc22f0aa673800da838595a3e7347e584 # v0.15.6 48 | env: 49 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 50 | MERGE_LABELS: "" 51 | MERGE_METHOD: "squash" 52 | MERGE_DELETE_BRANCH: true 53 | -------------------------------------------------------------------------------- /src/checks/get/matchPin.ts: -------------------------------------------------------------------------------- 1 | import { type PinStatus, TextMatchingStrategy } from '@ipfs-shipyard/pinning-service-client' 2 | import { v4 as uuidv4 } from 'uuid' 3 | import { ApiCall } from '../../ApiCall.js' 4 | import { resultNotNull, responseOk } from '../../expectations/index.js' 5 | import { getInlineCid } from '../../utils/getInlineCid.js' 6 | import type { ServiceAndTokenPair } from '../../types.js' 7 | 8 | const matchApiCallExpectation = (parent: ApiCall, match: TextMatchingStrategy, name: string, actualName: string): void => { 9 | new ApiCall({ 10 | parent, 11 | pair: parent.pair, 12 | title: `Can retrieve pin with name '${name}' via the '${match}' TextMatchingStrategy`, 13 | fn: async (client) => client.pinsGet({ match, name }) 14 | }) 15 | .expect(responseOk()) 16 | .expect(resultNotNull()) 17 | .expect({ 18 | title: 'Count is equal to 1', 19 | fn: ({ result }) => result?.count === 1 20 | }) 21 | .expect({ 22 | title: 'Name matches name provided during creation', 23 | fn: ({ result }) => { 24 | const pinResultsIter = result?.results.values() 25 | const pinStatus: PinStatus | undefined = pinResultsIter?.next().value 26 | 27 | return pinStatus?.pin?.name === actualName 28 | } 29 | }) 30 | } 31 | /** 32 | * https://github.com/ipfs-shipyard/pinning-service-compliance/issues/9 33 | */ 34 | const matchPin = async (pair: ServiceAndTokenPair): Promise => { 35 | const name = uuidv4().toLowerCase() 36 | const nameLength = name.length 37 | const size = nameLength / 4 38 | const partialName = name.slice(size, nameLength - (size)) 39 | const cid = await getInlineCid() 40 | const mainApiCall = new ApiCall({ 41 | pair, 42 | title: `Can create a pin with name='${name}'`, 43 | fn: async (client) => client.pinsPost({ pin: { name, cid } }) 44 | }) 45 | .expect(responseOk()) 46 | .expect(resultNotNull()) 47 | .expect({ 48 | title: 'Name matches name provided during creation', 49 | fn: ({ result }) => result?.pin.name === name 50 | }) 51 | 52 | matchApiCallExpectation(mainApiCall, TextMatchingStrategy.Exact, name, name) 53 | matchApiCallExpectation(mainApiCall, TextMatchingStrategy.Iexact, name.toUpperCase(), name) 54 | matchApiCallExpectation(mainApiCall, TextMatchingStrategy.Partial, partialName, name) 55 | matchApiCallExpectation(mainApiCall, TextMatchingStrategy.Ipartial, partialName.toUpperCase(), name) 56 | await mainApiCall.runExpectations() 57 | } 58 | 59 | export { matchPin } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @ipfs-shipyard/pinning-service-compliance 2 | 3 | [![codecov](https://img.shields.io/codecov/c/github/ipfs-shipyard/pinning-service-compliance.svg?style=flat-square)](https://codecov.io/gh/ipfs-shipyard/pinning-service-compliance) 4 | [![CI](https://img.shields.io/github/actions/workflow/status/ipfs-shipyard/pinning-service-compliance/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs-shipyard/pinning-service-compliance/actions/workflows/js-test-and-release.yml?query=branch%3Amain) 5 | 6 | > The compliance test suite for [IPFS Pinning Service API Spec](https://ipfs.github.io/pinning-services-api-spec/) 7 | 8 | ## Getting started 9 | 10 | ### Run the compliance checker against a service: 11 | 12 | ***Disclaimer***: It is recommended to use an `auth_token` separate from your production/live services. The compliance checks will do their best not to corrupt any existing pins you have, but consistent tests without consistent data is challenging. 13 | 14 | ```bash 15 | npx @ipfs-shipyard/pinning-service-compliance -s 16 | ``` 17 | 18 | ## Development 19 | 20 | ### Run the script 21 | 22 | ```bash 23 | npm ci 24 | npm run build 25 | 26 | npm start -- -s $API_ENDPOINT $ACCESS_TOKEN 27 | # or multiple endpoints 28 | npm start -- -s $API_ENDPOINT1 $ACCESS_TOKEN1 -s $API_ENDPOINT2 $ACCESS_TOKEN2 29 | ``` 30 | 31 | ### Debugging 32 | 33 | To debug problems, you should use the `-d` flag, and the `dev-start` script: 34 | 35 | ```bash 36 | npm run dev-start -- -s $API_ENDPOINT $ACCESS_TOKEN 37 | ``` 38 | 39 | ## FAQ 40 | 41 | ### What is a Compliance Check? 42 | A compliance check consists of: 43 | 44 | 1. An API call 45 | 2. A Payload 46 | 3. An expected response 47 | 4. A summary 48 | 49 | ### How to avoid typing secrets by hand? 50 | 51 | To avoid setting secrets by hand: 52 | 53 | ```bash 54 | cp .env-copy .env 55 | ``` 56 | Then replace all variables with the appropriate endpoints and tokens 57 | 58 | # License 59 | 60 | Licensed under either of 61 | 62 | - Apache 2.0, ([LICENSE-APACHE](https://github.com/ipfs-shipyard/pinning-service-compliance/LICENSE-APACHE) / ) 63 | - MIT ([LICENSE-MIT](https://github.com/ipfs-shipyard/pinning-service-compliance/LICENSE-MIT) / ) 64 | 65 | # Contribution 66 | 67 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 68 | -------------------------------------------------------------------------------- /.github/workflows/publish-reports.yml: -------------------------------------------------------------------------------- 1 | name: Publish compliance reports 2 | on: 3 | 4 | # see https://docs.github.com/en/actions/using-workflows/reusing-workflows#creating-a-reusable-workflow 5 | workflow_call: 6 | inputs: 7 | type: 8 | description: 'If a workflow that calls this workflow is called via workflow_dispatch, it should pass this property' 9 | required: true 10 | default: 'nothing' 11 | type: string 12 | 13 | workflow_dispatch: 14 | inputs: 15 | type: 16 | description: 'Emulate either schedule, push, or pull_request' 17 | required: true 18 | default: 'schedule' 19 | type: choice 20 | options: 21 | - schedule 22 | - push 23 | - pull_request 24 | 25 | jobs: 26 | 27 | # Deploy to gh pages branch 28 | push-reports-to-gh-pages: 29 | runs-on: ubuntu-latest 30 | if: (github.event.inputs.type == 'push' || github.event_name == 'push') && github.ref == 'refs/heads/main' 31 | concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession. 32 | steps: 33 | - name: Reports Cache 34 | uses: actions/cache@v4 35 | with: 36 | path: docs 37 | key: ${{ github.sha }}-pinata 38 | - name: Reports Cache 39 | uses: actions/cache@v4 40 | with: 41 | path: docs 42 | key: ${{ github.sha }}-nft 43 | - name: Reports Cache 44 | uses: actions/cache@v4 45 | with: 46 | path: docs 47 | key: ${{ github.sha }}-web3 48 | - name: Reports Cache 49 | uses: actions/cache@v4 50 | with: 51 | path: docs 52 | key: ${{ github.sha }}-filebase 53 | - name: Reports Cache 54 | uses: actions/cache@v4 55 | with: 56 | path: docs 57 | key: ${{ github.sha }}-crust 58 | - name: Reports Cache 59 | uses: actions/cache@v4 60 | with: 61 | path: docs 62 | key: ${{ github.sha }}-4everland 63 | - name: Reports Cache 64 | uses: actions/cache@v4 65 | with: 66 | path: docs 67 | key: ${{ github.sha }}-scaleway 68 | - name: Scheduled deployment 69 | uses: s0/git-publish-subdir-action@92faf786f11dfa44fc366ac3eb274d193ca1af7e 70 | env: 71 | REPO: self 72 | BRANCH: gh-pages 73 | FOLDER: docs 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | SQUASH_HISTORY: false 76 | MESSAGE: "Update published reports with changes from {sha} with message:\n{msg}" 77 | SKIP_EMPTY_COMMITS: false 78 | -------------------------------------------------------------------------------- /src/output/getHeader.ts: -------------------------------------------------------------------------------- 1 | import { Icons, packageVersion } from '../utils/constants.js' 2 | import { getHostnameFromUrl } from '../utils/getHostnameFromUrl.js' 3 | import { gitHash } from '../utils/gitHash.js' 4 | import { logger } from '../utils/logs.js' 5 | import { markdownLinkToTextLabel } from '../utils/markdownLinkToTextLabel.js' 6 | import { complianceCheckHeader } from './complianceCheckHeader.js' 7 | import { linkToCommit } from './linkToCommit.js' 8 | import { linkToGithubRepo } from './linkToGithubRepo.js' 9 | import { linkToHeading } from './linkToHeading.js' 10 | import { linkToNpm } from './linkToNpm.js' 11 | import type { ComplianceCheckDetails, PinsApiResponseTypes } from '../types.js' 12 | 13 | type RequiredHeaderProps = Pick, 'title' | 'successful' | 'pair'> 14 | 15 | interface HeaderOptions { 16 | markdownLinks: boolean 17 | } 18 | const getHeader = async (details: Array>, options: HeaderOptions = { markdownLinks: true }): Promise => { 19 | const endpointUrl = details[0].pair[0] 20 | const useMarkdownLinks = options.markdownLinks 21 | const hostname = getHostnameFromUrl(endpointUrl) 22 | let checks = 0 23 | let successes = 0 24 | 25 | const dateString = (new Date()).toISOString() 26 | let revisionString: string | null = null 27 | 28 | try { 29 | const currentRevision = await gitHash() 30 | 31 | revisionString = useMarkdownLinks ? linkToCommit(currentRevision) : currentRevision 32 | } catch (err) { 33 | logger.error('Could not obtain latest git hash', err) 34 | logger.info('No git repository, using npm version') 35 | revisionString = useMarkdownLinks ? linkToNpm() : packageVersion 36 | } 37 | 38 | const titles = details.map(({ title, successful }) => { 39 | checks++ 40 | const titleLink = useMarkdownLinks ? linkToHeading(title, complianceCheckHeader({ title, successful })) : title 41 | if (successful) { 42 | successes++ 43 | return `${Icons.SUCCESS} ${titleLink}` 44 | } 45 | return `${Icons.FAILURE} ${titleLink}` 46 | }) 47 | 48 | const reportHistory = linkToGithubRepo('Report History', `commits/gh-pages/${hostname}.md`) 49 | 50 | return ` 51 | # ${endpointUrl} compliance: 52 | 53 | Execution Date: ${dateString ?? '(Error getting date)'} 54 | 55 | Revision: ${revisionString ?? '(Error getting revision)'} 56 | 57 | ${useMarkdownLinks ? reportHistory : markdownLinkToTextLabel(reportHistory)} 58 | 59 | ## Summary (${successes}/${checks} successful) 60 | 61 | ${titles.join('\n\n ')} 62 | 63 | ` 64 | } 65 | 66 | export { getHeader } 67 | export type { RequiredHeaderProps } 68 | -------------------------------------------------------------------------------- /src/checks/edit/replacePin.ts: -------------------------------------------------------------------------------- 1 | import { ApiCall } from '../../ApiCall.js' 2 | import { responseCode, responseOk } from '../../expectations/index.js' 3 | import { getInlineCid } from '../../utils/getInlineCid.js' 4 | import { getRequestid } from '../../utils/getRequestid.js' 5 | import type { ServiceAndTokenPair } from '../../types.js' 6 | 7 | /** 8 | * https://github.com/ipfs-shipyard/pinning-service-compliance/issues/8 9 | * 10 | * - create a pin and then replace it via POST /pins/{requestid} 11 | * - expect different requestid in response 12 | * - confirm old requestid and CID are no longer to be found 13 | */ 14 | const replacePin = async (pair: ServiceAndTokenPair): Promise => { 15 | const cid = await getInlineCid() 16 | const createPinApiCall = new ApiCall({ 17 | pair, 18 | fn: async (client) => client.pinsPost({ pin: { cid } }), 19 | title: 'Can create and replace a pin\'s CID' 20 | }) 21 | const originalPin = await createPinApiCall.request 22 | createPinApiCall.expect({ 23 | title: 'Pin exists', 24 | fn: async ({ result }) => result != null 25 | }) 26 | 27 | const requestid = getRequestid(originalPin, createPinApiCall) 28 | createPinApiCall.expect({ 29 | title: `Could obtain requestid from new pin (${requestid})`, 30 | fn: () => requestid != null && requestid !== 'null' 31 | }) 32 | 33 | const newCid = await getInlineCid() 34 | 35 | const replaceCidApiCall = new ApiCall({ 36 | parent: createPinApiCall, 37 | pair, 38 | title: `Pin's with requestid '${requestid}' can have cid '${cid}' replaced with '${newCid}'`, 39 | fn: async (client) => client.pinsRequestidPost({ requestid, pin: { cid: newCid } }) 40 | }) 41 | 42 | const newPin = await replaceCidApiCall.request 43 | const newRequestid = getRequestid(newPin, replaceCidApiCall) 44 | 45 | createPinApiCall 46 | .expect(responseOk()) 47 | .expect({ 48 | title: 'Replaced pin has the new & expected CID', 49 | fn: async () => newPin?.pin.cid === newCid 50 | }) 51 | .expect({ 52 | title: 'Replaced pin has a different requestid', 53 | fn: async () => newRequestid !== requestid 54 | }) 55 | 56 | new ApiCall({ 57 | parent: replaceCidApiCall, 58 | pair, 59 | fn: async (client) => client.pinsRequestidGet({ requestid }), 60 | title: 'Get original pin via requestid' 61 | }) 62 | .expect(responseCode(404, 'Original Pin\'s requestid cannot be found')) 63 | 64 | new ApiCall({ 65 | parent: replaceCidApiCall, 66 | pair, 67 | fn: async (client) => client.pinsRequestidGet({ requestid: newRequestid }), 68 | title: 'Get new pin via requestid' 69 | }) 70 | .expect(responseCode(200, 'New Pin\'s requestid can be found')) 71 | 72 | await createPinApiCall.runExpectations() 73 | } 74 | 75 | export { replacePin } 76 | -------------------------------------------------------------------------------- /.github/workflows/check-compliance.yml: -------------------------------------------------------------------------------- 1 | name: Check pinning service compliance 2 | on: 3 | # This workflow should be required for 'test & maybe release' to succeed 4 | # see https://docs.github.com/en/actions/using-workflows/reusing-workflows#creating-a-reusable-workflow 5 | workflow_call: 6 | inputs: 7 | API_ENDPOINT: 8 | description: 'The API endpoint for the Pinning Service you want to use' 9 | type: string 10 | required: true 11 | fail_action_on_compliance_failure: 12 | description: 'Whether you want the action to fail if a compliance check fails' 13 | type: boolean 14 | default: true 15 | required: false 16 | secrets: 17 | API_TOKEN: 18 | required: true 19 | 20 | workflow_dispatch: 21 | inputs: 22 | type: 23 | description: 'Emulate either schedule, push, or pull_request' 24 | required: true 25 | default: 'schedule' 26 | type: choice 27 | options: 28 | - schedule 29 | - push 30 | - pull_request 31 | API_TOKEN: 32 | description: 'The API token for the Pinning Service you want to use' 33 | type: string 34 | required: true 35 | 36 | jobs: 37 | 38 | check-provided-service-compliance: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout 🛎️ 42 | uses: actions/checkout@v4 43 | - uses: ipfs/aegir/actions/cache-node-modules@master 44 | - name: Reports Cache 45 | uses: actions/cache@v4 46 | with: 47 | path: docs 48 | key: ${{ github.sha }} 49 | - run: npm run dev-start -- -s ${{ inputs.API_ENDPOINT }} ${{ secrets.API_TOKEN }} 50 | - uses: actions/upload-artifact@v4 51 | with: 52 | name: docs 53 | path: docs 54 | - name: Set outputs 55 | id: set_results 56 | run: | 57 | hostname="$( echo ${{inputs.API_ENDPOINT}} | perl -ne '/^[^\/]+[\/]+([^\/]+)/ && print $1')" 58 | echo "success=$(jq -r '.success' dist/docs/$hostname.json)" >> $GITHUB_OUTPUT 59 | echo "passed=$(jq -r '.passed' dist/docs/$hostname.json)" >> $GITHUB_OUTPUT 60 | echo "total=$(jq -r '.total' dist/docs/$hostname.json)" >> $GITHUB_OUTPUT 61 | - name: Notify 62 | uses: actions/github-script@v6 63 | with: 64 | script: | 65 | core.notice('${{ needs.run-compliance-tests.outputs.passed }}/${{ needs.run-compliance-tests.outputs.total}} checks passed') 66 | 67 | validate-compliance: 68 | runs-on: ubuntu-latest 69 | needs: check-provided-service-compliance 70 | if: ${{ inputs.fail_action_on_compliance_failure && needs.run-compliance-tests.outputs.success != true }} 71 | steps: 72 | - name: Fail 73 | uses: actions/github-script@v6 74 | with: 75 | script: | 76 | core.setFailed('Only ${{ needs.run-compliance-tests.outputs.passed }}/${{ needs.run-compliance-tests.outputs.total}} checks passed') 77 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { writeSync } from 'fs' 3 | import process from 'node:process' 4 | import { getAllPins, checkEmptyBearerToken, checkInvalidBearerToken, addPin, deleteAllPins, testPagination, deleteNewPin, replacePin, matchPin } from './checks/index.js' 5 | import { cli } from './cli/index.js' 6 | import { serviceAndToken } from './cli/options/index.js' 7 | import { writeHeaders } from './output/reporting.js' 8 | import { writeJsonResults } from './output/writeJsonResults.js' 9 | import { logger } from './utils/logs.js' 10 | import { getPinTracker } from './utils/pinTracker.js' 11 | import { globalReport } from './utils/report.js' 12 | import type { ServiceAndTokenPair } from './types.js' 13 | 14 | const validatePinningService = async (pair: ServiceAndTokenPair): Promise => { 15 | const complianceCheckFunctions = [checkEmptyBearerToken, checkInvalidBearerToken, addPin, deleteNewPin, getAllPins, replacePin, matchPin, testPagination, deleteAllPins] 16 | 17 | for await (const complianceCheckFn of complianceCheckFunctions) { 18 | logger.debug(`Starting compliance check '${complianceCheckFn.name}'`) 19 | try { 20 | await complianceCheckFn(pair) 21 | } catch (error) { 22 | logger.error(`Problem running compliance check: '${complianceCheckFn.name}'`, { error }) 23 | } finally { 24 | logger.debug(`Completed compliance check '${complianceCheckFn.name}'`) 25 | } 26 | } 27 | } 28 | 29 | const main = async (): Promise => { 30 | const argv = await cli.option('serviceAndToken', { require: true, ...serviceAndToken }).argv 31 | 32 | for await (const serviceAndToken of argv.serviceAndToken) { 33 | try { 34 | /** 35 | * Ensure the pinTracker can get the initial count of pins before we start 36 | */ 37 | await getPinTracker(serviceAndToken) 38 | await validatePinningService(serviceAndToken) 39 | } catch (err) { 40 | logger.error('could not validate pinning service') 41 | logger.error(err) 42 | } 43 | try { 44 | await writeJsonResults(serviceAndToken) 45 | } catch (err) { 46 | logger.error(err) 47 | } 48 | } 49 | try { 50 | await writeHeaders() 51 | } catch (err) { 52 | logger.error(err) 53 | } 54 | } 55 | 56 | const getUncaughtListener = (type: 'unhandledRejection' | 'uncaughtException' | 'uncaughtExceptionMonitor'): NodeJS.UncaughtExceptionListener => (err, origin) => { 57 | logger.error(type, err, origin) 58 | writeSync( 59 | process.stderr.fd, 60 | `Caught exception: ${JSON.stringify({ err, origin }, null, 2)}\n` 61 | ) 62 | } 63 | process.on('uncaughtExceptionMonitor', getUncaughtListener('uncaughtExceptionMonitor')) 64 | process.on('unhandledRejection', getUncaughtListener('unhandledRejection')) 65 | process.on('uncaughtException', getUncaughtListener('uncaughtException')) 66 | 67 | main().catch((err) => { 68 | logger.error(err) 69 | logger.error('Exiting process due to unexpected error') 70 | process.exit(1) 71 | }).finally(() => { 72 | logger.debug(globalReport) 73 | }) 74 | -------------------------------------------------------------------------------- /src/checks/delete/deleteAllPins.ts: -------------------------------------------------------------------------------- 1 | import { ApiCall } from '../../ApiCall.js' 2 | import { responseOk } from '../../expectations/index.js' 3 | import { allPinStatuses } from '../../utils/constants.js' 4 | import { getOldestPinCreateDate } from '../../utils/getOldestPinCreateDate.js' 5 | import { getRequestid } from '../../utils/getRequestid.js' 6 | import { getPinTracker, type PinTracker } from '../../utils/pinTracker.js' 7 | import type { ServiceAndTokenPair } from '../../types.js' 8 | import type { PinResults } from '@ipfs-shipyard/pinning-service-client' 9 | 10 | const MAX_LOOP_TIMES = 10 11 | let loopCount = 0 12 | /** 13 | * This function recursively adds 'pinsRequestidDelete' expectations, for all existing pins, 14 | * to the root apiCall passed in; paginating if necessary. 15 | * 16 | * @param apiCall - The root ApiCall instance that all expectations should be added to 17 | */ 18 | const addPinDeletionExpectations = async (apiCall: ApiCall, pinTracker: PinTracker): Promise => { 19 | const pinResults: PinResults | null = await apiCall.request 20 | 21 | if (pinResults != null) { 22 | for await (const pin of pinResults.results) { 23 | if (pinTracker.has(pin.pin.cid)) { 24 | const requestid = getRequestid(pin, apiCall) 25 | 26 | new ApiCall({ 27 | parent: apiCall, 28 | pair: apiCall.pair, 29 | fn: async (client) => client.pinsRequestidDelete({ requestid }), 30 | title: `Can delete pin with requestid '${requestid}'` 31 | }).expect(responseOk()) 32 | } 33 | } 34 | 35 | if (pinResults.count > pinTracker.originalPinCount) { 36 | /** 37 | * Prevent infinite loops for broken services. 38 | */ 39 | if (loopCount >= MAX_LOOP_TIMES) { 40 | return 41 | } else { 42 | loopCount++ 43 | } 44 | 45 | /** 46 | * Pagination doesn't need to work for this compliance check to succeed. 47 | * 48 | * If pagination is disabled, this should never run because the service 49 | * will return all pins in the first GET call. 50 | * 51 | * @see https://github.com/ipfs-shipyard/pinning-service-compliance/issues/118 52 | */ 53 | const before = getOldestPinCreateDate(pinResults.results) 54 | await addPinDeletionExpectations(new ApiCall({ 55 | parent: apiCall, 56 | pair: apiCall.pair, 57 | fn: async (client) => client.pinsGet({ status: allPinStatuses, before }), 58 | title: `Get all Pins created before '${before.toString()}'` 59 | }), pinTracker) 60 | } 61 | } 62 | } 63 | 64 | const deleteAllPins = async (pair: ServiceAndTokenPair): Promise => { 65 | loopCount = 0 66 | const pinTracker = await getPinTracker(pair) 67 | const mainApiCall = new ApiCall({ 68 | pair, 69 | fn: async (client) => client.pinsGet({ status: allPinStatuses }), 70 | title: 'Can delete all pins created during compliance checks' 71 | }) 72 | 73 | await addPinDeletionExpectations(mainApiCall, pinTracker) 74 | 75 | new ApiCall({ 76 | parent: mainApiCall, 77 | pair, 78 | fn: async (client) => client.pinsGet({ status: allPinStatuses }), 79 | title: 'Call pinsGet after deletions' 80 | }) 81 | .expect({ 82 | title: `Final pinsGet call returns the same count as before all compliance checks: '${pinTracker.originalPinCount}'`, 83 | fn: async ({ result }) => result?.count === pinTracker.originalPinCount 84 | }) 85 | 86 | await mainApiCall.runExpectations() 87 | } 88 | 89 | export { deleteAllPins } 90 | -------------------------------------------------------------------------------- /src/utils/getJoiSchema.ts: -------------------------------------------------------------------------------- 1 | import oas2joi from 'oas2joi' 2 | import { specLocation } from './constants.js' 3 | import { logger } from './logs.js' 4 | import type { PinningSpecJoiSchema } from '../types.js' 5 | import type { Schema as JoiSchema } from '@hapi/joi' 6 | 7 | type Schema = JoiSchema & InnerSchema 8 | interface InnerSchemaChild { 9 | key: string 10 | schema: JoiSchema 11 | } 12 | 13 | interface InnerSchema { 14 | _inner: { 15 | items: InnerSchema[] 16 | children: InnerSchemaChild[] 17 | dependencies: any[] 18 | patterns: any[] 19 | renames: any[] 20 | } 21 | } 22 | 23 | /** 24 | * find a child either from .children or .items in the InnerSchema 25 | */ 26 | const findChild = (innerSchema: InnerSchema, key: string): InnerSchemaChild | undefined => { 27 | if (innerSchema._inner.children != null) { 28 | return innerSchema._inner.children.find((child) => child.key === key) 29 | } else if (innerSchema._inner.items != null) { 30 | for (const item of innerSchema._inner.items) { 31 | const child = findChild(item, key) 32 | if (child != null) { 33 | return child 34 | } 35 | } 36 | } 37 | return undefined 38 | } 39 | 40 | const getInnerSchema = (schema: Schema | JoiSchema, path: string[]): JoiSchema => { 41 | let finalSchema = schema as Schema 42 | for (const key of path) { 43 | const child = findChild(finalSchema, key) 44 | if (child != null) { 45 | finalSchema = child.schema as Schema 46 | } else { 47 | throw new Error(`Could not find inner child with key ${key}`) 48 | } 49 | } 50 | 51 | return finalSchema as JoiSchema 52 | } 53 | 54 | const setInnerSchema = (rootSchema: Schema | JoiSchema, path: string[], newSchema: Schema | JoiSchema): void => { 55 | let childSchema = rootSchema as Schema 56 | let child: InnerSchemaChild | undefined 57 | for (const key of path) { 58 | child = findChild(childSchema, key) 59 | if (child != null) { 60 | childSchema = child.schema as Schema 61 | } else { 62 | throw new Error(`Could not find inner child with key ${key}`) 63 | } 64 | } 65 | if (child != null) { 66 | child.schema = newSchema 67 | } else { 68 | throw new Error(`No child was found using paths: ${path.join(', ')}`) 69 | } 70 | } 71 | 72 | const modifySchema = (schemaName: keyof PinningSpecJoiSchema, schema: JoiSchema): void => { 73 | switch (schemaName) { 74 | case 'PinStatus': { 75 | const namePath = ['pin', 'name'] 76 | const nameSchema = getInnerSchema(schema, namePath) 77 | setInnerSchema(schema, namePath, nameSchema.allow(null)) 78 | 79 | break 80 | } 81 | case 'PinResults': { 82 | const namePath = ['results', 'pin', 'name'] 83 | const nameSchema = getInnerSchema(schema, namePath) 84 | setInnerSchema(schema, namePath, nameSchema.allow(null)) 85 | 86 | break 87 | } 88 | default: { 89 | break 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * Cached schema object so we don't have to keep calling to the spec 96 | */ 97 | let schema: PinningSpecJoiSchema 98 | const getJoiSchema = async (schemaName: T): Promise => { 99 | try { 100 | schema = schema ?? await oas2joi(specLocation) 101 | } catch (err) { 102 | logger.error(`Could not get joi schema for '${schemaName}':`, err) 103 | throw err 104 | } 105 | try { 106 | modifySchema(schemaName, schema[schemaName]) 107 | } catch (err) { 108 | logger.error(`Could not modify joi schema for '${schemaName}':`, err) 109 | } 110 | return schema?.[schemaName] ?? undefined 111 | } 112 | 113 | export { getJoiSchema } 114 | -------------------------------------------------------------------------------- /src/utils/logs.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { format, createLogger, transports, type Logform, type transport, type Logger } from 'winston' 3 | import { argv } from '../cli/index.js' 4 | import { docsDir } from './constants.js' 5 | import { getHostnameFromUrl } from './getHostnameFromUrl.js' 6 | 7 | const { combine, splat, timestamp, printf } = format 8 | 9 | interface LoggerInfo extends Logform.TransformableInfo { 10 | timestamp: string 11 | /** 12 | * error: 0, 13 | * warn: 1, 14 | * info: 2, 15 | * http: 3, 16 | * verbose: 4, 17 | * debug: 5, 18 | * silly: 6 19 | */ 20 | level: 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly' 21 | message: string 22 | metadata: Record 23 | nested?: boolean 24 | messageOnly?: boolean 25 | } 26 | 27 | const myFormat = printf(({ level, message, timestamp, nested, messageOnly, ...metadata }: Logform.TransformableInfo) => { 28 | if (nested === true) { 29 | return `\t${message}` 30 | } 31 | if (messageOnly === true) { 32 | return `${message}` 33 | } 34 | 35 | return `${timestamp as LoggerInfo['timestamp']} [${level}] : ${message}` 36 | }) 37 | 38 | /** 39 | * supplying the cli with the '-v' or '--verbose' flag will output the full report details as they're generated. 40 | */ 41 | const verboseFilter = format((info: Logform.TransformableInfo) => { 42 | const { level } = info 43 | if (level === 'verbose') { 44 | return argv.verbose ? info : false 45 | } else if (argv.verbose && level === 'info') { 46 | // do not log info logs as they duplicate verbose logs w.r.t. the report output 47 | return false 48 | } 49 | return info 50 | }) 51 | 52 | /** 53 | * supplying the cli with the '-d' or '--debug' flag will output additional information about the state of the compliance checks in order to assist with debugging/troubleshooting 54 | */ 55 | const debugFilter = format((info: Logform.TransformableInfo, options) => { 56 | const { level } = info 57 | if (level === 'debug') { 58 | return argv.debug ? info : false 59 | } 60 | return info 61 | }) 62 | 63 | const getServiceLogger = (endpointUrl: string): Logger => { 64 | const hostname = getHostnameFromUrl(endpointUrl) 65 | const logFolder = join(docsDir, hostname) 66 | const loggerTransports: transport[] = [ 67 | ] 68 | if (argv.debug) { 69 | loggerTransports.push(new transports.File({ filename: join(logFolder, 'debug.log'), level: 'debug' })) 70 | } 71 | 72 | loggerTransports.push( 73 | // - Write all logs with importance level of `error` or less to `error.log` 74 | new transports.File({ filename: join(logFolder, 'error.log'), level: 'error' }), 75 | // - Write all logs with importance level of `info` or less to `combined.log` 76 | new transports.File({ filename: join(logFolder, 'combined.log') }) 77 | ) 78 | 79 | const logger = createLogger({ 80 | level: 'info', 81 | format: combine( 82 | format.colorize(), 83 | splat(), 84 | timestamp(), 85 | myFormat 86 | ), 87 | defaultMeta: { hostname }, 88 | transports: loggerTransports 89 | }) 90 | 91 | return logger 92 | } 93 | 94 | const getLogger = (): Logger => { 95 | return createLogger({ 96 | level: 'info', 97 | format: combine( 98 | verboseFilter(), 99 | debugFilter(), 100 | format.colorize(), 101 | splat(), 102 | timestamp(), 103 | myFormat 104 | ), 105 | defaultMeta: { }, 106 | transports: [ 107 | new transports.Console({ level: 'silly' }) 108 | ] 109 | }) 110 | } 111 | 112 | const logger = getLogger() 113 | 114 | export { getServiceLogger, getLogger, logger } 115 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { Schema, ValidationResult } from '@hapi/joi' 2 | import type { 3 | NodeFetch, 4 | ConfigurationParameters as ConfigurationParametersOrig, 5 | Configuration as ConfigurationOrig, 6 | RemotePinningServiceClient 7 | } from '@ipfs-shipyard/pinning-service-client' 8 | 9 | /** 10 | * This should move to the pinning-service-client package. 11 | * 12 | * @see https://github.com/ipfs-shipyard/js-pinning-service-http-client/issues/17 13 | */ 14 | declare module '@ipfs-shipyard/pinning-service-client' { 15 | // eslint-disable-next-line @typescript-eslint/no-namespace 16 | namespace NodeFetch { 17 | interface RequestContext { 18 | 19 | fetch: typeof fetch 20 | url: string 21 | init: RequestInit 22 | } 23 | 24 | interface ResponseContext { 25 | fetch: typeof fetch 26 | url: string 27 | init: RequestInit 28 | response: Response | any 29 | } 30 | 31 | interface FetchParams { 32 | url: string 33 | init: RequestInit 34 | } 35 | 36 | interface Middleware { 37 | // eslint-disable-next-line @typescript-eslint/no-invalid-void-type 38 | pre?(context: NodeFetch.RequestContext): Promise 39 | // eslint-disable-next-line @typescript-eslint/no-invalid-void-type 40 | post?(context: NodeFetch.ResponseContext): Promise 41 | } 42 | 43 | interface ConfigurationParameters extends ConfigurationParametersOrig { 44 | middleware: NodeFetch.Middleware[] 45 | } 46 | class Configuration extends ConfigurationOrig { 47 | constructor (options: NodeFetch.ConfigurationParameters): Configuration 48 | } 49 | } 50 | } 51 | 52 | interface ComplianceCheckDetailsCallbackArg extends NodeFetch.ResponseContext { 53 | response: Response 54 | errors: Error[] 55 | url: string 56 | } 57 | interface ProcessedResponse extends Response { 58 | text: string | null 59 | json: Record 60 | } 61 | 62 | interface ComplianceCheckDetailsCallback { 63 | (details: ComplianceCheckDetailsCallbackArg): Promise 64 | } 65 | 66 | interface ExpectationResult { 67 | error?: Error 68 | success: boolean 69 | title: string 70 | } 71 | 72 | type ServiceAndTokenPair = [endpointUrl: string, authToken: string | undefined] 73 | 74 | type ImplementableMethods = keyof Omit 75 | type PinsApiMethod = RemotePinningServiceClient[T] extends never ? never : T 76 | type PinsApiResponseTypes = Awaited> 77 | type SchemaNames = 'Delegates' | 'Failure' | 'Origins' | 'Pin' | 'PinMeta' | 'PinResults' | 'PinStatus' | 'StatusInfo' | 'Status' | 'TextMatchingStrategy' 78 | type PinningSpecJoiSchema = Record 79 | 80 | interface CheckResult { 81 | success: boolean 82 | } 83 | 84 | interface ComplianceCheckRequest { 85 | headers: Headers | string[][] | Record 86 | body: string 87 | } 88 | interface ComplianceCheckResponse { 89 | headers: Headers | string[][] | Record 90 | status: number 91 | statusText: string 92 | json: T | Record | null 93 | body: string 94 | } 95 | 96 | type Revision = string 97 | 98 | interface ComplianceCheckDetails { 99 | pair: ServiceAndTokenPair 100 | errors: Error[] 101 | url: string 102 | method: string 103 | title: string 104 | successful: boolean 105 | validationResult: ValidationResult | null 106 | request: ComplianceCheckRequest 107 | response: ComplianceCheckResponse 108 | result: T | null 109 | expectationResults: ExpectationResult[] 110 | 111 | } 112 | // eslint-disable-next-line @typescript-eslint/dot-notation 113 | interface ProcessedResponse extends Response { 114 | text: string | null 115 | json: Record 116 | } 117 | 118 | interface ComplianceCheckDetailsCallback { 119 | (details: ComplianceCheckDetailsCallbackArg): void 120 | } 121 | 122 | export type { 123 | ComplianceCheckDetails, 124 | ExpectationResult, 125 | ProcessedResponse, 126 | ComplianceCheckDetailsCallback, 127 | ComplianceCheckDetailsCallbackArg, 128 | PinningSpecJoiSchema, 129 | ServiceAndTokenPair, 130 | ImplementableMethods, 131 | PinsApiResponseTypes, 132 | Revision 133 | } 134 | -------------------------------------------------------------------------------- /src/checks/get/testPagination.ts: -------------------------------------------------------------------------------- 1 | import { ApiCall } from '../../ApiCall.js' 2 | import { resultNotNull, responseOk } from '../../expectations/index.js' 3 | import { allPinStatuses } from '../../utils/constants.js' 4 | import { getInlineCid } from '../../utils/getInlineCid.js' 5 | import { getOldestPinCreateDate } from '../../utils/getOldestPinCreateDate.js' 6 | import type { ServiceAndTokenPair } from '../../types.js' 7 | 8 | /** 9 | * https://github.com/ipfs-shipyard/pinning-service-compliance/issues/6 10 | * 11 | */ 12 | const testPagination = async (pair: ServiceAndTokenPair): Promise => { 13 | const pinsNeededToTestPagination = 15 14 | 15 | const mainPaginationApiCall = new ApiCall({ 16 | pair, 17 | title: 'Pagination: Get all pins, create new pins (optional), get first and second pages', 18 | fn: async (client) => client.pinsGet({ status: allPinStatuses }) 19 | }) 20 | .expect(responseOk()) 21 | .expect(resultNotNull()) 22 | 23 | let pinsNeededToBeCreated = pinsNeededToTestPagination 24 | 25 | try { 26 | const pins = await mainPaginationApiCall.request 27 | try { 28 | /** 29 | * Catching pins == null with try catch so we can get an error object instead of creating one. 30 | */ 31 | // @ts-expect-error - pins can be null here. 32 | pinsNeededToBeCreated = pinsNeededToTestPagination - pins.count 33 | } catch (error) { 34 | mainPaginationApiCall.addExpectationErrors([{ 35 | title: `Could not determine efficient number of pins to create. Creating required minimum of ${pinsNeededToTestPagination} new pins`, 36 | error: error as Error 37 | }]) 38 | } 39 | } catch (error) { 40 | mainPaginationApiCall.errors.push({ 41 | title: 'Unexpected error when waiting for pinsGet request to complete', 42 | error: error as Error 43 | }) 44 | } 45 | 46 | // add more pins so we have enough to paginate 47 | while (pinsNeededToBeCreated > 0) { 48 | const cid = await getInlineCid() 49 | new ApiCall({ 50 | parent: mainPaginationApiCall, 51 | pair, 52 | title: `Can create new pin for testing pagination cid='${cid}'`, 53 | fn: async (client) => client.pinsPost({ pin: { cid } }) 54 | }) 55 | .expect(responseOk()) 56 | .expect(resultNotNull()) 57 | 58 | pinsNeededToBeCreated-- 59 | } 60 | 61 | const firstPageOfPins = new ApiCall({ 62 | parent: mainPaginationApiCall, 63 | pair, 64 | title: 'Pagination: First page of pins', 65 | fn: async (client) => client.pinsGet({ status: allPinStatuses }) 66 | }) 67 | .expect(responseOk()) 68 | .expect(resultNotNull()) 69 | .expect({ 70 | title: `Count is greater than or equal to ${pinsNeededToTestPagination}`, 71 | fn: ({ result }) => (result?.count ?? 0) >= pinsNeededToTestPagination 72 | }) 73 | .expect({ 74 | title: 'Count is greater than the number of pins returned', 75 | fn: ({ result }) => (result?.results.size ?? 0) < (result?.count ?? 0) 76 | }) 77 | .expect({ 78 | title: 'Number of pins returned defaults to 10', 79 | fn: ({ result }) => result?.results.size === 10 80 | }) 81 | 82 | const requestIds = new Set() 83 | const firstPageResult = await firstPageOfPins.request 84 | let before = new Date() 85 | let firstPageSize = 0 86 | if (firstPageResult == null) { 87 | firstPageOfPins.addExpectationErrors([{ 88 | title: 'Result is null, cannot get oldest pin create date in order to paginate', 89 | error: new Error('First page result is null') 90 | }]) 91 | } else { 92 | before = getOldestPinCreateDate(firstPageResult.results) 93 | firstPageResult.results.forEach((pin) => { 94 | requestIds.add(pin.requestid) 95 | }) 96 | firstPageSize = firstPageResult.results.size 97 | } 98 | 99 | new ApiCall({ 100 | parent: mainPaginationApiCall, 101 | pair, 102 | title: 'Pagination: Retrieve the next page of pins', 103 | fn: async (client) => client.pinsGet({ status: allPinStatuses, before }) 104 | }) 105 | .expect(responseOk()) 106 | .expect(resultNotNull()) 107 | .expect({ 108 | title: 'The next page of pins doesn\'t contain any of previous pages pins', 109 | fn: ({ result }) => { 110 | if (result != null) { 111 | const secondPageResult = (result) 112 | const secondPageSize = secondPageResult.results.size 113 | 114 | secondPageResult.results.forEach((pin) => { 115 | requestIds.add(pin.requestid) 116 | }) 117 | return firstPageSize + secondPageSize === requestIds.size 118 | } else { 119 | throw new Error('Second page result is null') 120 | } 121 | } 122 | }) 123 | 124 | await mainPaginationApiCall.runExpectations() 125 | } 126 | 127 | export { testPagination } 128 | -------------------------------------------------------------------------------- /src/middleware/requestReponseLogger.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../utils/logs.js' 2 | import { getPinTracker } from '../utils/pinTracker.js' 3 | import { waitForDate } from '../utils/waitForDate.js' 4 | import type { ComplianceCheckDetailsCallbackArg, ServiceAndTokenPair } from '../types.js' 5 | import type { NodeFetch, Pin } from '@ipfs-shipyard/pinning-service-client' 6 | 7 | interface RequestResponseLoggerOptions { 8 | finalCb?(details: ComplianceCheckDetailsCallbackArg): void | Promise 9 | preCb?(context: NodeFetch.RequestContext): void | Promise 10 | postCb?(context: NodeFetch.ResponseContext): void | Promise 11 | pair: ServiceAndTokenPair 12 | } 13 | 14 | type RateLimitKey = string 15 | const getRateLimitKeyFromContext = (context: NodeFetch.ResponseContext | NodeFetch.RequestContext): RateLimitKey => { 16 | const { init, url } = context 17 | const { method } = init 18 | const urlWithoutQuery = url.split('?')[0] 19 | let key = method ?? 'Unknown' 20 | if (method === 'DELETE') { 21 | // The last path on a delete url is the requestid. 22 | key = `${key}:${urlWithoutQuery.split('/').slice(0, -1).join('/')}` 23 | } else { 24 | key = `${key}:${urlWithoutQuery}` 25 | } 26 | return key 27 | } 28 | 29 | const isPostMethod = (context: NodeFetch.RequestContext): boolean => { 30 | if (context.init.method === 'POST' || /^POST/.test(getRateLimitKeyFromContext(context))) { 31 | return true 32 | } 33 | return false 34 | } 35 | 36 | const rateLimitHandlers = new Map>>() 37 | const requestResponseLogger: (opts: RequestResponseLoggerOptions) => NodeFetch.Middleware = ({ preCb, postCb, finalCb, pair }) => { 38 | return ({ 39 | pre: async (context) => { 40 | logger.debug('In middleware.pre') 41 | try { 42 | if (preCb != null) await preCb(context) 43 | } catch (err) { 44 | logger.error(err) 45 | } 46 | 47 | const rateLimitKey = getRateLimitKeyFromContext(context) 48 | 49 | if (isPostMethod(context)) { 50 | if (context.init.body == null) { 51 | throw new Error('POST contains empty body') 52 | } 53 | const pinTracker = await getPinTracker(pair) 54 | try { 55 | const createdPin: Pin = JSON.parse(String(context.init.body)) 56 | const { cid: createdPinCID } = createdPin 57 | context.init.body = JSON.stringify({ 58 | ...createdPin, 59 | meta: { 60 | ...createdPin.meta, 61 | createdBy: process.env.npm_package_name 62 | } 63 | }) 64 | pinTracker.add(createdPinCID) 65 | } catch (err) { 66 | logger.error('Could not parse body to obtain CID for created PIN') 67 | throw err 68 | } 69 | } 70 | 71 | const promises = rateLimitHandlers.get(rateLimitKey) 72 | if (promises != null) { 73 | if (promises.length > 0) { 74 | try { 75 | await Promise.all(promises) 76 | } catch (err) { 77 | logger.error(err) 78 | } 79 | rateLimitHandlers.set(rateLimitKey, []) 80 | } 81 | } else { 82 | rateLimitHandlers.set(rateLimitKey, []) 83 | } 84 | 85 | return context 86 | }, 87 | 88 | post: async (context) => { 89 | logger.debug('In middleware.post') 90 | if (postCb != null) { 91 | try { 92 | await postCb(context) 93 | } catch (err) { 94 | logger.error('In middleware.post after failed postCb', err) 95 | } 96 | } 97 | const response = context.response as Response 98 | 99 | if (response.headers.has('x-ratelimit-reset') && response.headers.has('x-ratelimit-remaining')) { 100 | const rateLimitKey = getRateLimitKeyFromContext(context) 101 | const rateLimitReset = Number(response.headers.get('x-ratelimit-reset')) 102 | const dateOfReset = new Date(rateLimitReset * 1000) 103 | const rateLimit = Number(response.headers.get('x-ratelimit-limit')) 104 | const rateRemaining = Number(response.headers.get('x-ratelimit-remaining')) 105 | logger.debug(`${rateLimitKey}: Rate limit is ${rateLimit} and we have ${rateRemaining} tokens remaining.`) 106 | if (rateRemaining === 0) { 107 | logger.debug(`${rateLimitKey}: No rate tokens remaining, we need to wait until ${dateOfReset.toString()}`) 108 | const promises = rateLimitHandlers.get(rateLimitKey) 109 | if (promises != null) { 110 | promises.push(waitForDate(dateOfReset)) 111 | } else { 112 | rateLimitHandlers.set(rateLimitKey, [waitForDate(dateOfReset)]) 113 | } 114 | } 115 | } 116 | return response 117 | } 118 | }) 119 | } 120 | 121 | export type { RequestResponseLoggerOptions } 122 | export { requestResponseLogger } 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ipfs-shipyard/pinning-service-compliance", 3 | "version": "1.8.1", 4 | "description": "", 5 | "author": "Russell Dempsey <1173416+SgtPooki@users.noreply.github.com>", 6 | "license": "Apache-2.0 OR MIT", 7 | "homepage": "https://github.com/ipfs-shipyard/pinning-service-compliance#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/ipfs-shipyard/pinning-service-compliance.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/ipfs-shipyard/pinning-service-compliance/issues" 14 | }, 15 | "publishConfig": { 16 | "access": "public", 17 | "provenance": true 18 | }, 19 | "keywords": [ 20 | "ipfs" 21 | ], 22 | "bin": "./dist/src/index.js", 23 | "type": "module", 24 | "types": "./dist/src/index.d.ts", 25 | "files": [ 26 | "src", 27 | "dist", 28 | "!dist/test", 29 | "!**/*.tsbuildinfo" 30 | ], 31 | "exports": { 32 | ".": { 33 | "types": "./dist/src/index.d.ts", 34 | "import": "./dist/src/index.js" 35 | } 36 | }, 37 | "eslintConfig": { 38 | "extends": "ipfs", 39 | "parserOptions": { 40 | "project": true, 41 | "sourceType": "module" 42 | } 43 | }, 44 | "release": { 45 | "branches": [ 46 | "main" 47 | ], 48 | "plugins": [ 49 | [ 50 | "@semantic-release/commit-analyzer", 51 | { 52 | "preset": "conventionalcommits", 53 | "releaseRules": [ 54 | { 55 | "breaking": true, 56 | "release": "major" 57 | }, 58 | { 59 | "revert": true, 60 | "release": "patch" 61 | }, 62 | { 63 | "type": "feat", 64 | "release": "minor" 65 | }, 66 | { 67 | "type": "fix", 68 | "release": "patch" 69 | }, 70 | { 71 | "type": "docs", 72 | "release": "patch" 73 | }, 74 | { 75 | "type": "test", 76 | "release": "patch" 77 | }, 78 | { 79 | "type": "deps", 80 | "release": "patch" 81 | }, 82 | { 83 | "scope": "no-release", 84 | "release": false 85 | } 86 | ] 87 | } 88 | ], 89 | [ 90 | "@semantic-release/release-notes-generator", 91 | { 92 | "preset": "conventionalcommits", 93 | "presetConfig": { 94 | "types": [ 95 | { 96 | "type": "feat", 97 | "section": "Features" 98 | }, 99 | { 100 | "type": "fix", 101 | "section": "Bug Fixes" 102 | }, 103 | { 104 | "type": "chore", 105 | "section": "Trivial Changes" 106 | }, 107 | { 108 | "type": "docs", 109 | "section": "Documentation" 110 | }, 111 | { 112 | "type": "deps", 113 | "section": "Dependencies" 114 | }, 115 | { 116 | "type": "test", 117 | "section": "Tests" 118 | } 119 | ] 120 | } 121 | } 122 | ], 123 | "@semantic-release/changelog", 124 | "@semantic-release/npm", 125 | "@semantic-release/github", 126 | "@semantic-release/git" 127 | ] 128 | }, 129 | "scripts": { 130 | "clean": "aegir clean downloaded/*.yaml generated dist", 131 | "test": "run-s dep-check lint", 132 | "lint": "aegir lint", 133 | "build": "aegir build", 134 | "start": "node dist/src/index.js", 135 | "dep-check": "aegir dep-check", 136 | "dev-start": "NODE_OPTIONS='--unhandled-rejections=strict --trace-atomics-wait --trace-deprecation --trace-exit --trace-sigint --trace-uncaught --trace-warnings' node dist/src/index.js", 137 | "release": "aegir release", 138 | "rebuild": "run-s clean -- node_modules && npm install && run-s build" 139 | }, 140 | "dependencies": { 141 | "@hapi/joi": "^17.1.1", 142 | "@ipfs-shipyard/pinning-service-client": "^2.0.0", 143 | "chalk": "^5.3.0", 144 | "git-rev": "^0.2.1", 145 | "ipfsd-ctl": "^15.0.0", 146 | "kubo": "^0.30.0", 147 | "kubo-rpc-client": "^5.0.1", 148 | "marked": "^13.0.3", 149 | "marked-terminal": "^7.1.0", 150 | "multiformats": "^13.3.0", 151 | "oas2joi": "^2.0.2", 152 | "p-defer": "^4.0.1", 153 | "p-queue": "^8.0.1", 154 | "uuid": "^10.0.0", 155 | "winston": "^3.7.2", 156 | "yargs": "^17.5.1" 157 | }, 158 | "devDependencies": { 159 | "@types/git-rev": "^0.2.0", 160 | "@types/hapi__joi": "^17.1.8", 161 | "@types/marked": "^6.0.0", 162 | "@types/marked-terminal": "^6.1.1", 163 | "@types/node": "^22.7.4", 164 | "@types/uuid": "^10.0.0", 165 | "@types/yargs": "^17.0.10", 166 | "aegir": "^44.1.1", 167 | "npm-run-all": "^4.1.5" 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/output/reporting.ts: -------------------------------------------------------------------------------- 1 | // create separate markdown file per service 2 | import { constants as fsConstants } from 'fs' 3 | import { access, mkdir, writeFile, appendFile, readFile } from 'fs/promises' 4 | import { join } from 'path' 5 | import chalk from 'chalk' 6 | import { docsDir } from '../utils/constants.js' 7 | import { getHostnameFromUrl } from '../utils/getHostnameFromUrl.js' 8 | import { logger } from '../utils/logs.js' 9 | import { getFormatter } from './formatter.js' 10 | import { getHeader, type RequiredHeaderProps } from './getHeader.js' 11 | import { getReportEntry } from './getReportEntry.js' 12 | import type { ApiCall } from '../ApiCall.js' 13 | import type { ComplianceCheckDetails, PinsApiResponseTypes } from '../types.js' 14 | 15 | const successFormatter = getFormatter({ 16 | paragraph: chalk.reset, 17 | heading: chalk.green 18 | }) 19 | 20 | const failureFormatter = getFormatter({ 21 | paragraph: chalk.reset, 22 | heading: chalk.redBright 23 | }) 24 | 25 | const getReportFilePath = (hostname: string): string => { 26 | const filename = `${hostname}.md` 27 | return join(docsDir, filename) 28 | } 29 | 30 | // Need summary at the top for each service: # pass/fail 31 | const addToReport = async (details: ComplianceCheckDetails): Promise => { 32 | const reportEntryMarkdown = getReportEntry(details) 33 | const formatter = details.successful ? successFormatter : failureFormatter 34 | 35 | const hostname = getHostnameFromUrl(details.url) 36 | const reportFilePath = getReportFilePath(hostname) 37 | 38 | try { 39 | await access(docsDir, fsConstants.R_OK | fsConstants.W_OK) 40 | } catch (err) { 41 | try { 42 | await mkdir(docsDir, { recursive: true }) 43 | } catch (err) { 44 | logger.error(`Unexpected error when attempting to create docs directory ${docsDir}`) 45 | logger.error(err) 46 | 47 | return 48 | } 49 | } 50 | 51 | try { 52 | await appendFile(reportFilePath, reportEntryMarkdown) 53 | logger.debug(`Wrote markdown to ${reportFilePath}`) 54 | 55 | logger.verbose(await formatter(reportEntryMarkdown), { messageOnly: true }) 56 | } catch (err) { 57 | logger.error(`Unexpected error when attempting to write report entry markdown to ${reportFilePath}`) 58 | logger.error(err) 59 | } 60 | } 61 | 62 | const reportSummaryInfo = new Map>>() 63 | 64 | const addApiCallToReport = async (apiCall: ApiCall): Promise => { 65 | try { 66 | const { pair, errors, title, httpRequest, result, response, expectationResults, successful, text, validationResult } = await apiCall.reportData() 67 | const { url, headers: requestHeaders } = httpRequest 68 | const method = httpRequest.method ?? 'Unknown' 69 | const requestBody = await httpRequest.text() 70 | const responseBody = text ?? '' 71 | const hostname = getHostnameFromUrl(url) 72 | 73 | const headerProps: RequiredHeaderProps = { pair, title, successful } 74 | const complianceCheckDetails: ComplianceCheckDetails = { 75 | ...headerProps, 76 | errors: errors.map((expectationError) => expectationError.error), 77 | expectationResults, 78 | url, 79 | method, 80 | validationResult, 81 | result: result as T, 82 | request: { 83 | body: requestBody, 84 | headers: requestHeaders ?? {} 85 | }, 86 | response: { 87 | json: apiCall.json, 88 | body: responseBody, 89 | headers: response.headers, 90 | status: response.status, 91 | statusText: response.statusText 92 | } 93 | } 94 | if (apiCall.parent == null) { 95 | const hostReport = reportSummaryInfo.get(hostname) 96 | if (hostReport != null) { 97 | hostReport.push(headerProps) 98 | } else { 99 | reportSummaryInfo.set(hostname, [headerProps]) 100 | // clear out the file 101 | await writeFile(getReportFilePath(hostname), '') 102 | } 103 | } 104 | await addToReport(complianceCheckDetails) 105 | } catch (err) { 106 | logger.error(`Unexpected error while adding details of ApiCall '${apiCall.title}' to report`, err) 107 | } 108 | } 109 | 110 | const createReport = async (hostname: string, details: Array>): Promise => { 111 | const reportFilePath = getReportFilePath(hostname) 112 | 113 | // Write console output first 114 | const noMarkdownLinksHeader = await getHeader(details, { markdownLinks: false }) // Don't give npx users cluttered output. 115 | const consoleHeader = `${noMarkdownLinksHeader.replace(/#+ /gm, '')}\nSee the full report at ${reportFilePath}` 116 | logger.info(consoleHeader, { messageOnly: true }) 117 | 118 | // write file content 119 | const header = await getHeader(details) 120 | const fileContents = await readFile(reportFilePath, 'utf8') 121 | await writeFile(reportFilePath, header + fileContents) 122 | } 123 | 124 | const writeHeaders = async (): Promise => { 125 | for await (const [hostname, details] of reportSummaryInfo.entries()) { 126 | await createReport(hostname, details) 127 | } 128 | } 129 | 130 | export { addToReport, addApiCallToReport, writeHeaders } 131 | -------------------------------------------------------------------------------- /.github/workflows/build-reports.yml: -------------------------------------------------------------------------------- 1 | name: Build compliance reports 2 | on: 3 | # This workflow should be required for 'test & maybe release' to succeed 4 | # see https://docs.github.com/en/actions/using-workflows/reusing-workflows#creating-a-reusable-workflow 5 | workflow_call: 6 | 7 | workflow_dispatch: 8 | inputs: 9 | type: 10 | description: 'Emulate either schedule, push, or pull_request' 11 | required: true 12 | default: 'schedule' 13 | type: choice 14 | options: 15 | - schedule 16 | - push 17 | - pull_request 18 | 19 | jobs: 20 | 21 | check-crust-compliance: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 🛎️ 25 | uses: actions/checkout@v4 26 | - uses: ipfs/aegir/actions/cache-node-modules@master 27 | - name: Reports Cache 28 | uses: actions/cache@v4 29 | with: 30 | path: docs 31 | key: ${{ github.sha }}-crust 32 | - run: npm run dev-start -- -s ${{ secrets.CRUST_API_ENDPOINT }} ${{secrets.CRUST_API_TOKEN}} 33 | - uses: actions/upload-artifact@v4 34 | with: 35 | name: crust-logs 36 | path: dist/docs/pin.crustcode.com 37 | - uses: actions/upload-artifact@v4 38 | with: 39 | name: crust-report 40 | path: dist/docs/pin.crustcode.com.md 41 | 42 | check-pinata-compliance: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Checkout 🛎️ 46 | uses: actions/checkout@v4 47 | - uses: ipfs/aegir/actions/cache-node-modules@master 48 | - name: Reports Cache 49 | uses: actions/cache@v4 50 | with: 51 | path: docs 52 | key: ${{ github.sha }}-pinata 53 | - run: npm run dev-start -- -s ${{ secrets.PINATA_API_ENDPOINT }} ${{secrets.PINATA_API_TOKEN}} 54 | - uses: actions/upload-artifact@v4 55 | with: 56 | name: pinata-logs 57 | path: dist/docs/api.pinata.cloud 58 | - uses: actions/upload-artifact@v4 59 | with: 60 | name: pinata-report 61 | path: dist/docs/api.pinata.cloud.md 62 | 63 | check-filebase-compliance: 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Checkout 🛎️ 67 | uses: actions/checkout@v4 68 | - uses: ipfs/aegir/actions/cache-node-modules@master 69 | - name: Reports Cache 70 | uses: actions/cache@v4 71 | with: 72 | path: docs 73 | key: ${{ github.sha }}-filebase 74 | - run: npm run dev-start -- -s ${{ secrets.FILEBASE_API_ENDPOINT }} ${{secrets.FILEBASE_API_TOKEN}} 75 | - uses: actions/upload-artifact@v4 76 | with: 77 | name: filebase-logs 78 | path: dist/docs/api.filebase.io 79 | - uses: actions/upload-artifact@v4 80 | with: 81 | name: filebase-report 82 | path: dist/docs/api.filebase.io.md 83 | 84 | check-nft-dot-storage-compliance: 85 | runs-on: ubuntu-latest 86 | steps: 87 | - name: Checkout 🛎️ 88 | uses: actions/checkout@v4 89 | - uses: ipfs/aegir/actions/cache-node-modules@master 90 | - name: Reports Cache 91 | uses: actions/cache@v4 92 | with: 93 | path: docs 94 | key: ${{ github.sha }}-nft 95 | - run: npm run dev-start -- -s ${{ secrets.NFT_API_ENDPOINT }} ${{secrets.NFT_API_TOKEN}} 96 | - uses: actions/upload-artifact@v4 97 | with: 98 | name: nft-logs 99 | path: dist/docs/nft.storage 100 | - uses: actions/upload-artifact@v4 101 | with: 102 | name: nft-report 103 | path: dist/docs/nft.storage.md 104 | 105 | check-web3-dot-storage-compliance: 106 | runs-on: ubuntu-latest 107 | steps: 108 | - name: Checkout 🛎️ 109 | uses: actions/checkout@v4 110 | - uses: ipfs/aegir/actions/cache-node-modules@master 111 | - name: Reports Cache 112 | uses: actions/cache@v4 113 | with: 114 | path: docs 115 | key: ${{ github.sha }}-web3 116 | - run: npm run dev-start -- -s ${{ secrets.WEB3_API_ENDPOINT }} ${{secrets.WEB3_API_TOKEN}} 117 | - uses: actions/upload-artifact@v4 118 | with: 119 | name: web3-logs 120 | path: dist/docs/api.web3.storage 121 | - uses: actions/upload-artifact@v4 122 | with: 123 | name: web3-report 124 | path: dist/docs/api.web3.storage.md 125 | 126 | check-4everland-compliance: 127 | runs-on: ubuntu-latest 128 | steps: 129 | - name: Checkout 🛎️ 130 | uses: actions/checkout@v4 131 | - uses: ipfs/aegir/actions/cache-node-modules@master 132 | - name: Reports Cache 133 | uses: actions/cache@v4 134 | with: 135 | path: docs 136 | key: ${{ github.sha }}-4everland 137 | - run: npm run dev-start -- -s ${{ secrets.FOREVERLAND_API_ENDPOINT }} ${{secrets.FOREVERLAND_API_TOKEN}} 138 | - uses: actions/upload-artifact@v4 139 | with: 140 | name: 4everland-logs 141 | path: dist/docs/api.4everland.dev 142 | - uses: actions/upload-artifact@v4 143 | with: 144 | name: 4everland-report 145 | path: dist/docs/api.4everland.dev.md 146 | 147 | check-scaleway-compliance: 148 | runs-on: ubuntu-latest 149 | steps: 150 | - name: Checkout 🛎️ 151 | uses: actions/checkout@v4 152 | - uses: ipfs/aegir/actions/cache-node-modules@master 153 | - name: Reports Cache 154 | uses: actions/cache@v4 155 | with: 156 | path: docs 157 | key: ${{ github.sha }}-scaleway 158 | - run: npm run dev-start -- -s ${{ secrets.SCALEWAY_API_ENDPOINT }} ${{secrets.SCALEWAY_API_TOKEN}} 159 | - uses: actions/upload-artifact@v4 160 | with: 161 | name: scaleway-logs 162 | path: dist/docs/pl-waw.ipfs.labs.scw.cloud 163 | - uses: actions/upload-artifact@v4 164 | with: 165 | name: scaleway-report 166 | path: dist/docs/pl-waw.ipfs.labs.scw.cloud.md 167 | -------------------------------------------------------------------------------- /src/ApiCall.ts: -------------------------------------------------------------------------------- 1 | import pDefer, { type DeferredPromise } from 'p-defer' 2 | import { clientFromServiceAndTokenPair } from './clientFromServiceAndTokenPair.js' 3 | import { isError } from './guards/isError.js' 4 | import { isResponse } from './guards/isResponse.js' 5 | import { getSuccessIcon } from './output/getSuccessIcon.js' 6 | import { addApiCallToReport } from './output/reporting.js' 7 | import { Icons } from './utils/constants.js' 8 | import { getTextAndJson } from './utils/fetchSafe/getTextAndJson.js' 9 | import { getQueue } from './utils/getQueue.js' 10 | import { getServiceLogger, logger as consoleLogger } from './utils/logs.js' 11 | import { globalReport } from './utils/report.js' 12 | import type { ComplianceCheckDetailsCallbackArg, ExpectationResult, PinsApiResponseTypes, ServiceAndTokenPair } from './types.js' 13 | import type { Schema, ValidationError, ValidationResult } from '@hapi/joi' 14 | import type { RemotePinningServiceClient, RequestContext, ResponseContext } from '@ipfs-shipyard/pinning-service-client' 15 | import type { Logger } from 'winston' 16 | 17 | interface ApiCallOptions { 18 | pair: ServiceAndTokenPair 19 | fn(client: RemotePinningServiceClient): Promise 20 | schema?: Schema 21 | title: string 22 | parent?: ApiCall

| undefined 23 | } 24 | 25 | interface ExpectationError { 26 | error: Error 27 | title: string 28 | } 29 | 30 | interface ExpectationCallbackArg { 31 | responseContext: ResponseContext 32 | details: ComplianceCheckDetailsCallbackArg 33 | apiCall: ApiCall 34 | result: T | null 35 | } 36 | 37 | interface ExpectationFn { 38 | (arg: ExpectationCallbackArg): boolean | Promise 39 | } 40 | 41 | interface ApiCallExpectation { 42 | context?: ApiCall 43 | fn: ExpectationFn 44 | title: string 45 | } 46 | 47 | export interface ReportData { 48 | pair: ServiceAndTokenPair 49 | errors: ExpectationError[] 50 | title: string 51 | httpRequest: Request 52 | result: PinsApiResponseTypes | null 53 | response: Response 54 | expectationResults: ExpectationResult[] 55 | successful: boolean 56 | text: string | null 57 | validationErrors: ValidationError | undefined 58 | validationResult: ValidationResult | null 59 | } 60 | 61 | class ApiCall { 62 | request: Promise 63 | expectations: Array> = [] 64 | errors: ExpectationError[] = [] 65 | title: string 66 | pair: ServiceAndTokenPair 67 | client: RemotePinningServiceClient 68 | successful: boolean = true 69 | json: T | null = null 70 | text: string | null = null 71 | logger: Logger 72 | 73 | /** 74 | * A deferred promise that is only resolved after expectations have ran via `runExpectations`. 75 | * 76 | * Used to handle parent/child expectation ordering 77 | */ 78 | readonly afterExpectations = pDefer() 79 | parent: ApiCall

| null 80 | children: Array> = [] 81 | 82 | /** 83 | * These properties are only available after the request completes, and are private to prevent access prior to setting them. 84 | */ 85 | 86 | private details!: ComplianceCheckDetailsCallbackArg 87 | private result!: T | null 88 | private readonly responseContext: DeferredPromise = pDefer() 89 | private failureReason: Error | unknown 90 | private requestContext!: RequestContext 91 | private response!: Response 92 | private readonly expectationResults: ExpectationResult[] = [] 93 | private validationErrors: ValidationError | undefined 94 | private validationResult!: ValidationResult | null 95 | 96 | constructor ({ pair, fn, schema, title, parent }: ApiCallOptions) { 97 | globalReport.incrementApiCallsCount() 98 | this.logger = getServiceLogger(pair[0]) 99 | consoleLogger.debug(`Creating new ApiCall: ${title}`) 100 | this.saveRequest = this.saveRequest.bind(this) 101 | this.saveResponse = this.saveResponse.bind(this) 102 | this.saveDetails = this.saveDetails.bind(this) 103 | this.title = title 104 | this.pair = pair 105 | this.parent = parent ?? null 106 | 107 | this.client = clientFromServiceAndTokenPair(pair, { 108 | preCb: this.saveRequest, 109 | postCb: this.saveResponse 110 | }) 111 | if (schema != null) { 112 | this.addSchema(schema) 113 | } 114 | if (parent != null) { 115 | parent.addChild(this) 116 | } 117 | 118 | this.request = getQueue(pair[0]).add(async () => { 119 | try { 120 | if (parent != null) { 121 | consoleLogger.debug(`ApiCall '${title}' has parent, waiting for it to finish`) 122 | // parent API call must finish before we send subsequent requests 123 | await parent.request 124 | consoleLogger.debug(`ApiCall '${title}' parent has finished`) 125 | } 126 | // eslint-disable-next-line @typescript-eslint/return-await 127 | return await fn(this.client) 128 | } catch (err) { 129 | let error: Error 130 | if (isResponse(err)) { 131 | error = new Error('Invalid response caused unexpected error in pinning-service-client') 132 | } else if (isError(err)) { 133 | error = err 134 | } else { 135 | error = new Error(err as string) 136 | } 137 | this.errors.push({ 138 | title: 'Error running primary ApiCall fn', 139 | error 140 | }) 141 | throw error 142 | } 143 | }, { 144 | // https://github.com/sindresorhus/p-queue/issues/175 145 | throwOnTimeout: true 146 | }).then((result) => { 147 | this.result = result 148 | return result 149 | }).catch((reason) => { 150 | consoleLogger.debug('Caught error during primary fn', { error: reason }) 151 | this.failureReason = reason 152 | this.result = null 153 | return null 154 | }) 155 | } 156 | 157 | get httpResponse (): Response { 158 | return this.response 159 | } 160 | 161 | get httpRequest (): Request { 162 | const { init, url } = this.requestContext 163 | const request = new Request(url, init) 164 | 165 | return request 166 | } 167 | 168 | expect (expectation: ApiCallExpectation): this { 169 | this.expectations.push(expectation) 170 | return this 171 | } 172 | 173 | async runExpectations (fromParent = false): Promise { 174 | const hasParent = this.parent != null 175 | let padding = '' 176 | if (hasParent) { 177 | if (!fromParent) { 178 | consoleLogger.debug(`Ignoring runExpectations call for '${this.title}'. It will be called by the parent.`) 179 | return this 180 | } 181 | padding = '\t' 182 | } else { 183 | globalReport.incrementRunExpectationsCallCount() 184 | } 185 | consoleLogger.info(`${padding}${this.title}`, { messageOnly: true }) 186 | 187 | try { 188 | await this.request 189 | } catch (err) { 190 | consoleLogger.error('Error occurred while waiting for request to conclude', err) 191 | } 192 | 193 | const result = this.result 194 | for await (const expectation of this.expectations) { 195 | globalReport.incrementTotalExpectationsCount() 196 | const { fn, title } = expectation 197 | try { 198 | const success = await fn({ 199 | responseContext: await this.responseContext.promise, 200 | details: this.details, 201 | apiCall: this, 202 | result 203 | }) 204 | consoleLogger.info(`${padding}${getSuccessIcon(success)} ${title}`, { nested: true }) 205 | this.successful = this.successful && success 206 | 207 | this.expectationResults.push({ 208 | success, 209 | title 210 | }) 211 | } catch (error) { 212 | consoleLogger.info(`${padding}${Icons.ERROR} ${title}`, { nested: true }) 213 | consoleLogger.error('Unexpected error occurred while running expectation function', error) 214 | this.successful = false 215 | this.expectationResults.push({ 216 | success: false, 217 | error: error as Error, 218 | title 219 | }) 220 | this.errors.push({ title, error: error as Error }) 221 | } 222 | } 223 | 224 | this.afterExpectations.resolve() 225 | for await (const child of this.children) { 226 | await child.runExpectations(true) 227 | this.successful = this.successful && child.successful 228 | const { expectationResults, errors } = await child.reportData() 229 | this.addExpectationResults(expectationResults) 230 | this.addExpectationErrors(errors) 231 | } 232 | 233 | try { 234 | await addApiCallToReport(this) 235 | } catch (err) { 236 | consoleLogger.error(`Could not add details of ApiCall '${this.title}' to report`, err) 237 | } 238 | 239 | if (!hasParent) { 240 | if (this.successful) { 241 | globalReport.incrementPassedExpectationsCount() 242 | } else { 243 | globalReport.incrementFailedExpectationsCount() 244 | } 245 | } 246 | 247 | return this 248 | } 249 | 250 | addExpectationResults (results: ExpectationResult[]): void { 251 | if (results.length > 0) { 252 | this.expectationResults.push(...results) 253 | } 254 | } 255 | 256 | addExpectationErrors (errors: ExpectationError[]): void { 257 | if (errors.length > 0) { 258 | this.errors.push(...errors) 259 | } 260 | } 261 | 262 | addSchema (schema: Schema): void { 263 | this.expect({ 264 | title: 'Response object matches api spec schema', 265 | fn: async (): Promise => { 266 | const result = await this.request 267 | if (result != null || this.failureReason != null) { 268 | this.validationResult = schema.validate(this.json ?? this.failureReason, { abortEarly: false, convert: true }) 269 | if (this.validationResult.error != null || this.validationResult.errors != null) { 270 | const validationErrors = this.validationResult.errors ?? this.validationResult.error as ValidationError 271 | this.validationErrors = validationErrors 272 | return false 273 | } else { 274 | return true 275 | } 276 | } else { 277 | consoleLogger.debug('Result and failureReason are null') 278 | this.errors.push({ error: new Error('Could not compare against joi Schema'), title: 'Result and failureReason are both, unexpectedly, null' }) 279 | return false 280 | } 281 | } 282 | }) 283 | } 284 | 285 | async reportData (): Promise { 286 | await this.request 287 | let { pair, errors, title, httpRequest, result, response, expectationResults, successful, text, validationErrors, validationResult } = this 288 | 289 | if (response == null) { 290 | response = new Response(null, { status: 0, statusText: 'No response received' }) 291 | } 292 | 293 | return { pair, errors, title, httpRequest, result, response, expectationResults, successful, text, validationErrors, validationResult } 294 | } 295 | 296 | addChild (child: ApiCall): void { 297 | this.children.push(child) 298 | } 299 | 300 | private saveRequest (context: RequestContext): void { 301 | consoleLogger.debug(`${this.title}: Saving request context for '${context.url}'`) 302 | this.requestContext = context 303 | } 304 | 305 | private async saveResponse (context: ResponseContext): Promise { 306 | consoleLogger.debug(`${this.title}: Saving response context for '${context.url}'`) 307 | this.response = context.response as unknown as ApiCall['response'] 308 | this.responseContext.resolve(context) 309 | try { 310 | const { text, json, errors } = await getTextAndJson(this.response) 311 | this.text = text 312 | this.json = json as T 313 | this.addExpectationErrors(errors.map((err) => ({ error: err, title: 'Problem when attempting to get response text and json' }))) 314 | } catch (err) { 315 | this.errors.push(err as ExpectationError) 316 | } 317 | this.saveDetails({ 318 | ...context, 319 | url: context.url, 320 | init: context.init, 321 | fetch: context.fetch, 322 | errors: [], 323 | response: this.response 324 | }) 325 | } 326 | 327 | /** 328 | * Details is set in {requestResponseLogger} middleware after the call is complete 329 | */ 330 | private saveDetails (details: ComplianceCheckDetailsCallbackArg): void { 331 | this.details = details 332 | } 333 | } 334 | 335 | export { ApiCall } 336 | export type { ApiCallExpectation } 337 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.8.1](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.8.0...v1.8.1) (2024-10-02) 2 | 3 | ### Bug Fixes 4 | 5 | * CI and update repo ([#344](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/344)) ([91f1e08](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/91f1e08ba1b224ed3bf3351b1f3ac639d711bddb)) 6 | * update all deps and fix type errors ([#376](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/376)) ([2b0d781](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/2b0d7819e867d2ed86df0eaa0ad4d5882127b87c)) 7 | * use AbortSignal.timeout instead of setTimeout ([#375](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/375)) ([f65ab4e](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/f65ab4e2590234967a8efada067baee943ab5833)) 8 | 9 | ### Trivial Changes 10 | 11 | * Update .github/dependabot.yml [skip ci] ([9c8c9e4](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/9c8c9e4748cd0369260d1a0936768b5abf5c4271)) 12 | * update workflow ([dae6def](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/dae6defd08673152dd9186c74e754ea3886daf1b)) 13 | 14 | ### Dependencies 15 | 16 | * bump @ipfs-shipyard/pinning-service-client from 1.0.1 to 2.0.0 ([#357](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/357)) ([24dc773](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/24dc7734a8e216b846cb9675cdcf49618831e277)) 17 | 18 | ## [1.8.0](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.7.0...v1.8.0) (2023-09-12) 19 | 20 | 21 | ### Features 22 | 23 | * add scaleway pinning service ([#333](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/333)) ([ae3a975](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/ae3a975c8cdadc3fe5602a5fb48f676fcbb3b6f3)) 24 | 25 | ## [1.7.0](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.6.0...v1.7.0) (2023-07-17) 26 | 27 | 28 | ### Features 29 | 30 | * remove node-fetch ([#303](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/303)) ([750b1db](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/750b1db5d9db1c991bf41621bdb6c9a233d0f26b)) 31 | 32 | ## [1.6.0](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.5.0...v1.6.0) (2023-06-27) 33 | 34 | 35 | ### Features 36 | 37 | * replace ipfs-http-client with kubo-rpc-client ([#302](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/302)) ([f6edd8c](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/f6edd8c6fd917df4adaa3a997a977560e04c0335)) 38 | 39 | ## [1.5.0](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.4.1...v1.5.0) (2023-06-27) 40 | 41 | 42 | ### Features 43 | 44 | * add 4EVERLAND pinning ([#301](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/301)) ([e5fdf56](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/e5fdf5660ddd68a2146dbeb2a5579a75a6f941f2)) 45 | 46 | ## [1.4.1](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.4.0...v1.4.1) (2023-06-22) 47 | 48 | 49 | ### Trivial Changes 50 | 51 | * **deps:** bump actions/github-script from 3 to 6 ([#205](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/205)) ([d0df4e8](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/d0df4e8cc3e10f0e5adad7980344dc2d153c4a34)) 52 | * **deps:** bump pascalgn/automerge-action from 0.15.3 to 0.15.6 ([#281](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/281)) ([3d9aa58](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/3d9aa5823c6bc61067cbe09a4570e73a0c93c592)) 53 | * **deps:** bump s0/git-publish-subdir-action from 2.5.1 to 2.6.0 ([#251](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/251)) ([5b07f53](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/5b07f538aabab93f9aada6fe2a7ce86ad9aa3115)) 54 | * Update .github/dependabot.yml [skip ci] ([6af5a2c](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/6af5a2c4ff91784a2ba637192be22de09b335fba)) 55 | 56 | ## [1.4.0](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.3.4...v1.4.0) (2023-01-25) 57 | 58 | 59 | ### Features 60 | 61 | * add telemetry ([#274](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/274)) ([c0d25f3](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/c0d25f3cbff7a38bd91bb142637740d34152721a)) 62 | 63 | ## [1.3.4](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.3.3...v1.3.4) (2022-10-05) 64 | 65 | 66 | ### Trivial Changes 67 | 68 | * **deps:** bump ipfsd-ctl from 12.0.2 to 12.1.0 ([#221](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/221)) ([535ee80](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/535ee807e7fcc15cafb33b511016b6ac9ad6ac75)) 69 | 70 | ## [1.3.3](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.3.2...v1.3.3) (2022-10-05) 71 | 72 | 73 | ### Trivial Changes 74 | 75 | * **deps:** bump actions/upload-artifact from 2 to 3 ([#215](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/215)) ([b308964](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/b308964362cd0deaaef79a1e8d1abcf1fffc15c5)) 76 | 77 | ## [1.3.2](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.3.1...v1.3.2) (2022-09-23) 78 | 79 | 80 | ### Bug Fixes 81 | 82 | * crust.io report link ([83e33c2](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/83e33c218ad084002164d89fee05b1b32ee0f866)) 83 | 84 | ## [1.3.1](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.3.0...v1.3.1) (2022-09-23) 85 | 86 | 87 | ### Bug Fixes 88 | 89 | * crust.io report publishing ([b19bdd0](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/b19bdd03b3ed99a7125edd703948e117cf43a61a)) 90 | 91 | ## [1.3.0](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.2.4...v1.3.0) (2022-09-07) 92 | 93 | 94 | ### Features 95 | 96 | * create re-usable workflow for service providers to use, and export json ([#141](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/141)) ([108fdcb](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/108fdcb7f68117e8eb8c4cd31a8f94335983647c)), closes [#142](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/142) 97 | 98 | ## [1.2.4](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.2.3...v1.2.4) (2022-09-05) 99 | 100 | 101 | ### Trivial Changes 102 | 103 | * **deps:** bump ipfsd-ctl from 12.0.1 to 12.0.2 ([#194](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/194)) ([30d0bc0](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/30d0bc0cbd0b2578045457be08d9b49aee869af5)) 104 | 105 | ## [1.2.3](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.2.2...v1.2.3) (2022-09-02) 106 | 107 | 108 | ### Trivial Changes 109 | 110 | * **deps:** bump actions/upload-artifact from 2 to 3 ([#119](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/119)) ([ad9cfda](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/ad9cfdafb1c6a4ca72a7cbd3684c37898d4615a8)) 111 | * **deps:** bump go-ipfs from 0.14.0 to 0.15.0 ([#191](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/191)) ([50879d2](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/50879d2fdd8127f16122944a15edf8df54a2c6e7)) 112 | * **deps:** bump ipfsd-ctl from 11.0.1 to 12.0.1 ([#192](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/192)) ([c7fe568](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/c7fe568bc6e2ca5e21a45e954a78656586569699)) 113 | 114 | ## [1.2.2](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.2.1...v1.2.2) (2022-08-30) 115 | 116 | 117 | ### Bug Fixes 118 | 119 | * publishing filebase reports ([#189](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/189)) ([ff39295](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/ff39295f0a08494758f81a82065e9e232a727284)) 120 | 121 | 122 | ### Trivial Changes 123 | 124 | * **deps-dev:** bump aegir from 37.5.2 to 37.5.3 ([#190](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/190)) ([b7d1f46](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/b7d1f4664b0c7506e0a6a17f605f4155559211d7)) 125 | 126 | ## [1.2.1](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.2.0...v1.2.1) (2022-08-30) 127 | 128 | 129 | ### Trivial Changes 130 | 131 | * **deps-dev:** bump @types/node from 18.7.13 to 18.7.14 ([#186](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/186)) ([824eefb](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/824eefb84582671d3c44146c2f5217bd53bd205c)) 132 | * **deps-dev:** bump @types/yargs from 17.0.11 to 17.0.12 ([#188](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/188)) ([be11a1e](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/be11a1e42f79ca5dbba340c9367d1c9ef3e89f71)) 133 | * **deps-dev:** bump aegir from 37.5.1 to 37.5.2 ([#187](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/187)) ([0f29092](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/0f290924ad3ff2270110d96738d978c21015daec)) 134 | 135 | ## [1.2.0](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.1.6...v1.2.0) (2022-08-29) 136 | 137 | 138 | ### Features 139 | 140 | * Add Filebase pinning service ([#180](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/180)) ([43954a8](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/43954a855dbb675f54793ced44204e474ac56f78)) 141 | 142 | ## [1.1.6](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.1.5...v1.1.6) (2022-08-25) 143 | 144 | 145 | ### Trivial Changes 146 | 147 | * **deps:** bump ipfs from 0.63.3 to 0.63.5 ([#139](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/139)) ([19659d6](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/19659d63fcf0ab4d2704b736762c513b917b7b65)) 148 | 149 | ## [1.1.5](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.1.4...v1.1.5) (2022-08-24) 150 | 151 | 152 | ### Trivial Changes 153 | 154 | * **deps:** bump go-ipfs from 0.13.0 to 0.14.0 ([#177](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/177)) ([b799377](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/b7993771e3eafb2e455afc731f786eec2f410afc)) 155 | 156 | ## [1.1.4](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.1.3...v1.1.4) (2022-08-24) 157 | 158 | 159 | ### Trivial Changes 160 | 161 | * **deps-dev:** bump @types/node from 18.7.6 to 18.7.13 ([#184](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/184)) ([d61abcc](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/d61abccb21ecb32e6c87c12cf5baf0f77d5dad41)) 162 | * **deps:** bump lewagon/wait-on-check-action from 1.1.1 to 1.1.2 ([#183](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/183)) ([a36bf32](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/a36bf32944f3609dba4eaa7d40e8b7bd67c928a2)) 163 | * **deps:** bump s0/git-publish-subdir-action from 399aab378450f99b7de6767f62b0d1dbfcb58b53 to 2.5.1 ([#182](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/182)) ([aa559f6](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/aa559f65c80025fc7081e0d48201714d2b4169ee)) 164 | 165 | ## [1.1.3](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.1.2...v1.1.3) (2022-08-18) 166 | 167 | 168 | ### Trivial Changes 169 | 170 | * **deps-dev:** bump @types/node from 17.0.43 to 18.7.6 ([#173](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/173)) ([08e3e18](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/08e3e1879be49b039706165780846751c48c2992)) 171 | * **deps-dev:** bump @types/yargs from 17.0.10 to 17.0.11 ([#176](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/176)) ([c1d7da6](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/c1d7da6cdda7bce0bbccf35181012b4316e6d1d0)) 172 | * **deps-dev:** bump ts-node from 10.8.1 to 10.9.1 ([#174](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/174)) ([bd86249](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/bd862491f02447748558690d5bec8555124366b6)) 173 | * **deps:** bump node-fetch from 3.2.6 to 3.2.10 ([#178](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/178)) ([3293e82](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/3293e82e1c0a0432f79ce32576ec1980b0037812)) 174 | 175 | ## [1.1.2](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.1.1...v1.1.2) (2022-08-18) 176 | 177 | 178 | ### Trivial Changes 179 | 180 | * **deps-dev:** bump aegir from 37.3.0 to 37.5.1 ([#169](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/169)) ([0e40dc2](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/0e40dc22c8def869e73f524f8b5e3a622d1f3a39)) 181 | * **deps-dev:** bump ipfs-core-types from 0.11.0 to 0.11.1 ([#133](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/133)) ([b3edb0e](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/b3edb0e8bdb3321147cf52afd1d9f30f52b4f906)) 182 | * **deps-dev:** bump typescript from 4.7.3 to 4.7.4 ([#122](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/122)) ([f01bf0a](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/f01bf0a75ccca6bb0ea1275a8d7094b1a5553938)) 183 | * **deps:** bump ipfs-core from 0.15.2 to 0.15.4 ([#136](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/136)) ([59bbcb0](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/59bbcb0ee19fa6fefa908e8ff625c9434d50db25)) 184 | * **deps:** bump ipfs-http-client from 57.0.1 to 57.0.3 ([#137](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/137)) ([bf56cdd](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/bf56cdd37109899a5d5576974958e23d92be7a17)) 185 | * **deps:** bump multiformats from 9.6.5 to 9.7.1 ([#156](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/156)) ([6469dc7](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/6469dc7f832fb42b2fbbc9335e6a95c0866e5889)) 186 | * **deps:** bump p-queue from 7.2.0 to 7.3.0 ([#175](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/175)) ([3b2939c](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/3b2939c02f2d39c4dfa17dac196c1c9a294824f7)) 187 | * **deps:** bump winston from 3.7.2 to 3.8.1 ([#147](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/147)) ([ba4da8a](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/ba4da8a5705387dbb26db712cb52ae35b1e61758)) 188 | 189 | ## [1.1.1](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.1.0...v1.1.1) (2022-08-17) 190 | 191 | 192 | ### Trivial Changes 193 | 194 | * create CODEOWNERS file ([#144](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/144)) ([a259ad0](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/a259ad07b7cf0c9d76e9e4be4b89f240e5f60dee)) 195 | 196 | ## [1.1.0](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.0.10...v1.1.0) (2022-06-27) 197 | 198 | 199 | ### Features 200 | 201 | * compose github actions ([#128](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/128)) ([b5a3e08](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/b5a3e08553a84e9b155dd44f79d62f85eaf95259)) 202 | * test & release fails if reports workflow fails ([#126](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/126)) ([e7e606f](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/e7e606feffe0db66b68b440a7ccff6d00cffe1ea)) 203 | 204 | 205 | ### Bug Fixes 206 | 207 | * correct reusable workflow ([#127](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/127)) ([e52f066](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/e52f066763036015c50c284da4a1825af106c014)) 208 | * PinResults, Pin Delete Response Code and CID Generation ([#123](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/123)) ([128c70c](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/128c70c2ebd6fca9997dac3a7d3b855e1de95b2a)) 209 | 210 | 211 | ### Trivial Changes 212 | 213 | * fix on-main-push.yml ([#142](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/142)) ([ff47a19](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/ff47a197d67ab2a3feaefaaee22bdd11f5c2d4c9)) 214 | * update dispatchable workflows ([#134](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/134)) ([44f9034](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/44f90341288ad1c04b8c74b912c43a049b1628a9)) 215 | 216 | ## [1.0.10](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.0.9...v1.0.10) (2022-06-21) 217 | 218 | 219 | ### Bug Fixes 220 | 221 | * Only delete created pins ([#121](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/121)) ([2a2228a](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/2a2228a3879a4a1226cae6b0c22d9ef7356d2e75)) 222 | 223 | 224 | ### Trivial Changes 225 | 226 | * Add disclaimer regarding data integrity ([#120](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/120)) ([ea427ed](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/ea427ed0e8311d8050696a355efaf55de7fbe439)) 227 | 228 | ## [1.0.9](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.0.8...v1.0.9) (2022-06-17) 229 | 230 | 231 | ### Trivial Changes 232 | 233 | * **deps-dev:** bump aegir from 37.2.0 to 37.3.0 ([#117](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/117)) ([f945ce3](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/f945ce32b1511b0914b7548845aa2e221cdfac0e)) 234 | 235 | ## [1.0.8](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.0.7...v1.0.8) (2022-06-16) 236 | 237 | 238 | ### Bug Fixes 239 | 240 | * matchPin should confirm that a pin is in the response ([#116](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/116)) ([413d68a](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/413d68a408e8f6d43648323a24f45fbee041871d)), closes [#L1253-L1322](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/L1253-L1322) 241 | 242 | ## [1.0.7](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.0.6...v1.0.7) (2022-06-16) 243 | 244 | 245 | ### Bug Fixes 246 | 247 | * expect 401 for unauthorized responses ([#107](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/107)) ([4f45693](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/4f45693a6368c80f641aaa7e426efa87e7755e15)) 248 | 249 | ## [1.0.6](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.0.5...v1.0.6) (2022-06-15) 250 | 251 | 252 | ### Bug Fixes 253 | 254 | * npx usage succeeds ([#92](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/92)) ([23984c6](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/23984c62e7b26b115cc271fe2bd21f1739a27024)) 255 | 256 | ## [1.0.5](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.0.4...v1.0.5) (2022-06-15) 257 | 258 | 259 | ### Trivial Changes 260 | 261 | * **deps:** bump ipfs-http-client from 56.0.3 to 57.0.1 ([#72](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/72)) ([fe54f62](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/fe54f6245815b7b89708a13b9c90ad11d3e228c7)) 262 | 263 | ## [1.0.4](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.0.3...v1.0.4) (2022-06-15) 264 | 265 | 266 | ### Trivial Changes 267 | 268 | * **deps-dev:** bump @types/node from 17.0.42 to 17.0.43 ([#91](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/91)) ([4bcc5c6](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/4bcc5c624c12a43206d6c08cbc32409113f1e313)) 269 | * **deps:** bump ipfs-core from 0.14.3 to 0.15.2 ([#83](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/83)) ([634ea64](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/634ea64d7b42f147171e9a7dd05b18a07d12792a)) 270 | 271 | ## [1.0.3](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.0.2...v1.0.3) (2022-06-15) 272 | 273 | 274 | ### Trivial Changes 275 | 276 | * **deps-dev:** bump typescript from 4.6.4 to 4.7.3 ([#73](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/73)) ([2423a26](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/2423a26b362ad060560c726fb75ee86012f9139d)) 277 | 278 | ## [1.0.2](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.0.1...v1.0.2) (2022-06-15) 279 | 280 | 281 | ### Trivial Changes 282 | 283 | * **deps:** bump ipfs from 0.62.3 to 0.63.3 ([#85](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/85)) ([c4ab9d1](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/c4ab9d1c155868c58fc0ce82c3b6aa2b6659081a)) 284 | * **deps:** bump marked from 4.0.16 to 4.0.17 ([#82](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/82)) ([c02a39a](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/c02a39a3f5e25763282233d5fc4ff8d28af58bfe)) 285 | 286 | ## [1.0.1](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v1.0.0...v1.0.1) (2022-06-14) 287 | 288 | 289 | ### Trivial Changes 290 | 291 | * **deps-dev:** bump @types/node from 17.0.41 to 17.0.42 ([#88](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/88)) ([3b27f0e](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/3b27f0e942eaf64c7ca5444b090ab1bffa66cf44)) 292 | 293 | ## 1.0.0 (2022-06-14) 294 | 295 | 296 | ### Features 297 | 298 | * add npm bin to devcontainer PATH ([365c142](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/365c142216d3ead0c60cfb8c286c733ebc8cdc6e)) 299 | * auto-update github actions with dependabot ([#43](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/43)) ([87f7926](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/87f7926244e260da9c5ed052ab70e8768871f1f9)) 300 | * compliance check infrastructure ([7aa5663](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/7aa566376616e4a3a1c423ac8cc7ab8cac31502d)) 301 | * Export esm module ([#41](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/41)) ([acaeac6](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/acaeac655eea7267fbec216d59d1d161e5d41b7c)) 302 | * implement all compliance checks ([#17](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/17)) ([1223831](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/1223831e366a8e357d3cad424b523694d770fe1a)), closes [#5](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/5) [#4](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/4) [#6](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/6) [#7](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/7) [#8](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/8) [#28](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/28) [#25](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/25) 303 | * lplaceholder checks run via listr ([bbb1f81](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/bbb1f81009b6952b76c4719299fa878bd5a8acbb)) 304 | * Publish static reports via github pages ([#68](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/68)) ([5a6a7f5](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/5a6a7f58bc83c849076f015dbed1878afb42c0d8)), closes [#78](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/78) [#76](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/76) [#77](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/77) [#77](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/77) 305 | 306 | 307 | ### Bug Fixes 308 | 309 | * CI release succeeds ([#90](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/90)) ([810d4a2](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/810d4a28e99e608989d0cd43dda8e9622ea92714)) 310 | * Compliance check sum is consistent for all services ([#54](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/54)) ([ace2b57](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/ace2b5771e86fe76ee44119011b202ddd2a8dc91)), closes [#55](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/55) 311 | * devcontainer sees pinning-service-client ([c7a6dbb](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/c7a6dbb78a0c0edfb4600bebf895643cc7abec20)) 312 | 313 | 314 | ### Trivial Changes 315 | 316 | * **deps-dev:** bump @types/node from 17.0.25 to 17.0.34 ([#49](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/49)) ([a2a5d13](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/a2a5d136f34ac20ea0f43a49d2a7f37dcd7939f1)) 317 | * **deps-dev:** bump @types/node from 17.0.35 to 17.0.41 ([#79](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/79)) ([db8a6f7](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/db8a6f7962f63109ed3cbe24a95e145a89c360c2)) 318 | * **deps-dev:** bump aegir from 37.0.15 to 37.2.0 ([#86](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/86)) ([df4035f](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/df4035f30732fba6eee4cf8a12a6f7c9a5e31dcc)) 319 | * **deps-dev:** bump ipfs-core-types from 0.10.3 to 0.11.0 ([#64](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/64)) ([e623ad3](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/e623ad3bb986e649f7796b5e828b35d487cf5dfb)) 320 | * **deps-dev:** bump ts-node from 10.7.0 to 10.8.1 ([#75](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/75)) ([ff48f3b](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/ff48f3b4b2d77d6fd95590b87e692b1095e689cd)) 321 | * **deps:** bump actions/checkout from 2 to 3 ([#46](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/46)) ([8fbbcd8](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/8fbbcd8da415daf532618817df9a4f7f239e9edb)) 322 | * **deps:** bump actions/setup-node from 2 to 3 ([#48](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/48)) ([ee57c59](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/ee57c59e63b2b78f45ceb3a94cdd455c8ac13d4c)) 323 | * **deps:** bump github/codeql-action from 1 to 2 ([#47](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/47)) ([1dd488d](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/1dd488db040ff8c91dba42ae4ab82f4fcf9227d0)) 324 | * **deps:** bump go-ipfs from 0.12.2 to 0.13.0 ([#80](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/80)) ([baaaa8c](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/baaaa8c3a579b719a5fe2c714d735bbe67e0ba80)) 325 | * **deps:** bump ipfsd-ctl from 10.0.6 to 11.0.1 ([#58](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/58)) ([45771db](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/45771dbcdd216bce91b4f4175180087f16ff3bf7)) 326 | * **deps:** bump lewagon/wait-on-check-action from 0.2 to 1.1.1 ([#44](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/44)) ([9c8f5c0](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/9c8f5c05f3ba5a1abd4e68e1eccdf6ea400ac640)) 327 | * **deps:** bump node-fetch from 3.2.4 to 3.2.6 ([#81](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/81)) ([5bcb760](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/5bcb76009fa699f4f7f577700be0e750466421c3)) 328 | * **deps:** bump pascalgn/automerge-action from 0.13.1 to 0.15.3 ([#45](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/45)) ([65c19dc](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/65c19dce16057bb83cdae82623e3bbe8329d39aa)) 329 | * getting started instructions ([b7ff148](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/b7ff148475301e61551416eacad30fccf9516260)) 330 | * ignore .envrc ([54e5aae](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/54e5aaea5a6ef3564bb7fa3e8a3b6e5e5df59618)) 331 | * static report landing page provides context ([#87](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/87)) ([12fc841](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/12fc8415ffec9d4f7d047df31f556e8bd963dd6d)) 332 | * use gitignore.io ([e81ebd9](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/e81ebd94a0364912d4b128f339ff294783485984)) 333 | 334 | ## [0.0.3](https://github.com/ipfs-shipyard/pinning-service-compliance/compare/v0.0.2...v0.0.3) (2022-04-25) 335 | 336 | 337 | ### Bug Fixes 338 | 339 | * Use existing client if one is running ([830ea3b](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/830ea3b77daf8e67f67f94de2ec1b5acb7352a5f)), closes [#25](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/25) 340 | 341 | 342 | 343 | ## 0.0.2 (2022-04-21) 344 | 345 | 346 | ### Bug Fixes 347 | 348 | * devcontainer sees pinning-service-client ([c7a6dbb](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/c7a6dbb78a0c0edfb4600bebf895643cc7abec20)) 349 | * don't clobber docs dir ([4f460da](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/4f460daa6ab23747cf8415753235825188b06c29)) 350 | * exit with error code on failure ([7f972c8](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/7f972c8dda11dc0860807f85003eaa444feecc84)) 351 | * serviceAndToken option doesn't block other scripts ([d2e3764](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/d2e37642d832a9bbcd4b1f3aac065e14e53d8698)) 352 | * setup does not generate joi spec json files ([5a7ce1d](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/5a7ce1d39ac09cbf161fe1e023461e23e519dbb7)) 353 | 354 | 355 | ### Features 356 | 357 | * add CodeQL GH action ([664207c](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/664207c422b5f27d93c86ebe1501004982e76aac)) 358 | * add npm bin to devcontainer PATH ([365c142](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/365c142216d3ead0c60cfb8c286c733ebc8cdc6e)) 359 | * add pagination check (in progress) ([63b9114](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/63b91146a326be9e146ff6f303fb0d3c2641095e)) 360 | * Check add+delete pin compliance ([300d3ed](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/300d3ed1bc0b3e0ab20874f4dae08a6d10078c87)), closes [#7](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/7) 361 | * check compliance for adding pin ([5a4bcf4](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/5a4bcf4aeaa3d6caae6c11a8fed97258477a38ac)), closes [#5](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/5) 362 | * Check replacement of pin ([bdead5a](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/bdead5adf1fb8e8ac4f8ff22a61192eeba7dc6ab)), closes [#8](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/8) 363 | * compliance check infrastructure ([7aa5663](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/7aa566376616e4a3a1c423ac8cc7ab8cac31502d)) 364 | * default output is more readable ([0933690](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/0933690dea2ee3e5a1436a2db8830372cd3b565d)) 365 | * delete all pins ([069f7cf](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/069f7cf622a66e3e0ce8723ecf19f5dbb2224938)), closes [#4](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/4) 366 | * finish pagination compliance check ([79df589](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/79df589695847683c32039b396a2aa1e443110cb)), closes [#6](https://github.com/ipfs-shipyard/pinning-service-compliance/issues/6) 367 | * finishing up pagination test ([1b8a2f7](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/1b8a2f7186fba87a233718ef4e51117667b5cf5e)) 368 | * handle rate-limiting in middleware ([b7a2c98](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/b7a2c98f10d9dc7eb5cf3691c05ea91a220cce39)) 369 | * lplaceholder checks run via listr ([bbb1f81](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/bbb1f81009b6952b76c4719299fa878bd5a8acbb)) 370 | * output reports to markdown files ([a0343c1](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/a0343c1eb6c53bf10cb7fe6305b6db9a965645c0)) 371 | * page publish action uses matrix strategy ([e4ad6c8](https://github.com/ipfs-shipyard/pinning-service-compliance/commit/e4ad6c8825d924a8930ea92fffa4b0c8bf34c040)) 372 | --------------------------------------------------------------------------------