@react-vault/create-app 0.1.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/LICENSE +12 -0
- package/README.md +16 -0
- package/bin/create-app.js +8 -0
- package/claude-toolkit/README.md +131 -0
- package/claude-toolkit/agents/bfsi-accessibility-auditor.md +132 -0
- package/claude-toolkit/agents/bfsi-architect.md +156 -0
- package/claude-toolkit/agents/bfsi-code-reviewer.md +137 -0
- package/claude-toolkit/agents/bfsi-compliance-auditor.md +161 -0
- package/claude-toolkit/agents/bfsi-pii-scanner.md +142 -0
- package/claude-toolkit/agents/bfsi-pr-reviewer.md +114 -0
- package/claude-toolkit/agents/bfsi-security-reviewer.md +136 -0
- package/claude-toolkit/commands/bfsi-audit.md +46 -0
- package/claude-toolkit/commands/bfsi-doctor.md +97 -0
- package/claude-toolkit/commands/bfsi-review.md +46 -0
- package/claude-toolkit/commands/bfsi-scaffold.md +47 -0
- package/claude-toolkit/hooks/hooks.json +181 -0
- package/claude-toolkit/hooks/scripts/a11y-check.sh +63 -0
- package/claude-toolkit/hooks/scripts/audit-prompt.sh +36 -0
- package/claude-toolkit/hooks/scripts/block-destructive.sh +41 -0
- package/claude-toolkit/hooks/scripts/block-force-push.sh +30 -0
- package/claude-toolkit/hooks/scripts/format.sh +42 -0
- package/claude-toolkit/hooks/scripts/inject-context.sh +44 -0
- package/claude-toolkit/hooks/scripts/lint.sh +45 -0
- package/claude-toolkit/hooks/scripts/protect-files.sh +53 -0
- package/claude-toolkit/hooks/scripts/save-compliance-context.sh +35 -0
- package/claude-toolkit/hooks/scripts/scan-pii.sh +87 -0
- package/claude-toolkit/hooks/scripts/scan-secrets.sh +67 -0
- package/claude-toolkit/hooks/scripts/verify-clean.sh +50 -0
- package/claude-toolkit/package.json +22 -0
- package/claude-toolkit/plugin.json +31 -0
- package/claude-toolkit/skills/bfsi-api-endpoint/SKILL.md +105 -0
- package/claude-toolkit/skills/bfsi-commit/SKILL.md +102 -0
- package/claude-toolkit/skills/bfsi-compliance-check/SKILL.md +107 -0
- package/claude-toolkit/skills/bfsi-encrypt-helper/SKILL.md +127 -0
- package/claude-toolkit/skills/bfsi-error-message/SKILL.md +162 -0
- package/claude-toolkit/skills/bfsi-feature/SKILL.md +120 -0
- package/claude-toolkit/skills/bfsi-feature/references/architecture.md +69 -0
- package/claude-toolkit/skills/bfsi-feature/references/audit-events.md +70 -0
- package/claude-toolkit/skills/bfsi-feature/scripts/scaffold.mjs +136 -0
- package/claude-toolkit/skills/bfsi-form/SKILL.md +73 -0
- package/claude-toolkit/skills/bfsi-form/references/validation-regex.md +50 -0
- package/claude-toolkit/skills/bfsi-onboarding/SKILL.md +110 -0
- package/claude-toolkit/skills/bfsi-pii-field/SKILL.md +90 -0
- package/claude-toolkit/skills/bfsi-test-pattern/SKILL.md +179 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +339 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/templates/_shared/.claude/settings.json +31 -0
- package/templates/_shared/.env.local.sample +25 -0
- package/templates/_shared/.github/workflows/ci.yml +49 -0
- package/templates/_shared/CLAUDE.md +89 -0
- package/templates/_shared/README.md +50 -0
- package/templates/_shared/index.html +16 -0
- package/templates/_shared/package.json +73 -0
- package/templates/_shared/postcss.config.cjs +6 -0
- package/templates/_shared/src/app/App.tsx +13 -0
- package/templates/_shared/src/app/globals.css +64 -0
- package/templates/_shared/src/env.ts +33 -0
- package/templates/_shared/src/i18n/i18n.ts +18 -0
- package/templates/_shared/src/i18n/translations/en.json +54 -0
- package/templates/_shared/src/i18n/translations/hi.json +30 -0
- package/templates/_shared/src/main.tsx +16 -0
- package/templates/_shared/src/routes/ProtectedRoute.tsx +28 -0
- package/templates/_shared/src/routes/index.tsx +67 -0
- package/templates/_shared/src/shared/ErrorBoundary.tsx +60 -0
- package/templates/_shared/tailwind.config.ts +68 -0
- package/templates/_shared/tests/setup.ts +7 -0
- package/templates/_shared/tsconfig.json +33 -0
- package/templates/_shared/tsconfig.node.json +13 -0
- package/templates/_shared/vite.config.ts +47 -0
- package/templates/rtk-query/.claude/skills/axios-auth/SKILL.md +103 -0
- package/templates/rtk-query/.claude/skills/axios-auth/references/error-shape.md +84 -0
- package/templates/rtk-query/.claude/skills/axios-auth/references/full-code-walkthrough.md +146 -0
- package/templates/rtk-query/.claude/skills/axios-auth/references/notification-wiring.md +141 -0
- package/templates/rtk-query/.claude/skills/constants-organization/SKILL.md +112 -0
- package/templates/rtk-query/.claude/skills/constants-organization/references/example-files.md +134 -0
- package/templates/rtk-query/.claude/skills/constants-organization/references/tag-types-catalog.md +53 -0
- package/templates/rtk-query/.claude/skills/redux-store-integration/SKILL.md +159 -0
- package/templates/rtk-query/.claude/skills/redux-store-integration/references/localStorage-persistence.md +70 -0
- package/templates/rtk-query/.claude/skills/redux-store-integration/references/middleware-patterns.md +82 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/SKILL.md +148 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/references/cache-strategies.md +96 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/references/endpoint-cookbook.md +145 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/references/optimistic-update.md +53 -0
- package/templates/rtk-query/README.md +84 -0
- package/templates/rtk-query/package.partial.json +7 -0
- package/templates/rtk-query/src/app/App.tsx +23 -0
- package/templates/rtk-query/src/axiosconfig/axiosInstance.ts +26 -0
- package/templates/rtk-query/src/axiosconfig/baseQuery.ts +72 -0
- package/templates/rtk-query/src/axiosconfig/interceptor.ts +42 -0
- package/templates/rtk-query/src/redux/invalidateCacheMiddleware.ts +20 -0
- package/templates/rtk-query/src/redux/reduxHooks.ts +10 -0
- package/templates/rtk-query/src/redux/rootReducer.ts +18 -0
- package/templates/rtk-query/src/redux/store.ts +36 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/SKILL.md +109 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/references/error-shape.md +89 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/references/full-code-walkthrough.md +121 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/references/notification-pattern.md +109 -0
- package/templates/tanstack-query/.claude/skills/constants-organization/SKILL.md +144 -0
- package/templates/tanstack-query/.claude/skills/constants-organization/references/example-files.md +111 -0
- package/templates/tanstack-query/.claude/skills/constants-organization/references/query-key-factories.md +129 -0
- package/templates/tanstack-query/.claude/skills/query-client-setup/SKILL.md +165 -0
- package/templates/tanstack-query/.claude/skills/query-client-setup/references/devtools.md +67 -0
- package/templates/tanstack-query/.claude/skills/query-client-setup/references/global-handlers.md +94 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/SKILL.md +142 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/references/audited-mutation.md +144 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/references/optimistic-update.md +102 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/references/service-cookbook.md +151 -0
- package/templates/tanstack-query/README.md +63 -0
- package/templates/tanstack-query/package.partial.json +8 -0
- package/templates/tanstack-query/src/api/axiosInstance.ts +20 -0
- package/templates/tanstack-query/src/api/http.ts +62 -0
- package/templates/tanstack-query/src/api/queryClient.ts +28 -0
- package/templates/tanstack-query/src/app/App.tsx +20 -0
- package/templates/tanstack-query/src/services/example.ts +32 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* BFSI feature scaffolder.
|
|
4
|
+
*
|
|
5
|
+
* Reads templates from references/templates/ and writes a complete feature module.
|
|
6
|
+
*
|
|
7
|
+
* Usage: node scaffold.mjs <FeatureName> --variant=rtk|tanstack [--no-i18n]
|
|
8
|
+
*/
|
|
9
|
+
import { argv, exit, cwd } from 'node:process';
|
|
10
|
+
import { mkdir, readFile, writeFile, access } from 'node:fs/promises';
|
|
11
|
+
import { join, dirname } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
const TEMPLATES_DIR = join(__dirname, '..', 'references', 'templates');
|
|
17
|
+
|
|
18
|
+
const log = (msg) => console.error(`[bfsi-feature] ${msg}`);
|
|
19
|
+
const die = (msg) => {
|
|
20
|
+
log(`error: ${msg}`);
|
|
21
|
+
exit(2);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function parseArgs(args) {
|
|
25
|
+
const positional = args.filter((a) => !a.startsWith('--'));
|
|
26
|
+
const flags = Object.fromEntries(
|
|
27
|
+
args
|
|
28
|
+
.filter((a) => a.startsWith('--'))
|
|
29
|
+
.map((a) => {
|
|
30
|
+
const [k, v = 'true'] = a.replace(/^--/, '').split('=');
|
|
31
|
+
return [k, v];
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
return { positional, flags };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toKebab(s) {
|
|
38
|
+
return s.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function exists(p) {
|
|
42
|
+
try {
|
|
43
|
+
await access(p);
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function render(templateName, vars) {
|
|
51
|
+
const tplPath = join(TEMPLATES_DIR, templateName);
|
|
52
|
+
if (!(await exists(tplPath))) {
|
|
53
|
+
log(`warning: template missing: ${templateName} (skipping)`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
let content = await readFile(tplPath, 'utf8');
|
|
57
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
58
|
+
content = content.replaceAll(`{{${k}}}`, v);
|
|
59
|
+
}
|
|
60
|
+
return content;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function writeIfTemplate(templateName, dest, vars) {
|
|
64
|
+
const content = await render(templateName, vars);
|
|
65
|
+
if (content === null) return false;
|
|
66
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
67
|
+
await writeFile(dest, content, 'utf8');
|
|
68
|
+
log(`wrote ${dest}`);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function main() {
|
|
73
|
+
const { positional, flags } = parseArgs(argv.slice(2));
|
|
74
|
+
const Name = positional[0];
|
|
75
|
+
|
|
76
|
+
if (!Name) die('feature name is required');
|
|
77
|
+
if (!/^[A-Z][A-Za-z0-9]+$/.test(Name)) die('feature name must be PascalCase');
|
|
78
|
+
|
|
79
|
+
const variant = flags.variant || 'rtk';
|
|
80
|
+
if (!['rtk', 'tanstack'].includes(variant)) {
|
|
81
|
+
die(`unknown variant: ${variant} (expected rtk or tanstack)`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const kebab = toKebab(Name);
|
|
85
|
+
const root = cwd();
|
|
86
|
+
const featureDir = join(root, 'src', 'features', Name);
|
|
87
|
+
|
|
88
|
+
if (await exists(featureDir)) {
|
|
89
|
+
die(`feature directory already exists: ${featureDir}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const vars = {
|
|
93
|
+
Name,
|
|
94
|
+
name: Name.charAt(0).toLowerCase() + Name.slice(1),
|
|
95
|
+
kebab,
|
|
96
|
+
NAME: Name.toUpperCase(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Generate files (templates may not all exist yet — that's OK in v0.1)
|
|
100
|
+
const apiTemplate = variant === 'rtk' ? 'api.rtk.ts.tpl' : 'api.tanstack.ts.tpl';
|
|
101
|
+
await writeIfTemplate(apiTemplate, join(featureDir, 'api.ts'), vars);
|
|
102
|
+
await writeIfTemplate('schema.ts.tpl', join(featureDir, 'schema.ts'), vars);
|
|
103
|
+
await writeIfTemplate('types.ts.tpl', join(featureDir, 'types.ts'), vars);
|
|
104
|
+
await writeIfTemplate('constants.ts.tpl', join(featureDir, 'constants.ts'), vars);
|
|
105
|
+
await writeIfTemplate('routes.tsx.tpl', join(featureDir, 'routes.tsx'), vars);
|
|
106
|
+
await writeIfTemplate('index.ts.tpl', join(featureDir, 'index.ts'), vars);
|
|
107
|
+
await writeIfTemplate(
|
|
108
|
+
'containers.list.tsx.tpl',
|
|
109
|
+
join(featureDir, 'containers', `${Name}List.tsx`),
|
|
110
|
+
vars,
|
|
111
|
+
);
|
|
112
|
+
await writeIfTemplate(
|
|
113
|
+
'containers.form.tsx.tpl',
|
|
114
|
+
join(featureDir, 'containers', `${Name}Form.tsx`),
|
|
115
|
+
vars,
|
|
116
|
+
);
|
|
117
|
+
await writeIfTemplate(
|
|
118
|
+
'components.table.tsx.tpl',
|
|
119
|
+
join(featureDir, 'components', `${Name}Table.tsx`),
|
|
120
|
+
vars,
|
|
121
|
+
);
|
|
122
|
+
await writeIfTemplate(
|
|
123
|
+
'tests.schema.test.ts.tpl',
|
|
124
|
+
join(featureDir, '__tests__', 'schema.test.ts'),
|
|
125
|
+
vars,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
log(`done: feature "${Name}" scaffolded under src/features/${Name}/`);
|
|
129
|
+
log(`variant: ${variant}`);
|
|
130
|
+
log(`next: pnpm typecheck && pnpm lint --filter ${kebab}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
main().catch((err) => {
|
|
134
|
+
log(`unexpected error: ${err.message}`);
|
|
135
|
+
exit(2);
|
|
136
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bfsi-form
|
|
3
|
+
description: Creates a React Hook Form + Zod form with BFSI-secure defaults (no autocomplete on PII fields, paste prevention on sensitive inputs, audit on submit, error masking). Use when the user types /bfsi-form, asks to "create a form", "add a form to this page", "build a KYC form", or "scaffold a transfer form".
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
argument-hint: <FormName> [--fields "name:type,..."]
|
|
6
|
+
allowed-tools: Read Write Edit Glob Grep
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# BFSI Form Scaffold
|
|
10
|
+
|
|
11
|
+
Generates a `react-hook-form` + `zod` form with BFSI-secure defaults wired into shadcn/ui's `<Form>` primitives.
|
|
12
|
+
|
|
13
|
+
## What it adds
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
<TargetFile>.tsx
|
|
17
|
+
└── <FormName>Form # Container component
|
|
18
|
+
├── useFormWithZod() # from @react-vault/ui
|
|
19
|
+
├── audit-wrapped onSubmit # via useAuditedAction
|
|
20
|
+
├── PIIMaskedDisplay # auto-wrap for fields matching PII pattern
|
|
21
|
+
└── BFSI defaults:
|
|
22
|
+
├── autocomplete="off" on PII fields
|
|
23
|
+
├── onPaste guards on sensitive inputs
|
|
24
|
+
├── inputMode + pattern for known formats (PAN, Aadhaar, mobile)
|
|
25
|
+
├── error messages from i18n keys (never raw)
|
|
26
|
+
└── submit disabled while !isDirty || !isValid || isSubmitting
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Workflow
|
|
30
|
+
|
|
31
|
+
### Step 1: Determine field set
|
|
32
|
+
|
|
33
|
+
If `--fields` is provided, parse it (`name:type` comma-separated, e.g. `pan:string,amount:number,date:date`).
|
|
34
|
+
Otherwise, ask the user (via prompt in chat) which fields they need.
|
|
35
|
+
|
|
36
|
+
### Step 2: Build the Zod schema
|
|
37
|
+
|
|
38
|
+
For each field, choose a base from BFSI presets:
|
|
39
|
+
|
|
40
|
+
- `pan` → `z.string().regex(/^[A-Z]{5}[0-9]{4}[A-Z]$/, t('error.pan_invalid'))`
|
|
41
|
+
- `aadhaar` → `z.string().regex(/^\d{12}$/).refine(verhoeff)`
|
|
42
|
+
- `mobile` → `z.string().regex(/^[6-9]\d{9}$/)`
|
|
43
|
+
- `email` → `z.string().email()`
|
|
44
|
+
- `amount` → `z.number().positive().multipleOf(0.01)`
|
|
45
|
+
- `account_number` → `z.string().regex(/^\d{9,18}$/)`
|
|
46
|
+
- `ifsc` → `z.string().regex(/^[A-Z]{4}0[A-Z0-9]{6}$/)`
|
|
47
|
+
- `date` → `z.coerce.date()`
|
|
48
|
+
|
|
49
|
+
If a field name doesn't match a preset, fall back to `z.string().min(1)` and flag it for review.
|
|
50
|
+
|
|
51
|
+
### Step 3: Generate the form component
|
|
52
|
+
|
|
53
|
+
Use the template at `references/templates/form.tsx.tpl`. Imports from `@react-vault/ui` and `@react-vault/core`. Submit handler uses `useAuditedAction("<feature>.<form>.submitted")`.
|
|
54
|
+
|
|
55
|
+
### Step 4: Add i18n keys
|
|
56
|
+
|
|
57
|
+
Add label/placeholder/error keys to the project's translation files (en.json + hi.json placeholder).
|
|
58
|
+
|
|
59
|
+
### Step 5: Verify
|
|
60
|
+
|
|
61
|
+
Run `pnpm typecheck` on the new file. If `useFormWithZod` import fails, the project doesn't have `@react-vault/ui` installed — tell the user to install it.
|
|
62
|
+
|
|
63
|
+
## Conventions
|
|
64
|
+
|
|
65
|
+
- **Never** capture card numbers or CVVs in a regular form. Use `<PCITokenizedCardInput>` from `@react-vault/ui` which embeds a PCI-compliant iframe.
|
|
66
|
+
- **Never** persist form drafts of PII fields to localStorage. Use `sessionStorage` with the `secureStorage` wrapper from `@react-vault/core/storage`.
|
|
67
|
+
- **Submit handler returns a Promise.** Errors thrown inside it are caught by the form, surfaced via toast, and audited as `<form>.submission_failed`.
|
|
68
|
+
|
|
69
|
+
## References
|
|
70
|
+
|
|
71
|
+
- Templates: [`references/templates/`](references/templates/)
|
|
72
|
+
- Validation regex catalogue: [`references/validation-regex.md`](references/validation-regex.md)
|
|
73
|
+
- shadcn/ui Form primer: https://ui.shadcn.com/docs/components/form
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# BFSI Validation Regex Catalogue
|
|
2
|
+
|
|
3
|
+
All regex patterns used by the `bfsi-form` skill. Keep this file as the single source of truth — the `@react-vault/core/pii` module imports from here.
|
|
4
|
+
|
|
5
|
+
## Identity
|
|
6
|
+
|
|
7
|
+
| Field | Regex | Notes |
|
|
8
|
+
| --------------- | ------------------------------- | ------------------------------------------------------ |
|
|
9
|
+
| PAN | `^[A-Z]{5}[0-9]{4}[A-Z]$` | Permanent Account Number (Income Tax India) |
|
|
10
|
+
| Aadhaar | `^\d{12}$` | 12 digits + Verhoeff checksum (see `aadhaar.verhoeff`) |
|
|
11
|
+
| Passport | `^[A-Z][0-9]{7}$` | Indian passport |
|
|
12
|
+
| Voter ID | `^[A-Z]{3}\d{7}$` | EPIC number |
|
|
13
|
+
| Driving Licence | `^[A-Z]{2}\d{2} ?\d{4} ?\d{7}$` | State-prefix + sequence |
|
|
14
|
+
|
|
15
|
+
## Contact
|
|
16
|
+
|
|
17
|
+
| Field | Regex | Notes |
|
|
18
|
+
| -------------- | ---------------------------------------- | ---------------------- |
|
|
19
|
+
| Mobile (India) | `^[6-9]\d{9}$` | 10 digits starting 6-9 |
|
|
20
|
+
| Email | RFC 5322 lite (use `z.string().email()`) | Don't roll your own |
|
|
21
|
+
| Pincode | `^\d{6}$` | Indian postal code |
|
|
22
|
+
|
|
23
|
+
## Banking
|
|
24
|
+
|
|
25
|
+
| Field | Regex | Notes |
|
|
26
|
+
| -------------- | --------------------------------------------- | --------------------------------------- |
|
|
27
|
+
| Account Number | `^\d{9,18}$` | Bank-specific; widest common range |
|
|
28
|
+
| IFSC | `^[A-Z]{4}0[A-Z0-9]{6}$` | 4 alphas + `0` + 6 alphanumeric |
|
|
29
|
+
| MICR | `^\d{9}$` | Magnetic Ink Character Recognition code |
|
|
30
|
+
| UPI VPA | `^[a-zA-Z0-9.\-_]{2,256}@[a-zA-Z]{2,64}$` | virtual payment address |
|
|
31
|
+
| SWIFT/BIC | `^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$` | 8 or 11 chars |
|
|
32
|
+
| GSTIN | `^\d{2}[A-Z]{5}\d{4}[A-Z][A-Z\d][Z][A-Z\d]$` | Goods and Services Tax ID |
|
|
33
|
+
|
|
34
|
+
## Money
|
|
35
|
+
|
|
36
|
+
| Field | Pattern | Notes |
|
|
37
|
+
| ---------- | ---------------------------------------- | ------------------------------------------------------------------------------- |
|
|
38
|
+
| Amount (₹) | `z.number().positive().multipleOf(0.01)` | NEVER use float math — always paise as integer for internal, format for display |
|
|
39
|
+
| Percentage | `z.number().min(0).max(100)` | Interest rates, ratios |
|
|
40
|
+
|
|
41
|
+
## Card data — DO NOT USE
|
|
42
|
+
|
|
43
|
+
There are no regex patterns for card numbers, CVVs, expiry dates in this catalogue. **All card data must flow through `<PCITokenizedCardInput>` which uses a PCI-compliant iframe.** A bare `<input>` capturing card data brings your app into PCI-DSS scope — never worth it.
|
|
44
|
+
|
|
45
|
+
## Implementation notes
|
|
46
|
+
|
|
47
|
+
- All regex patterns are case-sensitive unless noted.
|
|
48
|
+
- For "uppercase-or-lowercase" inputs, normalise to uppercase in the schema's `.transform()` step — don't make the regex case-insensitive.
|
|
49
|
+
- For visual readability (e.g. account number with spaces), add a `display` mapper that formats; never store the formatted version.
|
|
50
|
+
- Aadhaar regex alone is not enough — add `.refine(aadhaarVerhoeff)` to validate the checksum digit.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bfsi-onboarding
|
|
3
|
+
description: Onboards a new developer to a Your Real Company BFSI project. Explains the project structure, key conventions, how features are organised, where security primitives live, and how the Claude toolkit assists day-to-day work. Use when the user is new to the codebase and asks "how does this project work", "where do I start", "give me an overview", "what's the architecture", or "how do I add a feature".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# BFSI Project Onboarding
|
|
7
|
+
|
|
8
|
+
You are explaining a Your Real Company BFSI React project to a developer who is new to it. Be concise but thorough. Adapt depth based on their background (ask once if unclear: "Are you new to React, or new to this specific BFSI starter?").
|
|
9
|
+
|
|
10
|
+
## The project at a glance
|
|
11
|
+
|
|
12
|
+
This project was scaffolded from `@react-vault/react-starter`. It's a **Vite + React + TypeScript SPA** with security, audit, and compliance primitives wired in by default.
|
|
13
|
+
|
|
14
|
+
Stack:
|
|
15
|
+
|
|
16
|
+
- **React 18** + **Vite 5** + **TypeScript strict**
|
|
17
|
+
- **Tailwind CSS** + **shadcn/ui** (components owned in `src/components/ui/`)
|
|
18
|
+
- **React Hook Form** + **Zod** for forms
|
|
19
|
+
- **RTK Query** OR **TanStack Query** for data fetching (check `package.json`)
|
|
20
|
+
- **react-router-dom v6** with `<ProtectedRoute>` + `<CanAccess>` guards
|
|
21
|
+
- **react-i18next** for i18n (en + hi default)
|
|
22
|
+
- **Vitest** + **Testing Library** + **Playwright**
|
|
23
|
+
|
|
24
|
+
## Where things live
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
src/
|
|
28
|
+
├── app/ # App.tsx, providers, root layout
|
|
29
|
+
├── features/<Feature>/ # ALL feature code lives here (api, containers, components, tests)
|
|
30
|
+
├── routes/ # Route config, ProtectedRoute, CanAccess
|
|
31
|
+
├── shared/ # Cross-feature components (ErrorBoundary, NotFound)
|
|
32
|
+
├── i18n/ # react-i18next setup + translations
|
|
33
|
+
├── env.ts # Zod-validated env vars
|
|
34
|
+
└── main.tsx # Entry point
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Security & audit primitives come from npm packages:
|
|
38
|
+
|
|
39
|
+
- `@react-vault/core` — encryption, PII utils, audit client, axios factory, auth helpers
|
|
40
|
+
- `@react-vault/ui` — Tailwind + shadcn + BFSI compositions (PIIMaskedDisplay, ConfirmModal, etc.)
|
|
41
|
+
|
|
42
|
+
## Day-to-day workflows
|
|
43
|
+
|
|
44
|
+
### Adding a new feature
|
|
45
|
+
|
|
46
|
+
Use `/bfsi-feature MyFeature` — generates the full directory.
|
|
47
|
+
|
|
48
|
+
### Adding an endpoint
|
|
49
|
+
|
|
50
|
+
Use `/bfsi-api-endpoint GET /my-resource --feature MyFeature` — adds typed endpoint with audit.
|
|
51
|
+
|
|
52
|
+
### Adding a form
|
|
53
|
+
|
|
54
|
+
Use `/bfsi-form MyForm --fields "pan:string,amount:number"` — generates RHF + Zod form with BFSI defaults.
|
|
55
|
+
|
|
56
|
+
### Masking a PII field in display
|
|
57
|
+
|
|
58
|
+
Use `/bfsi-pii-field pan user.pan` — wraps with `<PIIMaskedDisplay>`.
|
|
59
|
+
|
|
60
|
+
### Before a PR
|
|
61
|
+
|
|
62
|
+
Run `/bfsi-compliance-check` — runs OWASP + RBI + PCI checklist over the diff.
|
|
63
|
+
Optionally run `/bfsi-review` — spawns full multi-agent review.
|
|
64
|
+
|
|
65
|
+
### Committing
|
|
66
|
+
|
|
67
|
+
Use `/bfsi-commit` — generates audit-friendly Conventional Commits message.
|
|
68
|
+
|
|
69
|
+
## Critical conventions
|
|
70
|
+
|
|
71
|
+
1. **Container-component split**: containers hold side-effects (API, audit), components are pure JSX.
|
|
72
|
+
2. **All API responses are Zod-parsed**: runtime safety. Never trust the network shape.
|
|
73
|
+
3. **All mutations are audited**: use `useAuditedMutation` (RTK) or wrap with `useAuditedAction` (TanStack).
|
|
74
|
+
4. **All routes are protected**: `<ProtectedRoute permission="...">`. Defaults to authenticated-only if `permission` omitted, but explicit is better.
|
|
75
|
+
5. **PII never enters localStorage**: use `secureStorage` from `@react-vault/core/storage` (memory-first, sessionStorage fallback, encrypted IndexedDB option).
|
|
76
|
+
6. **No card data in HTML inputs**: use `<PCITokenizedCardInput>` from `@react-vault/ui`.
|
|
77
|
+
7. **No `any` types**: types flow from Zod schemas.
|
|
78
|
+
8. **All user-facing strings via `t()`**: never inline. Even error messages.
|
|
79
|
+
9. **Conventional Commits with BFSI types**: see `/bfsi-commit`. `security` and `compliance` are extra types beyond standard set.
|
|
80
|
+
|
|
81
|
+
## The Claude toolkit
|
|
82
|
+
|
|
83
|
+
The `.claude/` directory in this project enables a plugin called `toolkit`. Run `/hooks` to see registered hooks (file protection, secret scanner, formatter, PII scanner, etc.). Run `/plugin` to see the toolkit. Run `/bfsi-doctor` to verify everything's wired up.
|
|
84
|
+
|
|
85
|
+
Hooks may block you from:
|
|
86
|
+
|
|
87
|
+
- Editing `.env*`, `*.pem`, `credentials.json`, `.git/` files
|
|
88
|
+
- Running `rm -rf`, `git push --force` on protected branches
|
|
89
|
+
- Writing files that contain secret patterns (API keys, tokens)
|
|
90
|
+
- Writing files that introduce PII patterns into logs
|
|
91
|
+
|
|
92
|
+
These are not personal — they protect every dev from a class of mistake that's expensive in BFSI.
|
|
93
|
+
|
|
94
|
+
## Getting unstuck
|
|
95
|
+
|
|
96
|
+
- Architecture questions → ask `bfsi-architect` agent (`@bfsi-architect how should I structure ...`)
|
|
97
|
+
- Security questions → ask `bfsi-security-reviewer`
|
|
98
|
+
- Performance questions → ask `bfsi-performance-reviewer`
|
|
99
|
+
- Test patterns → look at `bfsi-test-pattern` reference skill (it'll auto-load when you ask)
|
|
100
|
+
- Stuck on an error → look at `bfsi-error-message` reference skill
|
|
101
|
+
|
|
102
|
+
## What NOT to do (common pitfalls)
|
|
103
|
+
|
|
104
|
+
- ❌ Don't bypass `useAuditedMutation` — every state change is auditable.
|
|
105
|
+
- ❌ Don't put PII in URL search params.
|
|
106
|
+
- ❌ Don't trust client-side permission checks alone — backend re-checks every API call.
|
|
107
|
+
- ❌ Don't use `localStorage` for tokens. Use the auth module's storage strategy.
|
|
108
|
+
- ❌ Don't use `dangerouslySetInnerHTML` without sanitisation.
|
|
109
|
+
- ❌ Don't write your own crypto — use `@react-vault/core/encryption`.
|
|
110
|
+
- ❌ Don't commit to `main` directly — always via PR.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bfsi-pii-field
|
|
3
|
+
description: Wraps a data field with PII masking (click-to-reveal + audit log) using PIIMaskedDisplay from @react-vault/ui. Use when the user types /bfsi-pii-field, asks to "mask this PAN", "hide the account number", "add masking to Aadhaar", or "make this field a PII field".
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
argument-hint: <field-type> <variable-or-prop>
|
|
6
|
+
allowed-tools: Read Edit Grep
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# BFSI PII Field
|
|
10
|
+
|
|
11
|
+
Wraps a value display with `<PIIMaskedDisplay type="pan" value={...} />` from `@react-vault/ui`.
|
|
12
|
+
|
|
13
|
+
## Supported types
|
|
14
|
+
|
|
15
|
+
| Type | Default mask | Reveal duration |
|
|
16
|
+
| ---------------- | ------------------ | --------------- |
|
|
17
|
+
| `pan` | `ABCDE****F` | 30s |
|
|
18
|
+
| `aadhaar` | `XXXX XXXX 1234` | 15s |
|
|
19
|
+
| `account_number` | `****1234` | 30s |
|
|
20
|
+
| `mobile` | `+91 ******1234` | 30s |
|
|
21
|
+
| `email` | `j***@example.com` | 30s |
|
|
22
|
+
| `card_last4` | `**** 1234` | 60s |
|
|
23
|
+
| `name` | initials only | 60s |
|
|
24
|
+
| `address` | first line + `…` | 60s |
|
|
25
|
+
| `dob` | masked, age shown | never |
|
|
26
|
+
|
|
27
|
+
## What it does
|
|
28
|
+
|
|
29
|
+
Given a JSX expression like:
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
<td>{user.pan}</td>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Replaces it with:
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
<td>
|
|
39
|
+
<PIIMaskedDisplay type="pan" value={user.pan} auditTarget={{ type: 'user', id: user.id }} />
|
|
40
|
+
</td>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This adds:
|
|
44
|
+
|
|
45
|
+
1. **Click-to-reveal** with audit log entry (`data.pan.revealed`)
|
|
46
|
+
2. **Auto re-mask** after the type-specific duration
|
|
47
|
+
3. **Copy guard** — copying the masked text returns "\*\*\*\*" not the value
|
|
48
|
+
4. **a11y** — `aria-label="masked PAN, click to reveal"`, screen-reader-friendly
|
|
49
|
+
|
|
50
|
+
## Workflow
|
|
51
|
+
|
|
52
|
+
### Step 1: Locate the field
|
|
53
|
+
|
|
54
|
+
If the user gives a variable like `user.pan`, find the occurrences in the current file. If multiple, ask which one.
|
|
55
|
+
|
|
56
|
+
### Step 2: Determine the type
|
|
57
|
+
|
|
58
|
+
If not provided, infer from the variable name:
|
|
59
|
+
|
|
60
|
+
- contains `pan` → `pan`
|
|
61
|
+
- contains `aadhaar` / `uid` → `aadhaar`
|
|
62
|
+
- contains `account` / `acc_no` → `account_number`
|
|
63
|
+
- contains `mobile` / `phone` → `mobile`
|
|
64
|
+
- contains `email` → `email`
|
|
65
|
+
- contains `dob` / `birth` → `dob`
|
|
66
|
+
- otherwise → ask the user.
|
|
67
|
+
|
|
68
|
+
### Step 3: Determine the audit target
|
|
69
|
+
|
|
70
|
+
The `auditTarget` prop tells the audit log which resource the field belongs to. Infer from context:
|
|
71
|
+
|
|
72
|
+
- If the variable is `user.pan`, target = `{ type: 'user', id: user.id }`
|
|
73
|
+
- If it's `kycRecord.aadhaar`, target = `{ type: 'kyc_record', id: kycRecord.id }`
|
|
74
|
+
- Otherwise, ask the user.
|
|
75
|
+
|
|
76
|
+
### Step 4: Add the import (if missing)
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { PIIMaskedDisplay } from '@react-vault/ui';
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Step 5: Replace + verify
|
|
83
|
+
|
|
84
|
+
Edit the file. Run `pnpm typecheck` on the changed file. Report success.
|
|
85
|
+
|
|
86
|
+
## When NOT to use
|
|
87
|
+
|
|
88
|
+
- **In a `<form>` input field.** Use `<SecureFormField>` instead — that handles paste guards and autocomplete prevention for input, not display.
|
|
89
|
+
- **In exported data.** Exports should use the same mask but emit a `data.<field>.exported` event with a higher severity (don't reveal in CSV without explicit user consent).
|
|
90
|
+
- **In server-rendered HTML.** Masking on the client only is insufficient for SSR contexts — the backend must also mask before sending. Flag this to the user if the file looks like SSR.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bfsi-test-pattern
|
|
3
|
+
description: Reference for BFSI testing patterns — security scenarios (auth bypass, injection, race conditions), accessibility tests, audit log assertions, PII masking checks, and form validation edge cases. Auto-loads when the user asks about writing tests, test coverage, security tests, a11y tests, or BFSI-specific test patterns.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# BFSI Testing Patterns
|
|
7
|
+
|
|
8
|
+
Reference for what to test in a Rsense BFSI project. Goes beyond happy-path unit tests.
|
|
9
|
+
|
|
10
|
+
## Tools
|
|
11
|
+
|
|
12
|
+
- **Vitest** for unit + integration tests
|
|
13
|
+
- **@testing-library/react** for component tests
|
|
14
|
+
- **@axe-core/react** for a11y assertions
|
|
15
|
+
- **MSW (Mock Service Worker)** for network mocking
|
|
16
|
+
- **Playwright** for E2E
|
|
17
|
+
|
|
18
|
+
## The BFSI test pyramid
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
/\
|
|
22
|
+
/ \ Playwright E2E (few)
|
|
23
|
+
/____\
|
|
24
|
+
/ \
|
|
25
|
+
/ RTL + \ Integration (some)
|
|
26
|
+
/ MSW \
|
|
27
|
+
/____________\
|
|
28
|
+
/ \
|
|
29
|
+
/ Vitest unit \ Unit (many)
|
|
30
|
+
/__________________\
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Required test categories per feature
|
|
34
|
+
|
|
35
|
+
For every feature, you need:
|
|
36
|
+
|
|
37
|
+
### 1. Schema tests (Zod)
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// src/features/Kyc/__tests__/schema.test.ts
|
|
41
|
+
import { describe, expect, it } from 'vitest';
|
|
42
|
+
import { kycSubmissionSchema } from '../schema';
|
|
43
|
+
|
|
44
|
+
describe('kycSubmissionSchema', () => {
|
|
45
|
+
it('accepts valid PAN', () => {
|
|
46
|
+
expect(() => kycSubmissionSchema.parse({ pan: 'ABCDE1234F', ... })).not.toThrow();
|
|
47
|
+
});
|
|
48
|
+
it('rejects malformed PAN', () => {
|
|
49
|
+
expect(() => kycSubmissionSchema.parse({ pan: 'abcd', ... })).toThrow(/invalid/i);
|
|
50
|
+
});
|
|
51
|
+
it('rejects Aadhaar with wrong checksum', () => {
|
|
52
|
+
expect(() => kycSubmissionSchema.parse({ aadhaar: '123456789012', ... })).toThrow();
|
|
53
|
+
});
|
|
54
|
+
it('strips unknown fields (whitelist)', () => {
|
|
55
|
+
const parsed = kycSubmissionSchema.parse({ pan: 'ABCDE1234F', extra: 'evil', ... });
|
|
56
|
+
expect(parsed).not.toHaveProperty('extra');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 2. Container tests (RTL + MSW)
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// src/features/Kyc/__tests__/containers.test.tsx
|
|
65
|
+
describe('KycList container', () => {
|
|
66
|
+
it('renders empty state when no records', async () => { /* ... */ });
|
|
67
|
+
it('masks PAN in the table by default', async () => {
|
|
68
|
+
render(<KycList />);
|
|
69
|
+
await screen.findByText(/loaded/i);
|
|
70
|
+
// PIIMaskedDisplay shows masked value
|
|
71
|
+
expect(screen.getByText('ABCDE****F')).toBeInTheDocument();
|
|
72
|
+
expect(screen.queryByText('ABCDE1234F')).not.toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
it('reveals PAN on click and fires audit event', async () => {
|
|
75
|
+
const audit = vi.spyOn(auditClient, 'record');
|
|
76
|
+
render(<KycList />);
|
|
77
|
+
await userEvent.click(screen.getByRole('button', { name: /reveal pan/i }));
|
|
78
|
+
expect(screen.getByText('ABCDE1234F')).toBeInTheDocument();
|
|
79
|
+
expect(audit).toHaveBeenCalledWith(expect.objectContaining({
|
|
80
|
+
event_name: 'data.pan.revealed',
|
|
81
|
+
}));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 3. Permission tests
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
describe('KycList route', () => {
|
|
90
|
+
it('redirects to /login when unauthenticated', () => { /* ... */ });
|
|
91
|
+
it('shows 403 when authenticated but lacks kyc.view permission', () => { /* ... */ });
|
|
92
|
+
it('renders for users with kyc.view', () => { /* ... */ });
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 4. Idempotency tests
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
describe('submitKyc mutation', () => {
|
|
100
|
+
it('includes Idempotency-Key header', async () => {
|
|
101
|
+
const spy = vi.spyOn(http, 'post');
|
|
102
|
+
await store.dispatch(api.endpoints.submitKyc.initiate(payload));
|
|
103
|
+
expect(spy).toHaveBeenCalledWith(
|
|
104
|
+
expect.anything(),
|
|
105
|
+
expect.objectContaining({
|
|
106
|
+
headers: expect.objectContaining({ 'Idempotency-Key': expect.any(String) }),
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
it('uses the same key on retry', async () => { /* ... */ });
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 5. A11y tests
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { axe } from 'jest-axe';
|
|
118
|
+
|
|
119
|
+
describe('KycForm a11y', () => {
|
|
120
|
+
it('has no a11y violations', async () => {
|
|
121
|
+
const { container } = render(<KycForm />);
|
|
122
|
+
expect(await axe(container)).toHaveNoViolations();
|
|
123
|
+
});
|
|
124
|
+
it('all inputs have labels', () => {
|
|
125
|
+
render(<KycForm />);
|
|
126
|
+
expect(screen.getByLabelText(/pan/i)).toBeInTheDocument();
|
|
127
|
+
expect(screen.getByLabelText(/aadhaar/i)).toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
it('error messages are announced (aria-live)', async () => { /* ... */ });
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 6. Security tests
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
describe('KycList security', () => {
|
|
137
|
+
it('does not log PAN to console', async () => {
|
|
138
|
+
const logSpy = vi.spyOn(console, 'log');
|
|
139
|
+
render(<KycList />);
|
|
140
|
+
await screen.findByText(/loaded/i);
|
|
141
|
+
const allLogArgs = logSpy.mock.calls.flat().join(' ');
|
|
142
|
+
expect(allLogArgs).not.toMatch(/[A-Z]{5}\d{4}[A-Z]/); // PAN pattern
|
|
143
|
+
});
|
|
144
|
+
it('does not store PAN in localStorage', async () => {
|
|
145
|
+
render(<KycList />);
|
|
146
|
+
await screen.findByText(/loaded/i);
|
|
147
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
148
|
+
const key = localStorage.key(i)!;
|
|
149
|
+
const value = localStorage.getItem(key) || '';
|
|
150
|
+
expect(value).not.toMatch(/[A-Z]{5}\d{4}[A-Z]/);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
it('does not include PAN in URLs (query / hash)', () => {
|
|
154
|
+
expect(window.location.search).not.toMatch(/[A-Z]{5}\d{4}[A-Z]/);
|
|
155
|
+
expect(window.location.hash).not.toMatch(/[A-Z]{5}\d{4}[A-Z]/);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 7. E2E (Playwright)
|
|
161
|
+
|
|
162
|
+
For each route, one E2E that:
|
|
163
|
+
- Logs in
|
|
164
|
+
- Navigates to the route
|
|
165
|
+
- Performs the primary user action
|
|
166
|
+
- Asserts the success state
|
|
167
|
+
- Asserts the audit event was sent (via mocked endpoint or test backend)
|
|
168
|
+
|
|
169
|
+
## Patterns to copy
|
|
170
|
+
|
|
171
|
+
When unsure, copy patterns from `src/features/_example/__tests__/`. The example feature is intentionally well-tested as a reference.
|
|
172
|
+
|
|
173
|
+
## Coverage targets
|
|
174
|
+
|
|
175
|
+
- Container components: 80%+ statement coverage
|
|
176
|
+
- Schemas (Zod): 100% (cheap and high-value)
|
|
177
|
+
- Pure components: 60%+ (style edge cases caught visually)
|
|
178
|
+
- E2E: one per route
|
|
179
|
+
- Security: every PII-handling component must have category 5 + 6 tests
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA2DA,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAyG1C"}
|