@mohasinac/appkit 3.8.1 → 3.8.2
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/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/security/index.d.ts +2 -1
- package/dist/security/index.js +5 -1
- package/dist/security/pii-encrypt.d.ts +2 -53
- package/dist/security/pii-encrypt.js +19 -100
- package/dist/security/pii-mask.d.ts +67 -0
- package/dist/security/pii-mask.js +100 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +9 -0
package/dist/index.d.ts
CHANGED
|
@@ -479,6 +479,7 @@ export { maskSecret } from "./security/index";
|
|
|
479
479
|
export { rateLimit } from "./security/index";
|
|
480
480
|
export { rateLimitByIdentifier } from "./security/index";
|
|
481
481
|
export { redactPii, safeDisplayName, safeDisplayEmail } from "./security/index";
|
|
482
|
+
export { isPiiEncrypted, maskName, maskEmail, maskPublicReview, maskPublicBid, maskPublicEventEntry, maskOfferForSeller, ENC_PREFIX, HMAC_PREFIX, } from "./security/index";
|
|
482
483
|
export { requireActiveAccount } from "./security/index";
|
|
483
484
|
export { requireAuth } from "./security/index";
|
|
484
485
|
export { requireEmailVerified } from "./security/index";
|
package/dist/index.js
CHANGED
|
@@ -885,6 +885,8 @@ export { rateLimitByIdentifier } from "./security/index";
|
|
|
885
885
|
// [CLIENT-SSR]-Runs in both SSR and browser â€" React component or hook that does not depend on browser-only APIs.
|
|
886
886
|
// redactPii - Shared export for redact pii.
|
|
887
887
|
export { redactPii, safeDisplayName, safeDisplayEmail } from "./security/index";
|
|
888
|
+
// [CLIENT-SSR]-Runs in both SSR and browser â€" pure display-masking helpers, no crypto dependency.
|
|
889
|
+
export { isPiiEncrypted, maskName, maskEmail, maskPublicReview, maskPublicBid, maskPublicEventEntry, maskOfferForSeller, ENC_PREFIX, HMAC_PREFIX, } from "./security/index";
|
|
888
890
|
// [CLIENT-SSR]-Runs in both SSR and browser â€" React component or hook that does not depend on browser-only APIs.
|
|
889
891
|
// requireActiveAccount - Shared export for require active account.
|
|
890
892
|
export { requireActiveAccount } from "./security/index";
|
package/dist/security/index.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ export type { RateLimitConfig, RateLimitResult } from "./rate-limit";
|
|
|
4
4
|
export { requireAuth, requireRole, requireOwnership, requireEmailVerified, requireActiveAccount, canChangeRole, getRoleLevel, } from "./authorization";
|
|
5
5
|
export type { UserRole } from "./authorization";
|
|
6
6
|
export { redactPii, safeDisplayName, safeDisplayEmail, maskIp } from "./pii-redact";
|
|
7
|
-
export { encryptValue, decryptValue, hmacBlindIndex, encryptPiiFields, decryptPiiFields, encryptPii, decryptPii,
|
|
7
|
+
export { encryptValue, decryptValue, hmacBlindIndex, encryptPiiFields, decryptPiiFields, encryptPii, decryptPii, piiBlindIndex, addPiiIndices, getPiiConfigError, encryptShippingAddress, decryptShippingAddress, encryptPayoutDetails, decryptPayoutDetails, encryptShippingConfig, decryptShippingConfig, encryptPayoutBankAccount, decryptPayoutBankAccount, } from "./pii-encrypt";
|
|
8
|
+
export { isPiiEncrypted, maskName, maskEmail, maskPublicReview, maskPublicBid, maskPublicEventEntry, maskOfferForSeller, ENC_PREFIX, HMAC_PREFIX, } from "./pii-mask";
|
|
8
9
|
export { USER_PII_FIELDS, USER_PII_INDEX_MAP, ADDRESS_PII_FIELDS, ORDER_PII_FIELDS, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, BID_PII_FIELDS, NEWSLETTER_PII_FIELDS, NEWSLETTER_PII_INDEX_MAP, TOKEN_PII_FIELDS, TOKEN_PII_INDEX_MAP, REVIEW_PII_FIELDS, REVIEW_PII_INDEX_MAP, OFFER_PII_FIELDS, CHAT_PII_FIELDS, EVENT_ENTRY_PII_FIELDS, LOTTERY_ENTRY_PII_FIELDS, PAYMENT_METHOD_PII_FIELDS, } from "./pii-schemas";
|
|
9
10
|
export { encryptSecret, decryptSecret, isSecretEncrypted, maskSecret, } from "./settings-encryption";
|
|
10
11
|
export type { Permission, RoleDefinition, RbacConfig, ResolvedUser, RbacHookReturn, } from "./rbac";
|
package/dist/security/index.js
CHANGED
|
@@ -2,7 +2,11 @@ export { generateNonce, buildCSP } from "./csp";
|
|
|
2
2
|
export { rateLimit, applyRateLimit, rateLimitByIdentifier, RateLimitPresets, clearRateLimitStore, } from "./rate-limit";
|
|
3
3
|
export { requireAuth, requireRole, requireOwnership, requireEmailVerified, requireActiveAccount, canChangeRole, getRoleLevel, } from "./authorization";
|
|
4
4
|
export { redactPii, safeDisplayName, safeDisplayEmail, maskIp } from "./pii-redact";
|
|
5
|
-
export { encryptValue, decryptValue, hmacBlindIndex, encryptPiiFields, decryptPiiFields, encryptPii, decryptPii,
|
|
5
|
+
export { encryptValue, decryptValue, hmacBlindIndex, encryptPiiFields, decryptPiiFields, encryptPii, decryptPii, piiBlindIndex, addPiiIndices, getPiiConfigError, encryptShippingAddress, decryptShippingAddress, encryptPayoutDetails, decryptPayoutDetails, encryptShippingConfig, decryptShippingConfig, encryptPayoutBankAccount, decryptPayoutBankAccount, } from "./pii-encrypt";
|
|
6
|
+
// Crypto-free display-masking helpers -- sourced from pii-mask.ts (not
|
|
7
|
+
// pii-encrypt.ts) so this barrel stays safe to re-export from the
|
|
8
|
+
// universal client+server index.ts. See pii-mask.ts's header comment.
|
|
9
|
+
export { isPiiEncrypted, maskName, maskEmail, maskPublicReview, maskPublicBid, maskPublicEventEntry, maskOfferForSeller, ENC_PREFIX, HMAC_PREFIX, } from "./pii-mask";
|
|
6
10
|
export { USER_PII_FIELDS, USER_PII_INDEX_MAP, ADDRESS_PII_FIELDS, ORDER_PII_FIELDS, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, BID_PII_FIELDS, NEWSLETTER_PII_FIELDS, NEWSLETTER_PII_INDEX_MAP, TOKEN_PII_FIELDS, TOKEN_PII_INDEX_MAP, REVIEW_PII_FIELDS, REVIEW_PII_INDEX_MAP, OFFER_PII_FIELDS, CHAT_PII_FIELDS, EVENT_ENTRY_PII_FIELDS, LOTTERY_ENTRY_PII_FIELDS, PAYMENT_METHOD_PII_FIELDS, } from "./pii-schemas";
|
|
7
11
|
export { encryptSecret, decryptSecret, isSecretEncrypted, maskSecret, } from "./settings-encryption";
|
|
8
12
|
export { DEFAULT_ROLES, resolvePermissions, hasPermission, hasAllPermissions, hasAnyPermission, createRbacHook, Can, createRequirePermission, createRequirePermissionSync, createRbacMiddleware, } from "./rbac";
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* Ciphertext format: "enc:v1:<iv_b64>:<ciphertext_b64>:<authtag_b64>"
|
|
10
10
|
* Blind index format: "hmac-sha256:<sha256_hex>"
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
export
|
|
12
|
+
import { ENC_PREFIX, HMAC_PREFIX, isPiiEncrypted } from "./pii-mask";
|
|
13
|
+
export { ENC_PREFIX, HMAC_PREFIX, isPiiEncrypted };
|
|
14
14
|
export declare function getPiiConfigError(): string | null;
|
|
15
15
|
/** Encrypt a plaintext string with AES-256-GCM. */
|
|
16
16
|
export declare function encryptValue(plaintext: string): string;
|
|
@@ -36,8 +36,6 @@ export declare function decryptPiiFields<T extends object>(doc: T, piiFields: st
|
|
|
36
36
|
export declare function encryptPii(plaintext: string | null | undefined): string | null | undefined;
|
|
37
37
|
/** Alias for decryptValue — decrypts a PII ciphertext. */
|
|
38
38
|
export declare function decryptPii(ciphertext: string | null | undefined): string | null | undefined;
|
|
39
|
-
/** Check if a value is encrypted by our PII system. */
|
|
40
|
-
export declare function isPiiEncrypted(value: string): boolean;
|
|
41
39
|
/** Alias for hmacBlindIndex. */
|
|
42
40
|
export declare function piiBlindIndex(plaintext: string): string;
|
|
43
41
|
/**
|
|
@@ -80,52 +78,3 @@ export declare function encryptPayoutBankAccount<T extends object>(bank: T | und
|
|
|
80
78
|
* Decrypt PII on a payout bankAccount sub-object from the payouts collection.
|
|
81
79
|
*/
|
|
82
80
|
export declare function decryptPayoutBankAccount<T extends object>(bank: T | undefined | null): T | undefined | null;
|
|
83
|
-
/**
|
|
84
|
-
* Mask a person's name for public display.
|
|
85
|
-
* Each word is reduced to its first letter followed by "***".
|
|
86
|
-
*
|
|
87
|
-
* "John Doe" → "J*** D***"
|
|
88
|
-
* "Alice" → "A***"
|
|
89
|
-
*
|
|
90
|
-
* If the value is still an encrypted blob (e.g. PII_ENCRYPTION_KEY not set),
|
|
91
|
-
* returns "Anonymous" so encrypted ciphertext never leaks to the UI.
|
|
92
|
-
*/
|
|
93
|
-
export declare function maskName(name: string | null | undefined): string;
|
|
94
|
-
/**
|
|
95
|
-
* Mask an email address for public display.
|
|
96
|
-
* "john.doe@example.com" → "j***@***.com"
|
|
97
|
-
*/
|
|
98
|
-
export declare function maskEmail(email: string | null | undefined): string;
|
|
99
|
-
/**
|
|
100
|
-
* Return a copy of a review document with PII fields masked for public display.
|
|
101
|
-
* Admin/owner endpoints should NOT call this — use the raw document instead.
|
|
102
|
-
*/
|
|
103
|
-
export declare function maskPublicReview<T extends {
|
|
104
|
-
userName: string;
|
|
105
|
-
}>(review: T): T;
|
|
106
|
-
/**
|
|
107
|
-
* Return a copy of a bid document with PII masked for public display
|
|
108
|
-
* (auction product page).
|
|
109
|
-
*/
|
|
110
|
-
export declare function maskPublicBid<T extends {
|
|
111
|
-
userName: string;
|
|
112
|
-
}>(bid: T): T;
|
|
113
|
-
/**
|
|
114
|
-
* Return a copy of an event-entry document with PII masked for the leaderboard.
|
|
115
|
-
* ipAddress and userEmail are never sent to the client;
|
|
116
|
-
* only userDisplayName needs masking for a public leaderboard view.
|
|
117
|
-
*/
|
|
118
|
-
export declare function maskPublicEventEntry<T extends {
|
|
119
|
-
userDisplayName?: string;
|
|
120
|
-
userEmail?: string;
|
|
121
|
-
ipAddress?: string;
|
|
122
|
-
}>(entry: T): Omit<T, "userEmail" | "ipAddress">;
|
|
123
|
-
/**
|
|
124
|
-
* Return a copy of an offer document with buyer PII masked for the seller view.
|
|
125
|
-
* The seller needs to know an offer was made and its amount, but the buyer's
|
|
126
|
-
* full name and email should remain private until the order is confirmed.
|
|
127
|
-
*/
|
|
128
|
-
export declare function maskOfferForSeller<T extends {
|
|
129
|
-
buyerName: string;
|
|
130
|
-
buyerEmail: string;
|
|
131
|
-
}>(offer: T): T;
|
|
@@ -9,22 +9,26 @@
|
|
|
9
9
|
* Ciphertext format: "enc:v1:<iv_b64>:<ciphertext_b64>:<authtag_b64>"
|
|
10
10
|
* Blind index format: "hmac-sha256:<sha256_hex>"
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
import { ENC_PREFIX, HMAC_PREFIX, isPiiEncrypted } from "./pii-mask";
|
|
13
|
+
// Re-exported for backward-compat with existing direct imports of these
|
|
14
|
+
// symbols from this file — the actual crypto-free definitions live in
|
|
15
|
+
// pii-mask.ts (see that file's header comment for why the split exists).
|
|
16
|
+
export { ENC_PREFIX, HMAC_PREFIX, isPiiEncrypted };
|
|
17
|
+
// crypto is a Node.js built-in. A bare, non-static `require(...)` call
|
|
18
|
+
// (never a top-level `import ... from "node:module"`/"crypto") is
|
|
19
|
+
// deliberate: bundlers targeting the browser (Turbopack in particular)
|
|
20
|
+
// statically resolve every top-level import in a module's chain before any
|
|
21
|
+
// tree-shaking pass, and hard-fail immediately on an unresolvable Node
|
|
22
|
+
// builtin even if the symbol using it is never actually consumed
|
|
23
|
+
// client-side. A bare `require` reference inside a function body is a
|
|
24
|
+
// runtime lookup, not a static import, so it's invisible to that pass.
|
|
25
|
+
// This only works where an ambient `require` global exists: Next.js's own
|
|
26
|
+
// server bundle (webpack/Turbopack) provides one automatically; the one
|
|
27
|
+
// place that doesn't is a standalone pure-ESM script importing
|
|
28
|
+
// @mohasinac/appkit directly (appkit/scripts/seed-cli.mjs), which sets
|
|
29
|
+
// `globalThis.require` itself before importing appkit — see that file.
|
|
30
|
+
function nodeCrypto() { return require("crypto"); }
|
|
25
31
|
const ALGO = "aes-256-gcm";
|
|
26
|
-
export const ENC_PREFIX = "enc:v1:";
|
|
27
|
-
export const HMAC_PREFIX = "hmac-sha256:";
|
|
28
32
|
function normalizePiiSecretValue(raw) {
|
|
29
33
|
return (raw ?? "").replace(/(?:\\r|\\n|\r|\n)+$/g, "").trim();
|
|
30
34
|
}
|
|
@@ -139,10 +143,6 @@ export function decryptPii(ciphertext) {
|
|
|
139
143
|
return ciphertext;
|
|
140
144
|
return decryptValue(ciphertext);
|
|
141
145
|
}
|
|
142
|
-
/** Check if a value is encrypted by our PII system. */
|
|
143
|
-
export function isPiiEncrypted(value) {
|
|
144
|
-
return typeof value === "string" && value.startsWith(ENC_PREFIX);
|
|
145
|
-
}
|
|
146
146
|
/** Alias for hmacBlindIndex. */
|
|
147
147
|
export function piiBlindIndex(plaintext) {
|
|
148
148
|
return hmacBlindIndex(plaintext);
|
|
@@ -278,84 +278,3 @@ export function decryptPayoutBankAccount(bank) {
|
|
|
278
278
|
return bank;
|
|
279
279
|
return decryptPiiFields({ ...bank }, []);
|
|
280
280
|
}
|
|
281
|
-
// --- Public display masking -------------------------------------------------
|
|
282
|
-
/**
|
|
283
|
-
* Mask a person's name for public display.
|
|
284
|
-
* Each word is reduced to its first letter followed by "***".
|
|
285
|
-
*
|
|
286
|
-
* "John Doe" → "J*** D***"
|
|
287
|
-
* "Alice" → "A***"
|
|
288
|
-
*
|
|
289
|
-
* If the value is still an encrypted blob (e.g. PII_ENCRYPTION_KEY not set),
|
|
290
|
-
* returns "Anonymous" so encrypted ciphertext never leaks to the UI.
|
|
291
|
-
*/
|
|
292
|
-
export function maskName(name) {
|
|
293
|
-
if (!name || typeof name !== "string")
|
|
294
|
-
return "Anonymous";
|
|
295
|
-
if (isPiiEncrypted(name))
|
|
296
|
-
return "Anonymous";
|
|
297
|
-
return name
|
|
298
|
-
.trim()
|
|
299
|
-
.split(/\s+/)
|
|
300
|
-
.filter(Boolean)
|
|
301
|
-
.map((word) => word[0] + "***")
|
|
302
|
-
.join(" ");
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Mask an email address for public display.
|
|
306
|
-
* "john.doe@example.com" → "j***@***.com"
|
|
307
|
-
*/
|
|
308
|
-
export function maskEmail(email) {
|
|
309
|
-
if (!email || typeof email !== "string")
|
|
310
|
-
return "***@***.***";
|
|
311
|
-
if (isPiiEncrypted(email))
|
|
312
|
-
return "***@***.***";
|
|
313
|
-
const atIdx = email.indexOf("@");
|
|
314
|
-
if (atIdx < 0)
|
|
315
|
-
return "***";
|
|
316
|
-
const local = email.slice(0, atIdx);
|
|
317
|
-
const domain = email.slice(atIdx + 1);
|
|
318
|
-
const dotIdx = domain.lastIndexOf(".");
|
|
319
|
-
const domainMasked = dotIdx > 0 ? "***" + domain.slice(dotIdx) : "***";
|
|
320
|
-
return (local[0] ?? "*") + "***@" + domainMasked;
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Return a copy of a review document with PII fields masked for public display.
|
|
324
|
-
* Admin/owner endpoints should NOT call this — use the raw document instead.
|
|
325
|
-
*/
|
|
326
|
-
export function maskPublicReview(review) {
|
|
327
|
-
return { ...review, userName: maskName(review.userName) };
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Return a copy of a bid document with PII masked for public display
|
|
331
|
-
* (auction product page).
|
|
332
|
-
*/
|
|
333
|
-
export function maskPublicBid(bid) {
|
|
334
|
-
return { ...bid };
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Return a copy of an event-entry document with PII masked for the leaderboard.
|
|
338
|
-
* ipAddress and userEmail are never sent to the client;
|
|
339
|
-
* only userDisplayName needs masking for a public leaderboard view.
|
|
340
|
-
*/
|
|
341
|
-
export function maskPublicEventEntry(entry) {
|
|
342
|
-
const { userEmail: _e, ipAddress: _ip, ...rest } = entry;
|
|
343
|
-
return {
|
|
344
|
-
...rest,
|
|
345
|
-
...(rest.userDisplayName !== undefined
|
|
346
|
-
? { userDisplayName: maskName(rest.userDisplayName) }
|
|
347
|
-
: {}),
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
/**
|
|
351
|
-
* Return a copy of an offer document with buyer PII masked for the seller view.
|
|
352
|
-
* The seller needs to know an offer was made and its amount, but the buyer's
|
|
353
|
-
* full name and email should remain private until the order is confirmed.
|
|
354
|
-
*/
|
|
355
|
-
export function maskOfferForSeller(offer) {
|
|
356
|
-
return {
|
|
357
|
-
...offer,
|
|
358
|
-
buyerName: maskName(offer.buyerName),
|
|
359
|
-
buyerEmail: maskEmail(offer.buyerEmail),
|
|
360
|
-
};
|
|
361
|
-
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PII display-masking helpers — pure string manipulation, zero runtime
|
|
3
|
+
* dependencies. Split out from pii-encrypt.ts so these can be exported
|
|
4
|
+
* from the universal client+server barrel (index.ts): pii-encrypt.ts has
|
|
5
|
+
* a top-level `import { createRequire } from "node:module"` (needed for
|
|
6
|
+
* the AES-256-GCM/HMAC functions to work under both Next.js's bundler and
|
|
7
|
+
* a pure-ESM standalone script context) that poisons the whole module for
|
|
8
|
+
* client bundling — Turbopack statically resolves every export in a file
|
|
9
|
+
* before tree-shaking, so re-exporting even one crypto-free symbol from
|
|
10
|
+
* pii-encrypt.ts still pulls `node:module` into the client graph and hard
|
|
11
|
+
* fails the browser build. This file has no such import and is safe
|
|
12
|
+
* anywhere `ReviewModal`/`ReviewDetailShell`/`ReviewsList` (client
|
|
13
|
+
* components displaying masked reviewer names) can reach it.
|
|
14
|
+
*/
|
|
15
|
+
export declare const ENC_PREFIX = "enc:v1:";
|
|
16
|
+
export declare const HMAC_PREFIX = "hmac-sha256:";
|
|
17
|
+
/** Check if a value is encrypted by our PII system. */
|
|
18
|
+
export declare function isPiiEncrypted(value: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Mask a person's name for public display.
|
|
21
|
+
* Each word is reduced to its first letter followed by "***".
|
|
22
|
+
*
|
|
23
|
+
* "John Doe" → "J*** D***"
|
|
24
|
+
* "Alice" → "A***"
|
|
25
|
+
*
|
|
26
|
+
* If the value is still an encrypted blob (e.g. PII_ENCRYPTION_KEY not set),
|
|
27
|
+
* returns "Anonymous" so encrypted ciphertext never leaks to the UI.
|
|
28
|
+
*/
|
|
29
|
+
export declare function maskName(name: string | null | undefined): string;
|
|
30
|
+
/**
|
|
31
|
+
* Mask an email address for public display.
|
|
32
|
+
* "john.doe@example.com" → "j***@***.com"
|
|
33
|
+
*/
|
|
34
|
+
export declare function maskEmail(email: string | null | undefined): string;
|
|
35
|
+
/**
|
|
36
|
+
* Return a copy of a review document with PII fields masked for public display.
|
|
37
|
+
* Admin/owner endpoints should NOT call this — use the raw document instead.
|
|
38
|
+
*/
|
|
39
|
+
export declare function maskPublicReview<T extends {
|
|
40
|
+
userName: string;
|
|
41
|
+
}>(review: T): T;
|
|
42
|
+
/**
|
|
43
|
+
* Return a copy of a bid document with PII masked for public display
|
|
44
|
+
* (auction product page).
|
|
45
|
+
*/
|
|
46
|
+
export declare function maskPublicBid<T extends {
|
|
47
|
+
userName: string;
|
|
48
|
+
}>(bid: T): T;
|
|
49
|
+
/**
|
|
50
|
+
* Return a copy of an event-entry document with PII masked for the leaderboard.
|
|
51
|
+
* ipAddress and userEmail are never sent to the client;
|
|
52
|
+
* only userDisplayName needs masking for a public leaderboard view.
|
|
53
|
+
*/
|
|
54
|
+
export declare function maskPublicEventEntry<T extends {
|
|
55
|
+
userDisplayName?: string;
|
|
56
|
+
userEmail?: string;
|
|
57
|
+
ipAddress?: string;
|
|
58
|
+
}>(entry: T): Omit<T, "userEmail" | "ipAddress">;
|
|
59
|
+
/**
|
|
60
|
+
* Return a copy of an offer document with buyer PII masked for the seller view.
|
|
61
|
+
* The seller needs to know an offer was made and its amount, but the buyer's
|
|
62
|
+
* full name and email should remain private until the order is confirmed.
|
|
63
|
+
*/
|
|
64
|
+
export declare function maskOfferForSeller<T extends {
|
|
65
|
+
buyerName: string;
|
|
66
|
+
buyerEmail: string;
|
|
67
|
+
}>(offer: T): T;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PII display-masking helpers — pure string manipulation, zero runtime
|
|
3
|
+
* dependencies. Split out from pii-encrypt.ts so these can be exported
|
|
4
|
+
* from the universal client+server barrel (index.ts): pii-encrypt.ts has
|
|
5
|
+
* a top-level `import { createRequire } from "node:module"` (needed for
|
|
6
|
+
* the AES-256-GCM/HMAC functions to work under both Next.js's bundler and
|
|
7
|
+
* a pure-ESM standalone script context) that poisons the whole module for
|
|
8
|
+
* client bundling — Turbopack statically resolves every export in a file
|
|
9
|
+
* before tree-shaking, so re-exporting even one crypto-free symbol from
|
|
10
|
+
* pii-encrypt.ts still pulls `node:module` into the client graph and hard
|
|
11
|
+
* fails the browser build. This file has no such import and is safe
|
|
12
|
+
* anywhere `ReviewModal`/`ReviewDetailShell`/`ReviewsList` (client
|
|
13
|
+
* components displaying masked reviewer names) can reach it.
|
|
14
|
+
*/
|
|
15
|
+
export const ENC_PREFIX = "enc:v1:";
|
|
16
|
+
export const HMAC_PREFIX = "hmac-sha256:";
|
|
17
|
+
/** Check if a value is encrypted by our PII system. */
|
|
18
|
+
export function isPiiEncrypted(value) {
|
|
19
|
+
return typeof value === "string" && value.startsWith(ENC_PREFIX);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Mask a person's name for public display.
|
|
23
|
+
* Each word is reduced to its first letter followed by "***".
|
|
24
|
+
*
|
|
25
|
+
* "John Doe" → "J*** D***"
|
|
26
|
+
* "Alice" → "A***"
|
|
27
|
+
*
|
|
28
|
+
* If the value is still an encrypted blob (e.g. PII_ENCRYPTION_KEY not set),
|
|
29
|
+
* returns "Anonymous" so encrypted ciphertext never leaks to the UI.
|
|
30
|
+
*/
|
|
31
|
+
export function maskName(name) {
|
|
32
|
+
if (!name || typeof name !== "string")
|
|
33
|
+
return "Anonymous";
|
|
34
|
+
if (isPiiEncrypted(name))
|
|
35
|
+
return "Anonymous";
|
|
36
|
+
return name
|
|
37
|
+
.trim()
|
|
38
|
+
.split(/\s+/)
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.map((word) => word[0] + "***")
|
|
41
|
+
.join(" ");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Mask an email address for public display.
|
|
45
|
+
* "john.doe@example.com" → "j***@***.com"
|
|
46
|
+
*/
|
|
47
|
+
export function maskEmail(email) {
|
|
48
|
+
if (!email || typeof email !== "string")
|
|
49
|
+
return "***@***.***";
|
|
50
|
+
if (isPiiEncrypted(email))
|
|
51
|
+
return "***@***.***";
|
|
52
|
+
const atIdx = email.indexOf("@");
|
|
53
|
+
if (atIdx < 0)
|
|
54
|
+
return "***";
|
|
55
|
+
const local = email.slice(0, atIdx);
|
|
56
|
+
const domain = email.slice(atIdx + 1);
|
|
57
|
+
const dotIdx = domain.lastIndexOf(".");
|
|
58
|
+
const domainMasked = dotIdx > 0 ? "***" + domain.slice(dotIdx) : "***";
|
|
59
|
+
return (local[0] ?? "*") + "***@" + domainMasked;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Return a copy of a review document with PII fields masked for public display.
|
|
63
|
+
* Admin/owner endpoints should NOT call this — use the raw document instead.
|
|
64
|
+
*/
|
|
65
|
+
export function maskPublicReview(review) {
|
|
66
|
+
return { ...review, userName: maskName(review.userName) };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Return a copy of a bid document with PII masked for public display
|
|
70
|
+
* (auction product page).
|
|
71
|
+
*/
|
|
72
|
+
export function maskPublicBid(bid) {
|
|
73
|
+
return { ...bid };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Return a copy of an event-entry document with PII masked for the leaderboard.
|
|
77
|
+
* ipAddress and userEmail are never sent to the client;
|
|
78
|
+
* only userDisplayName needs masking for a public leaderboard view.
|
|
79
|
+
*/
|
|
80
|
+
export function maskPublicEventEntry(entry) {
|
|
81
|
+
const { userEmail: _e, ipAddress: _ip, ...rest } = entry;
|
|
82
|
+
return {
|
|
83
|
+
...rest,
|
|
84
|
+
...(rest.userDisplayName !== undefined
|
|
85
|
+
? { userDisplayName: maskName(rest.userDisplayName) }
|
|
86
|
+
: {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Return a copy of an offer document with buyer PII masked for the seller view.
|
|
91
|
+
* The seller needs to know an offer was made and its amount, but the buyer's
|
|
92
|
+
* full name and email should remain private until the order is confirmed.
|
|
93
|
+
*/
|
|
94
|
+
export function maskOfferForSeller(offer) {
|
|
95
|
+
return {
|
|
96
|
+
...offer,
|
|
97
|
+
buyerName: maskName(offer.buyerName),
|
|
98
|
+
buyerEmail: maskEmail(offer.buyerEmail),
|
|
99
|
+
};
|
|
100
|
+
}
|
package/package.json
CHANGED
package/scripts/seed-cli.mjs
CHANGED
|
@@ -117,6 +117,15 @@ if (ACTION === "load" && !PII_KEY_PRESENT) {
|
|
|
117
117
|
// firebase-admin init
|
|
118
118
|
// ---------------------------------------------------------------------------
|
|
119
119
|
const require = createRequire(import.meta.url);
|
|
120
|
+
// appkit's pii-encrypt.ts calls a bare `require("crypto")` at runtime (not a
|
|
121
|
+
// static import — deliberately, so bundlers like Turbopack never see a
|
|
122
|
+
// node:module dependency and refuse to build the client bundle). A bare
|
|
123
|
+
// `require` identifier resolves via the global scope in Node ESM, so it must
|
|
124
|
+
// exist as a global before the dynamic `import("@mohasinac/appkit")` below
|
|
125
|
+
// ever triggers that code path. Only this standalone-script environment
|
|
126
|
+
// needs it — Next.js's own server bundle (webpack/Turbopack) provides an
|
|
127
|
+
// ambient `require` automatically.
|
|
128
|
+
globalThis.require = require;
|
|
120
129
|
const admin = require("firebase-admin");
|
|
121
130
|
|
|
122
131
|
function parsePrivateKey(raw) {
|