From ad6abdd4167065eeb5d38b4e7bb922d9f01e305e Mon Sep 17 00:00:00 2001 From: Mohit Tejani Date: Tue, 6 Oct 2026 12:47:32 +0530 Subject: [PATCH 1/7] updated grantToken and parseToken to support categories permissions for channels and uuids resources --- lib/types/index.d.ts | 123 +++++++++++++++++- src/core/components/token_manager.ts | 31 +++++ .../endpoints/access_manager/grant_token.ts | 43 +++++- src/core/types/api/access-manager.ts | 122 ++++++++++++++++- .../components/token_manager.test.ts | 52 ++++++++ .../integration/endpoints/grant_token.test.ts | 37 ++++++ .../access_manager_grant_token.test.ts | 68 ++++++++++ 7 files changed, 467 insertions(+), 9 deletions(-) diff --git a/lib/types/index.d.ts b/lib/types/index.d.ts index aa60c77c5..c8613331e 100644 --- a/lib/types/index.d.ts +++ b/lib/types/index.d.ts @@ -10022,6 +10022,38 @@ declare namespace PubNub { authorizedUserId?: never; }; + /** + * Category-level token permissions. + * + * Grants enumeration of every resource of that type. A resource-level or pattern `get` does not + * grant this permission. The only supported operation is `get`, which the wire encodes as `32`. + */ + export type CategoryTokenPermissions = { + /** + * Whether listing every resource of this type is permitted. + * + * A resource-level or pattern `get` does not grant this permission. + */ + get?: boolean; + }; + + /** + * Category-level grants. + * + * Permissions apply to a resource type as a whole rather than to a named resource or a RegEx + * pattern. Only `channels` and `uuids` are supported, and only the `get` operation. + */ + export type GrantCategories = { + /** + * Enumeration permission for channel metadata (`GET /v2/objects/{sub_key}/channels`). + */ + channels?: CategoryTokenPermissions; + /** + * Enumeration permission for uuid metadata (`GET /v2/objects/{sub_key}/uuids`). + */ + uuids?: CategoryTokenPermissions; + }; + /** * Generate token with permissions. * @@ -10047,6 +10079,13 @@ declare namespace PubNub { * Keys within each scope are RegEx patterns. `users` and `uuids` are mutually exclusive. */ patterns?: GrantScopes; + /** + * Category-level permissions. + * + * Grants enumeration of every channel or uuid on the subscribe key. A resource-level or pattern + * `get` does not imply this permission. A grant that carries only `categories` is valid. + */ + categories?: GrantCategories; /** * Extra metadata to be published with the request. * @@ -10127,6 +10166,13 @@ declare namespace PubNub { */ dataSync?: DataSyncScopePermissions; }; + /** + * Category-level permissions. + * + * Decoded from the token `cat` section. Present only when the grant requested category-level + * enumeration. `chan` maps to `channels` and `uuid` maps to `uuids`. + */ + categories?: TokenCategories; /** * The uuid that is exclusively authorized to use this token to make API requests. */ @@ -10141,6 +10187,36 @@ declare namespace PubNub { meta?: Payload; }; + /** + * Decoded category-level permissions. + * + * `get` is `true` when the token category bitmask includes `32`. + */ + export type CategoryPermissions = { + /** + * Whether listing every resource of this type is permitted. + * + * A resource-level or pattern `get` does not grant this permission. + */ + get: boolean; + }; + + /** + * Decoded category-level permission scopes. + * + * Decoded from the token `cat` wire keys `chan` and `uuid`. + */ + export type TokenCategories = { + /** + * Enumeration permission for channel metadata. + */ + channels?: CategoryPermissions; + /** + * Enumeration permission for uuid metadata. + */ + uuids?: CategoryPermissions; + }; + /** * Granted resource permissions. * @@ -10363,7 +10439,14 @@ declare namespace PubNub { * Common permissions audit response content. */ type BaseAuditResponse< - Level extends 'channel' | 'channel+auth' | 'channel-group' | 'channel-group+auth' | 'user' | 'subkey', + Level extends + | 'channel' + | 'channel+auth' + | 'channel-group' + | 'channel-group+auth' + | 'user' + | 'subkey' + | 'category', > = { /** * Permissions level. @@ -10377,6 +10460,13 @@ declare namespace PubNub { * Duration for which permissions has been granted. */ ttl?: number; + /** + * Category-level permissions. + * + * Present when the grant requested enumeration of every channel or uuid. A resource-level `get` + * does not populate this map. A combined grant may keep its existing `level` and still include it. + */ + categories?: CategoryPermissionsMap; }; /** @@ -10389,6 +10479,13 @@ declare namespace PubNub { auths: Record; }; + /** + * Category-level permissions keyed by `channels` or `uuids`. + * + * Each entry carries auth-key bits. Only `g` is meaningful for a category grant. + */ + type CategoryPermissionsMap = Record<'channels' | 'uuids', AuthKeysPermissions>; + /** * Single channel permissions audit result. */ @@ -10457,6 +10554,19 @@ declare namespace PubNub { objects: Record>; }; + /** + * Category-level permissions grant result. + * + * Returned when the grant requested enumeration of every channel or uuid and no other resource + * level is reported. + */ + type CategoryPermissionsResponse = BaseAuditResponse<'category'> & { + /** + * Per-category auth-key permissions. + */ + categories: CategoryPermissionsMap; + }; + /** * Response with permission information. */ @@ -10466,7 +10576,8 @@ declare namespace PubNub { | ChannelGroupPermissionsResponse | ChannelGroupsPermissionsResponse | UserPermissionsResponse - | SubKeyPermissionsResponse; + | SubKeyPermissionsResponse + | CategoryPermissionsResponse; /** * Audit permissions for provided auth keys / global permissions. @@ -10510,6 +10621,14 @@ declare namespace PubNub { * List of App Context UUID for which permissions should be granted. */ uuids?: string[]; + /** + * Categories for which enumeration permission should be granted. + * + * `channels` lists all channel metadata. `uuids` lists all uuid metadata. Requires {@link authKeys}. + * Only `get` may be `true` in the same request; `ttl: 1` with `get: true` is rejected. A + * resource-level `get` does not grant this permission. Sent as the `category` query parameter. + */ + categories?: Array<'channels' | 'uuids'>; /** * List of auth keys for which permissions should be granted on specified objects. * diff --git a/src/core/components/token_manager.ts b/src/core/components/token_manager.ts index ed52bd6f1..a4ed4ac94 100644 --- a/src/core/components/token_manager.ts +++ b/src/core/components/token_manager.ts @@ -72,6 +72,14 @@ type RawToken = { * Additional information which has been added to the token. */ meta?: Payload; + + /** + * Category-level permissions. + * + * Present only when the grant requested enumeration of every channel (`chan`) or uuid (`uuid`). + * Values are the `GET` bitmask (`32`). + */ + cat?: Partial>; }; // endregion @@ -203,6 +211,9 @@ export class TokenManager { const patternDataSync = this.extractDataSyncScopes(parsed.pat); if (patternDataSync) (result.patterns ??= {}).dataSync = patternDataSync; + const categories = this.extractCategories(parsed.cat); + if (categories) result.categories = categories; + if (parsed.meta && Object.keys(parsed.meta).length > 0) result.meta = parsed.meta; return result; @@ -240,6 +251,26 @@ export class TokenManager { return permissionsResult; } + /** + * Extract category-level permissions from the token `cat` section. + * + * `chan` maps to `channels` and `uuid` maps to `uuids`. `get` is set from the `32` bit. The + * section is omitted when `cat` is absent or empty. + * + * @param cat - Raw `cat` section decoded from the token. + * + * @returns Human-readable category permissions, or `undefined` when none are present. + */ + private extractCategories(cat?: Partial>): PAM.TokenCategories | undefined { + if (!cat) return undefined; + + const result: PAM.TokenCategories = {}; + if (typeof cat.chan === 'number') result.channels = { get: (cat.chan & 32) === 32 }; + if (typeof cat.uuid === 'number') result.uuids = { get: (cat.uuid & 32) === 32 }; + + return Object.keys(result).length > 0 ? result : undefined; + } + /** * Extract DataSync permission scopes from a token permissions section. * diff --git a/src/core/endpoints/access_manager/grant_token.ts b/src/core/endpoints/access_manager/grant_token.ts index 3900da324..998a10742 100644 --- a/src/core/endpoints/access_manager/grant_token.ts +++ b/src/core/endpoints/access_manager/grant_token.ts @@ -80,6 +80,12 @@ type PermissionPayload = { */ type ProjectionsPayload = Record<'res' | 'pat', Record>; +/** + * + * A single `GET` per resource type. Only `channels` and `uuids` are valid. + */ +type CategoriesPayload = Partial>; + /** * Wire-level `meta` section. * @@ -143,14 +149,15 @@ export class GrantTokenRequest extends AbstractRequest { @@ -203,7 +210,8 @@ export class GrantTokenRequest extends AbstractRequest> = {}; + const permissions: Record | CategoriesPayload> = + {}; const resourcePermissions: PermissionPayload = {}; const patternPermissions: PermissionPayload = {}; const mapPermissions = ( @@ -277,6 +285,11 @@ export class GrantTokenRequest extends AbstractRequest 0 ? result : undefined; } + /** + * Build the `permissions.categories` payload. + * + * Category grants are a single integer per resource type, not a per-id bitmask. Only `get: true` + * is encoded, Any other runtime flag is ignored so a non-TypeScript caller cannot send a + * value other than `32`. The payload is omitted entirely when no category is granted. + * + * @returns Encoded categories payload, or `undefined` when no category `get` is set. + */ + private buildCategories(): CategoriesPayload | undefined { + const categories = 'categories' in this.parameters ? this.parameters.categories : undefined; + if (!categories) return undefined; + + const result: CategoriesPayload = {}; + if (categories.channels?.get === true) result.channels = 32; + if (categories.uuids?.get === true) result.uuids = 32; + + return Object.keys(result).length > 0 ? result : undefined; + } + /** * Extract permissions bit from permission configuration object. * diff --git a/src/core/types/api/access-manager.ts b/src/core/types/api/access-manager.ts index f2a190320..0cbeeecfa 100644 --- a/src/core/types/api/access-manager.ts +++ b/src/core/types/api/access-manager.ts @@ -392,6 +392,39 @@ type AuthorizedPrincipal = authorizedUserId?: never; }; +/** + * Category-level token permissions. + * + * Grants enumeration of every resource of that type. A resource-level or pattern `get` does not + * grant this permission. The only supported operation is `get`, which the wire encodes as `32`. + */ +export type CategoryTokenPermissions = { + /** + * Whether listing every resource of this type is permitted. + * + * A resource-level or pattern `get` does not grant this permission. + */ + get?: boolean; +}; + +/** + * Category-level grants. + * + * Permissions apply to a resource type as a whole rather than to a named resource or a RegEx + * pattern. Only `channels` and `uuids` are supported, and only the `get` operation. + */ +export type GrantCategories = { + /** + * Enumeration permission for channel metadata (`GET /v2/objects/{sub_key}/channels`). + */ + channels?: CategoryTokenPermissions; + + /** + * Enumeration permission for uuid metadata (`GET /v2/objects/{sub_key}/uuids`). + */ + uuids?: CategoryTokenPermissions; +}; + /** * Generate token with permissions. * @@ -420,6 +453,14 @@ type BaseGrantTokenParameters = { */ patterns?: GrantScopes; + /** + * Category-level permissions. + * + * Grants enumeration of every channel or uuid on the subscribe key. A resource-level or pattern + * `get` does not imply this permission. A grant that carries only `categories` is valid. + */ + categories?: GrantCategories; + /** * Extra metadata to be published with the request. * @@ -516,6 +557,14 @@ export type Token = { dataSync?: DataSyncScopePermissions; }; + /** + * Category-level permissions. + * + * Decoded from the token `cat` section. Present only when the grant requested category-level + * enumeration. `chan` maps to `channels` and `uuid` maps to `uuids`. + */ + categories?: TokenCategories; + /** * The uuid that is exclusively authorized to use this token to make API requests. */ @@ -532,6 +581,37 @@ export type Token = { meta?: Payload; }; +/** + * Decoded category-level permissions. + * + * `get` is `true` when the token category bitmask includes `32`. + */ +export type CategoryPermissions = { + /** + * Whether listing every resource of this type is permitted. + * + * A resource-level or pattern `get` does not grant this permission. + */ + get: boolean; +}; + +/** + * Decoded category-level permission scopes. + * + * Decoded from the token `cat` wire keys `chan` and `uuid`. + */ +export type TokenCategories = { + /** + * Enumeration permission for channel metadata. + */ + channels?: CategoryPermissions; + + /** + * Enumeration permission for uuid metadata. + */ + uuids?: CategoryPermissions; +}; + /** * Granted resource permissions. * @@ -784,7 +864,7 @@ type UserPermissions = { * Common permissions audit response content. */ type BaseAuditResponse< - Level extends 'channel' | 'channel+auth' | 'channel-group' | 'channel-group+auth' | 'user' | 'subkey', + Level extends 'channel' | 'channel+auth' | 'channel-group' | 'channel-group+auth' | 'user' | 'subkey' | 'category', > = { /** * Permissions level. @@ -800,6 +880,14 @@ type BaseAuditResponse< * Duration for which permissions has been granted. */ ttl?: number; + + /** + * Category-level permissions. + * + * Present when the grant requested enumeration of every channel or uuid. A resource-level `get` + * does not populate this map. A combined grant may keep its existing `level` and still include it. + */ + categories?: CategoryPermissionsMap; }; /** @@ -812,6 +900,13 @@ type AuthKeysPermissions = { auths: Record; }; +/** + * Category-level permissions keyed by `channels` or `uuids`. + * + * Each entry carries auth-key bits. Only `g` is meaningful for a category grant. + */ +type CategoryPermissionsMap = Record<'channels' | 'uuids', AuthKeysPermissions>; + /** * Single channel permissions audit result. */ @@ -882,6 +977,19 @@ type SubKeyPermissionsResponse = BaseAuditResponse<'subkey'> & { objects: Record>; }; +/** + * Category-level permissions grant result. + * + * Returned when the grant requested enumeration of every channel or uuid and no other resource + * level is reported. + */ +type CategoryPermissionsResponse = BaseAuditResponse<'category'> & { + /** + * Per-category auth-key permissions. + */ + categories: CategoryPermissionsMap; +}; + /** * Response with permission information. */ @@ -891,7 +999,8 @@ export type PermissionsResponse = | ChannelGroupPermissionsResponse | ChannelGroupsPermissionsResponse | UserPermissionsResponse - | SubKeyPermissionsResponse; + | SubKeyPermissionsResponse + | CategoryPermissionsResponse; // region Audit /** @@ -943,6 +1052,15 @@ export type GrantParameters = { */ uuids?: string[]; + /** + * Categories for which enumeration permission should be granted. + * + * `channels` lists all channel metadata. `uuids` lists all uuid metadata. Requires {@link authKeys}. + * Only `get` may be `true` in the same request; `ttl: 1` with `get: true` is rejected. A + * resource-level `get` does not grant this permission. Sent as the `category` query parameter. + */ + categories?: Array<'channels' | 'uuids'>; + /** * List of auth keys for which permissions should be granted on specified objects. * diff --git a/test/integration/components/token_manager.test.ts b/test/integration/components/token_manager.test.ts index be4d0fe7b..abcb91008 100644 --- a/test/integration/components/token_manager.test.ts +++ b/test/integration/components/token_manager.test.ts @@ -2,9 +2,27 @@ /* eslint no-console: 0 */ import assert from 'assert'; +import CborSync from 'cbor-sync'; import PubNub from '../../../src/node/index'; +/** + * Encode a minimal access token, including the `res` / `pat` keys `parseToken` always reads. + */ +const encodeToken = (overrides: Record = {}) => { + const encoded = CborSync.encode({ + v: 2, + t: 1628109699, + ttl: 60, + res: { chan: {}, grp: {} }, + pat: { chan: {}, grp: {} }, + sig: Buffer.alloc(32, 1), + ...overrides, + }); + + return Buffer.from(encoded).toString('base64'); +}; + describe('#components/token_manager', () => { let pubnub: PubNub; @@ -158,6 +176,40 @@ describe('#components/token_manager', () => { delete: true, }); }); + + it('contains category enumeration permissions', () => { + const permissions = pubnub.parseToken(encodeToken({ cat: { chan: 32, uuid: 32 } }))!; + + assert.deepEqual(permissions.categories, { + channels: { get: true }, + uuids: { get: true }, + }); + }); + + it('omits uuid categories when only channel enumeration is granted', () => { + const permissions = pubnub.parseToken(encodeToken({ cat: { chan: 32 } }))!; + + assert.deepEqual(permissions.categories?.channels, { get: true }); + assert.strictEqual(permissions.categories?.uuids, undefined); + }); + + it('omits categories when the token has no cat section', () => { + const permissions = pubnub.parseToken(encodeToken())!; + + assert.strictEqual(permissions.categories, undefined); + }); + + it('does not treat a resource-level get as a category grant', () => { + const permissions = pubnub.parseToken( + encodeToken({ + res: { chan: { 'channel-1': 32 }, grp: {} }, + pat: { chan: {}, grp: {} }, + }), + )!; + + assert.strictEqual(permissions.resources?.channels?.['channel-1']?.get, true); + assert.strictEqual(permissions.categories, undefined); + }); }); describe('supports token update', () => { diff --git a/test/integration/endpoints/grant_token.test.ts b/test/integration/endpoints/grant_token.test.ts index e29c7e5ed..1280827ce 100644 --- a/test/integration/endpoints/grant_token.test.ts +++ b/test/integration/endpoints/grant_token.test.ts @@ -89,6 +89,7 @@ describe('grant token endpoint', () => { resources: Record>; patterns: Record>; meta: Record; + categories?: { channels?: number; uuids?: number }; }; }; @@ -158,6 +159,10 @@ describe('grant token endpoint', () => { ); }); + it('fail on empty categories', async () => { + await assertRejects({ ttl: 1440, categories: {} }, 'Missing values for either Resources or Patterns'); + }); + it('should reject mixing `users` with `uuids`', async () => { await assertRejects( { @@ -617,5 +622,37 @@ describe('grant token endpoint', () => { }); }); }); + + describe('##categories', () => { + it('should grant only category enumeration', async () => { + const { scope, body } = mockGrant(); + + const token = await pubnub.grantToken({ + ttl: 1440, + categories: { + channels: { get: true }, + uuids: { get: true }, + }, + }); + + assert.strictEqual(token, 'token'); + assert.strictEqual(scope.isDone(), true); + assert.deepEqual(body().permissions.categories, { channels: GET, uuids: GET }); + }); + + it('should encode categories alongside channel resources', async () => { + const { scope, body } = mockGrant(); + + await pubnub.grantToken({ + ttl: 1440, + resources: { channels: { 'channel-1': { read: true } } }, + categories: { uuids: { get: true } }, + }); + + assert.strictEqual(scope.isDone(), true); + assert.deepEqual(body().permissions.resources.channels, { 'channel-1': READ }); + assert.deepEqual(body().permissions.categories, { uuids: GET }); + }); + }); }); }); diff --git a/test/unit/access_manager/access_manager_grant_token.test.ts b/test/unit/access_manager/access_manager_grant_token.test.ts index a288b5bd9..60eb82c89 100644 --- a/test/unit/access_manager/access_manager_grant_token.test.ts +++ b/test/unit/access_manager/access_manager_grant_token.test.ts @@ -129,6 +129,37 @@ describe('GrantTokenRequest', () => { }); assert.equal(validBothRequest.validate(), undefined); }); + + it('should pass validation with only categories', () => { + const request = new GrantTokenRequest({ + keySet: defaultKeySet, + ttl: 60, + categories: { + channels: { get: true }, + }, + }); + assert.equal(request.validate(), undefined); + }); + + it('should reject empty categories', () => { + const request = new GrantTokenRequest({ + keySet: defaultKeySet, + ttl: 60, + categories: {}, + }); + assert.equal(request.validate(), 'Missing values for either Resources or Patterns'); + }); + + it('should reject categories when get is false', () => { + const request = new GrantTokenRequest({ + keySet: defaultKeySet, + ttl: 60, + categories: { + channels: { get: false }, + }, + }); + assert.equal(request.validate(), 'Missing values for either Resources or Patterns'); + }); }); describe('terminology synonym validation', () => { @@ -852,6 +883,43 @@ describe('GrantTokenRequest', () => { const body = parseBodyAsString(transportRequest.body!); assert.equal(body.permissions.resources.channels['channel-engineering-001'], 16); }); + + it('should encode category get permissions as 32', () => { + const request = new GrantTokenRequest({ + keySet: defaultKeySet, + ttl: 60, + categories: { + channels: { get: true }, + uuids: { get: true }, + }, + }); + + const transportRequest = request.request(); + const body = parseBodyAsString(transportRequest.body!); + assert.deepEqual(body.permissions.categories, { channels: 32, uuids: 32 }); + }); + + it('should omit categories when none are granted', () => { + const request = new GrantTokenRequest(defaultParameters); + + const transportRequest = request.request(); + const body = parseBodyAsString(transportRequest.body!); + assert.equal(body.permissions.categories, undefined); + }); + + it('should encode categories alongside channel resources', () => { + const request = new GrantTokenRequest({ + ...defaultParameters, + categories: { + uuids: { get: true }, + }, + }); + + const transportRequest = request.request(); + const body = parseBodyAsString(transportRequest.body!); + assert.equal(body.permissions.resources.channels.test_channel, 3); + assert.deepEqual(body.permissions.categories, { uuids: 32 }); + }); }); describe('response parsing', () => { From ab4efcf2e4fe7d4b7d96b727fc5f8ba722ddf683 Mon Sep 17 00:00:00 2001 From: Mohit Tejani Date: Tue, 6 Oct 2026 12:48:11 +0530 Subject: [PATCH 2/7] grant method update for categories permission support --- src/core/endpoints/access_manager/grant.ts | 21 ++ test/integration/endpoints/access.test.ts | 118 +++++++++++ .../access_manager_grant.test.ts | 190 ++++++++++++++++++ 3 files changed, 329 insertions(+) create mode 100644 test/unit/access_manager/access_manager_grant.test.ts diff --git a/src/core/endpoints/access_manager/grant.ts b/src/core/endpoints/access_manager/grant.ts index 731c68091..2003513ec 100644 --- a/src/core/endpoints/access_manager/grant.ts +++ b/src/core/endpoints/access_manager/grant.ts @@ -105,6 +105,7 @@ export class GrantRequest extends AbstractRequest 0) { + if (authKeys.length === 0) return 'authKeys are required for grant request on categories'; + if (categories.some((category) => category !== 'channels' && category !== 'uuids')) + return 'Invalid category: only channels and uuids are supported'; + if (read || write || manage || del || update || join) + return 'Category permissions must be exactly the get permission'; + if (ttl === 1 && get) return 'One-minute category grants are not supported'; + } } async parse(response: TransportResponse): Promise { @@ -149,6 +168,7 @@ export class GrantRequest extends AbstractRequest 0 ? { 'channel-group': channelGroups.join(',') } : {}), ...(authKeys && authKeys?.length > 0 ? { auth: authKeys.join(',') } : {}), ...(uuids && uuids?.length > 0 ? { 'target-uuid': uuids.join(',') } : {}), + ...(categories && categories.length > 0 ? { category: categories.join(',') } : {}), r: read ? '1' : '0', w: write ? '1' : '0', m: manage ? '1' : '0', diff --git a/test/integration/endpoints/access.test.ts b/test/integration/endpoints/access.test.ts index df879d7f4..9cba71482 100644 --- a/test/integration/endpoints/access.test.ts +++ b/test/integration/endpoints/access.test.ts @@ -518,6 +518,124 @@ describe('access endpoints', () => { }); }); }); + + it('issues the correct RESTful request for categories', (done) => { + const scope = utils + .createNock() + .get('/v2/auth/grant/sub-key/mySubscribeKey') + .query( + (query) => + query.category === 'channels,uuids' && + query.auth === 'auth_key' && + query.g === '1' && + query.r === '0' && + query.channel === undefined && + query['channel-group'] === undefined && + query['target-uuid'] === undefined, + ) + .reply( + 200, + JSON.stringify({ + message: 'Success', + payload: { + level: 'category', + subscribe_key: 'mySubscribeKey', + ttl: 1440, + categories: { + channels: { auths: { auth_key: { r: 0, w: 0, m: 0, d: 0, g: 1, u: 0, j: 0 } } }, + uuids: { auths: { auth_key: { r: 0, w: 0, m: 0, d: 0, g: 1, u: 0, j: 0 } } }, + }, + }, + service: 'Access Manager', + status: 200, + }), + { 'content-type': 'text/javascript' }, + ); + + pubnub.grant({ categories: ['channels', 'uuids'], authKeys: ['auth_key'], get: true }, (status, response) => { + try { + assert.equal(status.error, false); + assert.equal(response!.level, 'category'); + if (response?.level === 'category') assert.equal(response.categories.channels.auths.auth_key.g, 1); + assert.equal(scope.isDone(), true); + done(); + } catch (error) { + done(error); + } + }); + }); + + it('issues a combined channel and category grant', (done) => { + const scope = utils + .createNock() + .get('/v2/auth/grant/sub-key/mySubscribeKey') + .query((query) => query.channel === 'ch1' && query.category === 'uuids' && query.g === '1' && query.r === '0') + .reply( + 200, + JSON.stringify({ + message: 'Success', + payload: { level: 'channel', subscribe_key: 'mySubscribeKey', channels: {} }, + service: 'Access Manager', + status: 200, + }), + { 'content-type': 'text/javascript' }, + ); + + pubnub.grant({ channels: ['ch1'], categories: ['uuids'], authKeys: ['auth_key'], get: true }, (status) => { + try { + assert.equal(status.error, false); + assert.equal(scope.isDone(), true); + done(); + } catch (error) { + done(error); + } + }); + }); + + it('rejects a category grant without authKeys', (done) => { + const scope = utils.createNock().get('/v2/auth/grant/sub-key/mySubscribeKey').query(true).reply(200, {}); + + pubnub.grant({ categories: ['channels'], get: true }, (status) => { + try { + assert.equal(status.error, true); + assert.equal(status.message, 'authKeys are required for grant request on categories'); + assert.equal(scope.isDone(), false); + done(); + } catch (error) { + done(error); + } + }); + }); + + it('rejects a category grant with a permission other than get', (done) => { + const scope = utils.createNock().get('/v2/auth/grant/sub-key/mySubscribeKey').query(true).reply(200, {}); + + pubnub.grant({ categories: ['channels'], authKeys: ['auth_key'], read: true }, (status) => { + try { + assert.equal(status.error, true); + assert.equal(status.message, 'Category permissions must be exactly the get permission'); + assert.equal(scope.isDone(), false); + done(); + } catch (error) { + done(error); + } + }); + }); + + it('rejects a one-minute category grant', (done) => { + const scope = utils.createNock().get('/v2/auth/grant/sub-key/mySubscribeKey').query(true).reply(200, {}); + + pubnub.grant({ categories: ['channels'], authKeys: ['auth_key'], get: true, ttl: 1 }, (status) => { + try { + assert.equal(status.error, true); + assert.equal(status.message, 'One-minute category grants are not supported'); + assert.equal(scope.isDone(), false); + done(); + } catch (error) { + done(error); + } + }); + }); }); }); diff --git a/test/unit/access_manager/access_manager_grant.test.ts b/test/unit/access_manager/access_manager_grant.test.ts new file mode 100644 index 000000000..51d45d620 --- /dev/null +++ b/test/unit/access_manager/access_manager_grant.test.ts @@ -0,0 +1,190 @@ +/* global describe, it, beforeEach */ + +import assert from 'assert'; +import { GrantRequest } from '../../../src/core/endpoints/access_manager/grant'; +import { TransportResponse } from '../../../src/core/types/transport-response'; +import { KeySet } from '../../../src/core/types/api'; +import * as PAM from '../../../src/core/types/api/access-manager'; + +describe('GrantRequest', () => { + let defaultKeySet: KeySet; + + beforeEach(() => { + defaultKeySet = { + publishKey: 'test_publish_key', + subscribeKey: 'test_subscribe_key', + secretKey: 'test_secret_key', + }; + }); + + describe('categories', () => { + it('should encode category enumeration as a comma-separated query value', () => { + const request = new GrantRequest({ + keySet: defaultKeySet, + authKeys: ['auth_key', 'auth_key_2'], + categories: ['channels', 'uuids'], + get: true, + }); + + assert.equal(request.validate(), undefined); + + const { queryParameters } = request.request(); + assert.equal(queryParameters!.category, 'channels,uuids'); + assert.equal(queryParameters!.auth, 'auth_key,auth_key_2'); + assert.equal(queryParameters!.g, '1'); + assert.equal(queryParameters!.r, '0'); + assert.equal(queryParameters!.w, '0'); + assert.equal(queryParameters!.m, '0'); + assert.equal(queryParameters!.d, '0'); + assert.equal(queryParameters!.u, '0'); + assert.equal(queryParameters!.j, '0'); + }); + + it('should omit category when none are granted', () => { + const request = new GrantRequest({ + keySet: defaultKeySet, + channels: ['ch1'], + authKeys: ['auth_key'], + }); + + const { queryParameters } = request.request(); + assert.equal(queryParameters!.category, undefined); + assert.equal(queryParameters!.channel, 'ch1'); + }); + + it('should encode a category revoke as g=0', () => { + const request = new GrantRequest({ + keySet: defaultKeySet, + authKeys: ['auth_key'], + categories: ['channels'], + get: false, + }); + + assert.equal(request.validate(), undefined); + assert.equal(request.request().queryParameters!.category, 'channels'); + assert.equal(request.request().queryParameters!.g, '0'); + }); + + it('should require authKeys for categories', () => { + const request = new GrantRequest({ + keySet: defaultKeySet, + categories: ['channels'], + get: true, + }); + + assert.equal(request.validate(), 'authKeys are required for grant request on categories'); + }); + + it('should reject a permission other than get', () => { + const request = new GrantRequest({ + keySet: defaultKeySet, + authKeys: ['auth_key'], + categories: ['uuids'], + get: true, + read: true, + }); + + assert.equal(request.validate(), 'Category permissions must be exactly the get permission'); + }); + + it('should reject a one-minute category grant', () => { + const request = new GrantRequest({ + keySet: defaultKeySet, + authKeys: ['auth_key'], + categories: ['channels'], + get: true, + ttl: 1, + }); + + assert.equal(request.validate(), 'One-minute category grants are not supported'); + }); + + it('should allow a one-minute category revoke', () => { + const request = new GrantRequest({ + keySet: defaultKeySet, + authKeys: ['auth_key'], + categories: ['channels'], + get: false, + ttl: 1, + }); + + assert.equal(request.validate(), undefined); + }); + + it('should reject an unsupported category name', () => { + const request = new GrantRequest({ + keySet: defaultKeySet, + authKeys: ['auth_key'], + categories: ['users' as 'channels'], + get: true, + }); + + assert.equal(request.validate(), 'Invalid category: only channels and uuids are supported'); + }); + + it('should encode categories alongside channels when only get is set', () => { + const request = new GrantRequest({ + keySet: defaultKeySet, + authKeys: ['auth_key'], + channels: ['ch1'], + categories: ['uuids'], + get: true, + }); + + assert.equal(request.validate(), undefined); + + const { queryParameters } = request.request(); + assert.equal(queryParameters!.channel, 'ch1'); + assert.equal(queryParameters!.category, 'uuids'); + assert.equal(queryParameters!.g, '1'); + }); + + it('should reject categories alongside channels when another permission is set', () => { + const request = new GrantRequest({ + keySet: defaultKeySet, + authKeys: ['auth_key'], + channels: ['ch1'], + categories: ['channels'], + read: true, + }); + + assert.equal(request.validate(), 'Category permissions must be exactly the get permission'); + }); + + it('should return category permissions from the service payload', async () => { + const request = new GrantRequest({ + keySet: defaultKeySet, + authKeys: ['auth_key'], + categories: ['channels'], + get: true, + }); + const payload: PAM.PermissionsResponse = { + level: 'category', + subscribe_key: 'test_subscribe_key', + ttl: 1440, + categories: { + channels: { auths: { auth_key: { r: 0, w: 0, m: 0, d: 0, g: 1, u: 0, j: 0 } } }, + uuids: { auths: { auth_key: { r: 0, w: 0, m: 0, d: 0, g: 1, u: 0, j: 0 } } }, + }, + }; + const response: TransportResponse = { + status: 200, + url: 'https://test.pubnub.com', + headers: { 'content-type': 'application/json' }, + body: new TextEncoder().encode( + JSON.stringify({ + status: 200, + message: 'Success', + payload, + service: 'Access Manager', + }), + ), + }; + + const parsed = await request.parse(response); + assert.equal(parsed.level, 'category'); + if (parsed.level !== 'category') return; + assert.deepEqual(parsed.categories.channels.auths.auth_key, payload.categories.channels.auths.auth_key); + }); + }); +}); From 1802c550e3ecc17ac87c5816d3a5897373471a8c Mon Sep 17 00:00:00 2001 From: Mohit Tejani Date: Tue, 6 Oct 2026 12:48:26 +0530 Subject: [PATCH 3/7] lib/dist updates --- lib/core/components/token_manager.js | 23 +++++++++++ lib/core/endpoints/access_manager/grant.js | 41 ++++++++++++------- .../endpoints/access_manager/grant_token.js | 35 ++++++++++++++-- 3 files changed, 80 insertions(+), 19 deletions(-) diff --git a/lib/core/components/token_manager.js b/lib/core/components/token_manager.js index e9a9da18a..da59b4eb7 100644 --- a/lib/core/components/token_manager.js +++ b/lib/core/components/token_manager.js @@ -117,6 +117,9 @@ class TokenManager { const patternDataSync = this.extractDataSyncScopes(parsed.pat); if (patternDataSync) ((_b = result.patterns) !== null && _b !== void 0 ? _b : (result.patterns = {})).dataSync = patternDataSync; + const categories = this.extractCategories(parsed.cat); + if (categories) + result.categories = categories; if (parsed.meta && Object.keys(parsed.meta).length > 0) result.meta = parsed.meta; return result; @@ -156,6 +159,26 @@ class TokenManager { permissionsResult.read = true; return permissionsResult; } + /** + * Extract category-level permissions from the token `cat` section. + * + * `chan` maps to `channels` and `uuid` maps to `uuids`. `get` is set from the `32` bit. The + * section is omitted when `cat` is absent or empty. + * + * @param cat - Raw `cat` section decoded from the token. + * + * @returns Human-readable category permissions, or `undefined` when none are present. + */ + extractCategories(cat) { + if (!cat) + return undefined; + const result = {}; + if (typeof cat.chan === 'number') + result.channels = { get: (cat.chan & 32) === 32 }; + if (typeof cat.uuid === 'number') + result.uuids = { get: (cat.uuid & 32) === 32 }; + return Object.keys(result).length > 0 ? result : undefined; + } /** * Extract DataSync permission scopes from a token permissions section. * diff --git a/lib/core/endpoints/access_manager/grant.js b/lib/core/endpoints/access_manager/grant.js index e1a3ab37f..3f6ff7da4 100644 --- a/lib/core/endpoints/access_manager/grant.js +++ b/lib/core/endpoints/access_manager/grant.js @@ -60,27 +60,28 @@ const JOIN_PERMISSION = false; */ class GrantRequest extends request_1.AbstractRequest { constructor(parameters) { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; - var _l, _m, _o, _p, _q, _r, _s, _t, _u, _v; + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; + var _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x; super(); this.parameters = parameters; // Apply defaults. - (_a = (_l = this.parameters).channels) !== null && _a !== void 0 ? _a : (_l.channels = []); - (_b = (_m = this.parameters).channelGroups) !== null && _b !== void 0 ? _b : (_m.channelGroups = []); - (_c = (_o = this.parameters).uuids) !== null && _c !== void 0 ? _c : (_o.uuids = []); - (_d = (_p = this.parameters).read) !== null && _d !== void 0 ? _d : (_p.read = READ_PERMISSION); - (_e = (_q = this.parameters).write) !== null && _e !== void 0 ? _e : (_q.write = WRITE_PERMISSION); - (_f = (_r = this.parameters).delete) !== null && _f !== void 0 ? _f : (_r.delete = DELETE_PERMISSION); - (_g = (_s = this.parameters).get) !== null && _g !== void 0 ? _g : (_s.get = GET_PERMISSION); - (_h = (_t = this.parameters).update) !== null && _h !== void 0 ? _h : (_t.update = UPDATE_PERMISSION); - (_j = (_u = this.parameters).manage) !== null && _j !== void 0 ? _j : (_u.manage = MANAGE_PERMISSION); - (_k = (_v = this.parameters).join) !== null && _k !== void 0 ? _k : (_v.join = JOIN_PERMISSION); + (_a = (_m = this.parameters).channels) !== null && _a !== void 0 ? _a : (_m.channels = []); + (_b = (_o = this.parameters).channelGroups) !== null && _b !== void 0 ? _b : (_o.channelGroups = []); + (_c = (_p = this.parameters).uuids) !== null && _c !== void 0 ? _c : (_p.uuids = []); + (_d = (_q = this.parameters).categories) !== null && _d !== void 0 ? _d : (_q.categories = []); + (_e = (_r = this.parameters).read) !== null && _e !== void 0 ? _e : (_r.read = READ_PERMISSION); + (_f = (_s = this.parameters).write) !== null && _f !== void 0 ? _f : (_s.write = WRITE_PERMISSION); + (_g = (_t = this.parameters).delete) !== null && _g !== void 0 ? _g : (_t.delete = DELETE_PERMISSION); + (_h = (_u = this.parameters).get) !== null && _h !== void 0 ? _h : (_u.get = GET_PERMISSION); + (_j = (_v = this.parameters).update) !== null && _j !== void 0 ? _j : (_v.update = UPDATE_PERMISSION); + (_k = (_w = this.parameters).manage) !== null && _k !== void 0 ? _k : (_w.manage = MANAGE_PERMISSION); + (_l = (_x = this.parameters).join) !== null && _l !== void 0 ? _l : (_x.join = JOIN_PERMISSION); } operation() { return operations_1.default.PNAccessManagerGrant; } validate() { - const { keySet: { subscribeKey, publishKey, secretKey }, uuids = [], channels = [], channelGroups = [], authKeys = [], } = this.parameters; + const { keySet: { subscribeKey, publishKey, secretKey }, uuids = [], channels = [], channelGroups = [], categories = [], authKeys = [], read, write, manage, delete: del, update, join, get, ttl, } = this.parameters; if (!subscribeKey) return 'Missing Subscribe Key'; if (!publishKey) @@ -91,6 +92,16 @@ class GrantRequest extends request_1.AbstractRequest { return 'authKeys are required for grant request on uuids'; if (uuids.length && (channels.length !== 0 || channelGroups.length !== 0)) return 'Both channel/channel group and uuid cannot be used in the same request'; + if (categories.length > 0) { + if (authKeys.length === 0) + return 'authKeys are required for grant request on categories'; + if (categories.some((category) => category !== 'channels' && category !== 'uuids')) + return 'Invalid category: only channels and uuids are supported'; + if (read || write || manage || del || update || join) + return 'Category permissions must be exactly the get permission'; + if (ttl === 1 && get) + return 'One-minute category grants are not supported'; + } } parse(response) { return __awaiter(this, void 0, void 0, function* () { @@ -101,8 +112,8 @@ class GrantRequest extends request_1.AbstractRequest { return `/v2/auth/grant/sub-key/${this.parameters.keySet.subscribeKey}`; } get queryParameters() { - const { channels, channelGroups, authKeys, uuids, read, write, manage, delete: del, get, join, update, ttl, } = this.parameters; - return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (channels && (channels === null || channels === void 0 ? void 0 : channels.length) > 0 ? { channel: channels.join(',') } : {})), (channelGroups && (channelGroups === null || channelGroups === void 0 ? void 0 : channelGroups.length) > 0 ? { 'channel-group': channelGroups.join(',') } : {})), (authKeys && (authKeys === null || authKeys === void 0 ? void 0 : authKeys.length) > 0 ? { auth: authKeys.join(',') } : {})), (uuids && (uuids === null || uuids === void 0 ? void 0 : uuids.length) > 0 ? { 'target-uuid': uuids.join(',') } : {})), { r: read ? '1' : '0', w: write ? '1' : '0', m: manage ? '1' : '0', d: del ? '1' : '0', g: get ? '1' : '0', j: join ? '1' : '0', u: update ? '1' : '0' }), (ttl || ttl === 0 ? { ttl } : {})); + const { channels, channelGroups, categories, authKeys, uuids, read, write, manage, delete: del, get, join, update, ttl, } = this.parameters; + return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (channels && (channels === null || channels === void 0 ? void 0 : channels.length) > 0 ? { channel: channels.join(',') } : {})), (channelGroups && (channelGroups === null || channelGroups === void 0 ? void 0 : channelGroups.length) > 0 ? { 'channel-group': channelGroups.join(',') } : {})), (authKeys && (authKeys === null || authKeys === void 0 ? void 0 : authKeys.length) > 0 ? { auth: authKeys.join(',') } : {})), (uuids && (uuids === null || uuids === void 0 ? void 0 : uuids.length) > 0 ? { 'target-uuid': uuids.join(',') } : {})), (categories && categories.length > 0 ? { category: categories.join(',') } : {})), { r: read ? '1' : '0', w: write ? '1' : '0', m: manage ? '1' : '0', d: del ? '1' : '0', g: get ? '1' : '0', j: join ? '1' : '0', u: update ? '1' : '0' }), (ttl || ttl === 0 ? { ttl } : {})); } } exports.GrantRequest = GrantRequest; diff --git a/lib/core/endpoints/access_manager/grant_token.js b/lib/core/endpoints/access_manager/grant_token.js index c9c175a23..791299394 100644 --- a/lib/core/endpoints/access_manager/grant_token.js +++ b/lib/core/endpoints/access_manager/grant_token.js @@ -42,16 +42,17 @@ class GrantTokenRequest extends request_1.AbstractRequest { } validate() { const { keySet: { subscribeKey, publishKey, secretKey }, resources, patterns, } = this.parameters; - // DataSync projections are a standalone grant target — a request carrying only projections - // (no resources / patterns permissions) is still valid. + // DataSync projections and category grants are standalone grant targets — a request carrying + // only projections or only categories (no resources / patterns permissions) is still valid. const hasProjections = this.buildProjections() !== undefined; + const hasCategories = this.buildCategories() !== undefined; if (!subscribeKey) return 'Missing Subscribe Key'; if (!publishKey) return 'Missing Publish Key'; if (!secretKey) return 'Missing Secret Key'; - if (!resources && !patterns && !hasProjections) + if (!resources && !patterns && !hasProjections && !hasCategories) return 'Missing either Resources or Patterns'; // A single token can grant DataSync `users`, `channels`, `groups`, and `dataSync` together. // together. `uuids` / `spaces` / `authorized_uuid` are the deprecated App Context terminology; @@ -77,7 +78,7 @@ class GrantTokenRequest extends request_1.AbstractRequest { } }); }); - if (permissionsEmpty && !hasProjections) + if (permissionsEmpty && !hasProjections && !hasCategories) return 'Missing values for either Resources or Patterns'; } parse(response) { @@ -153,6 +154,11 @@ class GrantTokenRequest extends request_1.AbstractRequest { permissions.uuid = `${uuid}`; permissions.resources = resourcePermissions; permissions.patterns = patternPermissions; + // Category grants are a single integer per resource type. Omit the key entirely when none are + // set so tokens that don't use categories stay byte-for-byte identical. + const categories = this.buildCategories(); + if (categories) + permissions.categories = categories; // Merge DataSync projections into `meta` under `pn-projections`, preserving user-supplied meta. // `pn-projections` is omitted entirely when no projections are set. const projections = this.buildProjections(); @@ -219,6 +225,27 @@ class GrantTokenRequest extends request_1.AbstractRequest { result.pat = pat; return Object.keys(result).length > 0 ? result : undefined; } + /** + * Build the `permissions.categories` payload. + * + * Category grants are a single integer per resource type, not a per-id bitmask. Only `get: true` + * is encoded, as `32`. Any other runtime flag is ignored so a non-TypeScript caller cannot send a + * value other than `32`. The payload is omitted entirely when no category is granted. + * + * @returns Encoded categories payload, or `undefined` when no category `get` is set. + */ + buildCategories() { + var _a, _b; + const categories = 'categories' in this.parameters ? this.parameters.categories : undefined; + if (!categories) + return undefined; + const result = {}; + if (((_a = categories.channels) === null || _a === void 0 ? void 0 : _a.get) === true) + result.channels = 32; + if (((_b = categories.uuids) === null || _b === void 0 ? void 0 : _b.get) === true) + result.uuids = 32; + return Object.keys(result).length > 0 ? result : undefined; + } /** * Extract permissions bit from permission configuration object. * From 8da772696a7743b968aaca446c1ff092e56e05d3 Mon Sep 17 00:00:00 2001 From: Mohit Tejani Date: Tue, 6 Oct 2026 12:53:01 +0530 Subject: [PATCH 4/7] package-lock.json update --- package-lock.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 36f9b4519..e4897028d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pubnub", - "version": "13.0.1", + "version": "13.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pubnub", - "version": "13.0.1", + "version": "13.0.3", "license": "SEE LICENSE IN LICENSE", "dependencies": { "agentkeepalive": "^3.5.2", @@ -3749,6 +3749,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.0.tgz", From 7e18f03dc5bf605e06cab431ea94ee98ddf23c05 Mon Sep 17 00:00:00 2001 From: Mohit Tejani <60129002+mohitpubnub@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:13:52 +0530 Subject: [PATCH 5/7] Update README with project badges and image --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f84c42e4a..fc48c49e7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# PubNub JavaScript SDK (V4) +image + +# PubNub JavaScript SDK [![Codacy Badge](https://api.codacy.com/project/badge/Grade/2859917905c549b8bfa27630ff276fce)](https://www.codacy.com/app/PubNub/javascript?utm_source=github.com&utm_medium=referral&utm_content=pubnub/javascript&utm_campaign=Badge_Grade) [![npm](https://img.shields.io/npm/v/pubnub.svg)]() From a69bcca61bbd779b025ae9668028fd51c03c7c86 Mon Sep 17 00:00:00 2001 From: Mohit Tejani Date: Tue, 6 Oct 2026 13:23:32 +0530 Subject: [PATCH 6/7] update package-lock.json for dev deps version upgrade --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e4897028d..ff0145ad5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7879,9 +7879,9 @@ "license": "Apache-2.0" }, "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ { From 1f96d6c51a710f9a9ac5934f796f85c3d0af1865 Mon Sep 17 00:00:00 2001 From: Mohit Tejani Date: Tue, 6 Oct 2026 13:23:57 +0530 Subject: [PATCH 7/7] remove coderabbit configuration --- .coderabbit.yaml | 106 ----------------------------------------------- 1 file changed, 106 deletions(-) delete mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml deleted file mode 100644 index 0ef4746e7..000000000 --- a/.coderabbit.yaml +++ /dev/null @@ -1,106 +0,0 @@ -# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json - -language: en-US - -reviews: - # Enable high-level summary of changes - high_level_summary: false - - # Add a poem... just kidding, disable it - poem: false - - # Collapse walkthrough to keep PR comments clean - collapse_walkthrough: true - - # Auto-review on every push - auto_review: - enabled: true - drafts: true - - # Sparse-checkout / review scope: include .github explicitly; exclude generated and vendor paths - path_filters: - - ".github/**" - - "!dist/**" - - "!lib/**" - - "!upload/**" - - "!node_modules/**" - - "!package-lock.json" - - "!.pubnub.yml" - - "!.vscode/**" - - # Path-based review instructions - path_instructions: - - path: "src/core/**" - instructions: | - This is the core SDK module. Pay close attention to: - - Backward compatibility of public API changes - - Proper TypeScript typing (strict mode is enabled) - - No platform-specific code (Node.js, Web, React Native specifics belong in their respective platform directories) - - Thread safety considerations for shared state - - path: "src/core/endpoints/**" - instructions: | - These are REST API endpoint implementations. Review for: - - Correct request/response type definitions - - Proper error handling and status code mapping - - Consistent parameter validation - - Adherence to PubNub REST API contracts - - path: "src/core/types/**" - instructions: | - TypeScript type definitions. Ensure: - - Types are precise and not overly permissive (avoid `any`) - - Exported types maintain backward compatibility - - Proper use of generics and utility types - - path: "src/event-engine/**" - instructions: | - State-machine-based subscription management. Review for: - - Correct state transitions and edge cases - - No leaked subscriptions or event listeners - - Proper cleanup on state exit - - path: "src/entities/**" - instructions: | - High-level subscription API (Channel, ChannelGroup, etc.). Review for: - - Proper event handler lifecycle management - - Memory leak prevention (listener cleanup) - - path: "src/transport/**" - instructions: | - Platform-specific HTTP transport implementations. Review for: - - Proper timeout and cancellation handling - - Correct header management - - Error propagation consistency across platforms - - path: "src/node/**" - instructions: "Node.js platform implementation. Ensure no browser/DOM APIs are used." - - path: "src/web/**" - instructions: "Browser platform implementation. Ensure no Node.js-specific APIs (fs, crypto, etc.) are used." - - path: "src/react_native/**" - instructions: "React Native platform implementation. Verify compatibility with RN runtime." - - path: "test/**" - instructions: | - Test files. Review for: - - Adequate coverage of edge cases - - Proper use of mocks/stubs (Sinon + Nock) - - No flaky patterns (hardcoded timeouts, race conditions) - - Tests that actually assert meaningful behavior - - path: ".github/**" - instructions: | - GitHub Actions workflows and repo automation. Review for: - - Valid workflow YAML (triggers, concurrency, job dependencies) - - Safe use of secrets and minimal required permissions - - Reasonable timeouts and matrix coverage; actionlint/YAMLlint findings should be addressed - - # Tools configuration - tools: - # Enable GitHub checks integration - github-checks: - enabled: true - timeout_ms: 120000 - yamllint: - enabled: true - actionlint: - enabled: true - -chat: - auto_reply: true - -knowledge_base: - mcp: - usage: enabled