Node.js Client API

ℹ️

These are the docs for the v2 release. For v1, see the docs and the migration guide.

ℹ️

For changes to the Node.js Client API, see Changelog.

First, install the oso-cloud package on NPM:


npm install oso-cloud

Before going through this guide, make sure you follow the Oso Cloud Quickstart to get your Oso Cloud API Key properly set in your environment.

The minimum supported Node.js version is 16.

Instantiating an Oso Cloud client

The Oso Cloud client provides an Oso class that takes your Oso Cloud URL and API key:


const { Oso } = require("oso-cloud");
const oso = new Oso("https://cloud.osohq.com", YOUR_API_KEY);
// Later, in an async function:
await oso.insert(["has_role", user, role, resource]);
// Wherever authorization needs to be performed
if (await oso.authorize(user, action, resource)) {
// Action is allowed
}

ℹ️

You should instantiate one client and share it across your application. Under the hood, it reuses connections to avoid paying the cost of negotiating a new connection on every request.

Generating TypeScript types from your policy

Version 2.0.0 of the Node.js SDK introduced much better TypeScript types across the entire API surface. However, the default types are agnostic to your authorization policy. If, for example, your authorization policy declares a "read" permission and you add an oso.authorize() call for "reda", the default types won't catch that typo at compile time.

To catch these errors at compile time, you can generate TypeScript types from your policy file(s) using the oso-cloud CLI. The generated types infer the set of possible facts that you can send to Oso Cloud and the set of possible queries you can make, so you know at compile time if your API call is valid.

ℹ️

The generated types will only include facts that are used by rules in your policy.

Generate types

Requires >= v0.16.0 of the oso-cloud CLI.

Generate types from your Polar policy file(s):


$ oso-cloud generate-types typescript path/to/policy.polar > path/to/polarTypes.d.ts

To ensure these types remain up-to-date with your policy, add a CI check that generates types and diffs the output against the checked-in version:


$ oso-cloud generate-types typescript path/to/policy.polar | diff path/to/polarTypes.d.ts -

Using the generated types

Requires >= v2.0.0 of the Node.js SDK.

Import the generated types and pass them to the Oso constructor as a type parameter:


import type { PolarTypes } from "./polarTypes";
const oso = new Oso<PolarTypes>(...);
// Assume your policy has a rule like:
// has_permission(user: User, "read", org: Org) if
// has_role(user, "member", org);
const alice = { type: “User”, id: “alice” };
const acme = { type: “Org”, id: “123” };
// Type Error - “reed” ≠ “read” 😦
await oso.authorize(alice, “reed”, acme);
// All good 😌
await oso.authorize(alice, “read”, acme);

Specifying an Oso Fallback host

If you have deployed Oso Fallback nodes to your infrastructure, you may specify the host when instantiating the Oso Cloud client.


// Assumes Oso Fallback is hosted at http://localhost:8080
const oso = new Oso("https://cloud.osohq.com", YOUR_API_KEY, {
fallbackUrl: "http://localhost:8080",
});

Passing application entities into the client

Under the hood, Oso Cloud represents an entity in your application as a combination of a type and an ID, which together uniquely identify the entity. The Node client represents these entities as objects with both type and id properties. For example:


const alice = { type: "User", id: "alice" };
const anvilsRepository = { type: "Repository", id: "anvils" };

You will pass objects like these into nearly every function call you make to the Node client.

Primitive value conversion

Additionally, for fields with primitive values you may pass an instance of the corresponding type directly instead of a full object:


oso.insert(["is_weird", 10, "yes", true]);
// equivalent to:
// [
// "is_weird",
// {type: "Integer", id: "10"},
// {type: "String", id: "yes"},
// {type: "Boolean", id: "true"}
// ]

Centralized Authorization Data API

Oso Cloud clients provide an API to manage authorization data stored directly in Oso Cloud.

Add fact: oso.insert([name, ...args])

Adds a fact named name with the provided arguments. Example:

await oso.insert([
  "has_role",
  {type: "User", id: "bob"},
  "owner",
  {type: "Organization", id: "acme"}
]);

Delete fact: oso.delete([name, ...args])

Deletes a fact, if possible. Does not throw an error if the given fact is not found. Example:


await oso.delete([
"has_role",
{ type: "User", id: "bob" },
"maintainer",
{
type: "Repository",
id: "anvils",
},
]);

When deleting facts, you can use null as a wildcard to delete many facts at once.


