diff --git a/src/oauth/OAuthApp.ts b/src/oauth/OAuthApp.ts index 384aefc..13c5315 100644 --- a/src/oauth/OAuthApp.ts +++ b/src/oauth/OAuthApp.ts @@ -11,41 +11,91 @@ import { getValueFromEnvironmentVariable } from '../utils/env-vars.js'; import type { BridgemanAccessibleAppClaims } from './types/BridgemanAccessibleAppClaims.js'; import type { AppSubscriptionTier } from './types/AppSubscriptionTier.js'; +import type { Addon } from './types/addons/Addon.js'; +import type { Webhook } from './types/Webhook.js'; type OAuthAppOptions = { + // ------------------ + // Basic app metadata + // ------------------ + /** The base URL of the app */ baseAppUrl?: URL, + /** The abbreviation of the app */ appAbbrv?: string, - /** If a subscription is required */ - subscriptionRequired?: boolean, - /** The subscription tiers available for the app */ - subscriptionTiers?: AppSubscriptionTier[], - /** The name of the app */ + + /** The (potentially localized) name of the app */ appName?: string | { /** Localized versions of the app name */ [language: string]: string }, + + // ----------------------------------------------------------- + // Necessary interconnected details (contacts, scopes, etc...) + // ----------------------------------------------------------- + /** The email addresses of the contacts for the app */ - contacts?: string[], - /** The scopes an app token COULD ask for (token scopes would have to ask for this or a subset of this list) */ - scopes?: Scopes[], - /** The URL of the app's logo */ - logo_url?: URL, - /** The URL of the app's terms of service */ - tos_url?: URL, - /** The URL of the app's privacy policy */ - policy_url?: URL, + contacts?: string[], + + /** The "available" scopes (scopes an app token COULD ask for - token scopes would have to ask for this or a subset of this list) */ + scopes?: Scopes[], + + // ----------------------------------------------------- + // Optional Mechanical Details (how the OAuth app works) + // ----------------------------------------------------- + /** The type of vault to use for the keystore */ vault_type?: "azure" | "hashicorp" | "file", + /** The default method for authentication */ auth_default_method?: "query" | "form_post" | "PAR", + /** Whether to use JWT as the default authentication method */ auth_default_use_JWT?: boolean, + /** The default response mode for authentication */ auth_default_response_mode?: 'query' | 'fragment' | 'form_post' + + // ----------------------------------------------------- + // For already registered apps (to stop re-registration) + // ----------------------------------------------------- + /** The client secret for the app (if this IS set registration WON'T be done. Because re-registering isn't supported) */ client_secret?: string + + // ---------------------------------- + // URL stuff (policies, logo, etc...) + // ---------------------------------- + + /** The URL of the app's logo */ + logo_url?: URL, + + /** The URL of the app's Terms of Service */ + tos_url?: URL, + + /** The URL of the app's Privacy Policy */ + policy_url?: URL, + + // ------------------------------ + // Webhooks (async notifications) + // ------------------------------ + + /** The webhooks supported by the app */ + webhooks?: Webhook[], + + // ------------------------------------------ + // Purchasable Stuff (Subscriptions + Addons) + // ------------------------------------------ + + /** If a subscription is required */ + subscriptionRequired?: boolean, + + /** The subscription tiers available for the app */ + subscriptionTiers?: AppSubscriptionTier[], + + /** Addons offered by the app */ + addons?: Addon[], }; export class OAuthApp extends App { @@ -54,19 +104,21 @@ export class OAuthApp extends App { private baseAppUrl?: URL; private appAbbrv?: string; - private subscriptionRequired?: boolean; - private subscriptionTiers?: AppSubscriptionTier[]; private appName?: string | { [language: string]: string }; private contacts?: string[]; private scopes?: Scopes[]; - private logo_url?: URL; - private tos_url?: URL; - private policy_url?: URL; private vault_type?: "azure" | "hashicorp" | "file"; private auth_default_method?: "query" | "form_post" | "PAR"; private auth_default_use_JWT?: boolean; private auth_default_response_mode?: 'query' | 'fragment' | 'form_post'; private client_secret?: string; + private logo_url?: URL; + private tos_url?: URL; + private policy_url?: URL; + private webhooks?: Webhook[]; + private subscriptionRequired?: boolean; + private subscriptionTiers?: AppSubscriptionTier[]; + private addons?: Addon[]; private client: Client; @@ -86,12 +138,12 @@ export class OAuthApp extends App { * | APP_ABBRV | Required if `options.appAbbrv` isn't provided | The abbreviation of the client (app) | * | APP_NAME | Required if `options.appName` isn't provided | The name of the client (only a single non-localized name is supported) | * | CONTACT_EMAIL | Required if `options.contacts` isn't provided | A comma-separated list of email addresses to list as contacts for the client | - * | LOGO_URL | Optional (used if `options.logo_url` isn't specified) | The URL of the client's (app's) logo | - * | TOS_URL | Optional (used if `options.tos_url` isn't specified) | The URL of the client's terms of service | - * | POLICY_URL | Optional (used if `options.policy_url` isn't specified) | The URL of the client's privacy policy | * | SCOPES | Required if `options.scopes` isn't provided | A comma-separated list of scopes available to the client | * | VAULT_TYPE | Required if in production (`NODE_ENV` is `production`) | The type of vault to use for the keystore (one of azure, hashicorp, or file) | * | APP_SECRET | Optional (Determines if app should register or not) | The client secret for the app (if this IS set registration WON'T be done) | + * | LOGO_URL | Optional (used if `options.logo_url` isn't specified) | The URL of the client's (app's) logo | + * | TOS_URL | Optional (used if `options.tos_url` isn't specified) | The URL of the client's terms of service | + * | POLICY_URL | Optional (used if `options.policy_url` isn't specified) | The URL of the client's privacy policy | * * @param onAuth The callback to call when a user logs in * @param saveSecret The callback to call to save the secret @@ -101,16 +153,24 @@ export class OAuthApp extends App { * @param options.appName The name of the app * @param options.contacts The email addresses of the contacts for the app * @param options.scopes The scopes an app token COULD ask for (token scopes would have to ask for this or a subset of this list) - * @param options.logo_url The URL of the app's logo - * @param options.tos_url The URL of the app's terms of service - * @param options.policy_url The URL of the app's privacy policy * @param options.vault_type The type of vault to use for the keystore * @param options.auth_default_method The default method for authentication * @param options.auth_default_use_JWT Whether to use JWT as the default authentication method * @param options.auth_default_response_mode The default response mode for authentication * @param options.client_secret The client secret for the app (if this IS set registration WON'T be done. Because re-registering isn't supported) + * @param options.logo_url The URL of the app's logo + * @param options.tos_url The URL of the app's terms of service + * @param options.policy_url The URL of the app's privacy policy + * @param options.webhooks The webhooks supported by the app + * @param options.subscriptionRequired If a subscription is required + * @param options.subscriptionTiers The subscription tiers available for the app + * @param options.addons Addons offered by the app */ - constructor(onAuth: OnAuthCallback, saveSecret: (secret: string) => void | Promise, options?: OAuthAppOptions) { + constructor( + onAuth: OnAuthCallback, + saveSecret: (secret: string) => void | Promise, + options?: OAuthAppOptions + ) { super(); this.onAuth = onAuth; this.saveSecret = saveSecret; @@ -118,19 +178,21 @@ export class OAuthApp extends App { if(typeof options !== 'undefined') { this.baseAppUrl = options.baseAppUrl; this.appAbbrv = options.appAbbrv; - this.subscriptionRequired = options.subscriptionRequired; - this.subscriptionTiers = options.subscriptionTiers; this.appName = options.appName; this.contacts = options.contacts; this.scopes = options.scopes; - this.logo_url = options.logo_url; - this.tos_url = options.tos_url; - this.policy_url = options.policy_url; this.vault_type = options.vault_type; this.auth_default_method = options.auth_default_method; this.auth_default_use_JWT = options.auth_default_use_JWT; this.auth_default_response_mode = options.auth_default_response_mode; this.client_secret = options.client_secret; + this.logo_url = options.logo_url; + this.tos_url = options.tos_url; + this.policy_url = options.policy_url; + this.webhooks = options.webhooks; + this.subscriptionRequired = options.subscriptionRequired; + this.subscriptionTiers = options.subscriptionTiers; + this.addons = options.addons; } } @@ -175,7 +237,9 @@ export class OAuthApp extends App { { client_abbreviation: appAbbrv, subscription_required: this.subscriptionRequired ?? false, - subscription_tiers: this.subscriptionTiers + subscription_tiers: this.subscriptionTiers, + addons: this.addons, + webhooks: this.webhooks }, this.appName, this.scopes, diff --git a/src/oauth/index.ts b/src/oauth/index.ts index 6912d48..68e9919 100644 --- a/src/oauth/index.ts +++ b/src/oauth/index.ts @@ -5,7 +5,7 @@ import type { Webhook } from './types/Webhook.js'; import { WebhookPurpose } from './types/WebhookPurpose.js'; import type { Addon } from './types/addons/Addon.js'; import { AddonType } from './types/addons/AddonType.js'; -import { BillingCadence as AddonBillingCadence } from './types/addons/BillingCadence.js'; +import { BillingCadence as AddonBillingCadence } from './types/BillingCadence.js'; import type { Schema as AddonSchema } from './types/addons/Schema.js'; import type { PricingStrategy } from './types/addons/PricingStrategy.js'; diff --git a/src/oauth/types/AppSubscriptionTier.ts b/src/oauth/types/AppSubscriptionTier.ts index 39f29bd..6939511 100644 --- a/src/oauth/types/AppSubscriptionTier.ts +++ b/src/oauth/types/AppSubscriptionTier.ts @@ -1,11 +1,44 @@ -/** App subscription tier (mostly for app registration) */ +import { BillingCadence } from './BillingCadence.js'; + +/** App subscription tier */ export interface AppSubscriptionTier { - id: string; - name: string; // e.g., "Free", "Pro" - cost: number; // e.g., 1000 (cents) - currency: string; // e.g., "USD" - description?: string; - features?: { + /** + * A unique identifier for the subscription tier. + * This is only necessary if two tiers have the same name for some reason. + */ + id?: string; + + /** The (potentially localized) name of the subscription tier */ + name: string | { [locale: string]: string }; + + pricing: { + /** + * The cost of the subscription tier (in cents). + * Can have multiple prices for different billing frequencies (e.g., monthly, yearly). + */ + value: number | { [frequency in BillingCadence]?: number }; + + /** The currency of the subscription tier */ + currency: 'CAD' | 'USD' | 'EUR' | 'GBP' | 'AUD'; + + /** If value is a singular number a billing cadence can be set this way */ + billingCadence?: BillingCadence; + }; + + /** A (potentially localized) description of the subscription tier */ + description?: string | { [locale: string]: string }; + + /** + * Features included in the subscription tier. + * + * This can be a list of feature names (ex. ["prioritySupport", "customBranding"]) + * Or a mapping of feature names to booleans indicating inclusion (ex. { "prioritySupport": true, "customBranding": false }) + * + * The difference is in their presentation to the user. + * - As a list each tier just has it's list below it. + * - As a mapping, all features are listed in a comparison table format (where all included features are shown with checkmarks and crosses to indicate exclusion). + */ + features?: string[] | { [featureName: string]: boolean; // e.g., { "prioritySupport": true, "customBranding": false } } } \ No newline at end of file diff --git a/src/oauth/types/addons/BillingCadence.ts b/src/oauth/types/BillingCadence.ts similarity index 100% rename from src/oauth/types/addons/BillingCadence.ts rename to src/oauth/types/BillingCadence.ts diff --git a/src/oauth/types/addons/Addon.ts b/src/oauth/types/addons/Addon.ts index 7120f3b..bb50d7f 100644 --- a/src/oauth/types/addons/Addon.ts +++ b/src/oauth/types/addons/Addon.ts @@ -1,5 +1,5 @@ import { AddonType } from './AddonType.js'; -import { BillingCadence } from './BillingCadence.js'; +import { BillingCadence } from '../BillingCadence.js'; import { Schema } from './Schema.js'; import { PricingStrategy } from './PricingStrategy.js'; @@ -43,7 +43,12 @@ export type Addon = { // Subscription Specific Fields // ---------------------------- - billingCadence: BillingCadence; + /** + * Billing cadence for subscription addons. + * Note, if the pricing strategy has a `unit_amount` with multiple billing cadences, + * this field can be omitted / is ignored to allow the purchaser to select at buy time. + */ + billingCadence?: BillingCadence; } | { /** Type of addon/purchase */ type: AddonType.ONE_TIME; diff --git a/src/oauth/types/addons/PricingStrategy.ts b/src/oauth/types/addons/PricingStrategy.ts index cd5226a..0657ac2 100644 --- a/src/oauth/types/addons/PricingStrategy.ts +++ b/src/oauth/types/addons/PricingStrategy.ts @@ -1,3 +1,5 @@ +import { BillingCadence } from '../BillingCadence.js'; + /** The strategy/mechanism used to calculate the cost of a purchase of an addon at buy time */ export type PricingStrategy = { /** The currency in which the pricing is specified */ @@ -18,8 +20,14 @@ export type PricingStrategy = { */ type: 'per_unit', - /** Per Unit Price (in cents) */ - unit_amount: number, + /** + * Per Unit Price (in cents) + * + * Can be: + * - A single number (applies to all billing cadences) + * - Or an object specifying different amounts per billing cadence + */ + unit_amount: number | { [frequency in BillingCadence]?: number }, /** Parameter to use for unit calculation */ unit_parameter: string