@passes/reqs
v0.1.5
Published
Reqs (pronounced "rex" – /rɛks/) provides a high-level interface for using Pass Requests in your app.
Downloads
62
Readme
Reqs (pronounced "rex" – /rɛks/) provides a high-level interface for using Pass Requests in your app.
Installation
npm add @passes/reqs # or bun, yarn, pnpm
Defining a Pass Request Topic
To create a Pass Request topic, you need 3 things:
A Topic ID. A string to identify your Request Topic. This is what Pass Providers will use to interpret a Pass Request of your topic, so it should uniquely identify your Pass Request topic. This should use reverse-dns notation and be namespaced to your organization – for example
org.passes.my-example-request-topic
.A Request Body Codec. This translates the rich representation of the request body data your Pass Request topic uses to binary and back.
A Result Body Codec. This is just like the request body codec, but for your Pass Request's result body data.
Codecs
The Codecs
module includes many common codecs for your convenience. It's available at @passes/reqs/codecs
.
import * as Codecs from '@passes/reqs/codecs';
| Name | Rich Type |
| --------------------- | ------------------ |
| Codecs.BigInt
| bigint
|
| Codecs.Boolean
| boolean
|
| Codecs.Bytes
| Uint8Array
|
| Codecs.Json
| TJson
(generic) |
| Codecs.Number
| number
|
| Codecs.String
| string
|
| Codecs.Void
| undefined
|
You can also write your own codec to support any data type you'd like! The interface for a codec is:
type Codec<T> = {
encode(value: T) => Uint8Array;
decode(raw: Uint8Array) => T;
}
RequestTopic
Let's make a request topic that allows us to ask the user a yes-or-no question, and they can accept the request with a boolean representing their answer.
import { RequestTopic } from '@passes/reqs';
import * as Codecs from '@passes/reqs/codecs';
const yesOrNoQuestion = new RequestTopic({
id: 'org.passes.example.yes-or-no-question',
requestBodyCodec: Codecs.String,
resultBodyCodec: Codecs.Boolean,
});
Now, we can send our request like so:
const userAnswer = await yesOrNoQuestion.sendRequest('Have you ever been to Olive Garden?');
// ^ { status: 'accepted', body: true }
SignedRequestTopic
It's generally recommended to use signed Pass Requests, since they use asymmetric key cryptography to assert and verify that Pass Request results have been approved by the user.
To make a RequestTopic
signed, we simply create a SignedRequestTopic
to wrap it, and provide a signResult
or verifyResult
implementation...
import { RequestTopic, SignedRequestTopic, SignedBodyWrapper, SignedBodyWrapperHeader } from '@passes/reqs';
import * as Codecs from '@passes/reqs/codecs';
// For this demo implementation, we'll use the SubtleCrypto API
const keypair = await crypto.subtle.generateKey(keyParams, true, ['sign', 'verify']);
const yesOrNoQuestion = new SignedRequestTopic({
// We're wrapping the same request topic we defined above
requestTopic: new RequestTopic({
id: 'org.passes.example.yes-or-no-question',
requestBodyCodec: Codecs.String,
resultBodyCodec: Codecs.Boolean,
}),
signResult: async (body: boolean): Promise<SignedBodyWrapperHeader> => ({
publicKey: await crypto.subtle.exportKey(
keyFormat,
keyPair.value.publicKey
),
signature: new Uint8Array(await crypto.subtle.sign(
keyParams,
keyPair.value.privateKey,
Codecs.Boolean.encode(body)
)),
}),
verifyResult: async (signed: SignedBodyWrapper<Boolean>): Promise<boolean> =>
crypto.subtle.verify(
keyParams,
publicKey: await crypto.subtle.importKey(keyFormat, signed.header.publicKey, keyParams, true, ['verify']),
signed.header.signature,
Codecs.Boolean.encode(signed.body)
),
});
Building a Pass Provider
For building a Pass Provider, reqs
exports a PassProviders
namespace with relevent APIs, and some of the common APIs will be useful as well.
parseTopic
To get the topic of a Pass Request before you know its RequestTopic
, you can use parseTopic
on the raw request bytes.
import { parseTopic } from '@passes/reqs';
const id = parseTopic(
// Replace this with a raw request
new Uint8Array([/* ... */]),
);
PassProviders.providePass
When your user signs up or re-authenticates with your Pass Provider, you can send a providePass
Pass Request to ask them if they want to direct future Pass Requests to your Pass Provider.
import * as PassProviders from '@passes/reqs/topics/pass-providers';
const { status } = await PassProviders.providePass('https://my-pass-provider.com', 'optional-user-id');
if (status === 'accepted') {
// Future Pass Requests to this user will be sent to your Pass Provider for handling
}
Messaging.sendResult
Once your user has accepted or rejected a Pass Request, you can send the result back to the requesting app via Messaging.sendResult
.
import * as Messaging from '@passes/reqs/messaging';
// Note: `handleRequest` is a placeholder for your handling logic for the given request topic
const result = await handleRequest(request);
await Messaging.sendResult(result);
Putting It All Together
Here's an example of how to use the above APIs together to implement support for a set of Pass Request topics in a basic Web Pass Provider.
import * as Messaging from '@passes/reqs/messaging';
import * as PassProviders from '@passes/reqs/topics/pass-providers';
import * as SupportedRequestTopics from './supported-request-topics'; // A map of the request topics supported by your Pass Provider
// Called when your user signs in to set your app as their Pass Provider
async function onUserAuthn(userToken) {
await PassProviders.providePass(
// The URI of your pass provider
'https://your-pass-provider.com',
// A token you can use later to identify the user when presenting a Pass Request UI to them - for example, a JWT
userToken,
);
}
// Presents a UI for the user to review and handle the incoming Pass Request, and sends the result to the requesting app
async function handlePassRequest(request: Uint8Array) {
const id = parseTopic(request);
switch (id) {
case SupportedRequestTopics.GetUserEmail.RequestTopic: {
const requestTopic = SupportedRequestTopics.Example1.RequestTopic;
try {
const requestBody = await requestTopic.decodeRequest(request);
const result = await presentRequestReviewUIAndGetResult(requestTopic, requestBody);
await Messaging.sendResult(result);
} catch (error) {
await Messaging.sendResult(await requestTopic.encodeResult({ status: 'exception', message: error.message }));
}
break;
}
// ... (other supported request topics)
default:
// Communicate to your user that the incoming request topic is not supported by your Pass Provider
await Messaging.sendResult(EnvelopeV0.encodeResult({ status: 'unsupported' }));
}
}