// remove all of bob's roles across all resources
await oso.delete(["has_role", { type: "User", id: "bob" }, null, null]);

Transactionally delete and add facts: oso.batch((tx) => { })

ℹ️

For Oso Cloud developer accounts, batch calls are limited to 20 facts. If you attempt to send more than 20 facts, these functions will throw an error.

Allows deleting and inserting many facts in one atomic transaction. Deletions and insertions are run in the order they appear in the callback. Example:


await oso.batch((tx) => {
tx.insert([
"has_role",
{ type: "User", id: "bob" },
"owner",
{ type: "Organization", id: "acme" },
]);
tx.delete([
"has_role",
{ type: "User", id: "bob" },
"maintainer",
{ type: "Repository", id: "anvil" },
]);
});

Get facts: oso.get([name, ...args])

ℹ️

For Oso Cloud developer accounts, Get calls are limited to 1000 results. If you have more than 1000 facts, the function will throw an error.

Get facts that are stored in Oso Cloud. Can be used to check the existence of a particular fact, or used to fetch all facts that have a particular argument:


// Get one fact:
await oso.get([
"has_role",
{ type: "User", id: "bob" },
"admin",
{
type: "Repository",
id: "anvils",
},
]);
// => [
// [
// "has_role",
// {type: "User", id: "bob"},
// {type: "String", id: "admin"},
// {type: "Repository", id: "anvils"}
// ]
// ]
// List all role-related facts on the `anvils` repo
await oso.get(["has_role", null, null, { type: "Repository", id: "anvils" }]);
// => [
// [
// "has_role",
// {type: "User", id: "bob"},
// {type: "String", id: "admin"},
// {type: "Repository", id: "anvils"}
// ],
// //...other has_role facts
// ]

Note that null behaves like a wildcard for get calls: passing null, null, anvils means "find all facts where anvils is the third argument, regardless of other arguments".

oso.get() only returns facts you've explicitly added. If you want to return a list of authorized resources, use the Check API. For example, to answer "on which resources can a given user perform a given action", use oso.list(). If you want to query for arbitrary information that can be derived from your facts and policy, use the Query Builder API.

Check API

ℹ️

For Oso Cloud developer accounts, the number of context facts per request is limited to 20; and the number of records returned is limited to 1000.

Context facts

The Check AP lets you provide context facts with each request. When Oso Cloud performs a check, it considers the request's context facts in addition to any other centralized authorization data. Context facts are only used in the API call in which they're provided-- they do not persist across requests.

For more details, see Context Facts.

Check a permission: oso.authorize(actor, action, resource)

Determines whether or not an action is allowed, based on a combination of authorization data and policy logic. Example:


const alice = { type: "User", id: "alice" };
const anvilsRepository = { type: "Repository", id: "anvils" };
const authorized = await oso.authorize(alice, "read", anvilsRepository);
if (!authorized) {
throw new Error("Action is not allowed");
}

You may provide an array of context facts as an optional fourth argument to this method. Example:


const issue = { type: "Issue", id: "anvils-1" };
const authorized = await oso.authorize(alice, "read", issue, [
["has_relation", issue, "parent", anvilsRepository], // a context fact
]);

List authorized resources: oso.list(actor, action, resourceType)

Fetches a list of resource IDs on which an actor can perform a particular action. Example:


const alice = { type: "User", id: "alice" };
const repositoryIds = await oso.list(alice, "read", "Repository");
// => ["acme"]

You may provide an array of context facts as an optional fourth argument to this method. Example:


const anvilsRepository = { type: "Repository", id: "anvils" };
const acmeRepository = { type: "Repository", id: "acme" };
const issueOnAcmeRepository = { type: "Issue", id: "acme-1" };
const issueOnAnvilsRepository = { type: "Issue", id: "anvils-2" };
const repositoryIds = await oso.list(alice, "read", "Issue", [
// context facts
["has_relation", issueOnAnvilsRepository, "parent", anvilsRepository][
("has_relation", issueOnAcmeRepository, "parent", acmeRepository)
],
]);
// => ["acme-1"]

List authorized actions: oso.actions(actor, resource)

Fetches a list of actions which an actor can perform on a particular resource. Example:


const alice = { type: "User", id: "alice" };
const acmeRepository = { type: "Repository", id: "acme" };
await oso.actions(user, acmeRepository);
// => ["read"]

