@oasislabs/parcel
Advanced tools
Comparing version 0.1.6 to 0.1.7-manual.1
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%" | ||
} |
323
src/app.ts
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>; |
122
src/grant.ts
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; | ||
}>; |
402
src/http.ts
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; | ||
}>; |
367
src/index.ts
@@ -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; | ||
}>; |
433
src/token.ts
@@ -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); | ||
} |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
180892
54
3707
Yes
10
3
+ Addedabort-controller@^3.0.0
+ Addedky@^0.26.0
+ Addednode-fetch@^2.6.1
+ Addedweb-streams-polyfill@^3.0.1
+ Added@types/node@22.12.0(transitive)
+ Addedabort-controller@3.0.0(transitive)
+ Addedevent-target-shim@5.0.1(transitive)
+ Addedky@0.26.0(transitive)
+ Addednode-fetch@2.7.0(transitive)
+ Addedtr46@0.0.3(transitive)
+ Addedtype-fest@0.20.2(transitive)
+ Addedweb-streams-polyfill@3.3.3(transitive)
+ Addedwebidl-conversions@3.0.1(transitive)
+ Addedwhatwg-url@5.0.0(transitive)
- Removedaxios@^0.19.2
- Removedreadable-stream@^3.6.0
- Removed@types/node@22.13.0(transitive)
- Removedaxios@0.19.2(transitive)
- Removedfollow-redirects@1.5.10(transitive)
- Removedinherits@2.0.4(transitive)
- Removedreadable-stream@3.6.2(transitive)
- Removedsafe-buffer@5.2.1(transitive)
- Removedstring_decoder@1.3.0(transitive)
- Removedtype-fest@0.17.0(transitive)
- Removedutil-deprecate@1.0.2(transitive)
Updatedtype-fest@^0.20.0