Trustflows client helpers for Solid applications. This package provides a lightweight browser-first auth helper plus UMA utilities for Trustflows-compatible services.
npm install trustflows-clientMake sure a dereferenceable Client ID exists for this application. Make sure the resource server can get this JSON-LD file, and that client_id is the same as the URL where this file is hosted.
{
"@context": "https://www.w3.org/ns/solid/oidc-context.jsonld",
"client_id": "http://localhost:8080/app/client-id.jsonld",
"client_name": "App Name",
"redirect_uris": [ "http://localhost:8080/app/logged-in-screen" ],
"post_logout_redirect_uris": [ "http://localhost:8080/app/logged-out-screen" ]
}This client ID file can then be used to log in a user, make sure to use the same redirect URI as in the file.
import {
getDefaultAuth,
configureDefaultAuth
} from "trustflows-client";
configureDefaultAuth({
persistTokens: false,
});
const auth = getDefaultAuth();
await auth.login(
"https://idp.example",
"http://localhost:8080/app/client-id.jsonld",
"http://localhost:8080/app/logged-in-screen"
);configureDefaultAuth() and the Auth constructor accept these options:
fetch: customfetchimplementation (useful for tests or custom networking).storage: storage provider (defaults tosessionStoragein the browser).claimResolvers: add or override UMA claim resolvers.persistTokens: whether to persist OIDC tokens in storage. Defaults totrue.
Notes:
configureDefaultAuth()must be called beforegetDefaultAuth(); after the default instance is created, callingconfigureDefaultAuth()will throw.- Token persistence stores an
oidc_tokensJSON blob instorageand hydrates it on startup. SetpersistTokens: falseto opt out.
After redirecting back to your application, you can handle the incoming redirect and create an authenticated fetch function.
import {
getDefaultAuth,
configureDefaultAuth
} from "trustflows-client";
configureDefaultAuth({
persistTokens: false,
});
const auth = getDefaultAuth();
await auth.handleIncomingRedirect();
const authFetch = auth.createAuthFetch();
const loggedIn = await auth.isLoggedIn();auth.createAuthFetch() returns a fetch-compatible function. It first tries the request
unauthenticated and, only on a 401, inspects the WWW-Authenticate header: a UMA challenge is
satisfied through the UMA flow, otherwise the request is retried with the OIDC bearer token. Use it
exactly like the global fetch:
const authFetch = auth.createAuthFetch();
const response = await authFetch("https://pod.example/private.txt");The returned function accepts an optional third AuthFetchOptions argument so behaviour can be tuned
per request. Set accessRequest: true to ask for access when the authorization server denies the
request (i.e. the requesting party does not currently have access and the token endpoint responds
with a 4xx error). When enabled, the client POSTs a sotw:EvaluationRequest to the authorization
server's /requests endpoint and the access request's response is returned from the fetch call.
const authFetch = auth.createAuthFetch();
// Standard authenticated request — rejects if access is denied.
await authFetch("https://pod.example/private.txt");
// Same request, but ask for access if it is denied.
const response = await authFetch("https://pod.example/private.txt", undefined, {
accessRequest: true,
});
// The HTTP method of the request determines the requested action:
// GET (and anything else) -> odrl:read
// PUT / POST / PATCH -> odrl:write
// DELETE -> odrl:delete
await authFetch("https://pod.example/private.txt", { method: "PUT" }, {
accessRequest: true,
});The access request is sent on behalf of the logged-in user's WebID, so make sure the user is logged
in (auth.webId is set) before using accessRequest: true. Because the access request only asks
for access, the resource is not returned; inspect the returned response (for example a 201 Created)
to confirm the request was accepted.
You can add custom UMA claim resolvers in your application without modifying this package.
import {
Auth,
type ClaimResolverDefinition,
} from "trustflows-client";
const myResolver: ClaimResolverDefinition = {
id: "custom-claim",
match: {
claim_type: "my-custom-claim",
issuer: "https://idp.example",
},
priority: 10,
resolve: async (requiredClaim, auth) => {
// Custom logic to resolve the claim
return {
claim_token: "custom-token",
claim_token_format: "custom-format",
};
},
};
auth.addClaimResolver(myResolver);The id field must be unique among all registered claim resolvers and is used for logging and debugging purposes.
match can use any of: claim_token_format, claim_type, issuer, name, friendly_name. Each field can be a
string, RegExp, or a predicate function. The priority field is used to determine the order in which resolvers are
tried (higher priority first). The resolve function is called when the resolver matches a claim request. It receives
the required claim and the auth entry that can be used to create the claim.
The Aggregator class manages a single Aggregator Instance on an Aggregator Server: it discovers or
creates the instance for the logged-in user, keeps it cached, and lets you find, deploy, and delete the
services that run on it. It builds on the authenticated fetch, so all instance and service requests are
authorized automatically.
Pass the Aggregator Server base URL (where the Server Description is served) and an authenticated
Auth instance. init() resolves the instance to use:
- a previously cached instance for this server and user, otherwise
- the first instance returned by the server's management endpoint, otherwise
- a newly created instance.
import { Aggregator, getDefaultAuth } from "trustflows-client";
const auth = getDefaultAuth();
await auth.handleIncomingRedirect();
const aggregator = new Aggregator({
serverUrl: "https://aggregator.example/",
auth,
});
await aggregator.init();
console.log(aggregator.instanceUrl); // the resolved Aggregator Instance URLConstructor options:
serverUrl(required): the Aggregator Server base URL.auth(required): an authenticatedAuthinstance (the user must be logged in).creationFlow: which management flow to use when an instance has to be created —"none","provision","authorization_code", or"device_code". Defaults to the first flow advertised by the server.authorizationServer: the UMA Authorization Server governing the Aggregator's resources. Required for the interactive creation flows (can also be passed tostartCreation).cache: whether to persist the discovered instance and services. Defaults totrue.storage: the storage used for the cache and pending interactive-flow state. Defaults tolocalStorage.
Use delete() to remove the instance (and clear it from the cache):
await aggregator.delete();none and provision complete automatically inside init(). The interactive flows
(authorization_code and device_code) need user interaction, so init() throws an
AggregatorAuthorizationRequiredError and you drive the flow with the generic startCreation() /
finishCreation() pair. startCreation() returns a discriminated step describing what to do next.
import { Aggregator, AggregatorAuthorizationRequiredError } from "trustflows-client";
const aggregator = new Aggregator({
serverUrl: "https://aggregator.example/",
auth,
creationFlow: "authorization_code",
authorizationServer: "https://as.example",
});
try {
await aggregator.init();
} catch (error) {
if (error instanceof AggregatorAuthorizationRequiredError) {
const step = await aggregator.startCreation({
redirectUri: "https://app.example/callback",
});
if (step.type === "redirect") {
// authorization_code: send the user to the IdP.
window.location.href = step.authorizationUrl;
} else if (step.type === "device") {
// device_code: show the verification details to the user.
console.log(step.user_code, step.verification_uri);
}
}
}After the user returns (authorization_code) or while the device flow is pending (device_code), call
finishCreation() to complete it. For device_code this polls until the user authorizes; for
authorization_code the code and state are read from the current URL when omitted.
// authorization_code, after the redirect back to your app:
await aggregator.finishCreation();
// device_code:
await aggregator.finishCreation(); // resolves once the user has authorizedThe same startCreation() / finishCreation() pair also performs a token update when the instance
already exists but its tokens are no longer valid (init() throws an
AggregatorAuthorizationRequiredError with reason: "token-update" in that case).
getService() returns information about a service that performs a given transformation, creating it if
it does not exist yet. It resolves in order: the cache, a scan of the Service Collection (matching the
transformation, optional implementation, and parameter bindings), then deployment.
const service = await aggregator.getService({
transformation: "https://aggregator.example/transformations#QueryView",
// Optional implementation:
// implementation: "https://aggregator.example/transformations#QueryViewImpl",
parameters: {
// A string is treated as a literal term:
"https://aggregator.example/transformations#Query": "SELECT * WHERE { ?s ?p ?o }",
// Use an object with `type: "iri"` for an IRI term:
"https://aggregator.example/transformations#Source": {
value: "http://example.org/source1",
type: "iri",
},
},
});
console.log(service.service); // the Service Description Endpoint URL
console.log(service.outputs); // { "<fno:Output IRI>": ["<dcat:accessURL>", ...] }
console.log(service.provenanceLog); // the provenance log URL, if the service exposes oneoutputs maps each fno:Output IRI to the list of its distribution access URLs. When an output has
multiple distributions (for example different formats), all of their access URLs are returned and you
can fetch the Service Description yourself to decide which one to use.
List every service currently deployed, or delete one by its Service Description URL:
const services = await aggregator.getServiceCollection();
await aggregator.deleteService(service.service);All service methods require init() to have completed; calling them earlier throws an
AggregatorNotInitializedError.