You may provide an array of context facts as an optional third argument to this method. Example:


const issueOnAcmeRepository = { type: "Issue", id: "acme-1" };
await oso.actions(alice, issueOnAcmeRepository, [
["has_relation", issueOnAcmeRepository, "parent", acmeRepository], // a context fact
]);
// => ["read"]

Query for any rule: oso.buildQuery([predicate, ...args])

Query Oso Cloud for any predicate and any combination of concrete and wildcard arguments. Unlike oso.get, which only lists facts you've added, you can use oso.buildQuery to list derived information about any rule in your policy. Example:


const actor = { type: "User", id: "bob" };
const repository = typedVar("Repository");
// Query for all the repos `User:bob` can `read`
await oso.buildQuery(["allow", actor, "read", repository]).evaluate(repository);
// => [ "acme", "anvils" ]

Query Builder API

The oso.buildQuery() API is a builder-style API where you chain methods to construct a query and then execute it.

oso.buildQuery([predicate, ...args])

The oso.buildQuery() function takes the name of the rule you want to query and a list of arguments. The arguments can be concrete values (e.g., "read" or { type: "User", id: "bob" }) or type-constrained variables constructed via the typedVar function:


const actor = { type: "User", id: "bob" };
const repository = typedVar("Repository");
// Query for all the repositories bob can read
oso.buildQuery(["allow", actor, "read", repository]);
// => QueryBuilder { ... }

Note: once you've finished building up your query, you must call evaluate to run it and get the results.

QueryBuilder.and([predicate, ...args])

This function adds another condition that must be true of the query results.

For example:


const actor = { type: "User", id: "bob" };
const repository = typedVar("Repository");
const folder = { type: "Folder", id: "folder-1" };
// Query for all the repositories this user can read...
oso
.buildQuery(["allow", actor, "read", repository])
//... and require the repositories to belong to the given folder.
.and(["has_relation", repository, "folder", folder]);
// => QueryBuilder { ... }

Note: once you've finished building up your query, you must call evaluate to run it and get the results.

QueryBuilder.in(variable, values)

This function requires a given typedVar query variable to be included in a given set of values. You can only call in once per variable per query. Calling in a second time with the same variable on the same query builder will throw an error.

For example:


const actor = { type: "User", id: "bob" };
const repositories = ["acme", "anvil"];
const action = typedVar("String");
const repository = typedVar("Repository");
// Query for all the actions this user can perform on any repository...
oso
.buildQuery(["allow", actor, action, repository])
// ...given that the repository's ID is in the given list of IDs.
.in(repository, repositories);
// => QueryBuilder { ... }

Note: once you've finished building up your query, you must call evaluate to run it and get the results.

QueryBuilder.withContextFacts(contextFacts)

This function adds the given context facts to the query. For example:


const actor = { type: "User", id: "bob" };
const repository = typedVar("Repository");
// Query for all the repositories bob can read...
oso
.buildQuery(["allow", actor, "read", repository])
// ...while including the fact that bob owns acme
.withContextFacts([
["has_role", actor, "owner", { type: "Repository", id: "acme" }],
]);
// => QueryBuilder { ... }

For more information on context facts, see this section.

Note: once you've finished building up your query, you must call evaluate to run it and get the results.

QueryBuilder.evaluate()

This function evaluates the built query, fetching the results from Oso.

The return type of this function varies based on the arguments you pass in.

  • If you pass no arguments, this function returns a boolean. For example:

    const allowed = await oso
    .buildQuery(["allow", actor, action, resource])
    .evaluate();
    // => true if the given actor can perform the given action on the given resource

  • If you pass a single typedVar query variable, this function returns a list of values for that variable. For example:

    const action = typedVar("String");
    const actions = await oso
    .buildQuery(["allow", actor, action, resource])
    .evaluate(action);
    // => all the actions the actor can perform on the given resource- eg. ["read", "write"]

  • If you pass a tuple of typedVar query variables, this function returns a list of tuples of values for those variables. For example:

    const action = typedVar("String");
    const repository = typedVar("Repository");
    const pairs = await oso
    .buildQuery(["allow", actor, action, repository])
    .evaluate([action, repository]);
    // => an array of pairs of allowed actions and repo IDs-
    // eg. [["read", "acme"], ["read", "anvil"], ["write", "anvil"]]

  • If you pass a Map mapping one typedVar query variable (call it K) to another (call it V), returns a Map grouping unique values of K to unique values of V for each value of K. For example:

    const action = typedVar("String");
    const repository = typedVar("Repository");
    const map = await oso
    .buildQuery(["allow", actor, action, repository])
    .evaluate(new Map([[repository, action]]));
    // => a map of repo IDs to allowed actions-
    // eg. new Map(Object.entries({ "acme": ["read"], "anvil": ["read", "write"]}))

