New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@oasislabs/parcel

Package Overview
Dependencies
Maintainers
74
Versions
38
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@oasislabs/parcel - npm Package Compare versions

Comparing version 0.1.6 to 0.1.7-manual.1

lib/app.d.ts

172

package.json
{
"name": "@oasislabs/parcel",
"version": "0.1.6",
"license": "Apache-2.0",
"author": "Oasis Labs <feedback@oasislabs.com>",
"main": "lib/src/index.js",
"types": "lib/src/index.d.ts",
"files": [
"lib",
"src"
"name": "@oasislabs/parcel",
"version": "0.1.7-manual.1",
"license": "Apache-2.0",
"author": "Oasis Labs <feedback@oasislabs.com>",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"type": "module",
"files": [
"lib",
"src"
],
"scripts": {
"build": "tsc -b",
"fmt": "xo --fix && prettier --write {tsconfig,package}.json",
"lint": "xo && prettier --check {tsconfig,package}.json",
"test": "jest",
"test:cy": "start-test 'parcel serve test/cypress/fixtures/index.html -p 4444' 4444 'cypress run'",
"coverage": "jest --coverage && yarn test:cy",
"doc": "typedoc --options docs/typedoc.json",
"prepublishOnly": "yarn build"
},
"jest": {
"coverageDirectory": "coverage/jest",
"coveragePathIgnorePatterns": [
"test/*"
],
"scripts": {
"build": "tsc -b",
"lint": "xo --fix",
"lint-no-fix": "xo",
"test": "jest",
"test:cy": "start-test 'parcel serve test/cypress/fixtures/index.html -p 4444' 4444 'cypress run'",
"coverage": "jest --coverage && yarn test:cy",
"doc": "typedoc --options docs/typedoc.json",
"prepublishOnly": "yarn build"
"coverageReporters": [
"lcov",
"text",
"cobertura"
],
"moduleFileExtensions": [
"js",
"ts"
],
"moduleNameMapper": {
"^@oasislabs/parcel$": "<rootDir>/src/index",
"^\\./(app|client|compute|consent|dataset|filter|grant|http|identity|model|polyfill|token).js$": "<rootDir>/src/$1",
"^@oasislabs/parcel/(.*)$": "<rootDir>/src/$1"
},
"jest": {
"coverageDirectory": "coverage/jest",
"coveragePathIgnorePatterns": [
"test/*"
],
"coverageReporters": [
"lcov",
"text",
"cobertura"
],
"moduleFileExtensions": [
"js",
"ts"
],
"moduleNameMapper": {
"^@oasislabs/parcel$": "<rootDir>/src/index",
"^@oasislabs/parcel/(.*)$": "<rootDir>/src/$1"
},
"testEnvironment": "node",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"\\.ts$": "ts-jest"
}
"testEnvironment": "node",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"\\.ts$": "ts-jest",
"\\.js$": "babel-jest"
},
"nyc": {
"report-dir": "./coverage/cypress"
},
"devDependencies": {
"@apidevtools/swagger-parser": "^10.0.2",
"@babel/core": "^7.11.6",
"@cypress/code-coverage": "^3.8.1",
"@types/jest": "^26.0.9",
"@types/jsonwebtoken": "^8.5.0",
"@types/jsrsasign": "^8.0.5",
"@types/node": "^14.0.20",
"@types/openapi-sampler": "^1.0.0",
"@types/uuid": "^8.3.0",
"ajv": "^6.12.5",
"cypress": "^5.3.0",
"eslint-plugin-cypress": "^2.11.2",
"jest": "^26.2.2",
"jsonwebtoken": "^8.5.1",
"jwk-to-pem": "^2.0.4",
"nock": "^13.0.4",
"openapi-types": "^7.0.1",
"parcel-bundler": "^1.12.4",
"parcel-plugin-bundle-visualiser": "^1.2.0",
"start-server-and-test": "^1.11.5",
"ts-jest": "^26.1.4",
"typedoc": "^0.18.0",
"typescript": "^3.9.7",
"uuid": "^8.3.0",
"xo": "^0.33.1"
},
"dependencies": {
"@types/readable-stream": "^2.3.9",
"axios": "^0.19.2",
"eventemitter3": "^4.0.4",
"form-data": "^3.0.0",
"jsrsasign": "^10.0.0",
"param-case": "^3.0.3",
"readable-stream": "^3.6.0",
"type-fest": "^0.17.0"
},
"browserslist": ">2%"
"transformIgnorePatterns": [
"<rootDir>/node_modules/?!(ky|node-fetch)"
]
},
"nyc": {
"report-dir": "./coverage/cypress"
},
"devDependencies": {
"@apidevtools/swagger-parser": "^10.0.2",
"@babel/core": "^7.11.6",
"@cypress/code-coverage": "^3.8.1",
"@types/jest": "^26.0.9",
"@types/jsonwebtoken": "^8.5.0",
"@types/jsrsasign": "^8.0.5",
"@types/node": "^14.0.20",
"@types/node-fetch": "^2.5.8",
"@types/uuid": "^8.3.0",
"ajv": "^6.12.5",
"cypress": "^5.3.0",
"eslint-plugin-cypress": "^2.11.2",
"jest": "^26.2.2",
"jsonwebtoken": "^8.5.1",
"jwk-to-pem": "^2.0.4",
"nock": "^13.0.4",
"openapi-types": "^7.0.1",
"parcel-bundler": "^1.12.4",
"parcel-plugin-bundle-visualiser": "^1.2.0",
"start-server-and-test": "^1.11.5",
"ts-jest": "^26.4.4",
"typedoc": "^0.20.14",
"typescript": "^4.1.3",
"uuid": "^8.3.0",
"xo": "^0.36.1"
},
"dependencies": {
"@types/readable-stream": "^2.3.9",
"abort-controller": "^3.0.0",
"eventemitter3": "^4.0.4",
"form-data": "^3.0.0",
"jsrsasign": "^10.0.0",
"ky": "^0.26.0",
"node-fetch": "^2.6.1",
"param-case": "^3.0.3",
"type-fest": "^0.20.0",
"web-streams-polyfill": "^3.0.1"
},
"browserslist": ">2%"
}
import type { Opaque } from 'type-fest';
import { Consent, ConsentImpl } from './consent';
import type { ConsentCreateParams, ConsentId } from './consent';
import type { HttpClient } from './http';
import type { IdentityId, IdentityTokenVerifier } from './identity';
import type { Model, Page, PageParams, PODModel, ResourceId, WritableExcluding } from './model';
import { Consent, ConsentImpl } from './consent.js';
import type { ConsentCreateParams, ConsentId } from './consent.js';
import type { HttpClient } from './http.js';
import type { IdentityId, IdentityTokenVerifier } from './identity.js';
import type { Model, Page, PageParams, PODModel, ResourceId, WritableExcluding } from './model.js';

@@ -12,186 +12,193 @@ export type AppId = Opaque<ResourceId>;

export type PODApp = PODModel & {
acceptanceText?: string;
admins: ResourceId[];
brandingColor?: string;
category?: string;
collaborators: ResourceId[];
extendedDescription?: string;
homepage: string;
invitationText?: string;
inviteOnly: boolean;
invites?: ResourceId[];
logo?: string;
name: string;
organization: string;
owner: ResourceId;
participants: ResourceId[];
privacyPolicy: string;
published: boolean;
rejectionText?: string;
shortDescription: string;
termsAndConditions: string;
acceptanceText?: string;
admins: ResourceId[];
allowUserUploads: boolean;
brandingColor?: string;
category?: string;
collaborators: ResourceId[];
extendedDescription?: string;
homepageUrl: string;
invitationText?: string;
inviteOnly: boolean;
invites?: ResourceId[];
logoUrl: string;
name: string;
organization: string;
owner: ResourceId;
participants: ResourceId[];
published: boolean;
rejectionText?: string;
shortDescription: string;
termsAndConditions: string;
privacyPolicy: string;
trusted: boolean;
};
export class App implements Model {
public id: AppId;
public createdAt: Date;
public id: AppId;
public createdAt: Date;
/** The Identity that created the app. */
public owner: IdentityId;
public admins: IdentityId[];
/** Identities that can view participation of the app and modify un-privileged fields. */
public collaborators: IdentityId[];
/** The Identity that created the app. */
public owner: IdentityId;
public admins: IdentityId[];
/** Identities that can view participation of the app and modify un-privileged fields. */
public collaborators: IdentityId[];
/** Whether this app has been published. Consents may not be modified after publishing, */
public published: boolean;
/** If `true`, only invited Identities may the app. */
public inviteOnly: boolean;
/** Identities invited to participate in this app. */
public invites: IdentityId[];
/** The set of identities that are currently authorizing this app. */
public participants: IdentityId[];
/** Whether this app has been published. Consents may not be modified after publishing, */
public published: boolean;
/** If `true`, only invited Identities may participate in the app. */
public inviteOnly: boolean;
/** Identities invited to participate in this app. */
public invites: IdentityId[];
/** The set of identities that are currently authorizing this app. */
public participants: IdentityId[];
/** Allow non-admin users to upload datasets. */
public allowUserUploads: boolean;
public name: string;
/** The name of the app publisher's organization. */
public organization: string;
public shortDescription: string;
/** The app publisher's homepage URL. */
public homepage: string;
/** The privacy policy presented to the user when joining the app. */
public privacyPolicy: string;
/** The terms and conditions presented to the user when joining the app. */
public termsAndConditions: string;
public name: string;
/** The name of the app publisher's organization. */
public organization: string;
public shortDescription: string;
/** The app publisher's homepage URL. */
public homepageUrl: string;
/** A URL pointing to (or containing) the app's logo. */
public logoUrl: string;
/** The privacy policy presented to the user when joining the app. */
public privacyPolicy: string;
/** The terms and conditions presented to the user when joining the app. */
public termsAndConditions: string;
/** Text shown to the user when viewing the app's invite page. */
public invitationText?: string;
/** Text shown to the user after accepting the app's invitation. */
public acceptanceText?: string;
/** Text shown to the user after rejecting the app's invitation. */
public rejectionText?: string;
/** Text shown to the user when viewing the app's invite page. */
public invitationText?: string;
/** Text shown to the user after accepting the app's invitation. */
public acceptanceText?: string;
/** Text shown to the user after rejecting the app's invitation. */
public rejectionText?: string;
public extendedDescription?: string;
/** The app's branding color in RGB hex format (e.g. `#ff4212`). */
public brandingColor?: string;
/**
* Text describing the category of the app (e.g., health, finance) that can
* be used to search for the app.
*/
public category?: string;
/** A URL pointing to (or containing) the app's logo. */
public logo?: string;
public extendedDescription?: string;
/** The app's branding color in RGB hex format (e.g. `#ff4212`). */
public brandingColor?: string;
/**
* Text describing the category of the app (e.g., health, finance) that can
* be used to search for the app.
*/
public category?: string;
public trusted: boolean;
public constructor(private readonly client: HttpClient, pod: PODApp) {
this.acceptanceText = pod.acceptanceText;
this.admins = pod.admins as IdentityId[];
this.brandingColor = pod.brandingColor;
this.category = pod.category;
this.collaborators = pod.collaborators as IdentityId[];
this.createdAt = new Date(pod.createdAt);
this.owner = pod.owner as IdentityId;
this.extendedDescription = pod.extendedDescription;
this.homepage = pod.homepage;
this.id = pod.id as AppId;
this.invites = pod.invites as IdentityId[];
this.invitationText = pod.invitationText;
this.inviteOnly = pod.inviteOnly;
this.name = pod.name;
this.organization = pod.organization;
this.participants = pod.participants as IdentityId[];
this.privacyPolicy = pod.privacyPolicy;
this.published = pod.published;
this.rejectionText = pod.rejectionText;
this.shortDescription = pod.shortDescription;
this.termsAndConditions = pod.termsAndConditions;
this.logo = pod.logo;
}
public constructor(private readonly client: HttpClient, pod: PODApp) {
this.acceptanceText = pod.acceptanceText;
this.admins = pod.admins as IdentityId[];
this.allowUserUploads = pod.allowUserUploads;
this.brandingColor = pod.brandingColor;
this.category = pod.category;
this.collaborators = pod.collaborators as IdentityId[];
this.createdAt = new Date(pod.createdAt);
this.owner = pod.owner as IdentityId;
this.extendedDescription = pod.extendedDescription;
this.homepageUrl = pod.homepageUrl;
this.id = pod.id as AppId;
this.invites = pod.invites as IdentityId[];
this.invitationText = pod.invitationText;
this.inviteOnly = pod.inviteOnly;
this.name = pod.name;
this.organization = pod.organization;
this.participants = pod.participants as IdentityId[];
this.privacyPolicy = pod.privacyPolicy;
this.published = pod.published;
this.rejectionText = pod.rejectionText;
this.shortDescription = pod.shortDescription;
this.termsAndConditions = pod.termsAndConditions;
this.logoUrl = pod.logoUrl;
this.trusted = pod.trusted;
}
public async update(params: AppUpdateParams): Promise<App> {
Object.assign(this, await AppImpl.update(this.client, this.id, params));
return this;
}
public async update(params: AppUpdateParams): Promise<App> {
Object.assign(this, await AppImpl.update(this.client, this.id, params));
return this;
}
public async delete(): Promise<void> {
return AppImpl.delete_(this.client, this.id);
}
public async delete(): Promise<void> {
return AppImpl.delete_(this.client, this.id);
}
/**
* Creates a new consent that this app will request from users. The new consent
* will be added to `this.consents`.
*/
public async createConsent(params: ConsentCreateParams): Promise<Consent> {
return ConsentImpl.create(this.client, this.id, params);
}
/**
* Creates a new consent that this app will request from users. The new consent
* will be added to `this.consents`.
*/
public async createConsent(params: ConsentCreateParams): Promise<Consent> {
return ConsentImpl.create(this.client, this.id, params);
}
/**
* Returns the consents associated with this app.
*/
public async listConsents(): Promise<Page<Consent>> {
return ConsentImpl.list(this.client, this.id);
}
/**
* Returns the consents associated with this app.
*/
public async listConsents(): Promise<Page<Consent>> {
return ConsentImpl.list(this.client, this.id);
}
/**
* Deletes a consent from this app, revoking any access made by granting consent.
* will be removed from `this.consents`.
*/
public async deleteConsent(consentId: ConsentId): Promise<void> {
return ConsentImpl.delete_(this.client, this.id, consentId);
}
/**
* Deletes a consent from this app, revoking any access made by granting consent.
* will be removed from `this.consents`.
*/
public async deleteConsent(consentId: ConsentId): Promise<void> {
return ConsentImpl.delete_(this.client, this.id, consentId);
}
}
export namespace AppImpl {
export async function create(client: HttpClient, params: AppCreateParams): Promise<App> {
return client.create<PODApp>(APPS_EP, params).then((podApp) => new App(client, podApp));
}
export async function create(client: HttpClient, params: AppCreateParams): Promise<App> {
return client.create<PODApp>(APPS_EP, params).then((podApp) => new App(client, podApp));
}
export async function get(client: HttpClient, id: AppId): Promise<App> {
return client.get<PODApp>(endpointForId(id)).then((podApp) => new App(client, podApp));
}
export async function get(client: HttpClient, id: AppId): Promise<App> {
return client.get<PODApp>(endpointForId(id)).then((podApp) => new App(client, podApp));
}
export async function list(
client: HttpClient,
filter?: ListAppsFilter & PageParams,
): Promise<Page<App>> {
const podPage = await client.get<Page<PODApp>>(APPS_EP, filter);
const results = podPage.results.map((podApp) => new App(client, podApp));
return {
results,
nextPageToken: podPage.nextPageToken,
};
}
export async function list(
client: HttpClient,
filter?: ListAppsFilter & PageParams,
): Promise<Page<App>> {
const podPage = await client.get<Page<PODApp>>(APPS_EP, filter);
const results = podPage.results.map((podApp) => new App(client, podApp));
return {
results,
nextPageToken: podPage.nextPageToken,
};
}
export async function update(
client: HttpClient,
id: AppId,
params: AppUpdateParams,
): Promise<App> {
return client
.update<PODApp>(endpointForId(id), params)
.then((podApp) => new App(client, podApp));
}
export async function update(
client: HttpClient,
id: AppId,
params: AppUpdateParams,
): Promise<App> {
return client
.update<PODApp>(endpointForId(id), params)
.then((podApp) => new App(client, podApp));
}
export async function delete_(client: HttpClient, id: AppId): Promise<void> {
return client.delete(endpointForId(id));
}
export async function delete_(client: HttpClient, id: AppId): Promise<void> {
return client.delete(endpointForId(id));
}
}
const APPS_EP = '/apps';
const endpointForId = (id: AppId) => `/apps/${id}`;
export const APPS_EP = 'apps';
export const endpointForId = (id: AppId) => `${APPS_EP}/${id}`;
export type AppCreateParams =
| AppUpdateParams
| {
/** The credentials used to authorize clients acting as this app. */
identityTokenVerifiers: IdentityTokenVerifier[];
};
| AppUpdateParams
| {
/** The credentials used to authorize clients acting as this app. */
identityTokenVerifiers: IdentityTokenVerifier[];
};
export type AppUpdateParams = WritableExcluding<App, 'participants'>;
export type AppUpdateParams = WritableExcluding<App, 'participants' | 'trusted'>;
export type ListAppsFilter = Partial<{
/** Only return Apps owned by the provided Identity. */
owner: IdentityId;
/** Only return Apps owned by the provided Identity. */
owner: IdentityId;
/** Only return Apps for which the requester has the specified participation status. */
participation: AppParticipation;
/** Only return Apps for which the requester has the specified participation status. */
participation: AppParticipation;
}>;
export type AppParticipation = 'invited' | 'joined';

@@ -1,8 +0,9 @@

import type { Opaque } from 'type-fest';
import type { Except, Opaque } from 'type-fest';
import type { AppId } from './app';
import type { HttpClient } from './http';
import type { IdentityId } from './identity';
import type { Model, Page, PageParams, PODModel, ResourceId, WritableExcluding } from './model';
import type { PublicJWK } from './token';
import type { AppId } from './app.js';
import { endpointForId as endpointForApp } from './app.js';
import type { HttpClient } from './http.js';
import type { IdentityId } from './identity.js';
import type { Model, Page, PageParams, PODModel, ResourceId, WritableExcluding } from './model.js';
import type { PublicJWK } from './token.js';

@@ -12,124 +13,116 @@ export type ClientId = Opaque<ResourceId>;

export type PODClient = PODModel & {
creator: ResourceId;
appId: ResourceId;
name: string;
redirectUris: string[];
postLogoutRedirectUris: string[];
jsonWebKeys: PublicJWK[];
audience: string;
canHoldSecrets: boolean;
canActOnBehalfOfUsers: boolean;
isScript: boolean;
creator: ResourceId;
appId: ResourceId;
name: string;
redirectUris: string[];
postLogoutRedirectUris: string[];
publicKeys: PublicJWK[];
canHoldSecrets: boolean;
canActOnBehalfOfUsers: boolean;
isScript: boolean;
};
export class Client implements Model {
public id: ClientId;
public createdAt: Date;
public creator: IdentityId;
public appId: AppId;
public name: string;
public redirectUris: string[];
public postLogoutRedirectUris: string[];
public jsonWebKeys: PublicJWK[];
/** The allowed audience for this client's auth tokens. */
public audience: string;
public canHoldSecrets: boolean;
public canActOnBehalfOfUsers: boolean;
public isScript: boolean;
public id: ClientId;
public createdAt: Date;
public creator: IdentityId;
public appId: AppId;
public name: string;
public redirectUris: string[];
public postLogoutRedirectUris: string[];
public publicKeys: PublicJWK[];
public canHoldSecrets: boolean;
public canActOnBehalfOfUsers: boolean;
public isScript: boolean;
public constructor(private readonly client: HttpClient, pod: PODClient) {
this.id = pod.id as ClientId;
this.createdAt = new Date(pod.createdAt);
this.creator = pod.creator as IdentityId;
this.appId = pod.appId as AppId;
this.name = pod.name;
this.redirectUris = pod.redirectUris;
this.postLogoutRedirectUris = pod.postLogoutRedirectUris;
this.jsonWebKeys = pod.jsonWebKeys;
this.audience = pod.audience;
this.canHoldSecrets = pod.canHoldSecrets;
this.canActOnBehalfOfUsers = pod.canActOnBehalfOfUsers;
this.isScript = pod.isScript;
}
public constructor(private readonly client: HttpClient, pod: PODClient) {
this.id = pod.id as ClientId;
this.createdAt = new Date(pod.createdAt);
this.creator = pod.creator as IdentityId;
this.appId = pod.appId as AppId;
this.name = pod.name;
this.redirectUris = pod.redirectUris;
this.postLogoutRedirectUris = pod.postLogoutRedirectUris;
this.publicKeys = pod.publicKeys;
this.canHoldSecrets = pod.canHoldSecrets;
this.canActOnBehalfOfUsers = pod.canActOnBehalfOfUsers;
this.isScript = pod.isScript;
}
public async update(params: ClientUpdateParams): Promise<Client> {
Object.assign(this, await ClientImpl.update(this.client, this.appId, this.id, params));
return this;
}
public async update(params: ClientUpdateParams): Promise<Client> {
Object.assign(this, await ClientImpl.update(this.client, this.appId, this.id, params));
return this;
}
public async delete(): Promise<void> {
return this.client.delete(endpointForId(this.appId, this.id));
}
public async delete(): Promise<void> {
return this.client.delete(endpointForId(this.appId, this.id));
}
}
export namespace ClientImpl {
export async function create(
client: HttpClient,
appId: AppId,
params: ClientCreateParams,
): Promise<Client> {
return client
.create<PODClient>(endpointForCollection(appId), params)
.then((podClient) => new Client(client, podClient));
}
export async function create(
client: HttpClient,
appId: AppId,
params: ClientCreateParams,
): Promise<Client> {
return client
.create<PODClient>(endpointForCollection(appId), params)
.then((podClient) => new Client(client, podClient));
}
export async function get(
client: HttpClient,
appId: AppId,
clientId: ClientId,
): Promise<Client> {
return client
.get<PODClient>(endpointForId(appId, clientId))
.then((podClient) => new Client(client, podClient));
}
export async function get(client: HttpClient, appId: AppId, clientId: ClientId): Promise<Client> {
return client
.get<PODClient>(endpointForId(appId, clientId))
.then((podClient) => new Client(client, podClient));
}
export async function list(
client: HttpClient,
appId: AppId,
filter?: ListClientsFilter & PageParams,
): Promise<Page<Client>> {
const podPage = await client.get<Page<PODClient>>(endpointForCollection(appId), filter);
const results = podPage.results.map((podClient) => new Client(client, podClient));
return {
results,
nextPageToken: podPage.nextPageToken,
};
}
export async function list(
client: HttpClient,
appId: AppId,
filter?: ListClientsFilter & PageParams,
): Promise<Page<Client>> {
const podPage = await client.get<Page<PODClient>>(endpointForCollection(appId), filter);
const results = podPage.results.map((podClient) => new Client(client, podClient));
return {
results,
nextPageToken: podPage.nextPageToken,
};
}
export async function update(
client: HttpClient,
appId: AppId,
clientId: ClientId,
params: ClientUpdateParams,
): Promise<Client> {
return client
.update<PODClient>(endpointForId(appId, clientId), params)
.then((podClient) => new Client(client, podClient));
}
export async function update(
client: HttpClient,
appId: AppId,
clientId: ClientId,
params: ClientUpdateParams,
): Promise<Client> {
return client
.update<PODClient>(endpointForId(appId, clientId), params)
.then((podClient) => new Client(client, podClient));
}
export async function delete_(
client: HttpClient,
appId: AppId,
clientId: ClientId,
): Promise<void> {
return client.delete(endpointForId(appId, clientId));
}
export async function delete_(
client: HttpClient,
appId: AppId,
clientId: ClientId,
): Promise<void> {
return client.delete(endpointForId(appId, clientId));
}
}
const endpointForCollection = (appId: AppId) => `/apps/${appId}/clients`;
const endpointForCollection = (appId: AppId) => `${endpointForApp(appId)}/clients`;
const endpointForId = (appId: AppId, clientId: ClientId) =>
`${endpointForCollection(appId)}/${clientId}`;
`${endpointForCollection(appId)}/${clientId}`;
export type ClientCreateParams = ClientUpdateParams;
export type ClientUpdateParams = WritableExcluding<
Client,
'creator' | 'appId' | 'audience' | 'canHoldSecrets' | 'canActOnBehalfOfUsers' | 'isScript'
export type ClientCreateParams = WritableExcluding<
Client,
'creator' | 'appId' | 'canActOnBehalfOfUsers'
>;
export type ClientUpdateParams = Except<ClientCreateParams, 'canHoldSecrets' | 'isScript'>;
export type ListClientsFilter = Partial<{
/** Only return clients created by the provided identity. */
creator: IdentityId;
/** Only return clients created by the provided identity. */
creator: IdentityId;
/** Only return clients for the provided app. */
appId: AppId;
/** Only return clients for the provided app. */
appId: AppId;
}>;
import type { Opaque } from 'type-fest';
import type { AppId } from './app';
import type { Constraints } from './filter';
import type { HttpClient } from './http';
import type { IdentityId } from './identity';
import type { Model, Page, PageParams, PODModel, ResourceId } from './model';
import type { AppId } from './app.js';
import { endpointForId as endpointForApp } from './app.js';
import type { Constraints } from './filter.js';
import type { HttpClient } from './http.js';
import type { IdentityId } from './identity.js';
import type { Model, Page, PageParams, PODModel, ResourceId } from './model.js';

@@ -12,111 +13,110 @@ export type ConsentId = Opaque<ResourceId>;

export type PODConsent = PODModel &
ConsentCreateParams & {
appId: ResourceId;
};
ConsentCreateParams & {
appId: ResourceId;
};
export type ConsentCreateParams = {
/** The Grants to make when the App containing this Consent is joined. */
grants: GrantSpec[];
/** The Grants to make when the App containing this Consent is joined. */
grants: GrantSpec[];
/** The name of this consent. */
name: string;
/** The name of this consent. */
name: string;
/** The description of this consent seen by users when shown in an app. */
description: string;
/** The description of this consent seen by users when shown in an app. */
description: string;
/** Whether this Consent is automatically accepted when joining an App. */
required?: boolean;
/** The text seen by users when accepting this consent. */
allowText: string;
/** The text seen by users when accepting this consent. */
allowText: string;
/** The text seen by users when denying this consent. */
denyText: string;
/** The text seen by users when denying this consent. */
denyText: string;
};
export class Consent implements Model {
public id: ConsentId;
public appId: AppId;
public createdAt: Date;
/** The Grants to make when the App containing this Consent is joined. */
public grants: GrantSpec[];
/** Whether this Consent is automatically accepted when joining an App. */
public required: boolean;
public name: string;
/** The description of this consent seen by users when shown in an app. */
public description: string;
/** The text seen by users when accepting this consent. */
public allowText: string;
/** The text seen by users when denying this consent. */
public denyText: string;
public id: ConsentId;
public appId: AppId;
public createdAt: Date;
public constructor(private readonly client: HttpClient, pod: PODConsent) {
this.id = pod.id as ConsentId;
this.appId = pod.appId as AppId;
this.createdAt = new Date(pod.createdAt);
this.grants = pod.grants;
this.required = pod.required ?? false;
this.name = pod.name;
this.description = pod.description;
this.allowText = pod.allowText;
this.denyText = pod.denyText;
}
/** The Grants to make when the App containing this Consent is joined. */
public grants: GrantSpec[];
public name: string;
/** The description of this consent seen by users when shown in an app. */
public description: string;
/** The text seen by users when accepting this consent. */
public allowText: string;
/** The text seen by users when denying this consent. */
public denyText: string;
public constructor(private readonly client: HttpClient, pod: PODConsent) {
this.id = pod.id as ConsentId;
this.appId = pod.appId as AppId;
this.createdAt = new Date(pod.createdAt);
this.grants = pod.grants;
this.name = pod.name;
this.description = pod.description;
this.allowText = pod.allowText;
this.denyText = pod.denyText;
}
}
export namespace ConsentImpl {
export async function create(
client: HttpClient,
appId: AppId,
params: ConsentCreateParams,
): Promise<Consent> {
return client
.create<PODConsent>(endpointForCollection(appId), params)
.then((podConsent) => new Consent(client, podConsent));
}
export async function create(
client: HttpClient,
appId: AppId,
params: ConsentCreateParams,
): Promise<Consent> {
return client
.create<PODConsent>(endpointForCollection(appId), params)
.then((podConsent) => new Consent(client, podConsent));
}
export async function list(
client: HttpClient,
appId: AppId,
filter?: PageParams,
): Promise<Page<Consent>> {
const podPage = await client.get<Page<PODConsent>>(endpointForCollection(appId), filter);
const results = podPage.results.map((podConsent) => new Consent(client, podConsent));
return {
results,
nextPageToken: podPage.nextPageToken,
};
}
export async function list(
client: HttpClient,
appId: AppId,
filter?: PageParams,
): Promise<Page<Consent>> {
const podPage = await client.get<Page<PODConsent>>(endpointForCollection(appId), filter);
const results = podPage.results.map((podConsent) => new Consent(client, podConsent));
return {
results,
nextPageToken: podPage.nextPageToken,
};
}
export async function get(
client: HttpClient,
appId: AppId,
consentId: ConsentId,
): Promise<Consent> {
return client
.get<PODConsent>(endpointForId(appId, consentId))
.then((podConsent) => new Consent(client, podConsent));
}
export async function get(
client: HttpClient,
appId: AppId,
consentId: ConsentId,
): Promise<Consent> {
return client
.get<PODConsent>(endpointForId(appId, consentId))
.then((podConsent) => new Consent(client, podConsent));
}
export async function delete_(
client: HttpClient,
appId: AppId,
consentId: ConsentId,
): Promise<void> {
return client.delete(endpointForId(appId, consentId));
}
export async function delete_(
client: HttpClient,
appId: AppId,
consentId: ConsentId,
): Promise<void> {
return client.delete(endpointForId(appId, consentId));
}
}
const endpointForCollection = (appId: AppId) => `/apps/${appId}/consents`;
const endpointForCollection = (appId: AppId) => `${endpointForApp(appId)}/consents`;
const endpointForId = (appId: AppId, consentId: ConsentId) =>
`${endpointForCollection(appId)}/${consentId}`;
`${endpointForCollection(appId)}/${consentId}`;
export type GrantSpec = {
/** The symbolic granter. */
granter: GranterRef;
/** The symbolic granter. */
granter: GranterRef;
/** The symbolic grantee */
grantee?: GranteeRef;
/** The symbolic grantee */
grantee?: GranteeRef;
/** The Grant's filter. @see `Grant.filter`. */
filter?: Constraints;
/** The Grant's filter. @see `Grant.filter`. */
filter?: Constraints;
};

@@ -123,0 +123,0 @@

@@ -1,10 +0,9 @@

import axios, { CancelTokenSource } from 'axios';
import EventEmitter from 'eventemitter3';
import FormData from 'form-data';
import { Readable } from 'readable-stream';
import type { Readable } from 'readable-stream';
import type { JsonObject, Opaque, RequireExactlyOne, SetOptional } from 'type-fest';
import type { HttpClient, Download } from './http';
import type { IdentityId } from './identity';
import type { Model, Page, PageParams, PODModel, ResourceId, WritableExcluding } from './model';
import type { HttpClient, Download } from './http.js';
import type { IdentityId } from './identity.js';
import type { Model, Page, PageParams, PODModel, ResourceId, WritableExcluding } from './model.js';

@@ -14,110 +13,143 @@ export type DatasetId = Opaque<ResourceId>;

export type PODDataset = PODModel & {
id: ResourceId;
creator: ResourceId;
owner: ResourceId;
metadata?: DatasetMetadata;
id: ResourceId;
creator: ResourceId;
owner: ResourceId;
size: number;
details: DatasetDetails;
};
type DatasetMetadata = JsonObject & { tags?: string[] };
export type PODAccessEvent = {
createdAt: string;
dataset: ResourceId;
accessor: ResourceId;
};
type DatasetDetails = JsonObject & { title?: string; tags?: string[] };
export class Dataset implements Model {
public id: DatasetId;
public createdAt: Date;
public creator: IdentityId;
public owner: IdentityId;
/**
* Dataset metadata. The well-known value `tags` should be an array of strings if
* you want to use it with the `dataset.metadata.tags` filter.
*/
public metadata: DatasetMetadata;
public id: DatasetId;
public createdAt: Date;
public creator: IdentityId;
public size: number;
public owner: IdentityId;
public constructor(private readonly client: HttpClient, pod: PODDataset) {
this.id = pod.id as DatasetId;
this.createdAt = new Date(pod.createdAt);
this.creator = pod.creator as IdentityId;
this.owner = pod.owner as IdentityId;
this.metadata = pod.metadata ?? {};
}
/** Additional, optional information about the dataset. */
public details: DatasetDetails;
/**
* Downloads the private data referenced by the dataset if the authorized identity
* has been granted access.
* @returns the decrypted data as a stream
*/
public download(): Download {
return DatasetImpl.download(this.client, this.id);
}
public constructor(private readonly client: HttpClient, pod: PODDataset) {
this.id = pod.id as DatasetId;
this.createdAt = new Date(pod.createdAt);
this.creator = pod.creator as IdentityId;
this.owner = pod.owner as IdentityId;
this.size = pod.size;
this.details = pod.details;
}
public async update(params: DatasetUpdateParams): Promise<Dataset> {
Object.assign(this, await DatasetImpl.update(this.client, this.id, params));
return this;
}
/**
* Downloads the private data referenced by the dataset if the authorized identity
* has been granted access.
* @returns the decrypted data as a stream
*/
public download(): Download {
return DatasetImpl.download(this.client, this.id);
}
public async delete(): Promise<void> {
return DatasetImpl.delete_(this.client, this.id);
}
public async update(params: DatasetUpdateParams): Promise<Dataset> {
Object.assign(this, await DatasetImpl.update(this.client, this.id, params));
return this;
}
public async delete(): Promise<void> {
return DatasetImpl.delete_(this.client, this.id);
}
public async history(filter?: ListAccessLogFilter & PageParams): Promise<Page<AccessEvent>> {
return DatasetImpl.history(this.client, this.id, filter);
}
}
export namespace DatasetImpl {
export async function get(client: HttpClient, id: DatasetId): Promise<Dataset> {
return client
.get<PODDataset>(endpointForId(id))
.then((podDataset) => new Dataset(client, podDataset));
export async function get(client: HttpClient, id: DatasetId): Promise<Dataset> {
return client
.get<PODDataset>(endpointForId(id))
.then((podDataset) => new Dataset(client, podDataset));
}
export async function list(
client: HttpClient,
filter?: ListDatasetsFilter & PageParams,
): Promise<Page<Dataset>> {
let tagsFilter;
if (filter?.tags) {
const tagsSpec = filter.tags;
const prefix = Array.isArray(tagsSpec) || tagsSpec.all ? 'all' : 'any';
const tags = Array.isArray(tagsSpec) ? tagsSpec : tagsSpec.all ?? tagsSpec.any;
tagsFilter = `${prefix}:${tags.join(',')}`;
}
export async function list(
client: HttpClient,
filter?: ListDatasetsFilter & PageParams,
): Promise<Page<Dataset>> {
let tagsFilter;
if (filter?.tags) {
const tagsSpec = filter.tags;
const prefix = Array.isArray(tagsSpec) || tagsSpec.all ? 'all' : 'any';
const tags = Array.isArray(tagsSpec) ? tagsSpec : tagsSpec.all ?? tagsSpec.any;
tagsFilter = `${prefix}:${tags.join(',')}`;
}
const podPage = await client.get<Page<PODDataset>>(DATASETS_EP, {
...filter,
tags: tagsFilter,
});
const results = podPage.results.map((podDataset) => new Dataset(client, podDataset));
return {
results,
nextPageToken: podPage.nextPageToken,
};
}
const podPage = await client.get<Page<PODDataset>>(DATASETS_EP, {
...filter,
tags: tagsFilter,
});
const results = podPage.results.map((podDataset) => new Dataset(client, podDataset));
return {
results,
nextPageToken: podPage.nextPageToken,
};
}
export function upload(client: HttpClient, data: Storable, params?: DatasetUploadParams): Upload {
return new Upload(client, data, params);
}
export function upload(
client: HttpClient,
data: Storable,
params?: DatasetUploadParams,
): Upload {
return new Upload(client, data, params);
}
export function download(client: HttpClient, id: DatasetId): Download {
return client.download(endpointForId(id) + '/download');
}
export function download(client: HttpClient, id: DatasetId): Download {
return client.download(endpointForId(id) + '/download');
}
export async function history(
client: HttpClient,
id: DatasetId,
filter?: ListAccessLogFilter & PageParams,
): Promise<Page<AccessEvent>> {
const podPage = await client.get<Page<PODAccessEvent>>(endpointForId(id) + '/history', {
...filter,
// Dates must be string-ified since request parameters are of type JSONObject
// and doesn't support Date.
after: filter?.after?.getTime(),
before: filter?.before?.getTime(),
});
export async function update(
client: HttpClient,
id: DatasetId,
params: DatasetUpdateParams,
): Promise<Dataset> {
return client
.update<PODDataset>(endpointForId(id), params)
.then((podDataset) => new Dataset(client, podDataset));
}
const results = podPage.results.map((podAccessEvent) => {
return {
createdAt: new Date(podAccessEvent.createdAt),
dataset: podAccessEvent.dataset as DatasetId,
accessor: podAccessEvent.accessor as IdentityId,
};
});
return {
results,
nextPageToken: podPage.nextPageToken,
};
}
export async function delete_(client: HttpClient, id: DatasetId): Promise<void> {
return client.delete(endpointForId(id));
}
export async function update(
client: HttpClient,
id: DatasetId,
params: DatasetUpdateParams,
): Promise<Dataset> {
return client
.update<PODDataset>(endpointForId(id), params)
.then((podDataset) => new Dataset(client, podDataset));
}
export async function delete_(client: HttpClient, id: DatasetId): Promise<void> {
return client.delete(endpointForId(id));
}
}
const DATASETS_EP = '/datasets';
const endpointForId = (id: DatasetId) => `/datasets/${id}`;
const DATASETS_EP = 'datasets';
const endpointForId = (id: DatasetId) => `${DATASETS_EP}/${id}`;
export type DatasetUpdateParams = WritableExcluding<Dataset, 'creator'>;
export type DatasetUploadParams = SetOptional<DatasetUpdateParams, 'owner' | 'metadata'>;
export type DatasetUpdateParams = WritableExcluding<Dataset, 'creator' | 'size'>;
export type DatasetUploadParams = SetOptional<DatasetUpdateParams, 'owner' | 'details'>;

@@ -127,13 +159,25 @@ export type Storable = Uint8Array | Readable | Blob | string;

export type ListDatasetsFilter = Partial<{
creator: IdentityId;
owner: IdentityId;
sharedWith: IdentityId;
tags:
| string[]
| RequireExactlyOne<{
any: string[];
all: string[];
}>;
creator: IdentityId;
owner: IdentityId;
sharedWith: IdentityId;
tags:
| string[]
| RequireExactlyOne<{
any: string[];
all: string[];
}>;
}>;
export type AccessEvent = {
createdAt: Date;
dataset: DatasetId;
accessor: IdentityId;
};
export type ListAccessLogFilter = Partial<{
accessor: IdentityId;
after: Date;
before: Date;
}>;
/**

@@ -148,70 +192,69 @@ * An `Upload` is the result of calling `parcel.uploadDataset`.

export class Upload extends EventEmitter {
public aborted = false;
private readonly abortController: AbortController;
private readonly cancelToken: CancelTokenSource;
constructor(client: HttpClient, data: Storable, params?: DatasetUploadParams) {
super();
constructor(client: HttpClient, data: Storable, params?: DatasetUploadParams) {
super();
this.cancelToken = axios.CancelToken.source();
const form = new FormData();
this.abortController = new AbortController();
const appendPart = (name: string, data: Storable, contentType: string, length?: number) => {
if (typeof Blob === 'undefined') {
// If Blob isn't present, we're likely in Node and should use the `form-data` API.
form.append(name, data, {
contentType,
knownLength: length,
});
} else {
// If `Blob` eixsts, we're probably in the browser and will pefer to use it.
if (typeof data === 'string' || data instanceof Uint8Array) {
data = new Blob([data], { type: contentType });
} else if (data instanceof Readable) {
throw new TypeError('uploaded data must be a `Blob` or `Uint8Array`');
}
const form = new FormData();
form.append(name, data);
}
};
if (params) {
const paramsString = JSON.stringify(params);
appendPart('metadata', paramsString, 'application/json', paramsString.length);
const appendPart = (name: string, data: Storable, contentType: string, length?: number) => {
if (typeof Blob === 'undefined') {
// If Blob isn't present, we're likely in Node and should use the `form-data` API.
form.append(name, data, {
contentType,
knownLength: length,
});
} else {
// If `Blob` eixsts, we're probably in the browser and will pefer to use it.
if (typeof data === 'string' || data instanceof Uint8Array) {
data = new Blob([data], { type: contentType });
} else if ('pipe' in data) {
throw new TypeError('uploaded data must be a `string`, `Blob`, or `Uint8Array`');
}
appendPart('data', data, 'application/octet-stream', (data as any).length);
form.append(name, data);
}
};
client
.post<PODDataset>(DATASETS_EP, form, {
headers: form.getHeaders ? /* node */ form.getHeaders() : undefined,
cancelToken: this.cancelToken.token,
onUploadProgress: this.emit.bind(this, 'progress'),
validateStatus: (s) => s === 201 /* Created */,
})
.then((podDataset) => {
this.emit('finish', new Dataset(client, podDataset));
})
.catch((error: any) => {
if (!axios.isCancel(error)) {
this.emit('error', error);
}
});
if (params) {
const paramsString = JSON.stringify(params);
appendPart('metadata', paramsString, 'application/json', paramsString.length);
}
/** Aborts the upload. Emits an `abort` event and sets the `aborted` flag. */
public abort(): void {
this.cancelToken.cancel();
this.aborted = true;
this.emit('abort');
}
appendPart('data', data, 'application/octet-stream', (data as any).length);
/**
* @returns a `Promise` that resolves when the upload stream has finished.
*/
public get finished(): Promise<Dataset> {
return new Promise((resolve, reject) => {
this.on('finish', resolve);
this.on('error', reject);
});
}
client
.create<PODDataset>(DATASETS_EP, form, {
headers: 'getHeaders' in form ? /* node */ form.getHeaders() : undefined,
signal: this.abortController.signal,
})
.then((podDataset) => {
this.emit('finish', new Dataset(client, podDataset));
})
.catch((error: any) => {
this.emit('error', error);
});
}
/** Aborts the upload. Emits an `abort` event and sets the `aborted` flag. */
public abort(): void {
this.abortController.abort();
this.emit('abort');
}
public get aborted(): boolean {
return this.abortController.signal.aborted;
}
/**
* @returns a `Promise` that resolves when the upload stream has finished.
*/
public get finished(): Promise<Dataset> {
return new Promise((resolve, reject) => {
this.on('finish', resolve);
this.on('error', reject);
});
}
}
import type { Primitive } from 'type-fest';
import type { DatasetId } from './dataset';
import type { IdentityId } from './identity';
import type { DatasetId as $DatasetId } from './dataset.js';
import type { IdentityId } from './identity.js';
export namespace Constraints {
export type And = {
$and: Constraints[];
};
export type Or = {
$or: Constraints[];
};
export type Not = {
$not: Constraints;
};
export type DatasetId = {
'dataset.id': Comparison<DatasetId>;
};
export type DatasetCreator = {
'dataset.creator': Comparison<IdentityId>;
};
export type DatasetTags = {
'dataset.metadata.tags': ArrayOp<string>;
};
export type And = {
$and: Constraints[];
};
export type Or = {
$or: Constraints[];
};
export type Not = {
$not: Constraints;
};
export type DatasetId = {
'dataset.id': Comparison<$DatasetId>;
};
export type DatasetCreator = {
'dataset.creator': Comparison<IdentityId>;
};
export type DatasetTags = {
'dataset.details.tags': ArrayOp<string>;
};
}
export type Constraints =
| Constraints.Or
| Constraints.And
| Constraints.Not
| Constraints.DatasetId
| Constraints.DatasetCreator
| Constraints.DatasetTags;
| Constraints.Or
| Constraints.And
| Constraints.Not
| Constraints.DatasetId
| Constraints.DatasetCreator
| Constraints.DatasetTags;
export namespace Comparison {
export type In<T = Primitive> = {
$in: T[];
};
export type In<T = Primitive> = {
$in: T[];
};
export type Eq<T = Primitive> = {
$eq: T;
};
export type Eq<T = Primitive> = {
$eq: T;
};
export type Not<T = Primitive> = {
$not: Comparison<T>;
};
export type Not<T = Primitive> = {
$not: Comparison<T>;
};
}

