@pubinfo-pr/module-pinia-crypto 0.1.1

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.
@@ -0,0 +1,12 @@
1
+ import { PiniaPersistHookBasePayload, PiniaPersistReadHookPayload, PiniaPersistWriteHookPayload } from 'pubinfo-pr';
2
+ import { PiniaCryptoCipher, PiniaCryptoOptions } from './types';
3
+ interface PiniaCryptoRuntime {
4
+ enabled: boolean;
5
+ cipher?: PiniaCryptoCipher;
6
+ match: (payload: PiniaPersistHookBasePayload) => boolean;
7
+ logError: (phase: 'encrypt' | 'decrypt', payload: PiniaPersistHookBasePayload, error: unknown) => void;
8
+ }
9
+ export declare function createPiniaCryptoRuntime(options?: PiniaCryptoOptions): PiniaCryptoRuntime;
10
+ export declare function encryptPayload(payload: PiniaPersistWriteHookPayload, runtime: PiniaCryptoRuntime): PiniaPersistWriteHookPayload;
11
+ export declare function decryptPayload(payload: PiniaPersistReadHookPayload, runtime: PiniaCryptoRuntime): PiniaPersistReadHookPayload;
12
+ export {};
@@ -0,0 +1,26 @@
1
+ import { PiniaPersistHookBasePayload, PiniaPersistReadHookPayload, PiniaPersistWriteHookPayload } from 'pubinfo-pr';
2
+ export type StoreMatcher = string | RegExp | ((payload: PiniaPersistHookBasePayload) => boolean);
3
+ export interface PiniaCryptoCipher {
4
+ encrypt: (payload: PiniaPersistWriteHookPayload) => string;
5
+ decrypt: (payload: PiniaPersistReadHookPayload) => string;
6
+ }
7
+ export interface PiniaAesCipherOptions {
8
+ /** AES 秘钥,自动补齐/截断为 32 字节 */
9
+ secret?: string;
10
+ /** CBC 模式下的初始化向量,自动补齐/截断为 16 字节 */
11
+ iv?: string;
12
+ /** 加密模式,默认 CBC */
13
+ mode?: 'cbc' | 'ecb';
14
+ }
15
+ export interface PiniaCryptoOptions extends PiniaAesCipherOptions {
16
+ /** 是否启用插件 */
17
+ enable?: boolean;
18
+ /** 仅对匹配的 store 应用加密 */
19
+ include?: StoreMatcher | StoreMatcher[];
20
+ /** 排除不需要加密的 store */
21
+ exclude?: StoreMatcher | StoreMatcher[];
22
+ /** 自定义加密器(优先级高于 secret 配置) */
23
+ cipher?: PiniaCryptoCipher;
24
+ /** 开启后会输出详细日志 */
25
+ debug?: boolean;
26
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@pubinfo-pr/module-pinia-crypto",
3
+ "version": "0.1.1",
4
+ "description": "pubinfo 框架的加密集成",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ },
10
+ "./style.css": "./dist/index.css"
11
+ },
12
+ "main": "./dist/index.js",
13
+ "module": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "engines": {
20
+ "node": "^20.19.0 || >=22.12.0"
21
+ },
22
+ "peerDependencies": {
23
+ "pubinfo-pr": "0.1.1"
24
+ },
25
+ "dependencies": {
26
+ "crypto-js": "^4.2.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/crypto-js": "^4.2.2",
30
+ "pubinfo-pr": "0.1.1"
31
+ },
32
+ "scripts": {
33
+ "dev": "pubinfo build -w --sourcemap",
34
+ "build": "pubinfo build",
35
+ "openapi": "pnpx @pubinfo/openapi generate"
36
+ }
37
+ }
package/src/cipher.ts ADDED
@@ -0,0 +1,66 @@
1
+ import type { PiniaAesCipherOptions, PiniaCryptoCipher } from './types';
2
+ import CryptoJS from 'crypto-js';
3
+
4
+ const DEFAULT_SECRET = 'pubinfo-pinia-secret';
5
+ const DEFAULT_IV = 'pubinfo-pinia-iv';
6
+
7
+ export function createPiniaAesCipher(options: PiniaAesCipherOptions = {}): PiniaCryptoCipher {
8
+ const {
9
+ secret = DEFAULT_SECRET,
10
+ iv = DEFAULT_IV,
11
+ mode = 'cbc',
12
+ } = options;
13
+
14
+ const keyWordArray = CryptoJS.enc.Utf8.parse(normalizeLength(secret, 32));
15
+ const ivWordArray = mode === 'cbc' ? CryptoJS.enc.Utf8.parse(normalizeLength(iv, 16)) : undefined;
16
+ const cryptoMode = mode === 'ecb' ? CryptoJS.mode.ECB : CryptoJS.mode.CBC;
17
+
18
+ const buildConfig = () => ({
19
+ mode: cryptoMode,
20
+ padding: CryptoJS.pad.Pkcs7,
21
+ ...(ivWordArray ? { iv: ivWordArray } : {}),
22
+ });
23
+
24
+ return {
25
+ encrypt: ({ value }) => CryptoJS.AES.encrypt(value, keyWordArray, buildConfig()).toString(),
26
+ decrypt: ({ value }) => {
27
+ const decrypted = CryptoJS.AES.decrypt(value, keyWordArray, buildConfig()).toString(CryptoJS.enc.Utf8);
28
+ return isLikelyJson(decrypted) ? decrypted : value;
29
+ },
30
+ };
31
+ }
32
+
33
+ function normalizeLength(value: string, size: number) {
34
+ if (!value) {
35
+ return ''.padEnd(size, '0');
36
+ }
37
+ if (value.length === size) {
38
+ return value;
39
+ }
40
+ if (value.length > size) {
41
+ return value.slice(0, size);
42
+ }
43
+ return value.padEnd(size, '0');
44
+ }
45
+
46
+ function isLikelyJson(value: string) {
47
+ if (!value) {
48
+ return false;
49
+ }
50
+ const trimmed = value.trim();
51
+ if (!trimmed) {
52
+ return false;
53
+ }
54
+ const first = trimmed[0];
55
+ const validStarters = new Set(['{', '[', '"', 't', 'f', 'n', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
56
+ if (!validStarters.has(first)) {
57
+ return false;
58
+ }
59
+ try {
60
+ JSON.parse(trimmed);
61
+ return true;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { ModuleOptions } from 'pubinfo-pr';
2
+ import type { PiniaCryptoOptions } from './types';
3
+ import { createPiniaCryptoRuntime, decryptPayload, encryptPayload } from './runtime';
4
+ import 'uno.css';
5
+
6
+ export function piniaCrypto(options: PiniaCryptoOptions = {}): ModuleOptions {
7
+ const runtime = createPiniaCryptoRuntime(options);
8
+
9
+ return {
10
+ name: 'pubinfo:pinia-crypto',
11
+ enforce: 'pre',
12
+ hooks: {
13
+ 'pinia:persist:write': payload => encryptPayload(payload, runtime),
14
+ 'pinia:persist:read': payload => decryptPayload(payload, runtime),
15
+ },
16
+ };
17
+ }
18
+
19
+ export { createPiniaAesCipher } from './cipher';
20
+ export type { PiniaAesCipherOptions, PiniaCryptoCipher, PiniaCryptoOptions, StoreMatcher } from './types';
package/src/runtime.ts ADDED
@@ -0,0 +1,105 @@
1
+ import type {
2
+ PiniaPersistHookBasePayload,
3
+ PiniaPersistReadHookPayload,
4
+ PiniaPersistWriteHookPayload,
5
+ } from 'pubinfo-pr';
6
+ import type { PiniaCryptoCipher, PiniaCryptoOptions, StoreMatcher } from './types';
7
+ import { createPiniaAesCipher } from './cipher';
8
+
9
+ interface PiniaCryptoRuntime {
10
+ enabled: boolean
11
+ cipher?: PiniaCryptoCipher
12
+ match: (payload: PiniaPersistHookBasePayload) => boolean
13
+ logError: (phase: 'encrypt' | 'decrypt', payload: PiniaPersistHookBasePayload, error: unknown) => void
14
+ }
15
+
16
+ export function createPiniaCryptoRuntime(options: PiniaCryptoOptions = {}): PiniaCryptoRuntime {
17
+ const enabled = options.enable ?? true;
18
+ const include = normalizeMatchers(options.include);
19
+ const exclude = normalizeMatchers(options.exclude);
20
+ const cipher = options.cipher ?? createPiniaAesCipher({
21
+ secret: options.secret,
22
+ iv: options.iv,
23
+ mode: options.mode,
24
+ });
25
+ const debug = options.debug ?? false;
26
+
27
+ return {
28
+ enabled,
29
+ cipher,
30
+ match: (payload) => {
31
+ if (include.length && !include.some(matcher => matchStore(matcher, payload))) {
32
+ return false;
33
+ }
34
+ if (exclude.length && exclude.some(matcher => matchStore(matcher, payload))) {
35
+ return false;
36
+ }
37
+ return true;
38
+ },
39
+ logError: (phase, payload, error) => {
40
+ if (!debug) {
41
+ return;
42
+ }
43
+ console.error(`[pinia-crypto] ${phase} failed for store "${payload.storeId}"`, error);
44
+ },
45
+ };
46
+ }
47
+
48
+ export function encryptPayload(
49
+ payload: PiniaPersistWriteHookPayload,
50
+ runtime: PiniaCryptoRuntime,
51
+ ) {
52
+ if (!shouldProcess(payload, runtime)) {
53
+ return payload;
54
+ }
55
+ try {
56
+ const encrypted = runtime.cipher!.encrypt(payload);
57
+ if (typeof encrypted === 'string') {
58
+ payload.value = encrypted;
59
+ }
60
+ }
61
+ catch (error) {
62
+ runtime.logError('encrypt', payload, error);
63
+ }
64
+ return payload;
65
+ }
66
+
67
+ export function decryptPayload(
68
+ payload: PiniaPersistReadHookPayload,
69
+ runtime: PiniaCryptoRuntime,
70
+ ) {
71
+ if (!shouldProcess(payload, runtime)) {
72
+ return payload;
73
+ }
74
+ try {
75
+ const decrypted = runtime.cipher!.decrypt(payload);
76
+ if (typeof decrypted === 'string') {
77
+ payload.value = decrypted;
78
+ }
79
+ }
80
+ catch (error) {
81
+ runtime.logError('decrypt', payload, error);
82
+ }
83
+ return payload;
84
+ }
85
+
86
+ function shouldProcess(payload: PiniaPersistHookBasePayload, runtime: PiniaCryptoRuntime) {
87
+ return runtime.enabled && Boolean(runtime.cipher) && runtime.match(payload);
88
+ }
89
+
90
+ function normalizeMatchers(value?: StoreMatcher | StoreMatcher[]) {
91
+ if (!value) {
92
+ return [] as StoreMatcher[];
93
+ }
94
+ return Array.isArray(value) ? value : [value];
95
+ }
96
+
97
+ function matchStore(matcher: StoreMatcher, payload: PiniaPersistHookBasePayload) {
98
+ if (typeof matcher === 'string') {
99
+ return matcher === payload.storeId;
100
+ }
101
+ if (matcher instanceof RegExp) {
102
+ return matcher.test(payload.storeId);
103
+ }
104
+ return matcher(payload);
105
+ }
package/src/types.ts ADDED
@@ -0,0 +1,34 @@
1
+ import type {
2
+ PiniaPersistHookBasePayload,
3
+ PiniaPersistReadHookPayload,
4
+ PiniaPersistWriteHookPayload,
5
+ } from 'pubinfo-pr';
6
+
7
+ export type StoreMatcher = string | RegExp | ((payload: PiniaPersistHookBasePayload) => boolean);
8
+
9
+ export interface PiniaCryptoCipher {
10
+ encrypt: (payload: PiniaPersistWriteHookPayload) => string
11
+ decrypt: (payload: PiniaPersistReadHookPayload) => string
12
+ }
13
+
14
+ export interface PiniaAesCipherOptions {
15
+ /** AES 秘钥,自动补齐/截断为 32 字节 */
16
+ secret?: string
17
+ /** CBC 模式下的初始化向量,自动补齐/截断为 16 字节 */
18
+ iv?: string
19
+ /** 加密模式,默认 CBC */
20
+ mode?: 'cbc' | 'ecb'
21
+ }
22
+
23
+ export interface PiniaCryptoOptions extends PiniaAesCipherOptions {
24
+ /** 是否启用插件 */
25
+ enable?: boolean
26
+ /** 仅对匹配的 store 应用加密 */
27
+ include?: StoreMatcher | StoreMatcher[]
28
+ /** 排除不需要加密的 store */
29
+ exclude?: StoreMatcher | StoreMatcher[]
30
+ /** 自定义加密器(优先级高于 secret 配置) */
31
+ cipher?: PiniaCryptoCipher
32
+ /** 开启后会输出详细日志 */
33
+ debug?: boolean
34
+ }