Some queries have unconstrained results. For instance, maybe users with the admin role can read all Repository entities in your application. In this case, rather than returning an array containing the ID of every repository, evaluate will return an array containing the string "*". For example:


const repos = typedVar("Repository");
await oso
.buildQuery(["allow", { type: "User", id: "admin" }, "read", repos])
.evaluate(repos); // Return just the IDs of the repos admin can read
// => ["*"] // admin can read anything

Query Builder examples

Field-level access control

const actor = { type: "User", id: "alice" };
const resource = { type: "Repository", id: "anvil" };
const field = typedVar("Field");
const results = await oso
.buildQuery(["allow_field", actor, "read", resource, field])
.evaluate(field);
// => Returns a list of the fields alice can read on the given repo- eg.
// ["name", "stars"]

Checking a global permission

const actor = { type: "User", id: "alice" };
const result = await oso
.buildQuery(["has_permission", actor, "create_repository"])
.evaluate();
// => true if alice has the global "create_repository" permission

Fetching authorized actions for a collection of resources

const repos = ["anvil", "acme"];
const actor = { type: "User", id: "alice" };
const action = typedVar("String");
const repo = typedVar("Repository");
const results = await oso
.buildQuery(["allow", actor, action, repo])
.in(repo, repos)
.evaluate(new Map([[repo, action]]));
// => Returns a map of the given repos to the actions alice can perform on those repos- eg.
// new Map(Object.entries({ "anvil": ["read"], "acme": ["read", "write"] }))

Filtering out unauthorized resources from a collection

const repos = ["anvil", "acme"];
const actor = { type: "User", id: "bob" };
const repo = typedVar("Repository");
const results = await oso
.buildQuery(["allow", actor, "read", repo])
.in(repo, repos)
.evaluate(repo);
// => Returns the subset of `repos` that bob can read- eg.
// ["anvil"]

Filtering an authorize() query based on a relation

const actor = { type: "User", id: "bob" };
const repo = typedVar("Repository");
const org = { type: "Org", id: "coolguys" };
const results = await oso
.buildQuery(["allow", actor, "read", repo])
.and(["has_relation", repo, "parent", org])
.evaluate(repo);
// => Returns the IDs of the repos in the coolguys org that bob can read- eg.
// ["acme", "anvil"]

Learn more about how to query Oso Cloud.

Local Check API

The local check API lets you perform authorization using data that's distributed across Oso Cloud and your own database.

After creating your Local Authorization configuration, provide the path to the YAML file that specifies how to resolve facts in your database.


const oso = new Oso(..., {
dataBindings: "path/to/local_authorization_config.yaml"
});

For more information, see Local Authorization.

List authorized resources with local data: oso.listLocal(actor, action, resource_type, column)

Fetches a filter that can be applied to a database query to return just the resources on which an actor can perform an action. Example with Kysely (opens in a new tab):


const alice = { type: "User", id: "alice" };
const authorized_issues = await db
.selectFrom("issues")
.where(sql.raw<boolean>(await oso.listLocal(alice, "read", "Issue", "id")))
.selectAll()
.execute();

You may use the Kysely query builder (opens in a new tab) to combine this authorization filter with other things such as ordering and pagination.

You may provide a list of context facts as an optional final argument to this method.

Check a permission with local data: oso.authorizeLocal(actor, action, resource)

Fetches a query that can be run against your database to determine whether an actor can perform an action on a resource. Example with Kysely (opens in a new tab):


const alice = { type: "User", id: "alice" };
const swage_issue = { type: "Issue", id: "swage" };
const query = await oso.authorizeLocal(alice, "read", swage_issue);
const { allowed } = (await sql.raw<AuthorizeResult>(query).execute(db)).rows[0];
if (!allowed) {
throw new Error("Action is not allowed");
}

You may provide a list of context facts as an optional final argument to this method.

List authorized actions with local data: oso.actionsLocal(actor, resource)

Fetches a query that can be run against your database to fetch the actions an actor can perform on a resource. Example with Kysely (opens in a new tab):