@@ -50,10 +50,10 @@ type Comparison<T> = Comparison.In<T> | Comparison.Eq<T> | Comparison.Not<T>;

export namespace ArrayOp {
export type Any<T = Primitive> = {
$any: Comparison<T>;
};
export type Any<T = Primitive> = {
$any: Comparison<T>;
};
export type All<T = Primitive> = {
$all: T[];
};
export type All<T = Primitive> = {
$all: T[];
};
}
type ArrayOp<T> = ArrayOp.Any<T> | ArrayOp.All<T>;
import type { Opaque } from 'type-fest';
import type { ConsentId } from './consent';
import type { Constraints } from './filter';
import type { HttpClient } from './http';
import type { IdentityId } from './identity';
import type { Model, PODModel, ResourceId } from './model';
import type { ConsentId } from './consent.js';
import type { Constraints } from './filter.js';
import type { HttpClient } from './http.js';
import type { IdentityId } from './identity.js';
import type { Model, Page, PageParams, PODModel, ResourceId } from './model.js';

@@ -12,65 +12,83 @@ export type GrantId = Opaque<ResourceId>;

export type PODGrant = PODModel & {
granter: ResourceId;
grantee?: ResourceId;
consent?: ResourceId;
filter?: Constraints;
granter: ResourceId;
grantee?: ResourceId;
consent?: ResourceId;
filter?: Constraints;
};
export type GrantCreateParams = {
/**
* The singular Identity to which permission is given, or everyone;
*/
grantee: IdentityId | 'everyone';
/**
* The singular Identity to which permission is given, or everyone;
*/
grantee: IdentityId | 'everyone';
/** A filter that gives permission to only matching Datasets. */
filter?: Constraints;
/** A filter that gives permission to only matching Datasets. */
filter?: Constraints;
};
const GRANTS_EP = '/grants';
const GRANTS_EP = 'grants';
const endpointForId = (id: GrantId) => `${GRANTS_EP}/${id}`;
export class Grant implements Model {
public id: GrantId;
public createdAt: Date;
/** The Identity from which permission is given. */
public granter: IdentityId;
/**
* The Identity to which permission is given or everyone,
*/
public grantee: IdentityId | 'everyone';
/** A filter that gives permission to only matching Datasets. */
public filter?: Constraints;
/** The Consent that created this Grant, if any. */
public consent?: ConsentId;
public id: GrantId;
public createdAt: Date;
/** The Identity from which permission is given. */
public granter: IdentityId;
/**
* The Identity to which permission is given or everyone,
*/
public grantee: IdentityId | 'everyone';
/** A filter that gives permission to only matching Datasets. */
public filter?: Constraints;
/** The Consent that created this Grant, if any. */
public consent?: ConsentId;
public constructor(private readonly client: HttpClient, pod: PODGrant) {
this.id = pod.id as GrantId;
this.createdAt = new Date(pod.createdAt);
this.granter = pod.granter as IdentityId;
this.grantee = (pod.grantee as IdentityId) ?? 'everyone';
this.filter = pod.filter;
this.consent = pod.consent as ConsentId;
}
public constructor(private readonly client: HttpClient, pod: PODGrant) {
this.id = pod.id as GrantId;
this.createdAt = new Date(pod.createdAt);
this.granter = pod.granter as IdentityId;
this.grantee = (pod.grantee as IdentityId) ?? 'everyone';
this.filter = pod.filter;
this.consent = pod.consent as ConsentId;
}
public async delete(): Promise<void> {
return this.client.delete(endpointForId(this.id));
}
public async delete(): Promise<void> {
return this.client.delete(endpointForId(this.id));
}
}
export namespace GrantImpl {
export async function create(client: HttpClient, params: GrantCreateParams): Promise<Grant> {
return client
.create<PODGrant>(GRANTS_EP, params)
.then((podGrant) => new Grant(client, podGrant));
}
export async function create(client: HttpClient, params: GrantCreateParams): Promise<Grant> {
return client
.create<PODGrant>(GRANTS_EP, params)
.then((podGrant) => new Grant(client, podGrant));
}
export async function get(client: HttpClient, id: GrantId): Promise<Grant> {
return client
.get<PODGrant>(endpointForId(id))
.then((podGrant) => new Grant(client, podGrant));
}
export async function get(client: HttpClient, id: GrantId): Promise<Grant> {
return client.get<PODGrant>(endpointForId(id)).then((podGrant) => new Grant(client, podGrant));
}
export async function delete_(client: HttpClient, id: GrantId): Promise<void> {
return client.delete(endpointForId(id));
}
export async function list(
client: HttpClient,
filter?: ListGrantsFilter & PageParams,
): Promise<Page<Grant>> {
const podPage = await client.get<Page<PODGrant>>(GRANTS_EP, filter);
const results = podPage.results.map((podGrant) => new Grant(client, podGrant));
return {
results,
nextPageToken: podPage.nextPageToken,
};
}
export async function delete_(client: HttpClient, id: GrantId): Promise<void> {
return client.delete(endpointForId(id));
}
}
export type ListGrantsFilter = Partial<{
/** Only return grants from granter. */
granter?: IdentityId;
/** Only return grants for the provided app. */
grantee?: IdentityId;
}>;
import type { WriteStream } from 'fs';
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import type FormData from 'form-data';
import { PassThrough, Readable, Writable } from 'readable-stream';
import AbortController from 'abort-controller';
import FormData from 'form-data';
import type {
BeforeRequestHook,
NormalizedOptions,
Options as KyOptions,
ResponsePromise,
} from 'ky';
import ky from 'ky';
import { paramCase } from 'param-case';
import type { Readable, Writable } from 'stream';
import type { JsonObject } from 'type-fest';
import { version as packageVersion, name as packageName } from '../package.json';
import type { TokenProvider } from './token';
import type { TokenProvider } from './token.js';
import './polyfill.js'; // eslint-disable-line import/no-unassigned-import

