diff --git a/lib/api.ts b/lib/api.ts index f074973..26988a3 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -4,7 +4,7 @@ import { Logger } from './logger'; export const api = async ( method: 'GET' | 'POST' | 'PATCH' | 'DELETE', host: string | API_REGION, - resource: string, + path: string, clientId: string, userId: string, hashedUserId?: string, @@ -12,9 +12,7 @@ export const api = async ( logger?: Logger ): Promise => { const token = generateBasicTokenForUser(clientId, userId, hashedUserId); - const url = `https://${host}/${clientId}/users/${encodeURIComponent( - userId - )}/${resource}`; + const url = `https://${host}${path.startsWith('/') ? path : `/${path}`}`; const headers = { Authorization: `Basic ${token}` @@ -34,7 +32,7 @@ export const api = async ( try { const res = await fetch(url, { method, - body: JSON.stringify(data), + ...(data !== undefined ? { body: JSON.stringify(data) } : {}), headers }); @@ -77,9 +75,9 @@ export const generateBasicTokenForUser = ( userId: string, hashedUserId?: string ) => { - const token = hashedUserId - ? btoa(clientId + ':' + userId + ':' + hashedUserId) - : btoa(clientId + ':' + userId); - - return token; + // Always send the 3-part widget token (clientId:userId:hashedUserId). The + // hash slot stays empty when secureMode is off. The trailing colon is what + // lets the backend distinguish a widget token from a server + // clientId:clientSecret token. + return btoa(clientId + ':' + userId + ':' + (hashedUserId ?? '')); }; diff --git a/lib/client.ts b/lib/client.ts index 65b710f..cd91918 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -7,8 +7,6 @@ import { DeliveryOptionsForEmail, DeliveryOptionsForInappWeb, PostUserRequest, - SlackChannel, - SlackUser, User, UserAccountMetadata, WS_REGION @@ -36,9 +34,6 @@ type NotificationAPIClientSDKConfig = { // Debug mode: debug: boolean; - - // SDK Dev Mode: - sdkDevMode: boolean; }; const defaultConfig: NotificationAPIClientSDKConfig = { @@ -53,10 +48,15 @@ const defaultConfig: NotificationAPIClientSDKConfig = { ).toISOString(), onNewInAppNotifications: undefined, keepWebSocketAliveForSeconds: 24 * 60 * 60, // 24 hours - debug: false, - sdkDevMode: false + debug: false }; +const userResourcePath = () => + `/users/${encodeURIComponent(NotificationAPIClientSDK.config.userId)}`; + +const endUserResourcePath = () => + `/enduser/${encodeURIComponent(NotificationAPIClientSDK.config.userId)}`; + type NotificationAPIClientSDK = { config: NotificationAPIClientSDKConfig; logger: Logger; @@ -129,17 +129,6 @@ type NotificationAPIClientSDK = { user: { get: () => Promise; }; - slack: { - getOAuthUrl: (props?: { destinationUrl?: string }) => string; - getChannels: () => Promise<{ - channels: SlackChannel[]; - users: SlackUser[]; - me: SlackUser; - hasMoreChannels?: boolean; - hasMoreUsers?: boolean; - }>; - setChannel: (channelId: string) => Promise; - }; }; export const NotificationAPIClientSDK: NotificationAPIClientSDK = { @@ -185,31 +174,41 @@ export const NotificationAPIClientSDK: NotificationAPIClientSDK = { getNotifications: function (before, count) { return NotificationAPIClientSDK.rest.generic( 'GET', - `notifications/INAPP_WEB?count=${count}&before=${before}` + `/enduser/inapp?count=${count}&before=${encodeURIComponent(before)}` ); }, patchNotifications: function (params) { return NotificationAPIClientSDK.rest.generic( 'PATCH', - 'notifications/INAPP_WEB', + '/enduser/inapp', params ); }, getPreferences: function () { - return NotificationAPIClientSDK.rest.generic('GET', 'preferences'); + return NotificationAPIClientSDK.rest.generic( + 'GET', + '/enduser/preferences' + ); }, postPreferences: function (params) { return NotificationAPIClientSDK.rest.generic( 'POST', - 'preferences', + '/enduser/preferences', params ); }, postUser: function (params: PostUserRequest) { - return NotificationAPIClientSDK.rest.generic('POST', '', params); + return NotificationAPIClientSDK.rest.generic( + 'POST', + endUserResourcePath(), + params + ); }, getUserAccountMetadata: function () { - return NotificationAPIClientSDK.rest.generic('GET', 'account_metadata'); + return NotificationAPIClientSDK.rest.generic( + 'GET', + '/enduser/account-metadata' + ); } }, websocket: { @@ -420,47 +419,10 @@ export const NotificationAPIClientSDK: NotificationAPIClientSDK = { user: { get: async () => { - return NotificationAPIClientSDK.rest.generic('GET', '') as Promise; - } - }, - - slack: { - getOAuthUrl: (props?: { destinationUrl?: string }) => { - const sdkDevMode = NotificationAPIClientSDK.config.sdkDevMode; - const domain = sdkDevMode - ? 'localhost:3001' - : NotificationAPIClientSDK.config.host.replace('api.', 'app.'); - // if no redirectUri is provided, use the current page's URL - const destination = props?.destinationUrl || window.location.href; - - const state = encodeURIComponent( - JSON.stringify({ - destination, - userId: NotificationAPIClientSDK.config.userId, - clientId: NotificationAPIClientSDK.config.clientId, - hashedUserId: NotificationAPIClientSDK.config.hashedUserId - }) - ); - - const url = - 'https://slack.com/oauth/v2/authorize?' + - 'client_id=1146598856352.8825220259395' + - '&scope=chat:write,channels:read,channels:join,chat:write.customize,chat:write.public,groups:read,im:read,users:read' + - `&redirect_uri=https://${domain}/slack/oauth/callback` + - `&state=${state}`; - return url; - }, - getChannels: async () => { - const response = await NotificationAPIClientSDK.rest.generic( + return NotificationAPIClientSDK.rest.generic( 'GET', - 'slack/channels' - ); - return response; - }, - setChannel: async (channelId: string) => { - return NotificationAPIClientSDK.identify({ - slackChannel: channelId - }); + userResourcePath() + ) as Promise; } } }; diff --git a/lib/interfaces.ts b/lib/interfaces.ts index 3e38896..69caefd 100644 --- a/lib/interfaces.ts +++ b/lib/interfaces.ts @@ -174,68 +174,11 @@ export interface User { email?: string; number?: string; pushTokens?: PushToken[]; - webPushTokens?: WebPushToken[]; lastSeenTime?: string; createdAt?: string; updatedAt?: string; - slackChannel?: string; - slackToken?: OauthV2AccessResponse; } -interface WebAPICallResult { - ok: boolean; - error?: string; - response_metadata?: { - warnings?: string[]; - next_cursor?: string; - scopes?: string[]; - acceptedScopes?: string[]; - retryAfter?: number; - messages?: string[]; - }; -} - -interface AuthedUser { - access_token?: string; - expires_in?: number; - id?: string; - refresh_token?: string; - scope?: string; - token_type?: string; -} - -interface Enterprise { - id?: string; - name?: string; -} - -export interface IncomingWebhook { - channel?: string; - channel_id?: string; - configuration_url?: string; - url?: string; -} - -type OauthV2AccessResponse = WebAPICallResult & { - access_token?: string; - app_id?: string; - authed_user?: AuthedUser; - bot_user_id?: string; - enterprise?: Enterprise; - error?: string; - expires_in?: number; - incoming_webhook?: IncomingWebhook; - is_enterprise_install?: boolean; - needed?: string; - ok?: boolean; - provided?: string; - refresh_token?: string; - scope?: string; - team?: Enterprise; - token_type?: string; - warning?: string; -}; - export type PostUserRequest = Omit< Partial, 'lastSeenTime' | 'createdAt' | 'updatedAt' @@ -261,90 +204,6 @@ export interface Device { model?: string; } -export interface PushSubscription { - endpoint: string; - keys: { - p256dh: string; - auth: string; - }; -} - -export interface WebPushToken { - sub: PushSubscription; -} export interface UserAccountMetadata { logo: string; - environmentVapidPublicKey: string; - hasWebPushEnabled: boolean; -} - -/* Slack Types Start */ -export interface SlackChannel { - context_team_id?: string; - conversation_host_id?: string; - created?: number; - creator?: string; - id?: string; - internal_team_ids?: string[]; - is_archived?: boolean; - is_channel?: boolean; - is_ext_shared?: boolean; - is_general?: boolean; - is_global_shared?: boolean; - is_group?: boolean; - is_im?: boolean; - is_member?: boolean; - is_moved?: number; - is_mpim?: boolean; - is_org_default?: boolean; - is_org_mandatory?: boolean; - is_org_shared?: boolean; - is_pending_ext_shared?: boolean; - is_private?: boolean; - is_shared?: boolean; - is_user_deleted?: boolean; - name?: string; - name_normalized?: string; - num_members?: number; - pending_connected_team_ids?: string[]; - pending_shared?: string[]; - previous_names?: string[]; - priority?: number; - properties?: unknown; - purpose?: unknown; - shared_team_ids?: string[]; - topic?: unknown; - unlinked?: number; - updated?: number; - user?: string; -} - -export interface SlackUser { - color?: string; - deleted?: boolean; - enterprise_user?: unknown; - has_2fa?: boolean; - id?: string; - is_admin?: boolean; - is_app_user?: boolean; - is_bot?: boolean; - is_connector_bot?: boolean; - is_email_confirmed?: boolean; - is_invited_user?: boolean; - is_owner?: boolean; - is_primary_owner?: boolean; - is_restricted?: boolean; - is_ultra_restricted?: boolean; - is_workflow_bot?: boolean; - locale?: string; - name?: string; - profile?: unknown; - real_name?: string; - team_id?: string; - two_factor_type?: string; - tz?: string; - tz_label?: string; - tz_offset?: number; - updated?: number; - who_can_share_contact_card?: string; } diff --git a/package-lock.json b/package-lock.json index 80f52d5..d259bdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@notificationapi/core", - "version": "1.2.9", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@notificationapi/core", - "version": "1.2.9", + "version": "2.0.0", "devDependencies": { "@types/jest": "^29.5.3", "@types/node": "^20.14.10", diff --git a/package.json b/package.json index 40f0c48..c04c360 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@notificationapi/core", - "version": "1.2.9", + "version": "2.0.0", "type": "module", "main": "dist/main.js", "types": "dist/main.d.ts", diff --git a/tests/sdk.test.ts b/tests/sdk.test.ts index a04faf7..e6f99ed 100644 --- a/tests/sdk.test.ts +++ b/tests/sdk.test.ts @@ -38,7 +38,7 @@ describe('NotificationAPIClientSDK', () => { expect(global.fetch).toHaveBeenCalledWith( expect.stringContaining( - 'notifications/INAPP_WEB?count=100&before=2023-01-01T00:00:00Z' + '/enduser/inapp?count=100&before=2023-01-01T00%3A00%3A00Z' ), expect.any(Object) ); @@ -83,7 +83,7 @@ describe('NotificationAPIClientSDK', () => { }); expect(global.fetch).toHaveBeenCalledWith( - 'https://api.notificationapi.com/testClient/users/testUser/notifications/INAPP_WEB', + 'https://api.notificationapi.com/enduser/inapp', expect.objectContaining({ method: 'PATCH', body: JSON.stringify({ @@ -113,7 +113,7 @@ describe('NotificationAPIClientSDK', () => { }); expect(global.fetch).toHaveBeenCalledWith( - 'https://api.notificationapi.com/testClient/users/testUser/notifications/INAPP_WEB', + 'https://api.notificationapi.com/enduser/inapp', expect.objectContaining({ method: 'PATCH', body: JSON.stringify({ @@ -143,7 +143,7 @@ describe('NotificationAPIClientSDK', () => { }); expect(global.fetch).toHaveBeenCalledWith( - 'https://api.notificationapi.com/testClient/users/testUser/notifications/INAPP_WEB', + 'https://api.notificationapi.com/enduser/inapp', expect.objectContaining({ method: 'PATCH', body: expect.stringContaining('"opened":"'), @@ -170,7 +170,7 @@ describe('NotificationAPIClientSDK', () => { }); expect(global.fetch).toHaveBeenCalledWith( - 'https://api.notificationapi.com/testClient/users/testUser/notifications/INAPP_WEB', + 'https://api.notificationapi.com/enduser/inapp', expect.objectContaining({ method: 'PATCH', body: JSON.stringify({ @@ -203,7 +203,7 @@ describe('NotificationAPIClientSDK', () => { }); expect(global.fetch).toHaveBeenCalledWith( - 'https://api.notificationapi.com/testClient/users/testUser/notifications/INAPP_WEB', + 'https://api.notificationapi.com/enduser/inapp', expect.objectContaining({ method: 'PATCH', body: JSON.stringify({ @@ -234,7 +234,7 @@ describe('NotificationAPIClientSDK', () => { await sdk.getPreferences(); expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('preferences'), + expect.stringContaining('/enduser/preferences'), expect.objectContaining({ method: 'GET' }) @@ -259,7 +259,7 @@ describe('NotificationAPIClientSDK', () => { }); expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('preferences'), + expect.stringContaining('/enduser/preferences'), expect.objectContaining({ method: 'POST', body: JSON.stringify([ @@ -273,7 +273,7 @@ describe('NotificationAPIClientSDK', () => { ); }); - test('updateDeliveryOption should support SLACK channel', async () => { + test('updateDeliveryOption should support PUSH channel', async () => { const mockResponse = { json: jest.fn().mockResolvedValue({}) }; @@ -285,19 +285,19 @@ describe('NotificationAPIClientSDK', () => { }); await sdk.updateDeliveryOption({ - notificationId: 'testSlackNotification', - channel: 'SLACK', + notificationId: 'testPushNotification', + channel: 'PUSH', delivery: 'hourly' }); expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('preferences'), + expect.stringContaining('/enduser/preferences'), expect.objectContaining({ method: 'POST', body: JSON.stringify([ { - notificationId: 'testSlackNotification', - channel: 'SLACK', + notificationId: 'testPushNotification', + channel: 'PUSH', delivery: 'hourly' } ]) @@ -323,7 +323,7 @@ describe('NotificationAPIClientSDK', () => { expect(global.fetch).toHaveBeenCalledWith( expect.stringContaining( - 'https://api.notificationapi.com/testClient/users/testUser/' + 'https://api.notificationapi.com/enduser/testUser' ), expect.objectContaining({ method: 'POST', @@ -363,35 +363,13 @@ describe('NotificationAPIClientSDK', () => { expect(global.fetch).toHaveBeenCalledWith( expect.stringContaining( - 'https://api.notificationapi.com/testClient/users/testUser/account_metadata' + 'https://api.notificationapi.com/enduser/account-metadata' ), expect.objectContaining({ method: 'GET' }) ); }); - test('slack.getChannels should return hasMoreChannels and hasMoreUsers from API', async () => { - const mockResponse = { - json: jest.fn().mockResolvedValue({ - channels: [{ id: 'C123', name: 'general' }], - users: [{ id: 'U123', name: 'alice' }], - me: { id: 'U123', name: 'alice' }, - hasMoreChannels: true, - hasMoreUsers: false - }) - }; - (global.fetch as jest.Mock).mockResolvedValue(mockResponse); - - const sdk = NotificationAPIClientSDK.init({ - userId: 'testUser', - clientId: 'testClient' - }); - - const result = await sdk.slack.getChannels(); - - expect(result.hasMoreChannels).toBe(true); - expect(result.hasMoreUsers).toBe(false); - }); }); describe('api function', () => { @@ -404,14 +382,14 @@ describe('api function', () => { const result = await api( 'GET', 'api.notificationapi.com', - 'testResource', + '/testResource', 'testClient', 'testUser', 'hashedTestUser' ); expect(global.fetch).toHaveBeenCalledWith( - 'https://api.notificationapi.com/testClient/users/testUser/testResource', + 'https://api.notificationapi.com/testResource', expect.objectContaining({ method: 'GET', headers: { @@ -430,7 +408,7 @@ describe('api function', () => { await api( 'GET', 'api.notificationapi.com', - 'testResource', + '/testResource', 'testClient', 'testUser' ); @@ -449,7 +427,7 @@ describe('api function', () => { const result = await api( 'GET', 'api.notificationapi.com', - 'testResource', + '/testResource', 'testClient', 'testUser' );