@unito/integration-sdk
Version:
Integration SDK
78 lines (72 loc) • 2.84 kB
text/typescript
/**
* Forbids spreading the entire `credentials` object into a cache-key hash input
* inside `cache.ts`.
*
* Bad:
* crc32(JSON.stringify({ ...params, ...credentials }))
*
* Good (smartsheet/google-drive/freshservice pattern):
* if (!credentials.unitoCredentialId) {
* throw new HttpErrors.UnprocessableEntityError('...');
* }
* hashValue({ ...params, userIdentifier: credentials.unitoCredentialId });
*
* `unitoCredentialId` is Unito's internal record ID for the credential row — a
* non-secret UUID. Hashing it never puts secret material in memory. Do NOT
* fall back to `accessToken`.
*
* Why:
* 1. Cache thrash on credential rotation. Access tokens rotate on every OAuth
* refresh (and refresh tokens themselves rotate on use for some providers);
* spreading the full credential makes EVERY field a cache-key input, so a
* single-field rotation invalidates the whole cache.
* 2. Credential surface area. The full credential transits memory as a JSON
* string before hashing. Any error in this path can surface in stack
* traces or memory dumps.
* 3. Wrong unit of identity. Cache should be keyed on the stable identifier
* of the credential (`unitoCredentialId`), not the credential material.
*
* Scope:
* Only fires in files whose path ends in `/src/cache.ts`. `credentials.ts`
* has legitimate `...credentials` spread (enrichment patterns) and is never
* affected.
*/
import type { Rule } from 'eslint';
const CACHE_FILE_RE = /\/src\/cache\.ts$/;
const CREDENTIALS_IDENTIFIER_RE = /^cred(|s|entials)$/;
const rule: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
description: 'Disallow spreading the entire `credentials` object into a cache-key hash input inside `cache.ts`',
recommended: true,
},
schema: [],
messages: {
credentialsSpread:
'Do not spread `...{{name}}` into a cache-key hash input. Spreading the full credential makes every field (including rotating tokens) part of the cache key, causes cache thrash on token refresh, and surfaces credential material as a JSON string. Use a non-secret stable identifier instead, e.g. `credentials.unitoCredentialId` (throw if it is missing — never fall back to `accessToken`).',
},
},
create(context) {
const filename = context.filename ?? '';
if (!CACHE_FILE_RE.test(filename)) {
return {};
}
return {
SpreadElement(node) {
if (!node.argument || node.argument.type !== 'Identifier') {
return;
}
if (!CREDENTIALS_IDENTIFIER_RE.test(node.argument.name)) {
return;
}
context.report({
node,
messageId: 'credentialsSpread',
data: { name: node.argument.name },
});
},
};
},
};
export default rule;