@@ -15,161 +22,146 @@ const DEFAULT_API_URL = 'https://api.oasislabs.com/parcel/v1';

export type Config = Partial<{
apiUrl: string;
httpClient: AxiosInstance;
apiUrl: string;
httpClientConfig: KyOptions;
}>;
const EXPECTED_RESPONSE_CODES = new Map([
['POST', 201],
['PUT', 200],
['PATCH', 200],
['DELETE', 204],
]);
export class HttpClient {
public readonly apiUrl: string;
public readonly apiUrl: string;
private readonly axios: AxiosInstance;
private readonly ky: typeof ky;
public constructor(private readonly tokenProvider: TokenProvider, config?: Config) {
this.apiUrl = config?.apiUrl?.replace(/\/$/, '') ?? DEFAULT_API_URL;
this.axios =
config?.httpClient ??
axios.create({
baseURL: this.apiUrl,
});
public constructor(private readonly tokenProvider: TokenProvider, config?: Config) {
this.apiUrl = config?.apiUrl?.replace(/\/$/, '') ?? DEFAULT_API_URL;
this.ky = ky.create(config?.httpClientConfig ?? {}).extend({
prefixUrl: this.apiUrl,
headers: {
'x-requested-with': '@oasislabs/parcel',
},
hooks: {
beforeRequest: [
async (req) => {
req.headers.set('authorization', `Bearer ${await this.tokenProvider.getToken()}`);
},
],
afterResponse: [
async (req, opts, res) => {
// The `authorization` header is not re-sent by the browser, so redirects fail,
// and must be retried manually.
if (
res.redirected &&
(res.status === 401 || res.status === 403) &&
res.url.startsWith(this.apiUrl)
) {
return this.ky(res.url, {
method: req.method,
prefixUrl: '',
});
}
this.axios.interceptors.request.use(async (config) => {
return {
...config,
headers: {
...(await this.getHeaders()),
...config.headers,
},
};
});
}
// Wrap errors, for easier client handling (and maybe better messages).
if (isApiErrorResponse(res)) {
throw new ApiError(req, opts, res, (await res.json()).error);
}
public async get<T>(
endpoint: string,
params: JsonObject = {},
axiosConfig?: AxiosRequestConfig,
): Promise<T> {
const kebabCaseParams: JsonObject = {};
for (const [k, v] of Object.entries(params)) {
kebabCaseParams[paramCase(k)] = v;
}
const expectedStatus: number =
(req as any).expectedStatus ?? EXPECTED_RESPONSE_CODES.get(req.method);
if (res.ok && expectedStatus && res.status !== expectedStatus) {
const endpoint = res.url.replace(this.apiUrl, '');
throw new ApiError(
req,
opts,
res,
`${req.method} ${endpoint} returned unexpected status ${expectedStatus}. expected: ${res.status}.`,
);
}
},
],
},
});
}
return this.axios
.get(endpoint, Object.assign({ params: kebabCaseParams }, axiosConfig))
.then((r) => r.data);
public async get<T>(
endpoint: string,
params: Record<string, string | number | boolean | undefined> = {},
requestOptions?: KyOptions,
): Promise<T> {
let hasParams = false;
const kebabCaseParams: Record<string, string | number | boolean> = {};
for (const [k, v] of Object.entries(params)) {
if (v !== undefined) {
hasParams = true;
kebabCaseParams[paramCase(k)] = v;
}
}
/** Convenience method for POSTing and expecting a 201 response */
public async create<T>(endpoint: string, data: JsonObject): Promise<T> {
return this.post(endpoint, data, {
validateStatus: (s) => s === 201,
});
}
return this.ky
.get(endpoint, {
searchParams: hasParams ? kebabCaseParams : undefined,
...requestOptions,
})
.json();
}
public async post<T>(
endpoint: string,
data: JsonObject | FormData | undefined,
axiosConfig?: AxiosRequestConfig,
): Promise<T> {
return this.axios.post(endpoint, data, axiosConfig).then((r) => r.data);
}
/** Convenience method for POSTing and expecting a 201 response */
public async create<T>(
endpoint: string,
data: JsonObject | FormData,
requestOptions?: KyOptions,
): Promise<T> {
return this.post(endpoint, data, requestOptions);
}
public async update<T>(endpoint: string, params: JsonObject): Promise<T> {
return this.put(endpoint, params);
public async post<T>(
endpoint: string,
data: JsonObject | FormData | undefined,
requestOptions?: KyOptions,
): Promise<T> {
const opts = requestOptions ?? {};
if (data !== undefined) {
if ('getBuffer' in data && typeof data.getBuffer === 'function') {
opts.body = data.getBuffer(); // It's the polyfill.
} else if (data instanceof FormData) {
opts.body = data as any;
} else {
opts.json = data;
}
}
public async put<T>(endpoint: string, params: JsonObject): Promise<T> {
return this.axios.put(endpoint, params).then((r) => r.data);
}
return this.ky.post(endpoint, opts).json();
}
public async delete(endpoint: string): Promise<void> {
return this.axios
.delete(endpoint, {
validateStatus: (s) => s === 204,
})
.then(() => undefined);
}
public async update<T>(endpoint: string, params: JsonObject): Promise<T> {
return this.put(endpoint, params);
}
public download(endpoint: string): Download {
/* istanbul ignore if */
return 'fetch' in globalThis ? this.downloadBrowser(endpoint) : this.downloadNode(endpoint);
}
public async put<T>(endpoint: string, params: JsonObject): Promise<T> {
return this.ky.put(endpoint, { json: params }).json();
}
/* istanbul ignore next */
// This is tested using Cypress, which produces bogus line numbers.
private downloadBrowser(endpoint: string): Download {
const abortController = new AbortController();
const res = this.getHeaders().then(async (headers) => {
const res = await fetch(`${this.apiUrl}${endpoint}`, {
method: 'GET',
headers,
signal: abortController.signal,
});
public async delete(endpoint: string): Promise<void> {
await this.ky.delete(endpoint);
}
if (!res.ok) {
const errorMessage: string = (await res.json()).error;
const error = new Error(`failed to fetch dataset: ${errorMessage}`);
(error as any).response = res;
throw error;
}
public download(endpoint: string): Download {
return new Download(this.ky, endpoint);
}
}
return res;
});
const reader = res.then((res) => {
if (!res.body) return null;
return res.body.getReader();
});
const dl = new (class extends Download {
public _read(): void {
reader
.then(async (rdr) => {
if (!rdr) return this.push(null);
/** A beforeRequest hook that attaches context to the Request, for displaying in errors. */
function attachContext(context: string): BeforeRequestHook {
return (req) => {
(req as any).context = context;
};
}
let chunk;
do {
// eslint-disable-next-line no-await-in-loop
chunk = await rdr.read(); // Loop iterations are not independent.
if (!chunk.value) continue;
if (!this.push(chunk.value)) break;
} while (!chunk.done);
if (chunk.done) this.push(null);
})
.catch((error: any) => this.destroy(error));
}
public _destroy(error: Error, cb: (error?: Error) => void): void {
abortController.abort();
void res.then((res) => res.body?.cancel());
cb(error);
}
})();
res.catch((error) => dl.destroy(error));
return dl;
}
private downloadNode(endpoint: string): Download {
const cancelToken = axios.CancelToken.source();
const pt: Download = Object.assign(new PassThrough(), {
pipeTo: Download.prototype.pipeTo,
destroy: (error: any) => {
cancelToken.cancel();
PassThrough.prototype.destroy.call(pt, error);
},
});
this.axios
.get(endpoint, {
responseType: 'stream',
cancelToken: cancelToken.token,
})
.then((res) => {
res.data?.on('error', (error: Error) => pt.destroy(error)).pipe(pt);
})
.catch((error: Error) => pt.destroy(error));
return pt;
}
private async getHeaders(): Promise<Record<string, string>> {
return {
authorization: `Bearer ${await this.tokenProvider.getToken()}`,
'user-agent': `${packageName}/${packageVersion}`,
};
}
export function setExpectedStatus(status: number): BeforeRequestHook {
return (req) => {
(req as any).expectedStatus = status;
};
}

@@ -185,26 +177,104 @@

*/
export class Download extends Readable {
/** Convenience method for piping to a sink and waiting for writing to finish. */
public async pipeTo(sink: Writable | WriteStream | WritableStream): Promise<void> {
/* istanbul ignore if */ // This is tested using Cypress.
if ('getWriter' in sink) {
const writer = sink.getWriter();
return new Promise((resolve, reject) => {
this.on('error', reject)
.on('data', (chunk) => {
void writer.ready.then(async () => writer.write(chunk)).catch(reject);
})
.on('end', () => {
writer.ready
.then(async () => writer.close())
.then(resolve)
.catch(reject);
});
});
}
export class Download implements AsyncIterable<Uint8Array> {
private res?: ResponsePromise;
private readonly abortController: AbortController;
return new Promise((resolve, reject) => {
this.on('error', reject).pipe(sink).on('finish', resolve).on('error', reject);
});
public constructor(private readonly client: typeof ky, private readonly endpoint: string) {
this.abortController = new AbortController();
}
public async *[Symbol.asyncIterator]() {
const body = (await this.makeRequest())?.body;
if (!body) return;
/* istanbul ignore else: tested using Cypress */
if ((body as any).getReader === undefined) {
// https://github.com/node-fetch/node-fetch/issues/930
const bodyReadable = (body as any) as Readable;
yield* bodyReadable;
} else {
const rdr = body.getReader();
let chunk;
do {
chunk = await rdr.read();
if (chunk.value) yield chunk.value;
} while (!chunk.done);
}
}
public abort(): void {
this.abortController.abort();
}
public get aborted(): boolean {
return this.abortController.signal.aborted;
}
/**
* Convenience method for piping to a sink and waiting for writing to finish.
* This method must not be used alongside `getStream` or `AsyncIterable`.
*/
public async pipeTo(sink: Writable | WriteStream | WritableStream): Promise<void> {
if ('getWriter' in sink) {
const body = (await this.makeRequest()).body;
return body ? body.pipeTo(sink) : Promise.resolve();
}
const Readable = (await import('stream')).Readable;
return new Promise((resolve, reject) => {
Readable.from(this, { objectMode: false })
.on('error', reject)
.pipe(sink)
.on('error', reject)
.on('finish', resolve);
});
}
/**
* Lazily make the request. Helps avoid unhandled promise rejections when the request
* fails before a pipe or iterator handler is attached.
*/
// This funciton returns double promise to make both xo and TS happy. V8 doesn't care.
private async makeRequest(): Promise<ResponsePromise> {
if (!this.res) {
this.res = this.client.get(this.endpoint, {
signal: this.abortController.signal,
hooks: {
beforeRequest: [attachContext('dataset download')],
},
});
}
return this.res;
}
}
export class ApiError extends ky.HTTPError {
public readonly message: string;
public constructor(
request: Request,
options: NormalizedOptions,
response: Response,
message: string, // Workaround for https://github.com/sindresorhus/ky/issues/148.
) {
super(response, request, options);
this.name = 'ApiError';
const context: string = (request as any).context;
if (context) {
message = `error in ${context}: ${message}`;
}
this.message = message;
}
public static async fromHTTPError(error: ky.HTTPError): Promise<ApiError> {
const res = error.response;
return new ApiError(error.request, error.options, res, (await res.json()).error);
}
}
function isApiErrorResponse(response: Response): boolean {
return !response.ok && response.headers.get('content-type') === 'application/json';
}
import type { Opaque, SetOptional } from 'type-fest';
import type { AppId } from './app';
import { Consent } from './consent';
import type { ConsentId, PODConsent } from './consent';
import type { HttpClient } from './http';
import type { Model, Page, PageParams, PODModel, ResourceId, Writable } from './model';
import type { IdentityTokenClaims, PublicJWK } from './token';
import type { AppId } from './app.js';
import { Consent } from './consent.js';
import type { ConsentId, PODConsent } from './consent.js';
import type { HttpClient } from './http.js';
import { setExpectedStatus } from './http.js';
import type { Model, Page, PageParams, PODModel, ResourceId, Writable } from './model.js';
import type { IdentityTokenClaims, PublicJWK } from './token.js';
export type IdentityId = Opaque<ResourceId>;
export type IdentityId = Opaque<ResourceId>; // TODO: uniqueify with second arg
export type PODIdentity = PODModel & IdentityCreateParams;
const IDENTITIES_EP = '/identities';
const IDENTITIES_EP = 'identities';
const IDENTITIES_ME = `${IDENTITIES_EP}/me`;

@@ -19,125 +20,127 @@ const endpointForId = (id: IdentityId) => `${IDENTITIES_EP}/${id}`;

const endpointForConsent = (identityId: IdentityId, consentId: ConsentId) =>
`${endpointForConsents(identityId)}/${consentId}`;
`${endpointForConsents(identityId)}/${consentId}`;
export class Identity implements Model {
public id: IdentityId;
public createdAt: Date;
public tokenVerifiers: IdentityTokenVerifier[];
public id: IdentityId;
public createdAt: Date;
public tokenVerifiers: IdentityTokenVerifier[];
public constructor(private readonly client: HttpClient, pod: PODIdentity) {
this.id = pod.id as IdentityId;
this.createdAt = new Date(pod.createdAt);
this.tokenVerifiers = pod.tokenVerifiers;
}
public constructor(private readonly client: HttpClient, pod: PODIdentity) {
this.id = pod.id as IdentityId;
this.createdAt = new Date(pod.createdAt);
this.tokenVerifiers = pod.tokenVerifiers;
}
public async update(params: IdentityUpdateParams): Promise<Identity> {
Object.assign(this, await IdentityImpl.update(this.client, this.id, params));
return this;
}
public async update(params: IdentityUpdateParams): Promise<Identity> {
Object.assign(this, await IdentityImpl.update(this.client, this.id, params));
return this;
}
public async delete(): Promise<void> {
return IdentityImpl.delete_(this.client, this.id);
}
public async delete(): Promise<void> {
return IdentityImpl.delete_(this.client, this.id);
}
public async grantConsent(id: ConsentId): Promise<void> {
return IdentityImpl.grantConsent(this.client, this.id, id);
}
public async grantConsent(id: ConsentId): Promise<void> {
return IdentityImpl.grantConsent(this.client, this.id, id);
}
/** Fetches consents to which this identity has consented. */
public async listGrantedConsents(
filter?: ListGrantedConsentsFilter & PageParams,
): Promise<Page<Consent>> {
return IdentityImpl.listGrantedConsents(this.client, this.id, filter);
}
/** Fetches consents to which this identity has consented. */
public async listGrantedConsents(
filter?: ListGrantedConsentsFilter & PageParams,
): Promise<Page<Consent>> {
return IdentityImpl.listGrantedConsents(this.client, this.id, filter);
}
/** * Gets a granted consent by id. Useful for checking if a consent has been granted. */
public async getGrantedConsent(id: ConsentId): Promise<Consent> {
return IdentityImpl.getGrantedConsent(this.client, this.id, id);
}
/** * Gets a granted consent by id. Useful for checking if a consent has been granted. */
public async getGrantedConsent(id: ConsentId): Promise<Consent> {
return IdentityImpl.getGrantedConsent(this.client, this.id, id);
}
public async revokeConsent(id: ConsentId): Promise<void> {
return IdentityImpl.revokeConsent(this.client, this.id, id);
}
public async revokeConsent(id: ConsentId): Promise<void> {
return IdentityImpl.revokeConsent(this.client, this.id, id);
}
}
export namespace IdentityImpl {
export async function create(
client: HttpClient,
params: IdentityCreateParams,
): Promise<Identity> {
return client
.post<PODIdentity>(IDENTITIES_EP, params, {
validateStatus: (s) => s === 200 || s === 201,
})
.then((podIdentity) => new Identity(client, podIdentity));
}
export async function create(
client: HttpClient,
params: IdentityCreateParams,
): Promise<Identity> {
return client
.create<PODIdentity>(IDENTITIES_EP, params)
.then((podIdentity) => new Identity(client, podIdentity));
}
export async function current(client: HttpClient): Promise<Identity> {
return client
.get<PODIdentity>(IDENTITIES_ME)
.then((podIdentity) => new Identity(client, podIdentity));
}
export async function current(client: HttpClient): Promise<Identity> {
return client
.get<PODIdentity>(IDENTITIES_ME)
.then((podIdentity) => new Identity(client, podIdentity));
}
export async function get(client: HttpClient, id: IdentityId): Promise<Identity> {
return client
.get<PODIdentity>(endpointForId(id))
.then((podIdentity) => new Identity(client, podIdentity));
}
export async function get(client: HttpClient, id: IdentityId): Promise<Identity> {
return client
.get<PODIdentity>(endpointForId(id))
.then((podIdentity) => new Identity(client, podIdentity));
}
export async function update(
client: HttpClient,
id: IdentityId,
params: IdentityUpdateParams,
): Promise<Identity> {
return client
.update<PODIdentity>(endpointForId(id), params)
.then((podIdentity) => new Identity(client, podIdentity));
}
export async function update(
client: HttpClient,
id: IdentityId,
params: IdentityUpdateParams,
): Promise<Identity> {
return client
.update<PODIdentity>(endpointForId(id), params)
.then((podIdentity) => new Identity(client, podIdentity));
}
export async function delete_(client: HttpClient, id: IdentityId): Promise<void> {
return client.delete(endpointForId(id));
}
export async function delete_(client: HttpClient, id: IdentityId): Promise<void> {
return client.delete(endpointForId(id));
}
export async function grantConsent(
client: HttpClient,
identityId: IdentityId,
consentId: ConsentId,
): Promise<void> {
await client.post(endpointForConsent(identityId, consentId), undefined);
}
export async function grantConsent(
client: HttpClient,
identityId: IdentityId,
consentId: ConsentId,
): Promise<void> {
await client.post(endpointForConsent(identityId, consentId), undefined, {
hooks: {
beforeRequest: [setExpectedStatus(204)],
},
});
}
export async function listGrantedConsents(
client: HttpClient,
identityId: IdentityId,
filter?: ListGrantedConsentsFilter & PageParams,
): Promise<Page<Consent>> {
const podPage = await client.get<Page<PODConsent>>(endpointForConsents(identityId), filter);
const results = podPage.results.map((podConsent) => new Consent(client, podConsent));
return {
results,
nextPageToken: podPage.nextPageToken,
};
}
export async function listGrantedConsents(
client: HttpClient,
identityId: IdentityId,
filter?: ListGrantedConsentsFilter & PageParams,
): Promise<Page<Consent>> {
const podPage = await client.get<Page<PODConsent>>(endpointForConsents(identityId), filter);
const results = podPage.results.map((podConsent) => new Consent(client, podConsent));
return {
results,
nextPageToken: podPage.nextPageToken,
};
}
export async function getGrantedConsent(
client: HttpClient,
identityId: IdentityId,
consentId: ConsentId,
): Promise<Consent> {
return client
.get<PODConsent>(endpointForConsent(identityId, consentId))
.then((podConsent) => new Consent(client, podConsent));
}
export async function getGrantedConsent(
client: HttpClient,
identityId: IdentityId,
consentId: ConsentId,
): Promise<Consent> {
return client
.get<PODConsent>(endpointForConsent(identityId, consentId))
.then((podConsent) => new Consent(client, podConsent));
}
export async function revokeConsent(
client: HttpClient,
identityId: IdentityId,
consentId: ConsentId,
): Promise<void> {
await client.delete(endpointForConsent(identityId, consentId));
}
export async function revokeConsent(
client: HttpClient,
identityId: IdentityId,
consentId: ConsentId,
): Promise<void> {
await client.delete(endpointForConsent(identityId, consentId));
}
}
export type IdentityCreateParams = IdentityUpdateParams & {
tokenVerifiers: IdentityTokenVerifierCreate[];
tokenVerifiers: IdentityTokenVerifierCreate[];
};

@@ -147,3 +150,3 @@ export type IdentityUpdateParams = Writable<Identity>;

export type IdentityTokenVerifier = IdentityTokenClaims & {
publicKey: PublicJWK;
publicKey: PublicJWK;
};

@@ -154,4 +157,4 @@

export type ListGrantedConsentsFilter = Partial<{
/** Only return consents granted to this app. */
app: AppId;
/** Only return consents granted to this app. */
app: AppId;
}>;

@@ -1,185 +0,258 @@

import { AppImpl } from './app';
import type { App, AppCreateParams, AppId, AppUpdateParams, ListAppsFilter } from './app';
import { ClientImpl } from './client';
import type { App, AppCreateParams, AppId, AppUpdateParams, ListAppsFilter } from './app.js';
import { AppImpl } from './app.js';
import type {
Client,
ClientCreateParams,
ClientId,
ClientUpdateParams,
ListClientsFilter,
} from './client';
import { ConsentImpl } from './consent';
import type { Consent, ConsentCreateParams, ConsentId } from './consent';
import { DatasetImpl } from './dataset';
Client,
ClientCreateParams,
ClientId,
ClientUpdateParams,
ListClientsFilter,
} from './client.js';
import { ClientImpl } from './client.js';
import type { Consent, ConsentCreateParams, ConsentId } from './consent.js';
import { ConsentImpl } from './consent.js';
import type {
Dataset,
DatasetId,
DatasetUpdateParams,
DatasetUploadParams,
ListDatasetsFilter,
Storable,
Upload,
} from './dataset';
import { GrantImpl } from './grant';
import type { Grant, GrantCreateParams, GrantId } from './grant';
import { HttpClient } from './http';
import type { Config as ClientConfig, Download } from './http';
import { IdentityImpl } from './identity';
import type { Identity, IdentityCreateParams, IdentityId, IdentityUpdateParams } from './identity';
import type { Page, PageParams } from './model';
import { TokenProvider } from './token';
import type { ClientCredentials, PrivateJWK, PublicJWK, TokenSource } from './token';
AccessEvent,
Dataset,
DatasetId,
DatasetUpdateParams,
DatasetUploadParams,
ListAccessLogFilter,
ListDatasetsFilter,
Storable,
Upload,
} from './dataset.js';
import { DatasetImpl } from './dataset.js';
import type { Job, JobId, JobSpec, JobStatus } from './compute.js';
import {
ComputeImpl,
InputDatasetSpec,
JobPhase,
OutputDataset,
OutputDatasetSpec,
} from './compute.js';
import type { Grant, GrantCreateParams, GrantId } from './grant.js';
import { GrantImpl, ListGrantsFilter } from './grant.js';
import type { Config as ClientConfig, Download } from './http.js';
import { ApiError, HttpClient } from './http.js';
import type {
Identity,
IdentityCreateParams,
IdentityId,
IdentityUpdateParams,
} from './identity.js';
import { IdentityImpl } from './identity.js';
import type { Page, PageParams } from './model.js';
import type { ClientCredentials, PrivateJWK, PublicJWK, TokenSource } from './token.js';
import { TokenProvider } from './token.js';
export {
App,
AppCreateParams,
AppId,
AppUpdateParams,
ClientCredentials,
Consent,
ConsentCreateParams,
ConsentId,
Dataset,
DatasetId,
DatasetUpdateParams,
DatasetUploadParams,
Grant,
GrantCreateParams,
GrantId,
Identity,
IdentityCreateParams,
IdentityId,
IdentityUpdateParams,
Page,
PageParams,
PrivateJWK,
PublicJWK,
Storable,
TokenSource,
AccessEvent,
App,
AppCreateParams,
AppId,
AppUpdateParams,
Client,
ClientCreateParams,
ClientCredentials,
ClientId,
Consent,
ConsentCreateParams,
ConsentId,
Dataset,
DatasetId,
DatasetUpdateParams,
DatasetUploadParams,
ApiError,
Grant,
GrantCreateParams,
GrantId,
Identity,
IdentityCreateParams,
IdentityId,
IdentityUpdateParams,
InputDatasetSpec,
Job,
JobId,
JobPhase,
JobSpec,
JobStatus,
OutputDataset,
OutputDatasetSpec,
Page,
PageParams,
PrivateJWK,
PublicJWK,
Storable,
TokenSource,
};
export default class Parcel {
private currentIdentity?: Identity;
private readonly client: HttpClient;
private currentIdentity?: Identity;
private readonly client: HttpClient;
public constructor(tokenSource: TokenSource, config?: Config) {
const tokenProvider = TokenProvider.fromSource(tokenSource);
this.client = new HttpClient(tokenProvider, {
apiUrl: config?.apiUrl,
httpClient: config?.httpClient,
});
}
public constructor(tokenSource: TokenSource, config?: Config) {
const tokenProvider = TokenProvider.fromSource(tokenSource);
this.client = new HttpClient(tokenProvider, {
apiUrl: config?.apiUrl,
httpClientConfig: config?.httpClientConfig,
});
}
public get apiUrl() {
return this.client.apiUrl;
}
public get apiUrl() {
return this.client.apiUrl;
}
public async createIdentity(params: IdentityCreateParams): Promise<Identity> {
return IdentityImpl.create(this.client, params);
public async createIdentity(params: IdentityCreateParams): Promise<Identity> {
return IdentityImpl.create(this.client, params);
}
public async getCurrentIdentity(): Promise<Identity> {
if (!this.currentIdentity) {
this.currentIdentity = await IdentityImpl.current(this.client);
}
public async getCurrentIdentity(): Promise<Identity> {
if (!this.currentIdentity) {
this.currentIdentity = await IdentityImpl.current(this.client);
}
return this.currentIdentity;
}
return this.currentIdentity;
}
public uploadDataset(data: Storable, params?: DatasetUploadParams): Upload {
return DatasetImpl.upload(this.client, data, params);
}
public uploadDataset(data: Storable, params?: DatasetUploadParams): Upload {
return DatasetImpl.upload(this.client, data, params);
}
public async getDataset(id: DatasetId): Promise<Dataset> {
return DatasetImpl.get(this.client, id);
}
public async getDataset(id: DatasetId): Promise<Dataset> {
return DatasetImpl.get(this.client, id);
}
public async listDatasets(filter?: ListDatasetsFilter & PageParams): Promise<Page<Dataset>> {
return DatasetImpl.list(this.client, filter);
}
public async listDatasets(filter?: ListDatasetsFilter & PageParams): Promise<Page<Dataset>> {
return DatasetImpl.list(this.client, filter);
}
public downloadDataset(id: DatasetId): Download {
return DatasetImpl.download(this.client, id);
}
public downloadDataset(id: DatasetId): Download {
return DatasetImpl.download(this.client, id);
}
public async getDatasetHistory(
id: DatasetId,
filter?: ListAccessLogFilter & PageParams,
): Promise<Page<AccessEvent>> {
return DatasetImpl.history(this.client, id, filter);
}
public async updateDataset(id: DatasetId, update: DatasetUpdateParams): Promise<Dataset> {
return DatasetImpl.update(this.client, id, update);
}
public async updateDataset(id: DatasetId, update: DatasetUpdateParams): Promise<Dataset> {
return DatasetImpl.update(this.client, id, update);
}
public async deleteDataset(id: DatasetId): Promise<void> {
return DatasetImpl.delete_(this.client, id);
}
public async deleteDataset(id: DatasetId): Promise<void> {
return DatasetImpl.delete_(this.client, id);
}
public async createApp(params: AppCreateParams): Promise<App> {
return AppImpl.create(this.client, params);
}
public async createApp(params: AppCreateParams): Promise<App> {
return AppImpl.create(this.client, params);
}
public async getApp(id: AppId): Promise<App> {
return AppImpl.get(this.client, id);
}
public async getApp(id: AppId): Promise<App> {
return AppImpl.get(this.client, id);
}
public async listApps(filter?: ListAppsFilter & PageParams): Promise<Page<App>> {
return AppImpl.list(this.client, filter);
}
public async listApps(filter?: ListAppsFilter & PageParams): Promise<Page<App>> {
return AppImpl.list(this.client, filter);
}
public async updateApp(id: AppId, update: AppUpdateParams): Promise<App> {
return AppImpl.update(this.client, id, update);
}
public async updateApp(id: AppId, update: AppUpdateParams): Promise<App> {
return AppImpl.update(this.client, id, update);
}
public async deleteApp(id: AppId): Promise<void> {
return AppImpl.delete_(this.client, id);
}
public async deleteApp(id: AppId): Promise<void> {
return AppImpl.delete_(this.client, id);
}
public async createConsent(appId: AppId, params: ConsentCreateParams): Promise<Consent> {
return ConsentImpl.create(this.client, appId, params);
}
public async createConsent(appId: AppId, params: ConsentCreateParams): Promise<Consent> {
return ConsentImpl.create(this.client, appId, params);
}
public async listConsents(appId: AppId, filter: PageParams): Promise<Page<Consent>> {
return ConsentImpl.list(this.client, appId, filter);
}
public async listConsents(appId: AppId, filter: PageParams): Promise<Page<Consent>> {
return ConsentImpl.list(this.client, appId, filter);
}
public async deleteConsent(appId: AppId, consentId: ConsentId): Promise<void> {
return ConsentImpl.delete_(this.client, appId, consentId);
}
public async deleteConsent(appId: AppId, consentId: ConsentId): Promise<void> {
return ConsentImpl.delete_(this.client, appId, consentId);
}
public async createClient(appId: AppId, params: ClientCreateParams): Promise<Client> {
return ClientImpl.create(this.client, appId, params);
}
public async createClient(appId: AppId, params: ClientCreateParams): Promise<Client> {
return ClientImpl.create(this.client, appId, params);
}
public async getClient(appId: AppId, clientId: ClientId): Promise<Client> {
return ClientImpl.get(this.client, appId, clientId);
}
public async getClient(appId: AppId, clientId: ClientId): Promise<Client> {
return ClientImpl.get(this.client, appId, clientId);
}
public async listClients(
appId: AppId,
filter?: ListClientsFilter & PageParams,
): Promise<Page<Client>> {
return ClientImpl.list(this.client, appId, filter);
}
public async listClients(
appId: AppId,
filter?: ListClientsFilter & PageParams,
): Promise<Page<Client>> {
return ClientImpl.list(this.client, appId, filter);
}
public async updateClient(
appId: AppId,
clientId: ClientId,
update: ClientUpdateParams,
): Promise<Client> {
return ClientImpl.update(this.client, appId, clientId, update);
}
public async updateClient(
appId: AppId,
clientId: ClientId,
update: ClientUpdateParams,
): Promise<Client> {
return ClientImpl.update(this.client, appId, clientId, update);
}
public async deleteClient(appId: AppId, clientId: ClientId): Promise<void> {
return ClientImpl.delete_(this.client, appId, clientId);
}
public async deleteClient(appId: AppId, clientId: ClientId): Promise<void> {
return ClientImpl.delete_(this.client, appId, clientId);
}
public async createGrant(params: GrantCreateParams): Promise<Grant> {
return GrantImpl.create(this.client, params);
}
public async createGrant(params: GrantCreateParams): Promise<Grant> {
return GrantImpl.create(this.client, params);
}
public async getGrant(id: GrantId): Promise<Grant> {
return GrantImpl.get(this.client, id);
}
public async getGrant(id: GrantId): Promise<Grant> {
return GrantImpl.get(this.client, id);
}
public async deleteGrant(id: GrantId): Promise<void> {
return GrantImpl.delete_(this.client, id);
}
public async listGrants(filter?: ListGrantsFilter & PageParams): Promise<Page<Grant>> {
return GrantImpl.list(this.client, filter);
}
public async deleteGrant(id: GrantId): Promise<void> {
return GrantImpl.delete_(this.client, id);
}
/**
* Enqueues a new job.
* @param spec Specification for the job to enqueue.
* @result Job The new job, including a newly-assigned ID.
*/
public async submitJob(spec: JobSpec): Promise<Job> {
return ComputeImpl.submitJob(this.client, spec);
}
/**
* Lists all known jobs owned by the current user. The dispatcher keeps track of jobs for at most 24h after they complete.
* @param filter Controls pagination.
* @result Job Lists known jobs. Includes recently completed jobs.
*/
public async listJobs(filter: PageParams = {}): Promise<Page<Job>> {
return ComputeImpl.listJobs(this.client, filter);
}
/**
* Returns the full description of a known job, including its status.
*/
public async getJob(jobId: JobId): Promise<Job> {
return ComputeImpl.getJob(this.client, jobId);
}
/**
* Schedules the job for eventual termination/deletion. The job will be terminated at some point in the future on a best-effort basis.
* It is not an error to request to terminate an already-terminated or non-existing job.
* @param jobId The unique identifier of the job.
*/
public async terminateJob(jobId: JobId): Promise<void> {
return ComputeImpl.terminateJob(this.client, jobId);
}
}
export type Config = ClientConfig;

@@ -1,19 +0,19 @@

import type { ConditionalExcept, Except, JsonValue } from 'type-fest';
import type { ConditionalExcept, Except, JsonObject, JsonValue } from 'type-fest';
export type ResourceId = string; // Format from runtime: `<resource>-<id>`
export interface PODModel {
/** An undifferentiated model identifier. */
id: ResourceId;
export interface PODModel extends JsonObject {
/** An undifferentiated model identifier. */
id: ResourceId;
/** The number of seconds since the Unix epoch when this model was created */
createdAt: string;
/** The number of seconds since the Unix epoch when this model was created */
createdAt: string;
}
export interface Model {
/** The model's unique ID. */
id: ResourceId;
/** The model's unique ID. */
id: ResourceId;
/** The number of seconds since the Unix epoch when this model was created */
createdAt: Date;
/** The number of seconds since the Unix epoch when this model was created */
createdAt: Date;
}

@@ -23,14 +23,14 @@

export type WritableExcluding<T extends Model, ReadOnly extends keyof T> = ConditionalExcept<
Except<T, 'id' | 'createdAt' | ReadOnly>,
(...args: any[]) => any
Except<T, 'id' | 'createdAt' | ReadOnly>,
(...args: any[]) => any
>;
export type Page<T = JsonValue> = {
results: T[];
nextPageToken: string;
results: T[];
nextPageToken: string;
};
export type PageParams = Partial<{
pageSize: number;
nextPageToken: string;
pageSize: number;
nextPageToken: string;
}>;

@@ -1,41 +0,45 @@

import axios, { AxiosResponse } from 'axios';
import { KEYUTIL, KJUR } from 'jsrsasign';
import type { ResponsePromise } from 'ky';
import ky from 'ky';
import type { Except, JsonObject } from 'type-fest';
import './polyfill.js'; // eslint-disable-line import/no-unassigned-import
export const PARCEL_RUNTIME_AUD = 'https://api.oasislabs.com/parcel'; // TODO(#326)
export abstract class TokenProvider {
public static fromSource(source: TokenSource): TokenProvider {
if (typeof source === 'string') return new StaticTokenProvider(source);
if ('principal' in source) return new SelfIssuedTokenProvider(source);
if ('refreshToken' in source) return new RefreshingTokenProvider(source);
if ('clientId' in source) return new RenewingTokenProvider(source);
throw new Error(`unrecognized \`tokenSource\`: ${JSON.stringify(source)}`);
}
public static fromSource(source: TokenSource): TokenProvider {
if (typeof source === 'string') return new StaticTokenProvider(source);
if ('principal' in source) return new SelfIssuedTokenProvider(source);
if ('refreshToken' in source) return new RefreshingTokenProvider(source);
if ('clientId' in source) return new RenewingTokenProvider(source);
throw new Error(`unrecognized \`tokenSource\`: ${JSON.stringify(source)}`);
}
/** Returns a valid Bearer token to be presented to the Parcel gateway. */
public abstract getToken(): Promise<string>;
/** Returns a valid Bearer token to be presented to the Parcel gateway. */
public abstract getToken(): Promise<string>;
}
export type TokenSource =
| string
| RenewingTokenProviderParams
| RefreshingTokenProviderParams
| SelfIssuedTokenProviderParams;
| string
| RenewingTokenProviderParams
| RefreshingTokenProviderParams
| SelfIssuedTokenProviderParams;
/** A `TokenProvider` hands out OIDC access tokens. */
export abstract class ExpiringTokenProvider implements TokenProvider {
protected token?: Token;
protected token?: Token;
public static isTokenProvider(thing: any): thing is TokenProvider {
return thing && typeof thing.getToken === 'function';
}
public static isTokenProvider(thing: any): thing is TokenProvider {
return thing && typeof thing.getToken === 'function';
}
/** Returns a valid Bearer token to be presented to the Parcel gateway. */
public async getToken(): Promise<string> {
if (this.token === undefined || this.token.isExpired())
this.token = await this.renewToken();
return this.token.toString();
}
/** Returns a valid Bearer token to be presented to the Parcel gateway. */
public async getToken(): Promise<string> {
if (this.token === undefined || this.token.isExpired()) this.token = await this.renewToken();
return this.token.toString();
}
/** Returns a renewed `Token`. */
protected abstract renewToken(): Promise<Token>;
/** Returns a renewed `Token`. */
protected abstract renewToken(): Promise<Token>;
}

@@ -45,26 +49,26 @@

export class StaticTokenProvider implements TokenProvider {
public constructor(private readonly token: string) {}
public constructor(private readonly token: string) {}
public async getToken(): Promise<string> {
return this.token;
}
public async getToken(): Promise<string> {
return this.token;
}
}
export type RenewingTokenProviderParams = {
clientId: string;
clientId: string;
privateKey: PrivateJWK;
privateKey: PrivateJWK;
/**
* The identity provider's OAuth token retrieval endpoint.
* If left undefined, the access token will be self-signed with the `clientId`
* as the issuer.
*/
tokenEndpoint: string;
/**
* The identity provider's OAuth token retrieval endpoint.
* If left undefined, the access token will be self-signed with the `clientId`
* as the issuer.
*/
tokenEndpoint: string;
/**
* A list of scopes that will be requested from the identity provider, which
* may be different from the scopes that the identity provider actually returns.
*/
scopes: string[];
/**
* A list of scopes that will be requested from the identity provider, which
* may be different from the scopes that the identity provider actually returns.
*/
scopes: string[];
};

@@ -74,56 +78,56 @@

export class RenewingTokenProvider extends ExpiringTokenProvider {
private readonly clientId: string;
private readonly tokenEndpoint: string;
private readonly scopes: string[];
private readonly privateKey: string; // PKCS8-encoded
private readonly keyId: string;
private readonly clientId: string;
private readonly tokenEndpoint: string;
private readonly scopes: string[];
private readonly privateKey: string; // PKCS8-encoded
private readonly keyId: string;
private readonly clientAssertionLifetime = 1 * 60 * 60; // 1 hour
private readonly clientAssertionLifetime = 1 * 60 * 60; // 1 hour
public constructor({
clientId,
privateKey: privateJWK,
scopes,
tokenEndpoint,
}: RenewingTokenProviderParams) {
super();
public constructor({
clientId,
privateKey: privateJWK,
scopes,
tokenEndpoint,
}: RenewingTokenProviderParams) {
super();
const { privateKey, keyId } = jwkToPem(privateJWK);
this.privateKey = privateKey;
this.keyId = keyId;
const { privateKey, keyId } = jwkToPem(privateJWK);
this.privateKey = privateKey;
this.keyId = keyId;
this.clientId = clientId;
this.tokenEndpoint = tokenEndpoint;
this.scopes = scopes;
}
this.clientId = clientId;
this.tokenEndpoint = tokenEndpoint;
this.scopes = scopes;
}
protected async renewToken(): Promise<Token> {
const clientAssertion = makeJWT({
privateKey: this.privateKey,
keyId: this.keyId,
payload: {
sub: this.clientId,
iss: this.clientId,
aud: this.tokenEndpoint,
jti: KJUR.crypto.Util.getRandomHexOfNbytes(8),
},
lifetime: this.clientAssertionLifetime,
});
protected async renewToken(): Promise<Token> {
const clientAssertion = makeJWT({
privateKey: this.privateKey,
keyId: this.keyId,
payload: {
sub: this.clientId,
iss: this.clientId,
aud: this.tokenEndpoint,
jti: KJUR.crypto.Util.getRandomHexOfNbytes(8),
},
lifetime: this.clientAssertionLifetime,
});
const authParams = new URLSearchParams();
authParams.append('grant_type', 'client_credentials');
authParams.append('client_assertion', clientAssertion);
authParams.append(
'client_assertion_type',
'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
);
authParams.append('scope', this.scopes.join(' '));
const authParams = new URLSearchParams();
authParams.append('grant_type', 'client_credentials');
authParams.append('client_assertion', clientAssertion);
authParams.append(
'client_assertion_type',
'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
);
authParams.append('scope', this.scopes.join(' '));
return Token.fromResponse(axios.post(this.tokenEndpoint, authParams));
}
return Token.fromResponse(ky.post(this.tokenEndpoint, { body: authParams }));
}
}
export type RefreshingTokenProviderParams = {
refreshToken: string;
tokenEndpoint: string;
refreshToken: string;
tokenEndpoint: string;
};

@@ -133,43 +137,46 @@

export class RefreshingTokenProvider extends ExpiringTokenProvider {
private refreshToken: string;
private readonly tokenEndpoint: string;
private refreshToken: string;
private readonly tokenEndpoint: string;
public constructor({ refreshToken, tokenEndpoint }: RefreshingTokenProviderParams) {
super();
this.refreshToken = refreshToken;
this.tokenEndpoint = tokenEndpoint;
}
public constructor({ refreshToken, tokenEndpoint }: RefreshingTokenProviderParams) {
super();
this.refreshToken = refreshToken;
this.tokenEndpoint = tokenEndpoint;
}
protected async renewToken(): Promise<Token> {
const refreshParams = new URLSearchParams();
refreshParams.append('grant_type', 'refresh_token');
refreshParams.append('refresh_token', this.refreshToken);
protected async renewToken(): Promise<Token> {
const refreshParams = new URLSearchParams();
refreshParams.append('grant_type', 'refresh_token');
refreshParams.append('refresh_token', this.refreshToken);
return Token.fromResponse(
axios.post(this.tokenEndpoint, refreshParams).then((refreshResponse) => {
this.refreshToken = refreshResponse.data.refresh_token;
return refreshResponse;
}),
);
}
const res = ky.post(this.tokenEndpoint, { body: refreshParams });
res
.then(async (refreshResponse) => {
this.refreshToken = (await refreshResponse.clone().json()).refresh_token;
})
.catch(() => {
// Do nothing. The promise lives on.
});
return Token.fromResponse(res);
}
}
export type SelfIssuedTokenProviderParams = {
/** The `sub` and `iss` claims of the provided access token. */
principal: string;
/** The `sub` and `iss` claims of the provided access token. */
principal: string;
/** The private key that will be used to sign the access token. */
privateKey: PrivateJWK;
/** The private key that will be used to sign the access token. */
privateKey: PrivateJWK;
/**
* A list of scopes that will be added as claims.
* The default is all scopes.
*/
scopes?: string[];
/**
* A list of scopes that will be added as claims.
* The default is all scopes.
*/
scopes?: string[];
/**
* Duration for which the issued token is valid, in seconds.
* Defaults to one hour;
*/
tokenLifetime?: number;
/**
* Duration for which the issued token is valid, in seconds.
* Defaults to one hour;
*/
tokenLifetime?: number;
};

@@ -179,74 +186,78 @@

export class SelfIssuedTokenProvider extends ExpiringTokenProvider {
private readonly principal: string;
private readonly privateKey: string;
private readonly keyId: string;
private readonly scopes: string[];
private readonly tokenLifetime: number;
private readonly principal: string;
private readonly privateKey: string;
private readonly keyId: string;
private readonly scopes: string[];
private readonly tokenLifetime: number;
public constructor({
principal,
privateKey: privateJWK,
scopes,
tokenLifetime,
}: SelfIssuedTokenProviderParams) {
super();
public constructor({
principal,
privateKey: privateJWK,
scopes,
tokenLifetime,
}: SelfIssuedTokenProviderParams) {
super();
const { privateKey, keyId } = jwkToPem(privateJWK);
this.privateKey = privateKey;
this.keyId = keyId;
const { privateKey, keyId } = jwkToPem(privateJWK);
this.privateKey = privateKey;
this.keyId = keyId;
this.principal = principal;
this.scopes = scopes ?? ['parcel.*'];
this.tokenLifetime = tokenLifetime ?? 1 * 60 * 60 /* one hour */;
}
this.principal = principal;
this.scopes = scopes ?? ['parcel.*'];
this.tokenLifetime = tokenLifetime ?? 1 * 60 * 60 /* one hour */;
}
protected async renewToken(): Promise<Token> {
const expiry = Date.now() / 1000 + this.tokenLifetime;
const token = makeJWT({
privateKey: this.privateKey,
keyId: this.keyId,
payload: {
sub: this.principal,
iss: this.principal,
aud: 'parcel-runtime',
scope: this.scopes,
},
lifetime: this.tokenLifetime,
});
return new Token(token, expiry);
}
protected async renewToken(): Promise<Token> {
const expiry = Date.now() / 1000 + this.tokenLifetime;
const token = makeJWT({
privateKey: this.privateKey,
keyId: this.keyId,
payload: {
sub: this.principal,
iss: this.principal,
aud: PARCEL_RUNTIME_AUD,
scope: this.scopes,
},
lifetime: this.tokenLifetime,
});
return new Token(token, expiry);
}
}
class Token {
public constructor(private readonly token: string, private readonly expiry: number) {}
public constructor(private readonly token: string, private readonly expiry: number) {}
public static async fromResponse(response: Promise<AxiosResponse>): Promise<Token> {
const requestTime = Date.now();
const body = (await response).data;
return new Token(body.access_token, requestTime + body.expires_in * 1000);
}
public static async fromResponse(response: ResponsePromise): Promise<Token> {
const requestTime = Date.now();
const body: {
access_token: string;
request_time: number;
expires_in: number;
} = await response.json();
return new Token(body.access_token, requestTime + body.expires_in * 1000);
}
public isExpired(): boolean {
return this.expiry <= Date.now();
}
public isExpired(): boolean {
return this.expiry <= Date.now();
}
public toString(): string {
return this.token;
}
public toString(): string {
return this.token;
}
}
type BaseJWK = {
kty: string;
alg: string;
use?: 'sig';
kid?: string;
kty: string;
alg: string;
use?: 'sig';
kid?: string;
};
export type PrivateES256JWK = BaseJWK & {
kty: 'EC';
alg: 'ES256';
crv: 'P-256';
x: string;
y: string;
d: string;
kty: 'EC';
alg: 'ES256';
crv: 'P-256';
x: string;
y: string;
d: string;
};

@@ -259,11 +270,11 @@ export type PublicES256JWK = Except<PrivateES256JWK, 'd'>;

export type IdentityTokenClaims = {
/** The token's subject. */
sub: string;
/** The token's subject. */
sub: string;
/** The token's issuer. */
iss: string;
/** The token's issuer. */
iss: string;
};
export type ClientCredentials = IdentityTokenClaims & {
privateKey: PrivateJWK;
privateKey: PrivateJWK;
};

@@ -273,42 +284,40 @@

function jwkToPem(jwk: PrivateJWK): { privateKey: string; keyId: string } {
if (jwk.kty !== 'EC' || jwk.alg !== 'ES256') {
throw new Error(
`Unsupported private key. Expected \`alg: 'ES256'\` but was \`${jwk.alg}\` }`,
);
}
if (jwk.kty !== 'EC' || jwk.alg !== 'ES256') {
throw new Error(`Unsupported private key. Expected \`alg: 'ES256'\` but was \`${jwk.alg}\` }`);
}
const kjurJWK = JSON.parse(JSON.stringify(jwk));
const keyId = jwk.kid ?? KJUR.jws.JWS.getJWKthumbprint(kjurJWK);
kjurJWK.crv = 'secp256r1'; // KJUR's preferred name for name for P-256
const privateKey = (KEYUTIL.getPEM(KEYUTIL.getKey(kjurJWK), 'PKCS8PRV') as unknown) as string; // The type definitions are wrong: they say `void` but it's actually `string`.
return {
privateKey,
keyId,
};
const kjurJWK = JSON.parse(JSON.stringify(jwk));
const keyId = jwk.kid ?? KJUR.jws.JWS.getJWKthumbprint(kjurJWK);
kjurJWK.crv = 'secp256r1'; // KJUR's preferred name for name for P-256
const privateKey = (KEYUTIL.getPEM(KEYUTIL.getKey(kjurJWK), 'PKCS8PRV') as unknown) as string; // The type definitions are wrong: they say `void` but it's actually `string`.
return {
privateKey,
keyId,
};
}
function makeJWT({
privateKey,
keyId,
payload,
lifetime,
privateKey,
keyId,
payload,
lifetime,
}: {
/** PKCS8 (PEM)-encoded private key */
privateKey: string;
keyId: string;
payload: JsonObject;
/** The token's lifetime in seconds. */
lifetime: number;
/** PKCS8 (PEM)-encoded private key */
privateKey: string;
keyId: string;
payload: JsonObject;
/** The token's lifetime in seconds. */
lifetime: number;
}): string {
const header = {
alg: 'ES256',
typ: 'JWT',
kid: keyId,
};
const header = {
alg: 'ES256',
typ: 'JWT',
kid: keyId,
};
const now = Math.floor(Date.now() / 1000);
payload.iat = now;
payload.exp = now + lifetime;
const now = Math.floor(Date.now() / 1000);
payload.iat = now;
payload.exp = now + lifetime;
return KJUR.jws.JWS.sign(null, header, payload, privateKey);
return KJUR.jws.JWS.sign(null, header, payload, privateKey);
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc