# backend-ts-pdp-sdk

Policy decision point (PDP) sdk client for Typescript

## Usage

### OwsPdp

The `OwsPdp` client is a Typescript class to interact with [ows-pdp endpoints](https://qa-ows-pdp.theorchard.io/redoc).

```typescript
import { OwsPdp } from "@theorchard/backend-ts-pdp-sdk";
import { v4 as uuidv4 } from "uuid";

// Basic instantiation with authorization getter
const authorizationGetter = async (): Promise<string> => {
  // Your logic to get authorization token
  return "Bearer your-token-here";
};

const correlationIdGetter = (): string => {
  // Your logic to generate correlation ID
  return uuidv4();
};

const owsPdp = new OwsPdp({
  environment: "qa",
  authorizationGetter,
  correlationIdGetter,
});

console.log(`owsPdp instance: ${owsPdp.basePath}`);
```

### PdpAuthorizationBackend

The `PdpAuthorizationBackend` provides convenience methods to interact with the `OwsPdp` object.

```typescript
import {
  PdpAuthorizationBackend,
  OwsPdp,
  GraphqlServerLogger,
} from "@theorchard/backend-ts-pdp-sdk";
import { v4 as uuidv4 } from "uuid";

const pdpAuthorizationBackend = new PdpAuthorizationBackend({
  environment: "qa",
  authorizationGetter: async () => "Bearer your-token",
  logger: new GraphqlServerLogger({
    info: logInfo,
    warn: logWarning,
    error: logError,
    debug: logDebug,
  }),
});
console.log(
  `pdpAuthorizationBackend instance: ${pdpAuthorizationBackend["owsPdp"].basePath}`,
);
```

### PdpAuthorizationBackend Methods

#### isAuthorized

Check authorization for a single resource.

```typescript
// Use id_to_uuid_exchange for tenant ID to UUID conversion,
const resultWithExchange = await pdpAuthorizationBackend.isAuthorized({
  action: "view",
  resourceId: "123",
  resourceType: "audience",
  resourceGetter: {
    getAttributes: async (params) => ({
      id_to_uuid_exchange_tenant: {
        tenant_type: "account",
        tenant_id: "7123",
      },
    }),
  },
});

console.log(`User is authorized: ${resultWithExchange}`);
```

The `IsAuthorizedParams` interface has the following fields.

| Parameter               | Type                      | Required | Description                                                                      |
| ----------------------- | ------------------------- | -------- | -------------------------------------------------------------------------------- |
| `action`                | `string`                  | Yes      | The action to authorize (e.g., "view", "edit", "delete")                         |
| `resourceId`            | `string`                  | Yes      | The unique identifier for the resource                                           |
| `resourceType`          | `string`                  | Yes      | The type of resource being authorized (e.g., "account", "audience")              |
| `resourceGetter`        | `ResourceGetter`          | Yes      | Object that implements the ResourceGetter interface to fetch resource attributes |
| `raiseWhenUnauthorized` | `boolean`                 | No       | Whether to throw an exception when unauthorized (default: false)                 |
| `resourceGetterParams`  | `Record<string, unknown>` | No       | Additional parameters passed to the resourceGetter's `getAttributes` method      |

##### PassThroughGetter

The `PassThroughGetter` forwards provided attributes directly for `isAuthorized`:

```typescript
import { PassThroughGetter } from "@theorchard/backend-ts-pdp-sdk";

const attributes = {
  tenant: {
    tenant_uuid: "d25a4cd1-e820-45f2-be5c-56edcfeb8298",
    tenant_type: "company_brand",
  },
};
const result = await pdpAuthorizationBackend.isAuthorized({
  action: "view",
  resourceId: "123",
  resourceType: "audience",
  resourceGetter: new PassThroughGetter(),
  resourceGetterParams: attributes,
});
console.log(`User is authorized to view audience 123: ${result}`);
```

##### Custom ResourceGetter

Create a custom ResourceGetter class for dynamic attribute resolution:

```typescript
import { ResourceGetter } from "@theorchard/backend-ts-pdp-sdk";

class CustomResourceGetter {
  async getAttributes(
    resourceGetterParams?: Record<string, unknown>,
  ): Promise<Record<string, unknown>> {
    // Extract resourceId from params if needed
    const resourceId = resourceGetterParams?.resourceId as string;

    // Your custom logic here - e.g., database lookup, API call, etc.
    console.log(`Getting attributes for resource: ${resourceId}`);

    return {
      id_to_uuid_exchange_tenant: {
        tenant_type: "account",
        tenant_id: "7123",
      },
    };
  }
}

// Usage with isAuthorized
const result = await pdpAuthorizationBackend.isAuthorized({
  action: "view",
  resourceId: "123",
  resourceType: "audience",
  resourceGetter: new CustomResourceGetter(),
  resourceGetterParams: {
    resourceId: 1,
  },
});

console.log(`User is authorized: ${result}`);
```

#### isAuthorizedMany

Check authorization for multiple resources of the same `type` and `action`.

```typescript
import { ResourceWithAttributes } from "@theorchard/backend-ts-pdp-sdk";
import assert from "assert";

const resourcesWithAttributes: ResourceWithAttributes[] = [
  {
    resourceId: "123",
    attributes: {
      tenant: {
        tenant_type: "account",
        tenant_uuid: "e055a30d-34de-450f-b1f2-1fa433ceb15a",
      },
    },
  },
  {
    resourceId: "456",
    attributes: {
      tenant: {
        tenant_type: "account",
        tenant_uuid: "ec1fd7e2-9c95-4e09-a037-e924e0244283",
      },
    },
  },
];

const results = await pdpAuthorizationBackend.isAuthorizedMany({
  action: "view",
  resourceType: "audience",
  resourcesWithAttributes,
  raiseWhenUnauthorized: false,
});

// Validate that the input and results are the same length.
assert(
  resourcesWithAttributes.length === results.length,
  "resourcesWithAttributes and results must have equal length",
);

// Results are returned in request order
const zippedResponse = resourcesWithAttributes.map((resource, index) => ({
  item: resource,
  isAuthorized: results[index],
}));

console.log(
  `Authorization results: ${JSON.stringify(zippedResponse, null, 2)}`,
);
```

The `IsAuthorizedManyParams` interface has the following fields:

| Parameter                 | Type                       | Required | Description                                                         |
| ------------------------- | -------------------------- | -------- | ------------------------------------------------------------------- |
| `action`                  | `string`                   | Yes      | The action to authorize (e.g., "view", "edit", "delete")            |
| `resourceType`            | `string`                   | Yes      | The type of resource being authorized (e.g., "account", "audience") |
| `resourcesWithAttributes` | `ResourceWithAttributes[]` | Yes      | Array of resources with their attributes                            |
| `raiseWhenUnauthorized`   | `boolean`                  | No       | Whether to throw an exception when unauthorized (default: false)    |

#### getAuthorizedTenants

Get the list of tenants for which the requester is authorized.

```typescript
const authorizedTenants = await pdpAuthorizationBackend.getAuthorizedTenants({
  action: "view",
  resourceType: "audience",
});

console.log(
  `User is authorized to view audience resources for tenants:`,
  authorizedTenants,
);
```

The `GetAuthorizedTenantsParams` interface has the following fields:

| Parameter      | Type     | Required | Description                                                         |
| -------------- | -------- | -------- | ------------------------------------------------------------------- |
| `action`       | `string` | Yes      | The action to authorize (e.g., "view", "edit", "delete")            |
| `resourceType` | `string` | Yes      | The type of resource being authorized (e.g., "account", "audience") |

#### isAuthorizedManyResourcesAndActions

Check authorization for multiple resources with different types and/or actions.

```typescript
const resourceActions: ResourceAction[] = [
  {
    resourceId: "123",
    attributes: {
      tenant: {
        tenant_type: "account",
        tenant_uuid: "e055a30d-34de-450f-b1f2-1fa433ceb15a",
      },
    },
    action: "view",
    resourceType: "statement_period",
  },
  {
    resourceId: "124",
    attributes: {
      tenant: {
        tenant_type: "account",
        tenant_uuid: "e055a30d-34de-450f-b1f2-1fa433ceb15a",
      },
    },
    action: "view",
    resourceType: "accounting_period",
  },
];

const results =
  await pdpAuthorizationBackend.isAuthorizedManyResourcesAndActions({
    resourceActions,
    raiseWhenUnauthorized: false,
  });

// Combine results with original requests
const zippedResponse = resourceActions.map((resourceAction, index) => ({
  item: resourceAction,
  isAuthorized: results[index],
}));

console.log(
  `Authorization results: ${JSON.stringify(zippedResponse, null, 2)}`,
);
```

The `IsAuthorizedManyResourcesAndActionsParams` interface has the following fields:

| Parameter               | Type               | Required | Description                                                      |
| ----------------------- | ------------------ | -------- | ---------------------------------------------------------------- |
| `resourceActions`       | `ResourceAction[]` | Yes      | Array of resources with their actions, types, and attributes     |
| `raiseWhenUnauthorized` | `boolean`          | No       | Whether to throw an exception when unauthorized (default: false) |

## Error Handling

### PdpAuthenticationError

The SDK distinguishes between authentication failures (invalid/missing credentials) and authorization failures (
insufficient permissions).

When the ows-pdp returns a **401 (Unauthorized)** status code, a `UnauthorizedException` is thrown. **This exception is
always propagated to the caller, regardless of the `raiseWhenUnauthorized` flag setting.**

This ensures that authentication issues are never silently ignored and can be handled distinctly from authorization
failures.

**Example:**

```typescript
import {
  PdpAuthorizationBackend,
  UnauthorizedException,
} from "@theorchard/backend-ts-pdp-sdk";

try {
  const authorized = await pdpAuthorizationBackend.isAuthorized({
    action: "view",
    resourceId: "123",
    resourceType: "audience",
    resourceGetter: myResourceGetter,
    raiseWhenUnauthorized: false, // Authorization errors won't throw...
  });

  if (authorized) {
    console.log("Access granted");
  } else {
    console.log("Access denied");
  }
} catch (error) {
  if (error instanceof UnauthorizedException) {
    // ...but authentication errors (401) will still throw!
    console.error("Authentication failed - token may be expired or invalid");
    // Handle authentication failure: refresh token, redirect to login, etc.
  } else {
    // Handle other errors (network issues, server errors, etc.)
    console.error("Authorization check failed:", error.message);
  }
}
```

**HTTP Status Code Handling:**

- **401**: Throws `PdpAuthenticationError` - Always propagated
- **Other errors**: Maintains existing error handling behavior

All authorization methods (`isAuthorized`, `isAuthorizedMany`, `getAuthorizedTenants`,
`isAuthorizedManyResourcesAndActions`) follow this error handling pattern.

## Loggers

The SDK provides a [Logger](./src/loggers/Logger.ts) interface to allow clients to use various logger
libraries when initializing a `PdpAuthorizationBackend`.

```typescript
const pdpAuthorizationBackend = new PdpAuthorizationBackend({
  environment: "qa",
  authorizationGetter: exampleAuthorizationGetter,
  logger: pdpSdkLogger,
});
```

The SDK provides the following concrete logger classes:

### GraphqlServerLogger

Logger class to support the log helper functions defined in [orchard-suite/graphql-server](https://github.com/theorchard/orchard-suite/blob/5a21ee756deffd146607a60643c4aa398aad0256/packages/graphql-server/src/utils/logger.ts#L55-L84).

Usage:

```typescript
import {
  logDebug,
  logError,
  logInfo,
  logWarning,
} from "@theorchard/graphql-server";
import { GraphqlServerLogger } from "@theorchard/backend-ts-pdp-sdk";

const pdpAuthorizationBackend = new PdpAuthorizationBackend({
  environment: "qa",
  authorizationGetter: exampleAuthorizationGetter,
  logger: new GraphqlServerLogger({
    info: logInfo,
    warn: logWarning,
    error: logError,
    debug: logDebug,
  }),
});
```

### OwsLogger

Wrapper class for the [OwsLogger](https://github.com/theorchard/orchard-suite/tree/master/packages/ows-logger) logger defined in `orchard-suite/ows-logger`.

Usage:

```typescript
import { createOwsLogger } from "@theorchard/ows-logger";
import { OwsLogger } from "@theorchard/backend-ts-pdp-sdk";

const realOwsLogger = createOwsLogger(opts);
const pdpAuthorizationBackend = new PdpAuthorizationBackend({
  environment: "qa",
  authorizationGetter: exampleAuthorizationGetter,
  logger: new OwsLogger(realOwsLogger),
});
```

### PowerToolsLogger

Logger class for the [aws-powertools/powertools-lambda-typescript](https://github.com/aws-powertools/powertools-lambda-typescript/tree/main/packages/logger)
logger.

Usage:

```typescript
import { Logger } from "@aws-lambda-powertools/logger";
import { PowerToolsLogger } from "@theorchard/backend-ts-pdp-sdk";

const powerToolsLogger = new Logger({ serviceName: "my-lambda" });
const pdpAuthorizationBackend = new PdpAuthorizationBackend({
  environment: "qa",
  authorizationGetter: exampleAuthorizationGetter,
  logger: new PowerToolsLogger(powerToolsLogger),
});
```

# GraphQL

## Configure `ServiceContext`

Update the interface for ServiceContext so that it is easy to interact with `fieldPermissions` from resolvers:

```typescript
import { AllowedTenantsServiceContext } from '@theorchard/backend-ts-pdp-sdk';

// Update the type for ServiceContext to extend the AllowedTenantsServiceContext interface
export interface ServiceContext
    extends ApolloContext,
        AllowedTenantsServiceContext {
          ...
        }
```

This means that when initializing the service context, you'll need something like:

```typescript
createContext: (ctx) => {
    return {
      ...ctx,
      allowedTenants: {} as Record<string, Promise<AllowedTenants>>,
    };
  },
```

This `allowedTenants` record is an in-memory map to store cached authorization decisions made by PP for the current request context.

## ppAllowedTenants directive

This directive will send an [allowed-tenants request to ows-pdp](https://qa-ows-pdp.theorchard.io/redoc#tag/identity/operation/get_my_allowed_tenants_identity_self_allowed_tenants__post)
and store the response in the request context in `fieldPermissions.allowedTenants`, as well as in the `serviceContext`'s `allowedTenants` in-memory cache..

To use the directive:

```graphql
# This can be added to `directives.graphql`
directive @ppAllowedTenants(
  resource: String!
  action: String!
) on FIELD_DEFINITION

type NrContribution {
  id: ID!
  value: Int @ppAllowedTenants(resource: "contribution", action: "view")
  externalId: String
    @ppAllowedTenants(resource: "contribution", action: "view_external_id")
}

type Query {
  nrSoundRecordingById(id: ID!): NrSoundRecording
    @ppAllowedTenants(resource: "contribution", action: "view")
}

extend type Mutation {
  createNrSoundRecordingAndContribution(
    nrsr: CreateNrSoundRecordingInput!
    nrContribution: NrContributionInput!
  ): NrSoundRecordingAndContribution!
    @ppAllowedTenants(resource: "contribution", action: "create")
}
```

- **Field Level**: Applies the `ppAllowedTenants` directive to the field, root query, or mutation.
- **Type Level**: Not supported.

## ppAllowedTenantsDirectiveTransformerFn

`ppAllowedTenantsDirectiveTransformerFn` is a [GraphQL transformer function](https://www.apollographql.com/docs/apollo-server/schema/directives#transformer-functions)
to add Permissions Platform access checks to your GraphQL API using a custom directive named `@ppAllowedTenants`.

To use:

```typescript
import { ppAllowedTenantsDirectiveTransformerFn } from "@theorchard/backend-ts-pdp-sdk";

const schema = createSchema({
  resolvers,
  schemaLocation: "./src/schema",
  schemaTransformers: [ppAllowedTenantsDirectiveTransformerFn],
});
```

Alternatively, you can pass the transformer through the startApolloServer `options` argument:

```typescript
import { ppAllowedTenantsDirectiveTransformerFn } from "@theorchard/backend-ts-pdp-sdk";
import {
  authorizationGetterFactory,
  correlationIdGetterFactory,
  logDebug,
  logError,
  logInfo,
  logWarning,
  startApolloServer,
} from "@theorchard/graphql-server";

// ppAllowedTenantsDirectiveTransformerFn requires:
// - allowedTenants: In-memory Map to store cached auth requests.
// - authorizationBackend: datasource to interact with ows-pdp
const { server, app } = await startApolloServer({
  config: yourConfig,
  createContext: (ctx) => {
    return {
      ...ctx,
      allowedTenants: {} as Record<string, Promise<AllowedTenants>>,
    };
  },
  createDataSources: (options) => {
    return {
      authorizationBackend: new PdpAuthorizationBackend({
        environment: env.name,
        authorizationGetter: authorizationGetterFactory(options.context),
        correlationIdGetter: correlationIdGetterFactory(options.context),
        logger: new GraphqlServerLogger({
          info: logInfo,
          warn: logWarning,
          error: logError,
          debug: logDebug,
        }),
      }),
    };
  },
  schemaTransformers: [ppAllowedTenantsDirectiveTransformerFn],
  // ... other options
});
```

To access `fieldPermissions` in the resolver.

```typescript
import { AllowedTenantsServiceContext } from "@theorchard/backend-ts-pdp-sdk";

const resolvers: IdentityResolvers = {
  async fieldResolver({ id }, args, context) {
    const { fieldPermissions } = context;
    if (fieldPermissions) {
      const { resourceType, action, allowedTenants } = fieldPermissions;
      logInfo(
        `[name] resourceType:${resourceType}, action:${action}, JWT.identity allowedTenants:${JSON.stringify(allowedTenants.allowedTenants, null, 2)}`,
      );
      logInfo(
        `[name] resourceType:${resourceType}, action:${action}, JWT.identity parentCompanyUuids:${JSON.stringify(allowedTenants.parentCompanyUuids, null, 2)}`,
      );
      logInfo(
        `[name] resourceType:${resourceType}, action:${action}, JWT.identity companyBrandUuids:${JSON.stringify(allowedTenants.companyBrandUuids, null, 2)}`,
      );
      logInfo(
        `[name] resourceType:${resourceType}, action:${action}, JWT.identity accountUuids:${JSON.stringify(allowedTenants.accountUuids, null, 2)}`,
      );
      logInfo(
        `[name] resourceType:${resourceType}, action:${action}, JWT.identity subaccountUuids:${JSON.stringify(allowedTenants.subaccountUuids, null, 2)}`,
      );
      logInfo(
        `[name] resourceType:${resourceType}, action:${action}, JWT.identity labelParticipantUuids:${JSON.stringify(allowedTenants.labelParticipantUuids, null, 2)}`,
      );
    }
    // ...
  },
};
```

### PpAllowedTenantsDirectiveTransformer class

The `PpAllowedTenantsDirectiveTransformer` class has an `isDirectiveEnabled` method that can be
overridden by clients to support custom logic to enable or disable the transformer.

This example shows how override `isDirectiveEnabled` with custom splitio logic.

```typescript
import { PpAllowedTenantsDirectiveTransformer } from "@theorchard/backend-ts-pdp-sdk";
import createSchema from "@theorchard/graphql-server";
import { isFeatureEnabled } from "@theorchard/connector-splitio";

/**
 * Custom PpAllowedTenantsDirectiveTransformer class that overrides `isDirectiveEnabled`.
 */
class FeatureFlaggedTransformer extends PpAllowedTenantsDirectiveTransformer {
  protected isDirectiveEnabled({ context }): boolean {
    // Assumes the service has setup a split client using `activateSplitClient` or `initSplitClient`
    return isFeatureEnabled("gql_nr_pp_allowed_actions", context.identity);
  }
}

// Instantiate the custom transformer instance
const transformer = new FeatureFlaggedTransformer();

// Pass the custom transformer to the `createSchema` method
// using an anonymous function or using `startApolloServer`.
const schema = createSchema({
  resolvers,
  schemaLocation: "./src/schema",
  schemaTransformers: [
    (schema: GraphQLSchema) => transformer.transform(schema),
  ],
});
```

## ppCanPerform directive

This directive is identical to [ppAllowedTenants](#ppallowedtenants-directive) except it raises a `GraphQLError` when allowed-tenants returns 0
tenants. The directive allows GraphQL developers to avoid boilerplate resolver code to abort when
`fieldPermissions.allowedTenants` is empty.

To use the directive:

```graphql
directive @ppCanPerform(resource: String!, action: String!) on FIELD_DEFINITION

type NrContribution {
  id: ID!
  value: Int @ppCanPerform(resource: "contribution", action: "view")
  externalId: String
    @ppCanPerform(resource: "contribution", action: "view_external_id")
}

type Query {
  nrSoundRecordingById(id: ID!): NrSoundRecording
    @ppCanPerform(resource: "contribution", action: "view")
}
```

- **Field Level**: Applies the `ppCanPerform` directive to the field, root query, or mutation.
- **Type Level**: Not supported.

### ppCanPerformDirectiveTransformerFn

`ppCanPerformDirectiveTransformerFn` is a [GraphQL transformer function](https://www.apollographql.com/docs/apollo-server/schema/directives#transformer-functions)
to add Permissions Platform access checks to your GraphQL API using a custom directive named `@ppCanPerform`.

To use:

```typescript
import { ppCanPerformDirectiveTransformerFn } from "@theorchard/backend-ts-pdp-sdk";

const schema = createSchema({
  resolvers,
  schemaLocation: "./src/schema",
  schemaTransformers: [ppCanPerformDirectiveTransformerFn],
});
```

### PpCanPerformDirectiveTransformer class

The `PpCanPerformDirectiveTransformer` class behaves the same
as [PpAllowedTenantsDirectiveTransformer](#ppallowedtenantsdirectivetransformer-class). Clients
can override `isDirectiveEnabled` to implement custom feature flag logic.