const alice = { type: "User", id: "alice" };
const swage_issue = { type: "Issue", id: "swage" };
const query = await oso.actionsLocal(alice, swage_issue);
const result = await sql.raw<{ actions: string }>(query).execute(db);
const actions = result.rows.map(({ actions }) => actions);

You may provide a list of context facts as an optional final argument to this method.

Query for any rule with local data: QueryBuilder

You can use the Query Builder to construct SQL queries that you can run against your database to answer arbitrary questions about authorization.

See the Query Builder API documentation for information on how to construct queries. Once you've built your query, instead of calling evaluate (which would evaluate your query exclusively against data stored in Oso Cloud), call either evaluateLocalSelect or evaluateLocalFilter to construct a SQL query which you can run against your database.

Local Query API

QueryBuilder.evaluateLocalSelect(columnNamesToQueryVars)

Fetches a complete SQL query representing the given Query Builder query. (See the Query Builder API documentation for information on how to construct queries.)

The argument to this function is an object that maps the column names that will appear in the SELECT clause of the SQL query to the Query Builder variables whose values should be selected. For example, query.evaluateLocalSelect({user_id: userVar}) will select all the authorized values of userVar (that is, all the values of userVar that satisfy the Query Builder query) into a column called "user_id".

If you pass in an empty object or omit this argument entirely, the SQL query will return a single row selecting a boolean column called result. This column will be true when there's some combination of data in your database that can satisfy the given Query Builder query and false otherwise.

Note that each query variable can appear at most once in the columnNamesToQueryVars mapping. For example, query.evaluateLocalSelect({user_id: userVar, another_user_id: userVar}) will throw a TypeError, because userVar appears twice in the mapping parameter.

Note that column names will be double-quoted and are thus case-sensitive.

Limitations

evaluateLocalSelect has the following limitations:

  • Queries that would return a wildcard for one of the selected query variables are currently unsupported.

    Example:

    policy.polar

    has_permission(user: User, "read", _: Repo) if is_global_admin(user);


    // alice is a global admin, and can read *any* repo
    await oso.insert(["is_global_admin", { type: "User", id: "alice" }]);
    // UNSUPPORTED: Attempt to query for each authorized user / repo pair
    const userVar = typedVar("User");
    const repoVar = typedVar("Repo");
    const sql = await oso
    .buildQuery(["allow", userVar, "read", repoVar])
    .evaluateLocalSelect({ user_id: userVar, repo_id: repoVar });
    // => throws `Error`, because alice can read any repo, so this query would return
    // a wildcard for the repos alice can read

QueryBuilder.evaluateLocalFilter(columnName, queryVar)

Fetches a SQL fragment representing the given Query Builder query that you can embed in the WHERE clause of some other SQL query. Use this to filter out unauthorized results from your SQL query.

(See the Query Builder API documentation for information on how to construct queries.)

columnName is the name of the column you want to filter in your SQL query, and queryVar is the query variable to filter against. For example, query.evaluateLocalFilter("user_id", userVar) will return a SQL fragment of the form "user_id" IN (...), constraining the column "user_id" to the authorized values of userVar.

Note that the column name will be double-quoted and is thus case-sensitive.

Local query examples

These examples all use Kysely (opens in a new tab) to execute the returned SQL queries, but you can use any database client you prefer.

Field-level access control

const actor = { type: "User", id: "alice" };
const resource = { type: "Repository", id: "anvil" };
const field = typedVar("Field");
const sqlQuery = await oso
.buildQuery(["allow_field", actor, "read", resource, field])
.evaluateLocalSelect({ field_name: field });
// => 'SELECT "field_name" FROM (... /* only the fields alice can read on anvil */)'
const { rows } = await sql.raw<{ field_name: string }>(sqlQuery).execute(db);
const fields = rows.map(({ field_name }) => field_name);
// => ["name", "stars"]

Checking a global permission

const actor = { type: "User", id: "alice" };
const sqlQuery = await oso
.buildQuery(["has_permission", actor, "create_repository"])
.evaluateLocalSelect();
// => 'SELECT EXISTS (... /* alice has the create_repository permission */) AS result'
const { rows } = await sql.raw<{ result: boolean }>(sqlQuery).execute(db);
const hasPermission = rows[0]["result"];
// => true

Fetching authorized actions for a paginated collection of resources

