parent
21d631c406
commit
fa141fed5d
File diff suppressed because it is too large
Load Diff
@ -1,158 +0,0 @@ |
|||||||
import type { LeafUri } from "./uriTypes"; |
|
||||||
import type { PresentLeaf } from "./resource/abstract/leaf/PresentLeaf"; |
|
||||||
import type { DataLeaf } from "./resource/abstract/leaf/DataLeaf"; |
|
||||||
import type { ResourceError } from "./resource/error/ResourceError"; |
|
||||||
import type { BinaryLeaf } from "./resource/abstract/leaf/BinaryLeaf"; |
|
||||||
import type { DatasetChanges } from "@ldo/rdf-utils"; |
|
||||||
import type { AbsentLeaf } from "./resource/abstract/leaf/AbsentLeaf"; |
|
||||||
|
|
||||||
export interface WaitingProcess<Args extends unknown[], Return> { |
|
||||||
name: string; |
|
||||||
args: Args; |
|
||||||
perform: (...args: Args) => Promise<Return>; |
|
||||||
awaitingResolutions: ((returnValue: Return) => void)[]; |
|
||||||
awaitingRejections: ((err: unknown) => void)[]; |
|
||||||
} |
|
||||||
|
|
||||||
export interface WaitingProcessOptions<Args extends unknown[], Return> { |
|
||||||
name: string; |
|
||||||
args: Args; |
|
||||||
perform: (...args: Args) => Promise<Return>; |
|
||||||
modifyLastProcess: ( |
|
||||||
lastProcess: WaitingProcess<unknown[], unknown>, |
|
||||||
args: Args, |
|
||||||
) => boolean; |
|
||||||
} |
|
||||||
|
|
||||||
export abstract class LeafRequestBatcher { |
|
||||||
private lastRequestTimestampMap: Record<string, number> = {}; |
|
||||||
private isWaiting: boolean = false; |
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
private processQueue: WaitingProcess<any[], any>[] = []; |
|
||||||
private shouldBatchAllRequests: boolean; |
|
||||||
private batchMilis: number; |
|
||||||
|
|
||||||
private triggerOrWaitProcess() { |
|
||||||
const processName = this.shouldBatchAllRequests |
|
||||||
? "any" |
|
||||||
: this.processQueue[0].name; |
|
||||||
|
|
||||||
// Set last request timestamp if not available
|
|
||||||
if (!this.lastRequestTimestampMap[processName]) { |
|
||||||
this.lastRequestTimestampMap[processName] = Date.UTC(0, 0, 0, 0, 0, 0, 0); |
|
||||||
} |
|
||||||
|
|
||||||
const lastRequestTimestamp = this.lastRequestTimestampMap[processName]; |
|
||||||
const timeSinceLastTrigger = Date.now() - lastRequestTimestamp; |
|
||||||
|
|
||||||
const triggerProcess = async () => { |
|
||||||
this.isWaiting = false; |
|
||||||
this.lastRequestTimestampMap[processName] = Date.now(); |
|
||||||
this.lastRequestTimestampMap["any"] = Date.now(); |
|
||||||
const processToTrigger = this.processQueue.shift(); |
|
||||||
if (processToTrigger) { |
|
||||||
try { |
|
||||||
const returnValue = await processToTrigger.perform( |
|
||||||
processToTrigger.args, |
|
||||||
); |
|
||||||
processToTrigger.awaitingResolutions.forEach((callback) => { |
|
||||||
callback(returnValue); |
|
||||||
}); |
|
||||||
} catch (err) { |
|
||||||
processToTrigger.awaitingRejections.forEach((callback) => { |
|
||||||
callback(err); |
|
||||||
}); |
|
||||||
} |
|
||||||
this.triggerOrWaitProcess(); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
if (timeSinceLastTrigger < this.batchMilis && !this.isWaiting) { |
|
||||||
this.isWaiting = true; |
|
||||||
setTimeout(triggerProcess, this.batchMilis - timeSinceLastTrigger); |
|
||||||
} else { |
|
||||||
triggerProcess(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
protected async queueProcess<ReturnType>( |
|
||||||
options: WaitingProcessOptions<unknown[], ReturnType>, |
|
||||||
): Promise<ReturnType> { |
|
||||||
return new Promise((resolve, reject) => { |
|
||||||
const lastProcessInQueue = |
|
||||||
this.processQueue[this.processQueue.length - 1]; |
|
||||||
if (lastProcessInQueue) { |
|
||||||
const didModifyLast = lastProcessInQueue |
|
||||||
? options.modifyLastProcess(lastProcessInQueue, options.args) |
|
||||||
: false; |
|
||||||
if (didModifyLast) { |
|
||||||
lastProcessInQueue.awaitingResolutions.push(resolve); |
|
||||||
lastProcessInQueue.awaitingRejections.push(reject); |
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
this.processQueue.push({ |
|
||||||
name: options.name, |
|
||||||
args: options.args, |
|
||||||
perform: options.perform, |
|
||||||
awaitingResolutions: [resolve], |
|
||||||
awaitingRejections: [reject], |
|
||||||
}); |
|
||||||
this.triggerOrWaitProcess(); |
|
||||||
|
|
||||||
// const READ_KEY = "read";
|
|
||||||
// const lastProcessInQueue =
|
|
||||||
// this.processQueue[this.processQueue.length - 1];
|
|
||||||
// if (lastProcessInQueue?.name === READ_KEY) {
|
|
||||||
// lastProcessInQueue.awaitingResolutions.push(resolve);
|
|
||||||
// lastProcessInQueue.awaitingRejections.push(reject);
|
|
||||||
// } else {
|
|
||||||
// const readProcess: WaitingProcess<[], PresentLeaf | ResourceError> = {
|
|
||||||
// name: READ_KEY,
|
|
||||||
// args: [],
|
|
||||||
// perform: this.performRead,
|
|
||||||
// awaitingResolutions: [resolve],
|
|
||||||
// awaitingRejections: [reject],
|
|
||||||
// };
|
|
||||||
// this.processQueue.push(readProcess);
|
|
||||||
// this.triggerOrWaitProcess();
|
|
||||||
// }
|
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
// All intance variables
|
|
||||||
uri: LeafUri; |
|
||||||
|
|
||||||
// Read Methods
|
|
||||||
read(): Promise<PresentLeaf | ResourceError> { |
|
||||||
const READ_KEY = "read"; |
|
||||||
return this.queueProcess({ |
|
||||||
name: READ_KEY, |
|
||||||
args: [], |
|
||||||
perform: this.performRead, |
|
||||||
modifyLastProcess: (last) => { |
|
||||||
return last.name === READ_KEY; |
|
||||||
}, |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
private performRead(): Promise<PresentLeaf | ResourceError> { |
|
||||||
console.log("Reading"); |
|
||||||
throw new Error("Doing Read"); |
|
||||||
} |
|
||||||
|
|
||||||
// Create Methods
|
|
||||||
abstract createData(overwrite?: boolean): Promise<DataLeaf | ResourceError>; |
|
||||||
|
|
||||||
abstract upload( |
|
||||||
blob: Blob, |
|
||||||
mimeType: string, |
|
||||||
overwrite?: boolean, |
|
||||||
): Promise<BinaryLeaf | ResourceError>; |
|
||||||
|
|
||||||
abstract updateData( |
|
||||||
changes: DatasetChanges, |
|
||||||
): Promise<DataLeaf | ResourceError>; |
|
||||||
|
|
||||||
abstract delete(): Promise<AbsentLeaf | ResourceError>; |
|
||||||
} |
|
@ -1,9 +0,0 @@ |
|||||||
interface loadingStatus; |
|
||||||
|
|
||||||
export class LoadingManager { |
|
||||||
private loadingStatus: Record<string, Array<[key: string, promise: Promise<unknown>]>> |
|
||||||
|
|
||||||
public registerProcess(): void { |
|
||||||
|
|
||||||
} |
|
||||||
} |
|
@ -1,15 +1,15 @@ |
|||||||
import type TypedEmitter from "typed-emitter"; |
// import type TypedEmitter from "typed-emitter";
|
||||||
import type { SolidLdoDataset } from "./SolidLdoDataset"; |
import type { SolidLdoDataset } from "./SolidLdoDataset"; |
||||||
import type { DocumentError } from "./document/errors/DocumentError"; |
// import type { DocumentError } from "./document/errors/DocumentError";
|
||||||
|
|
||||||
export type OnDocumentErrorCallback = (error: DocumentError) => void; |
// export type OnDocumentErrorCallback = (error: DocumentError) => void;
|
||||||
|
|
||||||
export type DocumentEventEmitter = TypedEmitter<{ |
// export type DocumentEventEmitter = TypedEmitter<{
|
||||||
documentError: OnDocumentErrorCallback; |
// documentError: OnDocumentErrorCallback;
|
||||||
}>; |
// }>;
|
||||||
|
|
||||||
export interface SolidLdoDatasetContext { |
export interface SolidLdoDatasetContext { |
||||||
solidLdoDataset: SolidLdoDataset; |
solidLdoDataset: SolidLdoDataset; |
||||||
documentEventEmitter: DocumentEventEmitter; |
// documentEventEmitter: DocumentEventEmitter;
|
||||||
fetch: typeof fetch; |
fetch: typeof fetch; |
||||||
} |
} |
||||||
|
@ -0,0 +1,125 @@ |
|||||||
|
import type { LeafUri } from "../uriTypes"; |
||||||
|
import { RequestBatcher } from "../util/RequestBatcher"; |
||||||
|
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; |
||||||
|
import { AbsentResult } from "./requesterResults/AbsentResult"; |
||||||
|
import { |
||||||
|
DataResult, |
||||||
|
TurtleFormattingError, |
||||||
|
} from "./requesterResults/DataResult"; |
||||||
|
import { BinaryResult } from "./requesterResults/BinaryResult"; |
||||||
|
import { |
||||||
|
HttpErrorResult, |
||||||
|
ServerHttpError, |
||||||
|
UnauthenticatedHttpError, |
||||||
|
UnexpectedHttpError, |
||||||
|
} from "./requesterResults/HttpErrorResult"; |
||||||
|
import { UnexpectedError } from "./requesterResults/ErrorResult"; |
||||||
|
import { parseRdf } from "@ldo/ldo"; |
||||||
|
import { namedNode } from "@rdfjs/data-model"; |
||||||
|
|
||||||
|
export type ReadResult = |
||||||
|
| AbsentResult |
||||||
|
| DataResult |
||||||
|
| BinaryResult |
||||||
|
| ServerHttpError |
||||||
|
| UnauthenticatedHttpError |
||||||
|
| UnexpectedHttpError |
||||||
|
| UnexpectedError |
||||||
|
| TurtleFormattingError; |
||||||
|
|
||||||
|
export class LeafRequester { |
||||||
|
private requestBatcher = new RequestBatcher(); |
||||||
|
|
||||||
|
// All intance variables
|
||||||
|
readonly uri: LeafUri; |
||||||
|
private context: SolidLdoDatasetContext; |
||||||
|
|
||||||
|
constructor(uri: LeafUri, context: SolidLdoDatasetContext) { |
||||||
|
this.uri = uri; |
||||||
|
this.context = context; |
||||||
|
} |
||||||
|
|
||||||
|
// Read Methods
|
||||||
|
read(): Promise<ReadResult> { |
||||||
|
const READ_KEY = "read"; |
||||||
|
return this.requestBatcher.queueProcess({ |
||||||
|
name: READ_KEY, |
||||||
|
args: [], |
||||||
|
perform: this.performRead.bind(this), |
||||||
|
modifyQueue: (queue, isLoading) => { |
||||||
|
if (queue.length === 0) { |
||||||
|
return isLoading[READ_KEY]; |
||||||
|
} else { |
||||||
|
return queue[queue.length - 1].name === READ_KEY; |
||||||
|
} |
||||||
|
}, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private async performRead(): Promise<ReadResult> { |
||||||
|
try { |
||||||
|
// Fetch options to determine the document type
|
||||||
|
const response = await this.context.fetch(this.uri); |
||||||
|
if (AbsentResult.is(response)) { |
||||||
|
return new AbsentResult(this.uri); |
||||||
|
} |
||||||
|
if (ServerHttpError.is(response)) { |
||||||
|
return new ServerHttpError(this.uri, response); |
||||||
|
} |
||||||
|
if (UnauthenticatedHttpError.is(response)) { |
||||||
|
return new UnauthenticatedHttpError(this.uri, response); |
||||||
|
} |
||||||
|
if (HttpErrorResult.isnt(response)) { |
||||||
|
return new UnexpectedHttpError(this.uri, response); |
||||||
|
} |
||||||
|
|
||||||
|
if (DataResult.is(response)) { |
||||||
|
// Parse Turtle
|
||||||
|
const rawTurtle = await response.text(); |
||||||
|
let loadedDataset; |
||||||
|
try { |
||||||
|
loadedDataset = await parseRdf(rawTurtle, { |
||||||
|
baseIRI: this.uri, |
||||||
|
}); |
||||||
|
} catch (err) { |
||||||
|
return new TurtleFormattingError( |
||||||
|
this.uri, |
||||||
|
err instanceof Error ? err.message : "Failed to parse rdf", |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// Start transaction
|
||||||
|
const transactionalDataset = |
||||||
|
this.context.solidLdoDataset.startTransaction(); |
||||||
|
const graphNode = namedNode(this.uri); |
||||||
|
// Destroy all triples that were once a part of this resouce
|
||||||
|
loadedDataset.deleteMatches(undefined, undefined, undefined, graphNode); |
||||||
|
// Add the triples from the fetched item
|
||||||
|
transactionalDataset.addAll(loadedDataset); |
||||||
|
transactionalDataset.commit(); |
||||||
|
return new DataResult(this.uri); |
||||||
|
} else { |
||||||
|
// Load Blob
|
||||||
|
const blob = await response.blob(); |
||||||
|
return new BinaryResult(this.uri, blob); |
||||||
|
} |
||||||
|
} catch (err) { |
||||||
|
return UnexpectedError.fromThrown(this.uri, err); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// // Create Methods
|
||||||
|
// abstract createDataResource(overwrite?: boolean): Promise<DataLeaf | ResourceError>;
|
||||||
|
|
||||||
|
// abstract upload(
|
||||||
|
// blob: Blob,
|
||||||
|
// mimeType: string,
|
||||||
|
// overwrite?: boolean,
|
||||||
|
// ): Promise<BinaryLeaf | ResourceError>;
|
||||||
|
|
||||||
|
// abstract updateData(
|
||||||
|
// changes: DatasetChanges,
|
||||||
|
// ): Promise<DataLeaf | ResourceError>;
|
||||||
|
|
||||||
|
// abstract delete(): Promise<AbsentLeaf | ResourceError>;
|
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
import { RequesterResult } from "./RequesterResult"; |
||||||
|
|
||||||
|
export class AbsentResult extends RequesterResult { |
||||||
|
type = "absent" as const; |
||||||
|
|
||||||
|
static is(response: Response): boolean { |
||||||
|
return response.status === 404; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
import { RequesterResult } from "./RequesterResult"; |
||||||
|
|
||||||
|
export class BinaryResult extends RequesterResult { |
||||||
|
type = "binary" as const; |
||||||
|
readonly blob: Blob; |
||||||
|
|
||||||
|
constructor(uri: string, blob: Blob) { |
||||||
|
super(uri); |
||||||
|
this.blob = blob; |
||||||
|
} |
||||||
|
|
||||||
|
static is(response: Response): boolean { |
||||||
|
const contentType = response.headers.get("content-type"); |
||||||
|
return !contentType || contentType !== "text/turtle"; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
import { ErrorResult } from "./ErrorResult"; |
||||||
|
import { RequesterResult } from "./RequesterResult"; |
||||||
|
|
||||||
|
export class DataResult extends RequesterResult { |
||||||
|
type = "data" as const; |
||||||
|
|
||||||
|
static is(response: Response): boolean { |
||||||
|
const contentType = response.headers.get("content-type"); |
||||||
|
return !!contentType && contentType === "text/turtle"; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class TurtleFormattingError extends ErrorResult { |
||||||
|
errorType = "turtleFormatting"; |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
export abstract class ErrorResult extends Error { |
||||||
|
readonly type = "error" as const; |
||||||
|
readonly uri: string; |
||||||
|
abstract readonly errorType: string; |
||||||
|
|
||||||
|
constructor(uri: string, message?: string) { |
||||||
|
super(message || "An error unkown error was encountered during a request."); |
||||||
|
this.uri = uri; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class UnexpectedError extends ErrorResult { |
||||||
|
error: Error; |
||||||
|
readonly errorType = "unexpected"; |
||||||
|
|
||||||
|
constructor(uri: string, error: Error) { |
||||||
|
super(uri, error.message); |
||||||
|
this.error = error; |
||||||
|
} |
||||||
|
|
||||||
|
static fromThrown(uri: string, err: unknown) { |
||||||
|
if (err instanceof Error) { |
||||||
|
return new UnexpectedError(uri, err); |
||||||
|
} else if (typeof err === "string") { |
||||||
|
return new UnexpectedError(uri, new Error(err)); |
||||||
|
} else { |
||||||
|
return new UnexpectedError( |
||||||
|
uri, |
||||||
|
new Error(`Error of type ${typeof err} thrown: ${err}`), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
import { ErrorResult } from "./ErrorResult"; |
||||||
|
|
||||||
|
export abstract class HttpErrorResult extends ErrorResult { |
||||||
|
public readonly status: number; |
||||||
|
public readonly headers: Headers; |
||||||
|
public readonly response: Response; |
||||||
|
|
||||||
|
constructor(uri: string, response: Response, message?: string) { |
||||||
|
super( |
||||||
|
uri, |
||||||
|
message || |
||||||
|
`Request for ${uri} returned ${response.status} (${response.statusText}).`, |
||||||
|
); |
||||||
|
this.status = response.status; |
||||||
|
this.headers = response.headers; |
||||||
|
this.response = response; |
||||||
|
} |
||||||
|
|
||||||
|
async getBodyForDebug(): Promise<string> { |
||||||
|
if (this.response.bodyUsed) { |
||||||
|
return `Could not get body for ${this.uri} that yeilded status ${this.status}. The body stream has already been consumed.`; |
||||||
|
} |
||||||
|
return await this.response.text(); |
||||||
|
} |
||||||
|
|
||||||
|
static isnt(response: Response) { |
||||||
|
return response.status < 200 || response.status >= 300; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class UnexpectedHttpError extends HttpErrorResult { |
||||||
|
errorType = "unexpectedHttp" as const; |
||||||
|
} |
||||||
|
|
||||||
|
export class UnauthenticatedHttpError extends HttpErrorResult { |
||||||
|
status: 401; |
||||||
|
errorType = "unauthenticated" as const; |
||||||
|
|
||||||
|
static is(response: Response) { |
||||||
|
return response.status === 404; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class ServerHttpError extends HttpErrorResult { |
||||||
|
errorType = "server" as const; |
||||||
|
|
||||||
|
static is(response: Response) { |
||||||
|
return response.status >= 500 && response.status < 600; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
export abstract class RequesterResult { |
||||||
|
readonly uri: string; |
||||||
|
abstract readonly type: string; |
||||||
|
constructor(uri: string) { |
||||||
|
this.uri = uri; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
import type { App } from "@solid/community-server"; |
||||||
|
import { LeafRequester } from "../src/requester/LeafRequester"; |
||||||
|
import crossFetch from "cross-fetch"; |
||||||
|
import { |
||||||
|
createApp, |
||||||
|
getSecret, |
||||||
|
refreshToken, |
||||||
|
type ISecretData, |
||||||
|
type ITokenData, |
||||||
|
getAuthenticatedFetch, |
||||||
|
} from "./solidServer.helper"; |
||||||
|
import { buildAuthenticatedFetch } from "@inrupt/solid-client-authn-core"; |
||||||
|
|
||||||
|
describe("Leaf Requester", () => { |
||||||
|
let app: App; |
||||||
|
let authFetch: typeof fetch; |
||||||
|
|
||||||
|
beforeAll(async () => { |
||||||
|
// Start up the server
|
||||||
|
// app = await createApp();
|
||||||
|
// await app.start();
|
||||||
|
|
||||||
|
authFetch = await getAuthenticatedFetch(); |
||||||
|
}); |
||||||
|
|
||||||
|
it("special request", async () => { |
||||||
|
const response = await authFetch( |
||||||
|
"https://solidweb.me/jackson/everything_public/anonexistentfile.json", |
||||||
|
{ |
||||||
|
method: "PUT", |
||||||
|
headers: { "content-type": "application/json+ld" }, |
||||||
|
body: JSON.stringify({ some: "test" }), |
||||||
|
}, |
||||||
|
); |
||||||
|
console.log("STATUS:", response.status); |
||||||
|
console.log("HEADERS:", response.headers); |
||||||
|
console.log("BODY:", await response.text()); |
||||||
|
}); |
||||||
|
|
||||||
|
it("reads", async () => { |
||||||
|
const leafRequester = new LeafRequester( |
||||||
|
"https://solidweb.me/jackson/everything-public/someotherfile.json", |
||||||
|
{ fetch: authFetch }, |
||||||
|
); |
||||||
|
|
||||||
|
await leafRequester.read(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,106 @@ |
|||||||
|
// Taken from https://github.com/comunica/comunica/blob/b237be4265c353a62a876187d9e21e3bc05123a3/engines/query-sparql/test/QuerySparql-solid-test.ts#L9
|
||||||
|
|
||||||
|
import * as path from "path"; |
||||||
|
import type { KeyPair } from "@inrupt/solid-client-authn-core"; |
||||||
|
import { |
||||||
|
buildAuthenticatedFetch, |
||||||
|
createDpopHeader, |
||||||
|
generateDpopKeyPair, |
||||||
|
} from "@inrupt/solid-client-authn-core"; |
||||||
|
import { AppRunner, resolveModulePath } from "@solid/community-server"; |
||||||
|
import "jest-rdf"; |
||||||
|
import fetch from "cross-fetch"; |
||||||
|
|
||||||
|
const config = [ |
||||||
|
{ |
||||||
|
|
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
const SERVER_DOMAIN = "https://solidweb.me"; |
||||||
|
|
||||||
|
// Use an increased timeout, since the CSS server takes too much setup time.
|
||||||
|
jest.setTimeout(40_000); |
||||||
|
|
||||||
|
export function createApp() { |
||||||
|
return new AppRunner().create( |
||||||
|
{ |
||||||
|
mainModulePath: resolveModulePath(""), |
||||||
|
typeChecking: false, |
||||||
|
}, |
||||||
|
resolveModulePath("config/default.json"), |
||||||
|
{}, |
||||||
|
{ |
||||||
|
port: 3_001, |
||||||
|
loggingLevel: "off", |
||||||
|
seededPodConfigJson: path.join( |
||||||
|
__dirname, |
||||||
|
"configs", |
||||||
|
"solid-css-seed.json", |
||||||
|
), |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export interface ISecretData { |
||||||
|
id: string; |
||||||
|
secret: string; |
||||||
|
} |
||||||
|
|
||||||
|
// From https://communitysolidserver.github.io/CommunitySolidServer/5.x/usage/client-credentials/
|
||||||
|
export async function getSecret(): Promise<ISecretData> { |
||||||
|
const result = await fetch(`${SERVER_DOMAIN}/idp/credentials/`, { |
||||||
|
method: "POST", |
||||||
|
headers: { "content-type": "application/json" }, |
||||||
|
body: JSON.stringify({ |
||||||
|
email: config[0].email, |
||||||
|
password: config[0].password, |
||||||
|
name: config[0].podName, |
||||||
|
}), |
||||||
|
}); |
||||||
|
const json = await result.json(); |
||||||
|
return json; |
||||||
|
} |
||||||
|
|
||||||
|
export interface ITokenData { |
||||||
|
accessToken: string; |
||||||
|
dpopKey: KeyPair; |
||||||
|
} |
||||||
|
|
||||||
|
// From https://communitysolidserver.github.io/CommunitySolidServer/5.x/usage/client-credentials/
|
||||||
|
export async function refreshToken({ |
||||||
|
id, |
||||||
|
secret, |
||||||
|
}: ISecretData): Promise<ITokenData> { |
||||||
|
const dpopKey = await generateDpopKeyPair(); |
||||||
|
const authString = `${encodeURIComponent(id)}:${encodeURIComponent(secret)}`; |
||||||
|
const tokenUrl = `${SERVER_DOMAIN}/.oidc/token`; |
||||||
|
const accessToken = await fetch(tokenUrl, { |
||||||
|
method: "POST", |
||||||
|
headers: { |
||||||
|
// The header needs to be in base64 encoding.
|
||||||
|
authorization: `Basic ${Buffer.from(authString).toString("base64")}`, |
||||||
|
"content-type": "application/x-www-form-urlencoded", |
||||||
|
dpop: await createDpopHeader(tokenUrl, "POST", dpopKey), |
||||||
|
}, |
||||||
|
body: "grant_type=client_credentials&scope=webid", |
||||||
|
}) |
||||||
|
.then((res) => res.json()) |
||||||
|
.then((res) => res.access_token); |
||||||
|
|
||||||
|
return { accessToken, dpopKey }; |
||||||
|
} |
||||||
|
|
||||||
|
export async function getAuthenticatedFetch() { |
||||||
|
// Generate secret
|
||||||
|
const secret = await getSecret(); |
||||||
|
|
||||||
|
// Get token
|
||||||
|
const token = await refreshToken(secret); |
||||||
|
|
||||||
|
// Build authenticated fetch
|
||||||
|
const authFetch = await buildAuthenticatedFetch(fetch, token.accessToken, { |
||||||
|
dpopKey: token.dpopKey, |
||||||
|
}); |
||||||
|
return authFetch; |
||||||
|
} |
Loading…
Reference in new issue