@memberjunction/lists 0.0.1 → 5.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +292 -28
- package/dist/AudienceResolver.d.ts +22 -0
- package/dist/AudienceResolver.d.ts.map +1 -0
- package/dist/AudienceResolver.js +25 -0
- package/dist/AudienceResolver.js.map +1 -0
- package/dist/ListOperations.d.ts +215 -0
- package/dist/ListOperations.d.ts.map +1 -0
- package/dist/ListOperations.js +829 -0
- package/dist/ListOperations.js.map +1 -0
- package/dist/ListSharing.d.ts +157 -0
- package/dist/ListSharing.d.ts.map +1 -0
- package/dist/ListSharing.js +568 -0
- package/dist/ListSharing.js.map +1 -0
- package/dist/deltaToken.d.ts +47 -0
- package/dist/deltaToken.d.ts.map +1 -0
- package/dist/deltaToken.js +123 -0
- package/dist/deltaToken.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -7
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-signed token attached to every `ListDelta`. The token is the only
|
|
3
|
+
* thing `ApplyDelta` will accept as proof that:
|
|
4
|
+
* - the preview was produced server-side (not forged by a client),
|
|
5
|
+
* - the preview describes a known target + source pairing,
|
|
6
|
+
* - the preview is fresh (within `TOKEN_TTL_MS`).
|
|
7
|
+
*
|
|
8
|
+
* Token format is `<base64url(payload-json)>.<base64url(hmac-sha256)>`. We
|
|
9
|
+
* deliberately avoid a JWT dependency — the payload shape is fixed, the
|
|
10
|
+
* algorithm is fixed, and we own both ends.
|
|
11
|
+
*
|
|
12
|
+
* Uses Node's built-in `node:crypto`. This module is server-only — clients
|
|
13
|
+
* never sign or verify tokens (they receive them inside a `ListDelta` and
|
|
14
|
+
* hand them back unchanged to `ApplyDelta`).
|
|
15
|
+
*
|
|
16
|
+
* Public functions remain async for backwards compatibility with callers
|
|
17
|
+
* that already `await` them; the underlying node:crypto APIs are synchronous,
|
|
18
|
+
* which is fine — HMAC on a tiny payload is sub-millisecond.
|
|
19
|
+
*/
|
|
20
|
+
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
|
|
21
|
+
import { TOKEN_TTL_MS, } from '@memberjunction/lists-base';
|
|
22
|
+
export class DeltaTokenVerificationError extends Error {
|
|
23
|
+
constructor(code, message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.Code = code;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Produce a deterministic fingerprint of a record-ID set. Used as the `ssig`
|
|
30
|
+
* field on the token payload — server-side `ApplyDelta` recomputes the
|
|
31
|
+
* fingerprint of the freshly resolved source and compares against the token
|
|
32
|
+
* to detect `STALE_DELTA`.
|
|
33
|
+
*/
|
|
34
|
+
export async function ComputeSourceSignature(recordIds) {
|
|
35
|
+
const sorted = [...recordIds].sort();
|
|
36
|
+
return createHash('sha256').update(sorted.join('\n'), 'utf8').digest('hex');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the HMAC secret. Server bootstrap is expected to set
|
|
40
|
+
* `MJ_LIST_DELTA_SECRET` (or call `SetDeltaTokenSecret` directly for tests).
|
|
41
|
+
* We refuse to fall back to a default — silent fallback to a known secret
|
|
42
|
+
* would let a misconfigured server hand out forgeable tokens.
|
|
43
|
+
*/
|
|
44
|
+
let injectedSecret;
|
|
45
|
+
export function SetDeltaTokenSecret(secret) {
|
|
46
|
+
injectedSecret = secret && secret.length > 0 ? secret : undefined;
|
|
47
|
+
}
|
|
48
|
+
function getSecret() {
|
|
49
|
+
const envSecret = process?.env?.MJ_LIST_DELTA_SECRET;
|
|
50
|
+
const secret = injectedSecret ?? envSecret;
|
|
51
|
+
if (!secret || secret.length === 0) {
|
|
52
|
+
throw new Error("MJ_LIST_DELTA_SECRET is not configured. Set the env var or call SetDeltaTokenSecret() at server boot.");
|
|
53
|
+
}
|
|
54
|
+
return secret;
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Base64URL helpers
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
function bufferToBase64Url(buf) {
|
|
60
|
+
return buf.toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
61
|
+
}
|
|
62
|
+
function base64UrlToBuffer(s) {
|
|
63
|
+
const padded = s + '='.repeat((4 - (s.length % 4)) % 4);
|
|
64
|
+
const std = padded.replace(/-/g, '+').replace(/_/g, '/');
|
|
65
|
+
return Buffer.from(std, 'base64');
|
|
66
|
+
}
|
|
67
|
+
function hmacOf(payloadEncoded) {
|
|
68
|
+
return createHmac('sha256', getSecret()).update(payloadEncoded, 'utf8').digest();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Sign a payload. Callers should pass `iat: Date.now()` and an `ssig`
|
|
72
|
+
* computed via `ComputeSourceSignature`. Returns the wire form.
|
|
73
|
+
*/
|
|
74
|
+
export async function SignDeltaToken(payload) {
|
|
75
|
+
const json = JSON.stringify(payload);
|
|
76
|
+
const payloadEncoded = bufferToBase64Url(Buffer.from(json, 'utf8'));
|
|
77
|
+
const sig = bufferToBase64Url(hmacOf(payloadEncoded));
|
|
78
|
+
return `${payloadEncoded}.${sig}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Verify a token: checks structural shape, HMAC, and TTL. Returns the parsed
|
|
82
|
+
* payload on success; throws `DeltaTokenVerificationError` otherwise.
|
|
83
|
+
*
|
|
84
|
+
* `timingSafeEqual` compares in time independent of the mismatch position,
|
|
85
|
+
* so a token-fuzzing attacker can't learn the secret from comparison-time
|
|
86
|
+
* side channels.
|
|
87
|
+
*/
|
|
88
|
+
export async function VerifyDeltaToken(token, now = Date.now()) {
|
|
89
|
+
if (typeof token !== 'string' || !token.includes('.')) {
|
|
90
|
+
throw new DeltaTokenVerificationError('INVALID_TOKEN', 'Malformed delta token');
|
|
91
|
+
}
|
|
92
|
+
const [payloadEncoded, sigEncoded] = token.split('.', 2);
|
|
93
|
+
if (!payloadEncoded || !sigEncoded) {
|
|
94
|
+
throw new DeltaTokenVerificationError('INVALID_TOKEN', 'Malformed delta token');
|
|
95
|
+
}
|
|
96
|
+
const providedSig = base64UrlToBuffer(sigEncoded);
|
|
97
|
+
const expectedSig = hmacOf(payloadEncoded);
|
|
98
|
+
// Length mismatch would throw from timingSafeEqual — guard explicitly so
|
|
99
|
+
// the error path stays uniform.
|
|
100
|
+
// `Uint8Array.from(...)` strips the Buffer→ArrayBufferLike narrowing that
|
|
101
|
+
// TS 5.9's node:crypto typings reject (they require a concrete ArrayBuffer,
|
|
102
|
+
// not the SharedArrayBuffer-tolerant union Buffer carries).
|
|
103
|
+
if (providedSig.length !== expectedSig.length ||
|
|
104
|
+
!timingSafeEqual(Uint8Array.from(providedSig), Uint8Array.from(expectedSig))) {
|
|
105
|
+
throw new DeltaTokenVerificationError('INVALID_TOKEN', 'Delta token signature mismatch');
|
|
106
|
+
}
|
|
107
|
+
let payload;
|
|
108
|
+
try {
|
|
109
|
+
const json = base64UrlToBuffer(payloadEncoded).toString('utf8');
|
|
110
|
+
payload = JSON.parse(json);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
throw new DeltaTokenVerificationError('INVALID_TOKEN', 'Delta token payload not parseable');
|
|
114
|
+
}
|
|
115
|
+
if (payload.v !== 1 || typeof payload.iat !== 'number' || typeof payload.ssig !== 'string') {
|
|
116
|
+
throw new DeltaTokenVerificationError('INVALID_TOKEN', 'Delta token payload shape invalid');
|
|
117
|
+
}
|
|
118
|
+
if (now - payload.iat > TOKEN_TTL_MS) {
|
|
119
|
+
throw new DeltaTokenVerificationError('EXPIRED_TOKEN', 'Delta token expired');
|
|
120
|
+
}
|
|
121
|
+
return payload;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=deltaToken.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deltaToken.js","sourceRoot":"","sources":["../src/deltaToken.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ListOperations, type DeltaTarget } from './ListOperations.js';
|
|
2
|
+
export { ListSharing, DEFAULT_INVITATION_TTL_MS } from './ListSharing.js';
|
|
3
|
+
export { AudienceResolver } from './AudienceResolver.js';
|
|
4
|
+
export { ComputeSourceSignature, DeltaTokenVerificationError, SetDeltaTokenSecret, SignDeltaToken, VerifyDeltaToken, } from './deltaToken.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,sBAAsB,EACtB,2BAA2B,EAC3B,mBAAmB,EACnB,cAAc,EACd,gBAAgB,GACjB,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @memberjunction/lists — server-side runtime. Pure types/interfaces are
|
|
2
|
+
// in @memberjunction/lists-base; per MJ's "no re-exports between packages"
|
|
3
|
+
// rule, this barrel only exports things defined within this package. Consumers
|
|
4
|
+
// that want shared types should import directly from @memberjunction/lists-base.
|
|
5
|
+
export { ListOperations } from './ListOperations.js';
|
|
6
|
+
export { ListSharing, DEFAULT_INVITATION_TTL_MS } from './ListSharing.js';
|
|
7
|
+
export { AudienceResolver } from './AudienceResolver.js';
|
|
8
|
+
export { ComputeSourceSignature, DeltaTokenVerificationError, SetDeltaTokenSecret, SignDeltaToken, VerifyDeltaToken, } from './deltaToken.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,2EAA2E;AAC3E,+EAA+E;AAC/E,iFAAiF;AAEjF,OAAO,EAAE,cAAc,EAAoB,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,sBAAsB,EACtB,2BAA2B,EAC3B,mBAAmB,EACnB,cAAc,EACd,gBAAgB,GACjB,MAAM,cAAc,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/lists",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "5.36.0",
|
|
5
|
+
"description": "MemberJunction: Lists — server-side TypeScript core for list materialization, set algebra, delta computation, lineage tracking, refresh modes, and sharing/invitations. Writes to MJ entities (List Detail, Resource Permission, List Invitation, Audit Log) — not safe to import from browser bundles. UI consumers should depend on @memberjunction/lists-base for type/interface sharing and call the server via GraphQL.",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"/dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "ts-node-dev src/index.ts",
|
|
13
|
+
"build": "tsc && tsc-alias -f",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"test:coverage": "vitest run --coverage"
|
|
17
|
+
},
|
|
18
|
+
"author": "MemberJunction.com",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"ts-node-dev": "^2.0.0",
|
|
22
|
+
"typescript": "^5.9.3",
|
|
23
|
+
"vitest": "^4.0.18"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@memberjunction/core": "5.36.0",
|
|
27
|
+
"@memberjunction/core-entities": "5.36.0",
|
|
28
|
+
"@memberjunction/global": "5.36.0",
|
|
29
|
+
"@memberjunction/lists-base": "5.36.0"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/MemberJunction/MJ"
|
|
34
|
+
}
|
|
10
35
|
}
|