const actor = { type: "User", id: "alice" };
const actionVar = typedVar("String");
const repoVar = typedVar("Repository");
const authorizedActionsAndRepos = await oso
.buildQuery(["allow", actor, actionVar, repoVar])
.evaluateLocalSelect({ action: actionVar, repo_id: repoVar });
// => 'SELECT "action", "repo_id" FROM (... /* pairs of alice's authorized actions / repos */)'
const sqlQuery = `
WITH authorized_actions AS (${authorizedActionsAndRepos})
SELECT repos.id AS id, authorized_actions.action AS action FROM repos
INNER JOIN authorized_actions ON repos.id = authorized_actions.repo_id
ORDER BY repos.id ASC LIMIT 10`;
const { rows } = await sql
.raw<{ id: number; action: string }>(sqlQuery)
.execute(db);
// => [{ id: 1, action: "read" }, { id: 1, action: "write" }, ...]

Filtering an authorize() query based on a relation

const actor = { type: "User", id: "bob" };
const repo = typedVar("Repository");
const org = { type: "Org", id: "coolguys" };
const sqlQuery = await oso
.buildQuery(["allow", actor, "read", repo])
.and(["has_relation", repo, "parent", org])
.evaluateLocalSelect({ repo_id: repo });
// => 'SELECT "repo_id" FROM (... /* only repos bob can read which belong to coolguys */)'
const { rows } = await sql.raw<{ repo_id: string }>(sqlQuery).execute(db);
const repoIds = rows.map(({ repo_id }) => repo_id);
// => ["acme", "anvil"]

Filtering on users who can read a certain resource

const actor = typedVar("User");
const resource = { type: "Repository", id: "anvil" };
const authorizedUserFragment = await oso
.buildQuery(["allow", actor, "read", resource])
.evaluateLocalFilter("user_id", actor);
// => '"user_id" IN (... /* only users who can read anvil */)'
const sqlQuery = `SELECT email FROM users WHERE ${authorizedUserFragment}`;
const { rows } = await sql.raw<{ email: string }>(sqlQuery).execute(db);
const emails = rows.map(({ email }) => email);
// => ["alice@example.com", "bob@example.com"]

Policy API

Update the active policy: oso.policy(policy)

Updates the policy in Oso Cloud. The string passed into this method should be written in Polar. Example:


await oso.policy("actor User {}");

This command will run any tests defined in your policy. If one or more of these tests fail, your policy will not be updated.

Get policy metadata: oso.getPolicyMetadata()

Returns metadata about the currently active policy. Example:


metadata = await oso.getPolicyMetadata();

returns:


resources: {
Organization: {
roles: ["admin", "member"],
permissions: [
"add_member",
"read",
"repository.create",
"repository.delete",
"repository.read",
],
relations: {},
},
User: {
roles: [],
permissions: [],
relations: {},
},
global: {
roles: [],
permissions: [],
relations: {},
},
}

See the Policy Metadata guide for more information on use cases.

Debugging

Request logs can be found in the Oso Cloud dashboard, but sometimes it's useful to print timings from the client so you can diagnose network problems. There are three configuration parameters you can set


var oso = new Oso(url, apiKey, { debug: { print: true } });

will print debug messages to stdout.


var oso = new Oso(url, apiKey, { debug: { file: "oso_debug.txt" } });

will append debug messages to the file you specify.

In both cases the messages will look like this. They show the route, the response code, the total request time and how much time was spent in Oso Cloud processing the request vs. network transport.


[oso] /facts 200 total: 4ms, server: 3ms network: 1ms
[oso] /policy 200 total: 9ms, server: 3ms network: 6ms
...

Custom Logger

The third option is logger which allows you to route Oso's debug timings to your own logger such as winston. The function signature is:


// Where level is one of the: 'debug', 'info', 'error'
// Message is the same as print without the '[oso] ' prefix.
// Metadata contains the following metadata: path, status, totalMs, serverMs, networkMs
function (level, message, metadata) {
}

Example:


const { Oso } = require("oso-cloud");
const winston = require("winston");
const logger = winston.createLogger({
level: "debug",
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
const oso = new Oso(url, apiKey, {
debug: {
logger: function (level, message, metadata) {
logger.log(level, message, metadata);
},
},
});

Talk to us

If you'd like to learn more about using Oso Cloud in your app or have any questions about this guide, schedule a 1x1 with an Oso team member. We're happy to help.

Get started with Oso Cloud →