{"id":31376514,"url":"https://github.com/substrate-system/request","last_synced_at":"2026-01-20T16:50:18.224Z","repository":{"id":177860949,"uuid":"656890791","full_name":"substrate-system/request","owner":"substrate-system","description":"Use Bearer tokens with web crypto to authenticate http requests.","archived":false,"fork":false,"pushed_at":"2025-09-08T03:37:29.000Z","size":127,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-20T02:24:32.911Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/substrate-system.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-06-21T21:29:11.000Z","updated_at":"2025-09-08T03:37:26.000Z","dependencies_parsed_at":"2024-02-24T19:41:45.898Z","dependency_job_id":"7d78dbda-c7ec-40e9-ba7a-86e123be790a","html_url":"https://github.com/substrate-system/request","commit_stats":null,"previous_names":["ssc-hermes/request","ssc-half-light/request","bicycle-codes/request","substrate-system/request"],"tags_count":67,"template":false,"template_full_name":"nichoth/template-ts","purl":"pkg:github/substrate-system/request","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/substrate-system%2Frequest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/substrate-system%2Frequest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/substrate-system%2Frequest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/substrate-system%2Frequest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/substrate-system","download_url":"https://codeload.github.com/substrate-system/request/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/substrate-system%2Frequest/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":277302099,"owners_count":25795361,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-27T02:00:08.978Z","response_time":73,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-09-28T03:53:43.516Z","updated_at":"2026-01-20T16:50:18.218Z","avatar_url":"https://github.com/substrate-system.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# request\n[![tests](https://img.shields.io/github/actions/workflow/status/substrate-system/request/nodejs.yml?style=flat-square)](https://github.com/substrate-system/request/actions/workflows/nodejs.yml)\n[![module](https://img.shields.io/badge/module-ESM%2FCJS-blue?style=flat-square)](README.md)\n![types](https://img.shields.io/npm/types/@substrate-system/request?style=flat-square)\n[![semantic versioning](https://img.shields.io/badge/semver-2.0.0-blue?logo=semver\u0026style=flat-square)](https://semver.org/)\n[![install size](https://flat.badgen.net/packagephobia/install/@substrate-system/request?cache-control=no-cache)](https://packagephobia.com/result?p=@substrate-system/request)\n[![GZip size](https://flat.badgen.net/bundlephobia/minzip/@substrate-system/request)](https://bundlephobia.com/package/@substrate-system/request)\n[![license](https://img.shields.io/badge/license-Big_Time-blue?style=flat-square)](LICENSE)\n\n\nUse a `Bearer` token in an HTTP request to verify identity. This will sign an\ninteger with the given crypto keypair, suitable for an access-control type\nof auth.\n\nThe sequence number is an always incrementing integer. It is expected that a\nserver, in addition to checking that the signature is valid,\nwould remember the previous sequence number for this DID / public key, and\ncheck that the given number is larger than the previous one, to prevent\nreplay attacks.\n\nYou can pass in either an integer or a localStorage instance. If you pass a\n`localStorage` instance, it will read the index `'__seq'`, which should be a\nnumber. If there is not a number stored there, it will start at `0`.\n\nThis package includes some opinions. I like to use [ky](https://github.com/sindresorhus/ky),\nso this includes an API that will create a `ky` instance.\n\n\n\u003cdetails\u003e\u003csummary\u003e\u003ch2\u003eContents\u003c/h2\u003e\u003c/summary\u003e\n\n\u003c!-- toc --\u003e\n\n- [Install](#install)\n- [Globals](#globals)\n- [Dependencies](#dependencies)\n- [Example](#example)\n  * [Clientside](#clientside)\n  * [Serverside](#serverside)\n- [API](#api)\n  * [SignedRequest](#signedrequest)\n  * [HeaderFactory](#headerfactory)\n  * [`createHeader`](#createheader)\n  * [`verify`](#verify)\n  * [`verifyParsed`](#verifyparsed)\n  * [`createToken`](#createtoken)\n  * [`encodeToken`](#encodetoken)\n- [More Examples](#more-examples)\n  * [Create an Instance](#create-an-instance)\n  * [Verify a Token](#verify-a-token)\n  * [Parse a Token](#parse-a-token)\n  * [Use localStorage for the sequence number](#use-localstorage-for-the-sequence-number)\n\n\u003c!-- tocstop --\u003e\n\n\u003c/details\u003e\n\n## Install\n```sh\nnpm i -S @substrate-system/request\n```\n\n## Globals\n\nThis reads and writes to the `__seq` key in `localStorage`.\n\n## Dependencies\n\nThis depends on [message](https://github.com/substrate-system/message) to\ncreate the signed token.\n\n\n## Example\n\nCreate a new [ky](https://github.com/sindresorhus/ky) instance that will add a\nsigned header to every request, and also set the latest sequence\nnumber in `localStorage`.\n\n### Clientside\n\n```js\nimport { EccKeys as Keys } from '@substrate-system/keys/ecc'\nimport { SignedRequest } from '@substrate-system/request'\nimport ky from 'ky'\n\nconst keys = await Keys.create()\nconst keypair = {\n    privateKey: keys.writeKey.privateKey\n    publicKey: keys.writeKey.publicKey\n}\n\n// create a ky instance\n// pass in the storage to use, or a sequence number to start with\nconst request = SignedRequest(ky, keypair, window.localStorage)\n\nconst response = await request.get('https://example.com')\n// request is sent with headers `{ Authorization: Bearer \u003ccredentials\u003e }`\n```\n\n\n### Serverside\n\nParse the header string, and check the sequence number.\n\n```ts\nimport {\n    verifyParsed,\n    parseHeader\n} from '@substrate-system/request'\nimport type { ParsedHeader } from '@substrate-system/request'\n\nconst headerString = request.headers.Authorization\nconst parsedHeader:ParsedHeader = parseHeader(headerString)\nconst { seq } = parsedHeader\n\n// ...get the previous sequence number somehow...\n\nconst isOk = await verifyParsed(parsedHeader)   // check signature\nconst isSequenceOk = (seq \u003e lastSequence)  // check sequence number\n```\n\nOr, pass in a sequence number to check that `parsedHeader.seq` is greater than.\n\n```js\nconst headerString = request.headers.Authorization\nconst parsedHeader = parseHeader(headerString)\nconst isOk = await verifyParsed(parsedHeader, 3)  // \u003c-- pass in a seq here\n```\n\n-------\n\n\n## API\n\n### SignedRequest\n\nPatch a `ky` instance so it makes all requests with a signed header.\n\n```ts\nimport type { KyInstance } from 'ky/distribution/types/ky'\n\nfunction SignedRequest (\n    ky:KyInstance,\n    keypair:CryptoKeyPair,\n    startingSeq:number|Storage,\n    opts?:Record\u003cstring, any\u003e\n):KyInstance\n```\n\nThe request will have an `Authorization` header, base64 encoded:\n\n```js\nrequest.headers.get('Authorization')\n// =\u003e \"Bearer eyJzZXEiOjEsIm...\"\n```\n\n#### Example\n\n```js\nimport ky from 'ky-universal'\nimport { SignedRequest } from '@substrate-system/request'\n\n// `req` is an instance of `ky`\nconst req = SignedRequest(ky, crypto, 0)\n\n// make a request\nawait req.get('https://example.com/')\n\n// ... later, on the server ...\nconst headerObject = parseHeader(request.headers.get('Authorization'))\n\n// =\u003e {\n//     seq: 1,\n//     author: 'did:key:z13V3Sog2YaUKh...\n//     signature: 'VyaxQayQdXU7qhcOfcsCq...\n// }\n```\n\n### HeaderFactory\n\nCreate a function that will create header tokens and read and write the\nsequence number from `localStorage`.\n\n```ts\nfunction HeaderFactory (\n    keypair:CryptoKeyPair,\n    opts?:Record\u003cstring, any\u003e,\n    ls?:Storage\n):()=\u003ePromise\u003c`Bearer ${string}`\u003e\n```\n\n#### Example\n\n```ts\ntest('header factory', async t =\u003e {\n    const localStorage = new LocalStorage('./test-storage')\n    localStorage.setItem('__seq', '0')\n\n    const createHeader = HeaderFactory(keypair, {}, localStorage)\n    const header = await createHeader()\n    const header2 = await createHeader()\n    t.ok(header.includes('Bearer'), 'should include \"Bearer\" text')\n\n    const token = parseHeader(header)\n    const token2 = parseHeader(header2)\n    t.equal(token.seq, 1, 'should start at 0 sequence')\n    t.equal(token2.seq, 2, 'should increment the sequence number')\n})\n\n/**\n * Optionally can pass in a params object and\n * a localStorage instance\n */\nconst createHeaderTwo = HeaderFactory(crypto, { test: 'param' }, localStorage)\n```\n\n### `createHeader`\n\nCreate the base64 encoded header string\n\n```ts\nasync function createHeader (\n    keypair:CryptoKeyPair,\n    seq:number,\n    opts?:Record\u003cstring, any\u003e,\n):Promise\u003c`Bearer ${string}`\u003e\n```\n\nThis will create a header that looks like this:\n```js\n`Bearer eyJzZXEiOj...`\n```\n\n### `verify`\nCheck that the signature matches the given public key. Optionally takes a\nsequence number, and will return false if the header's sequence is not greater\nthan the given sequence.\n\n```ts\n// take a base64 encoded header string\nfunction verify (header:string, seq?:number):Promise\u003cboolean\u003e\n```\n\n#### Example\n\n```js\nimport { verify } from '@substrate-system/request'\n\nconst isOk = await verify(header)\n```\n\n### `verifyParsed`\n\nCheck the validity of a parsed token. Optionally takes a sequence number. If a\n`seq` number is not passed in, then this will only verify the signature.\n\n```ts\nimport { SignedRequest as SignedMsg } from '@substrate-system/message'\n// take a parsed token\nfunction verifyParsed (\n    msg:SignedMsg\u003c{ seq:number }\u003e,\n    seq?:number\n):Promise\u003cboolean\u003e\n```\n\n#### Example\n\n```ts\nimport { verifyParsed, create as createToken } from '@substrate-system/request'\n\nconst token = await createToken(crypto, 1)\nconst isOk = await verifyParsed(parsedToken)\n```\n\n### `createToken`\n\nCreate a token object. This is the value that is encoded to make a header.\n\n```ts\nfunction createToken (\n    keypair:CryptoKeyPair,\n    seq:number,\n    opts?:Record\u003cstring, any\u003e\n):Promise\u003cToken\u003ctypeof opts\u003e\u003e\n```\n\n#### Example\n\nYou can pass additional arguments to `createToken`, which will be added to the\nsigned token object.\n\n```ts\nimport { createToken } from '@substrate-system/request'\n\nconst token = await createToken(crypto, 1, { example: 'testing' })\nt.equal(token.example, 'testing', 'should have an additional property')\n```\n\n### `encodeToken`\n\nEncode a token object as a base64 string\n\n```ts\nfunction encodeToken\u003cT\u003e (token:Token\u003cT\u003e):`Bearer ${string}`\n```\n\n#### Example\n\n```js\nimport { encodeToken } from '@substrate-system/request'\nconst encoded = encodeToken(token)\n```\n\n-------\n\n## More Examples\n\n### Create an Instance\n\nIn a web browser, pass an instance of [ky](https://github.com/sindresorhus/ky),\nand return an extended instance of `ky`, that will automatically add a\nsignature to the header as a `Bearer` token.\n\nThe header is a base64 encoded Bearer token. It looks like\n```\nBearer eyJzZXEiOjE...\n```\n\n```ts\nimport { test } from '@nichoth/tapzero'\nimport { AuthRequest, parseHeader, verify } from '@substrate-system/request'\nimport ky from 'ky-universal'\n\nlet header:string\n// header is a base64 encoded string: `Bearer ${base64string}`\n\nlet req:typeof ky\ntest('create instance', async t =\u003e {\n    req = SignedRequest(ky, keypair, 0)\n\n    await req.get('https://example.com/', {\n        hooks: {\n            afterResponse: [\n                (request:Request) =\u003e {\n                    const obj = parseHeader(\n                        request.headers.get('Authorization') as string\n                    )\n                    console.log('**header obj**', obj)\n                    t.ok(obj, 'should have an Authorization header in request')\n                    t.equal(obj.seq, 1, 'should have the right sequence')\n                }\n            ]\n        }\n    })\n})\n```\n\n### Verify a Token\n\nCheck if a given signature matches the given public key. You would probably\ncall this in server-side code. This only checks that the public key and\nsignature are ok together. In real life you would need to check that the\npublic key has meaning to your system in addition to calling `verify` here.\n\n```ts\ntest('parse header', t =\u003e {\n    const obj = parseHeader(header)  // first parse base64, then parse JSON\n    // {\n    //      seq: 1,\n    //      author: 'did:key:...',\n    //      signature: '123abc'\n    //}\n    t.equal(obj.seq, 1, 'should have the right sequence number')\n})\n\ntest('verify the header', async t =\u003e {\n    t.equal(await verify(header), true, 'should validate a valid token')\n    // also make sure that the sequence number is greater than the previous\n})\n```\n\n### Parse a Token\n\nThis is distinct from parsing a \"header\" because the token does not include\nthe text \"Bearer\".\n\n```ts\nimport { TokenFactory, parseToken, verifyParsed } from '@substrate-system/request'\n\ntest('token factory', async t =\u003e {\n    // this is client-side\n    const createToken = TokenFactory(crypto)\n    const token = await createToken()\n    t.ok(!token.includes('Bearer'),\n        'should not include \"Bearer\" text in the token')\n\n    // this is server-side\n    const parsedToken = parseToken(token)\n    t.equal(parsedToken.seq, 1, 'should include the first sequence number')\n    t.ok(parsedToken.author, 'should have \"author\" in the token')\n    t.ok(parsedToken.signature, 'should have \"signature\" in the token')\n\n    t.ok(verifyParsed(parsedToken), 'should verify a valid token')\n    // also, check that the `parsedToken.seq` has increased\n})\n```\n\n### Use localStorage for the sequence number\n\nPass in an instance of `localStorage`, and we will save the sequence number\nto `__seq` on any request.\n\n```ts\nimport { test } from '@nichoth/tapzero'\nimport ky from 'ky-universal'\nimport { LocalStorage } from 'node-localstorage'\nimport { SignedRequest, parseHeader } from '@substrate-system/request'\n\ntest('create an instance with localStorage', async t =\u003e {\n    const localStorage = new LocalStorage('./test-storage')\n    localStorage.setItem('__seq', 3)\n    const req = SignedRequest(ky, keypair, localStorage)\n\n    await req.get('https://example.com', {\n        hooks: {\n            afterResponse: [\n                (request:Request) =\u003e {\n                    const obj = parseHeader(\n                        request.headers.get('Authorization') as string\n                    )\n                    t.equal(obj.seq, 4,\n                        'should use localStorage to create the sequence')\n                }\n            ]\n        }\n    })\n\n    const seq = localStorage.getItem('__seq')\n    t.equal(seq, 4, 'should save the sequence number to localStorage')\n})\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsubstrate-system%2Frequest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsubstrate-system%2Frequest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsubstrate-system%2Frequest/lists"}