tillerhand 3.48.1 → 3.49.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.
- package/.claude/agents/board-chair.md +1 -1
- package/.claude/agents/security-auditor.md +1 -1
- package/.claude/agents/test-writer.md +1 -1
- package/.claude/board/board-product.md +1 -1
- package/.claude/board/council.md +1 -1
- package/.claude/board/rules.md +6 -6
- package/.claude/commands/adopt.md +3 -3
- package/.claude/commands/ask-mode.md +4 -4
- package/.claude/commands/board.md +1 -1
- package/.claude/commands/build.md +1 -1
- package/.claude/commands/design.md +4 -4
- package/.claude/commands/findings-business.md +1 -1
- package/.claude/commands/findings-legal.md +1 -1
- package/.claude/commands/findings-perf.md +1 -1
- package/.claude/commands/findings-security.md +2 -2
- package/.claude/commands/findings-tech.md +1 -1
- package/.claude/commands/findings-ux.md +1 -1
- package/.claude/commands/fix-bug.md +2 -2
- package/.claude/commands/handoff.md +2 -2
- package/.claude/commands/kit-update.md +2 -2
- package/.claude/commands/qa.md +1 -1
- package/.claude/commands/release.md +2 -2
- package/.claude/commands/research.md +3 -3
- package/.claude/commands/retro.md +1 -1
- package/.claude/commands/rollback.md +3 -3
- package/.claude/commands/start.md +4 -4
- package/.claude/commands/tools.md +1 -1
- package/.claude/commands/user-feedback.md +1 -1
- package/.claude/findings/lenses/business.md +1 -1
- package/.claude/findings/protocol.md +2 -2
- package/.claude/hooks/budget.mjs +1 -1
- package/.claude/hooks/checkpoint.mjs +2 -2
- package/.claude/hooks/commit-gate.mjs +18 -17
- package/.claude/hooks/council.mjs +37 -52
- package/.claude/hooks/dashboard-data.mjs +34 -42
- package/.claude/hooks/dashboard.mjs +18 -20
- package/.claude/hooks/destructive-guard.mjs +2 -2
- package/.claude/hooks/flow.mjs +1 -1
- package/.claude/hooks/freeze-guard.mjs +2 -2
- package/.claude/hooks/gate.mjs +21 -13
- package/.claude/hooks/labels.mjs +92 -152
- package/.claude/hooks/lang.mjs +25 -9
- package/.claude/hooks/messages.mjs +497 -494
- package/.claude/hooks/notify.mjs +1 -1
- package/.claude/hooks/prefs.mjs +10 -18
- package/.claude/hooks/preview.mjs +1 -1
- package/.claude/hooks/progress.mjs +7 -9
- package/.claude/hooks/qa-report.mjs +1 -3
- package/.claude/hooks/session-start.mjs +2 -3
- package/.claude/hooks/status-read.mjs +5 -5
- package/.claude/hooks/statusline.mjs +4 -4
- package/.claude/hooks/tools.mjs +3 -7
- package/.claude/hooks/usage.mjs +2 -2
- package/.claude/i18n/tr.json +496 -0
- package/.claude/security-patterns.json +1 -1
- package/.claude/skills/logo-design/SKILL.md +2 -2
- package/.claude/skills/motion-video/SKILL.md +1 -1
- package/.claude/skills/source-research/SKILL.md +1 -1
- package/.claude/skills/ui-motion/SKILL.md +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +1 -1
- package/README.md +2 -2
- package/README.tr.md +2 -2
- package/docs/architecture/overview.md +1 -1
- package/docs/decisions/log.md +1 -1
- package/docs/legal/privacy-kvkk.md +35 -35
- package/docs/operations/deploy-setup.md +1 -1
- package/docs/operations/feature-flags.md +1 -1
- package/docs/product/glossary.md +3 -3
- package/docs/product/launch.md +1 -1
- package/docs/product/prd.md +2 -2
- package/docs/product/roadmap.md +1 -1
- package/docs/product/specs/_template.md +1 -1
- package/docs/product/unit-economics.md +1 -1
- package/docs/quality/test-plan.md +5 -5
- package/docs/workflow/decision-protocol.md +1 -2
- package/docs/workflow/mcp-catalog.md +1 -1
- package/docs/workflow/skills.md +2 -2
- package/docs/workflow/usage-limits.md +1 -1
- package/kit/CHANGELOG.md +34 -0
- package/kit/VERSION +1 -1
- package/kit/guide.html +56 -56
- package/kit/guide.tr.html +10 -10
- package/kit/kit.mjs +7 -42
- package/kit/templates/addons/ai/.env.example.append +3 -3
- package/kit/templates/addons/tr/docs/legal/turkiye.md +55 -55
- package/kit/templates/addons/tr/types/backend/.env.example.append +2 -2
- package/kit/templates/addons/tr/types/backend/app/shared/payments.py +1 -1
- package/kit/templates/addons/tr/types/backend/tests/security/test_payment_webhook.py +3 -3
- package/kit/templates/addons/tr/types/web-saas/.env.example.append +2 -2
- package/kit/templates/addons/tr/types/web-saas/.github/workflows/payment-webhook.yml +3 -3
- package/kit/templates/addons/tr/types/web-saas/src/lib/payments/webhook-signature.ts +1 -1
- package/kit/templates/addons/tr/types/web-saas/tests/security/payment-webhook.mjs +4 -4
- package/kit/templates/backend/.github/dependabot.yml +1 -1
- package/kit/templates/backend/.github/workflows/live-monitoring.yml +1 -1
- package/kit/templates/backend/docs/architecture/stack.md +3 -3
- package/kit/templates/mobile/.github/dependabot.yml +1 -1
- package/kit/templates/mobile/.github/workflows/ci.yml +2 -2
- package/kit/templates/mobile/docs/architecture/stack.md +4 -4
- package/kit/templates/mobile/docs/architecture/structure.md +1 -1
- package/kit/templates/mobile/docs/design/design-guide.md +1 -1
- package/kit/templates/web-saas/.github/dependabot.yml +1 -1
- package/kit/templates/web-saas/.github/workflows/ci.yml +2 -2
- package/kit/templates/web-saas/.github/workflows/live-monitoring.yml +1 -1
- package/kit/templates/web-saas/docs/architecture/stack.md +4 -4
- package/kit/templates/web-saas/docs/design/design-guide.md +1 -1
- package/kit/tools/antigravity/.agents/hooks.json +5 -5
- package/package.json +1 -1
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Turkey Pack
|
|
2
2
|
|
|
3
|
-
> `tr`
|
|
4
|
-
> **
|
|
5
|
-
>
|
|
6
|
-
> `/findings-legal`
|
|
3
|
+
> Comes with the `tr` add-on (`--addons tr`). For a product that sells in Turkey or has Turkish users.
|
|
4
|
+
> **Not legal advice.** Every item links to a primary source and has a check date; legislation changes. Ask a lawyer or an accountant before launch.
|
|
5
|
+
> Before use, reopen the sources with `source-research`; if something changed, update this file and change the date.
|
|
6
|
+
> `/findings-legal` and `/findings-business` read this file.
|
|
7
7
|
|
|
8
|
-
## 1. KVKK:
|
|
8
|
+
## 1. KVKK: transfer abroad
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Checked: 2026-10-25. Source: [Law no. 6698 (KVKK), art. 9](https://www.mevzuat.gov.tr/MevzuatMetin/1.5.6698.pdf) (as amended by Law no. 7499, 2/3/2024)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Personal data going to a service abroad is a transfer: AI provider (user data in the prompt text), hosting and database region, email, analytics, error tracking. Order:
|
|
13
13
|
|
|
14
|
-
1.
|
|
15
|
-
2.
|
|
16
|
-
- **
|
|
17
|
-
3.
|
|
14
|
+
1. Is there an **adequacy decision** (art. 9/1-3)? The Personal Data Protection Board (KVKK Board) issues it; it is published in the Official Gazette. If there is one, transfer is allowed under a condition in art. 5 or art. 6.
|
|
15
|
+
2. If not, **appropriate safeguards** (art. 9/4): the **standard contract** announced by the Board, approved binding corporate rules, an undertaking permitted by the Board or (between public bodies) an agreement permitted by the Board. A condition in art. 5 or art. 6 is still required here, and the data subject must be able to exercise their rights in that country.
|
|
16
|
+
- **After the standard contract is signed, notify the Personal Data Protection Authority (KVKK Authority) within 5 business days** (art. 9/5).
|
|
17
|
+
3. If neither exists, only in **incidental** (non-repeated) cases (art. 9/6): informed explicit consent, necessity for performing a contract and the other cases listed in the article. An AI or hosting service that runs continuously is not "incidental".
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
- [ ] `docs/operations/accounts.md
|
|
21
|
-
- [ ]
|
|
22
|
-
- [ ]
|
|
23
|
-
- [ ]
|
|
19
|
+
Project to-do:
|
|
20
|
+
- [ ] For each external service in `docs/operations/accounts.md`: which country is the data in, what is the transfer basis (adequacy / standard contract / none)?
|
|
21
|
+
- [ ] If a standard contract is needed, it is signed with the provider and notified to the Authority within 5 business days; the date is in `accounts.md` → Note column.
|
|
22
|
+
- [ ] The privacy notice (`docs/legal/privacy-kvkk.md`) states the transfer abroad, the recipient group and the purpose (art. 10).
|
|
23
|
+
- [ ] No unneeded personal data goes into the AI prompt (data minimization); identity data is masked where possible.
|
|
24
24
|
|
|
25
|
-
## 2. e-Fatura
|
|
25
|
+
## 2. e-Fatura and e-Arşiv Fatura
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Checked: 2026-10-25. Source: [General Communiqué on the Tax Procedure Law no. 509 (VUK General Communiqué No. 509), current version](https://cdn.gib.gov.tr/api/gibportal-file/file/getFile?objectKey=MEVZUAT_TEBLIGLER%2FUNIVERSAL%2F2026%2FMEVZUAT_TEBLIGLER_2026_VukTeb509_Guncel.pdf) (GİB, with the amendments of Communiqué no. 589 applied) · [e-Belge](https://ebelge.gib.gov.tr/)
|
|
28
28
|
|
|
29
|
-
- **e-Fatura
|
|
30
|
-
- **e-Arşiv**: e-Fatura
|
|
31
|
-
-
|
|
29
|
+
- **e-Fatura obligation** (IV.1.4): those whose gross sales revenue was **3 million TL** or more in accounting periods of 2022 or later. For those selling goods/services on their own site or in another electronic medium, intermediary service providers and internet listing/advertising intermediaries, the threshold is **500 thousand TL**. Those who exceed the threshold switch by the start of the seventh month of the following year (IV.1.5).
|
|
30
|
+
- **e-Arşiv**: those in e-Fatura also switch to e-Arşiv (IV.2.4.1). Invoices of those not in e-Arşiv are issued as e-Arşiv Fatura (through the GİB portal or a private integrator) from 1/1/2026, regardless of amount. For the simplified procedure and the business account basis this date is 1/1/2027 (IV.2.4.3).
|
|
31
|
+
- **Internet sales invoice** (IV.2.4.5): The invoice is delivered electronically. It carries the statement "Bu satış internet üzerinden yapılmıştır.", the web address where the sale was made, the payment method and date, the shipping carrier's information, the shipping date and a return section.
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
- [ ]
|
|
35
|
-
- [ ]
|
|
36
|
-
- [ ]
|
|
33
|
+
Project to-do:
|
|
34
|
+
- [ ] Invoicing method chosen: GİB portal (manual, small volume) or a private integrator API (automatic). The choice is in `docs/decisions/log.md`.
|
|
35
|
+
- [ ] The invoice flow is triggered when payment succeeds; if the invoice cannot be issued, the order is not lost (retry queue).
|
|
36
|
+
- [ ] The accountant approved the threshold and the method.
|
|
37
37
|
|
|
38
|
-
## 3.
|
|
38
|
+
## 3. Payment notifications (iyzico, PayTR)
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Checked: 2026-10-25. Sources: [iyzico webhook](https://docs.iyzico.com/en/advanced/webhook) · [iyzico Retrieve Payment](https://docs.iyzico.com/en/advanced/retrieve-payment) · [iyzico CF Retrieve](https://docs.iyzico.com/en/payment-methods/checkoutform/cf-implementation/cf-retrieve) · [iyzico live / sandbox](https://docs.iyzico.com/en/getting-started/preliminaries/live-vs-sandbox) · [PayTR iFrame API step 2](https://dev.paytr.com/en/iframe-api/iframe-api-2-adim) (the pages have no date)
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
- web-saas: `src/lib/payments/webhook-signature.ts` → `verifyIyzico`, `verifyPaytr`;
|
|
44
|
-
- backend: `app/shared/payments.py` → `verify_iyzico`, `verify_paytr`;
|
|
45
|
-
-
|
|
42
|
+
A payment result is processed from the notification the provider sends from its server, not from the browser redirect. An order is not confirmed before the notification signature is verified. The code is ready:
|
|
43
|
+
- web-saas: `src/lib/payments/webhook-signature.ts` → `verifyIyzico`, `verifyPaytr`; adversarial test `tests/security/payment-webhook.mjs` (CI: `payment-webhook` workflow; locally Node 22.18 or later)
|
|
44
|
+
- backend: `app/shared/payments.py` → `verify_iyzico`, `verify_paytr`; adversarial test `tests/security/test_payment_webhook.py`
|
|
45
|
+
- mobile: the payment notification comes to the server; use the backend template or a Supabase Edge Function. Do not take the card inside the app; redirect to the provider's payment page.
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
1.
|
|
49
|
-
2.
|
|
50
|
-
3.
|
|
51
|
-
- iyzico: `X-IYZ-SIGNATURE-V3`
|
|
52
|
-
- PayTR:
|
|
53
|
-
4. **
|
|
54
|
-
5.
|
|
55
|
-
- PayTR:
|
|
56
|
-
- iyzico:
|
|
57
|
-
6. **
|
|
58
|
-
7.
|
|
47
|
+
Flow:
|
|
48
|
+
1. Open a sandbox account (iyzico: sandbox-merchant.iyzipay.com, API `https://sandbox-api.iyzipay.com`; PayTR: test mode in the merchant panel). Keys use the names in `.env.example` and live **on the server only**; you enter the values.
|
|
49
|
+
2. Payment is started on the server; the amount and order number come from the order in the database, not from the client. In iyzico, give the order number as `conversationId` on every payment: the signature covers this field too, and verification rejects it if empty.
|
|
50
|
+
3. The notification endpoint (route) verifies the signature first. If it returns `false`, return 4xx and log; do not log the request body (personal data).
|
|
51
|
+
- iyzico: `X-IYZ-SIGNATURE-V3` header. The signature feature must be on in the account (request it from iyzico). Subscription notifications are outside the scope of this code.
|
|
52
|
+
- PayTR: the `hash` field in the POST; the response is plain text `OK`, no session check. Take the fields from the form body as text; do not convert `total_amount` to a number and write it back (this breaks the signature).
|
|
53
|
+
4. **A valid signature does not mean the payment succeeded.** A failed payment also arrives signed. Confirm the order only if the result is a success: PayTR `status` = `success`, iyzico `status` = `SUCCESS` and `iyziEventType` the type you expect. In the other cases do not confirm; still return a success response to the provider.
|
|
54
|
+
5. Compare the amount and currency with the order:
|
|
55
|
+
- PayTR: `total_amount` in the notification (kuruş: 34.56 TL → 3456).
|
|
56
|
+
- iyzico: the notification has no amount. Query the server: CF Retrieve with `token` for the checkout form, Retrieve Payment with `paymentId` for the others. The returned `paymentStatus`, `paidPrice`, `currency` and `conversationId` must match the order.
|
|
57
|
+
6. **A repeated notification** (the provider retries, an attacker can resend an old notification; the iyzico signature has no timestamp): confirm the order with a single atomic update (`UPDATE … SET status='paid' WHERE id=… AND status='pending'`) or put a unique constraint on the payment id (`merchant_oid` / `paymentId`). Do not read first and write after; two concurrent notifications would both pass. Return a success response to the repeated one too.
|
|
58
|
+
7. The payment feature ships behind a flag (`docs/operations/feature-flags.md`); run `/findings-security` before going live.
|
|
59
59
|
|
|
60
|
-
- [ ]
|
|
61
|
-
- [ ]
|
|
62
|
-
- [ ] Prod
|
|
60
|
+
- [ ] The signature adversarial test is green (unsigned and altered notifications are rejected).
|
|
61
|
+
- [ ] The notification endpoint test shows: a failed-payment notification does not confirm the order; when the same notification arrives twice (and concurrently), the order is processed once; a payment with a different amount is not confirmed. These tests are written for the project's own route.
|
|
62
|
+
- [ ] Prod keys are only in the hosting panel; sandbox and prod are separate.
|
|
63
63
|
|
|
64
|
-
## 4.
|
|
64
|
+
## 4. Turkish text: İ/ı
|
|
65
65
|
|
|
66
|
-
`"I".toLowerCase()` → `"i"`,
|
|
66
|
+
`"I".toLowerCase()` → `"i"`, but in Turkish it is `"ı"`. `"i".toUpperCase()` → `"I"`, in Turkish it is `"İ"`. The default sort puts the letters `ç, ğ, ı, ö, ş, ü` at the end of the alphabet. Add these cases to the tests of every feature that does search, sorting, uppercasing and email comparison:
|
|
67
67
|
|
|
68
68
|
```js
|
|
69
|
-
//
|
|
69
|
+
// Text shown as Turkish: the language is given explicitly
|
|
70
70
|
expect('ISPARTA'.toLocaleLowerCase('tr')).toBe('ısparta');
|
|
71
71
|
expect('istanbul'.toLocaleUpperCase('tr')).toBe('İSTANBUL');
|
|
72
72
|
expect(['Zeynep', 'Çağla', 'Ali', 'Ömer'].sort(new Intl.Collator('tr').compare)).toEqual(['Ali', 'Çağla', 'Ömer', 'Zeynep']);
|
|
73
|
-
//
|
|
73
|
+
// Case-insensitive search: searching "İstanbul" finds "istanbul"
|
|
74
74
|
expect(new Intl.Collator('tr', { sensitivity: 'base' }).compare('İstanbul', 'istanbul')).toBe(0);
|
|
75
|
-
//
|
|
76
|
-
expect('INFO@
|
|
75
|
+
// Machine text (email, code, URL, key): no language-dependent conversion
|
|
76
|
+
expect('INFO@EXAMPLE.COM'.toLowerCase()).toBe('info@example.com');
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
- [ ]
|
|
80
|
-
- [ ]
|
|
79
|
+
- [ ] In text shown to the user use `toLocaleLowerCase('tr')` / `Intl.Collator('tr')`; in machine text such as email, username and code use the language-independent `toLowerCase()`.
|
|
80
|
+
- [ ] Database sorting: a Turkish collation in Postgres (e.g. ICU `tr-TR-x-icu`) or sorting in the application.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Turkey add-on: payment provider (server only; you can delete the one you do not use). Details: docs/legal/turkiye.md
|
|
2
2
|
IYZICO_API_KEY=
|
|
3
3
|
IYZICO_SECRET_KEY=
|
|
4
|
-
IYZICO_BASE_URL=https://sandbox-api.iyzipay.com #
|
|
4
|
+
IYZICO_BASE_URL=https://sandbox-api.iyzipay.com # live: https://api.iyzipay.com
|
|
5
5
|
PAYTR_MERCHANT_ID=
|
|
6
6
|
PAYTR_MERCHANT_KEY=
|
|
7
7
|
PAYTR_MERCHANT_SALT=
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Payment provider notification signatures (iyzico, PayTR).
|
|
2
2
|
|
|
3
3
|
Verify before reading anything else from the request; an unverified notification is rejected.
|
|
4
|
-
Steps and sources: docs/legal/turkiye.md -> "
|
|
4
|
+
Steps and sources: docs/legal/turkiye.md -> "Payment notifications".
|
|
5
5
|
Sources (checked 2026-10-25): docs.iyzico.com/en/advanced/webhook · dev.paytr.com/en/iframe-api/iframe-api-2-adim
|
|
6
6
|
"""
|
|
7
7
|
|
|
@@ -19,14 +19,14 @@ SALT = "test-merchant-salt"
|
|
|
19
19
|
DIRECT = {
|
|
20
20
|
"iyziEventType": "API_AUTH",
|
|
21
21
|
"paymentId": "22416750",
|
|
22
|
-
"paymentConversationId": "
|
|
22
|
+
"paymentConversationId": "order-42",
|
|
23
23
|
"status": "SUCCESS",
|
|
24
24
|
}
|
|
25
25
|
CHECKOUT = {
|
|
26
26
|
"iyziEventType": "CHECKOUT_FORM_AUTH",
|
|
27
27
|
"iyziPaymentId": "22416751",
|
|
28
28
|
"token": "tok-abc",
|
|
29
|
-
"paymentConversationId": "
|
|
29
|
+
"paymentConversationId": "order-43",
|
|
30
30
|
"status": "SUCCESS",
|
|
31
31
|
}
|
|
32
32
|
PAYTR = {"merchant_oid": "SIP42", "status": "success", "total_amount": "3456"}
|
|
@@ -66,7 +66,7 @@ class IyzicoTest(unittest.TestCase):
|
|
|
66
66
|
self.assertFalse(verify_iyzico({**DIRECT, "status": "FAILURE"}, sig, SECRET))
|
|
67
67
|
self.assertFalse(
|
|
68
68
|
verify_iyzico(
|
|
69
|
-
{**DIRECT, "paymentConversationId": "
|
|
69
|
+
{**DIRECT, "paymentConversationId": "order-99"}, sig, SECRET
|
|
70
70
|
)
|
|
71
71
|
)
|
|
72
72
|
self.assertFalse(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Turkey add-on: payment provider (server only; you can delete the one you do not use). Details: docs/legal/turkiye.md
|
|
2
2
|
IYZICO_API_KEY=
|
|
3
3
|
IYZICO_SECRET_KEY=
|
|
4
|
-
IYZICO_BASE_URL=https://sandbox-api.iyzipay.com #
|
|
4
|
+
IYZICO_BASE_URL=https://sandbox-api.iyzipay.com # live: https://api.iyzipay.com
|
|
5
5
|
PAYTR_MERCHANT_ID=
|
|
6
6
|
PAYTR_MERCHANT_KEY=
|
|
7
7
|
PAYTR_MERCHANT_SALT=
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
1
|
+
# Payment notification signature adversarial test (tr add-on). Runs with fake keys; no secret needed.
|
|
2
|
+
# If red, do not merge: a fake or altered payment notification may be accepted.
|
|
3
3
|
name: payment-webhook
|
|
4
4
|
|
|
5
5
|
on:
|
|
@@ -18,5 +18,5 @@ jobs:
|
|
|
18
18
|
- uses: actions/setup-node@v7
|
|
19
19
|
with:
|
|
20
20
|
node-version: 22
|
|
21
|
-
# Node 22.18+ .ts
|
|
21
|
+
# Node 22.18+ runs a .ts module directly; no dependency install needed
|
|
22
22
|
- run: node --test tests/security/payment-webhook.mjs
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Payment provider notification signatures (iyzico, PayTR). Verify before reading anything else from the request;
|
|
2
|
-
// an unverified notification is rejected. Steps and sources: docs/legal/turkiye.md → "
|
|
2
|
+
// an unverified notification is rejected. Steps and sources: docs/legal/turkiye.md → "Payment notifications".
|
|
3
3
|
// Sources (checked 2026-10-25): docs.iyzico.com/en/advanced/webhook · dev.paytr.com/en/iframe-api/iframe-api-2-adim
|
|
4
4
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
5
5
|
|
|
@@ -17,8 +17,8 @@ const iyzicoSign = (b, secret = SECRET) => {
|
|
|
17
17
|
};
|
|
18
18
|
const paytrSign = (f, key = KEY, salt = SALT) => createHmac('sha256', key).update(f.merchant_oid + salt + f.status + f.total_amount).digest('base64');
|
|
19
19
|
|
|
20
|
-
const direct = { iyziEventType: 'API_AUTH', paymentId: '22416750', paymentConversationId: '
|
|
21
|
-
const checkout = { iyziEventType: 'CHECKOUT_FORM_AUTH', iyziPaymentId: '22416751', token: 'tok-abc', paymentConversationId: '
|
|
20
|
+
const direct = { iyziEventType: 'API_AUTH', paymentId: '22416750', paymentConversationId: 'order-42', status: 'SUCCESS' };
|
|
21
|
+
const checkout = { iyziEventType: 'CHECKOUT_FORM_AUTH', iyziPaymentId: '22416751', token: 'tok-abc', paymentConversationId: 'order-43', status: 'SUCCESS' };
|
|
22
22
|
const paytr = { merchant_oid: 'SIP42', status: 'success', total_amount: '3456' };
|
|
23
23
|
|
|
24
24
|
test('iyzico: a correctly signed notification is accepted (direct and checkout form)', () => {
|
|
@@ -30,7 +30,7 @@ test('iyzico: a correctly signed notification is accepted (direct and checkout f
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
test('iyzico: a payment id rounded by JSON.parse (above 2^53) is rejected', () => {
|
|
33
|
-
const body = JSON.parse('{"iyziEventType":"CHECKOUT_FORM_AUTH","iyziPaymentId":9007199254740993,"token":"tok-abc","paymentConversationId":"
|
|
33
|
+
const body = JSON.parse('{"iyziEventType":"CHECKOUT_FORM_AUTH","iyziPaymentId":9007199254740993,"token":"tok-abc","paymentConversationId":"order-43","status":"SUCCESS"}');
|
|
34
34
|
const sent = { ...body, iyziPaymentId: '9007199254740993' };
|
|
35
35
|
assert.equal(verifyIyzico(body, iyzicoSign(sent), SECRET), false);
|
|
36
36
|
assert.equal(verifyIyzico(body, iyzicoSign(body), SECRET), false, 'even when signed over the rounded value');
|
|
@@ -39,7 +39,7 @@ test('iyzico: a payment id rounded by JSON.parse (above 2^53) is rejected', () =
|
|
|
39
39
|
test('iyzico: forged, altered or unsigned notifications are rejected', () => {
|
|
40
40
|
const sig = iyzicoSign(direct);
|
|
41
41
|
assert.equal(verifyIyzico({ ...direct, status: 'FAILURE' }, sig, SECRET), false, 'status changed after signing');
|
|
42
|
-
assert.equal(verifyIyzico({ ...direct, paymentConversationId: '
|
|
42
|
+
assert.equal(verifyIyzico({ ...direct, paymentConversationId: 'order-99' }, sig, SECRET), false, 'order switched');
|
|
43
43
|
assert.equal(verifyIyzico(direct, iyzicoSign(direct, 'attacker-key'), SECRET), false, 'signed with another key');
|
|
44
44
|
assert.equal(verifyIyzico(direct, undefined, SECRET), false, 'no header');
|
|
45
45
|
assert.equal(verifyIyzico(direct, '', SECRET), false, 'empty header');
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# It also raises an alarm if the /health response takes longer than LIVE_MAX_SECONDS (Variables, default 2) seconds.
|
|
4
4
|
# If something breaks it opens an issue labeled "live-monitoring" (it does not open a new one if one is already open).
|
|
5
5
|
# To enable:
|
|
6
|
-
# 1. Settings > Variables > LIVE_URL = https://api.
|
|
6
|
+
# 1. Settings > Variables > LIVE_URL = https://api.example.com
|
|
7
7
|
# 2. Critical endpoint tests (that delete no data, limited to a test account): as pytest under tests/live/
|
|
8
8
|
# 3. If needed, Settings > Secrets > LIVE_TEST_TOKEN (a test account with restricted rights in prod)
|
|
9
9
|
name: Live monitoring
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
| Error tracking | Sentry | |
|
|
42
42
|
| Cron monitoring | Healthchecks.io (dead man's switch) | Alarm if a job does not run |
|
|
43
43
|
|
|
44
|
-
## Commands
|
|
44
|
+
## Commands
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
47
|
uv sync # install dependencies
|
|
@@ -68,7 +68,7 @@ app/
|
|
|
68
68
|
│ ├── router.py # HTTP endpoints (thin)
|
|
69
69
|
│ ├── service.py # business logic
|
|
70
70
|
│ ├── repository.py # database access
|
|
71
|
-
│ ├── models.py # SQLAlchemy
|
|
71
|
+
│ ├── models.py # SQLAlchemy models
|
|
72
72
|
│ ├── schemas.py # Pydantic input/output schemas
|
|
73
73
|
│ ├── dependencies.py
|
|
74
74
|
│ └── exceptions.py
|
|
@@ -76,7 +76,7 @@ app/
|
|
|
76
76
|
└── jobs/ # scheduled and background jobs (idempotent)
|
|
77
77
|
alembic/ # migration'lar
|
|
78
78
|
tests/
|
|
79
|
-
├── <feature>/ # tests by feature (test_AC_01_1_...
|
|
79
|
+
├── <feature>/ # tests by feature (test_AC_01_1_... acceptance tests included)
|
|
80
80
|
└── live/ # live monitoring tests
|
|
81
81
|
Dockerfile pyproject.toml uv.lock
|
|
82
82
|
```
|
|
@@ -66,8 +66,8 @@ jobs:
|
|
|
66
66
|
if: hashFiles('supabase/config.toml') != ''
|
|
67
67
|
run: |
|
|
68
68
|
# Only the needed keys are taken; the output is not run as a shell command
|
|
69
|
-
|
|
70
|
-
while IFS="=" read -r
|
|
69
|
+
keys=$(npx -y supabase@2.120.0 status -o env | grep -E '^(API_URL|ANON_KEY|SERVICE_ROLE_KEY|DB_URL|PUBLISHABLE_KEY|SECRET_KEY)=')
|
|
70
|
+
while IFS="=" read -r name value; do value="${value%\"}"; export "$name=${value#\"}"; done <<< "$keys"
|
|
71
71
|
node --test tests/security/rls.mjs
|
|
72
72
|
|
|
73
73
|
semgrep:
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
| Error tracking | Sentry (`@sentry/react-native`) | |
|
|
37
37
|
| Test | Jest + React Native Testing Library, Maestro (e2e) | |
|
|
38
38
|
|
|
39
|
-
## Commands
|
|
39
|
+
## Commands
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
42
|
npx expo start # development
|
|
43
43
|
npx expo lint
|
|
44
44
|
npx tsc --noEmit
|
|
45
45
|
npm run test # jest
|
|
46
|
-
npx expo install [
|
|
46
|
+
npx expo install [package] # add a package (picks the SDK-compatible version, not npm install)
|
|
47
47
|
npx expo-doctor # dependency compatibility check
|
|
48
48
|
eas build --profile preview --platform all # test build
|
|
49
49
|
eas build --profile production --platform all # store build
|
|
@@ -72,7 +72,7 @@ src/
|
|
|
72
72
|
│ ├── types.ts
|
|
73
73
|
│ └── index.ts
|
|
74
74
|
├── components/ # shared components: Button, TextField, Screen, EmptyState...
|
|
75
|
-
├── hooks/ # shared hooks (
|
|
75
|
+
├── hooks/ # shared hooks (useFlag etc.)
|
|
76
76
|
├── lib/ # supabase, sentry, secure storage, analytics clients
|
|
77
77
|
├── theme/ # design tokens (from DESIGN.md)
|
|
78
78
|
├── config/ # environment variables, constants
|
|
@@ -124,7 +124,7 @@ maestro test .maestro/ --format junit --output test-results/junit-e2e.xml
|
|
|
124
124
|
|
|
125
125
|
**Feature flags** (`docs/operations/feature-flags.md`): It must be possible to turn a feature off without a store update. The app reads the flag remotely at launch and caches the last known value; with no network, the safe default (off) is used:
|
|
126
126
|
- PostHog feature flags or Firebase Remote Config; or a Supabase `feature_flags` table.
|
|
127
|
-
- Reading goes through a single hook: `hooks/
|
|
127
|
+
- Reading goes through a single hook: `hooks/useFlag.ts`. Features that take payments, use AI or send notifications are not released without a flag.
|
|
128
128
|
|
|
129
129
|
## Common AI mistakes (watch for these)
|
|
130
130
|
|
|
@@ -22,7 +22,7 @@ src/
|
|
|
22
22
|
│ ├── types.ts
|
|
23
23
|
│ └── index.ts
|
|
24
24
|
├── components/ # shared components: Button, TextField, Screen, EmptyState...
|
|
25
|
-
├── hooks/ # shared hooks (
|
|
25
|
+
├── hooks/ # shared hooks (useFlag etc.)
|
|
26
26
|
├── lib/ # supabase, sentry, secure storage, analytics clients
|
|
27
27
|
├── theme/ # design tokens (from DESIGN.md)
|
|
28
28
|
├── config/ # environment variables, constants
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
> Decisions are made with `/design` using **visual previews** (`docs/workflow/decision-protocol.md` §5). If a new color, size or component
|
|
9
9
|
> is needed, it is asked as a decision first, then added to DESIGN.md. No raw colors or arbitrary sizes in code.
|
|
10
10
|
|
|
11
|
-
## 1. Chosen direction
|
|
11
|
+
## 1. Chosen direction
|
|
12
12
|
|
|
13
13
|
| Area | Choice | Skill | Preview | Date |
|
|
14
14
|
|---|---|---|---|---|
|
|
@@ -82,8 +82,8 @@ jobs:
|
|
|
82
82
|
if: hashFiles('supabase/config.toml') != ''
|
|
83
83
|
run: |
|
|
84
84
|
# Only the needed keys are taken; the output is not run as a shell command
|
|
85
|
-
|
|
86
|
-
while IFS="=" read -r
|
|
85
|
+
keys=$(npx -y supabase@2.120.0 status -o env | grep -E '^(API_URL|ANON_KEY|SERVICE_ROLE_KEY|DB_URL|PUBLISHABLE_KEY|SECRET_KEY)=')
|
|
86
|
+
while IFS="=" read -r name value; do value="${value%\"}"; export "$name=${value#\"}"; done <<< "$keys"
|
|
87
87
|
node --test tests/security/rls.mjs
|
|
88
88
|
|
|
89
89
|
semgrep:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Why 3 hours: GitHub Free gives 2,000 Actions minutes a month in a private repo; an hourly run (~3 min) would cost ~2,160 min (docs.github.com, 2026-10-25).
|
|
3
3
|
# If something breaks it opens an issue labeled "live-monitoring" (it does not open a new one if one is already open).
|
|
4
4
|
# To enable:
|
|
5
|
-
# 1. Settings > Variables > LIVE_URL = https://
|
|
5
|
+
# 1. Settings > Variables > LIVE_URL = https://example.com
|
|
6
6
|
# 2. Create a separate test account with restricted rights in prod: Settings > Secrets > LIVE_TEST_EMAIL, LIVE_TEST_PASSWORD
|
|
7
7
|
# 3. Write the tests under tests/live/*.spec.ts (flows that delete no data, limited to the test account). smoke.spec.ts comes ready:
|
|
8
8
|
# does the home page open, is it within LIVE_MAX_MS (Variables, default 3000), are there errors in the console.
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
| 8 | Email | **Resend** (recommended: developer friendly, React email templates; free plan gives 3,000 emails per month, 100 per day (source: https://resend.com/pricing, 2026-10-26)) · Postmark (very high deliverability) · Amazon SES (cheapest, hard to set up) |
|
|
20
20
|
| 9 | Analytics | **Plausible** (cookieless, KVKK friendly; no free plan, starts at 9 dollars per month, 30-day trial (source: https://plausible.io/docs/billing, 2026-10-26)) · PostHog (product analytics, funnels, feature flags) · Umami (self-host, free) · None |
|
|
21
21
|
| 10 | Error tracking | **Sentry** (recommended) · Highlight / Better Stack |
|
|
22
|
-
| 11 | Language support | **
|
|
22
|
+
| 11 | Language support | **One language: the users' language** (recommended for the MVP) · Two or more languages (next-intl; setting it up from the start is cheaper than adding it later) |
|
|
23
23
|
| 12 | Test | **Vitest + Playwright** (recommended) · Playwright only (little logic, mostly UI) |
|
|
24
24
|
|
|
25
25
|
## Chosen technologies
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
| Analytics | Plausible / PostHog | Watch cookie consent |
|
|
42
42
|
| Test | Vitest (unit), Playwright (e2e) | |
|
|
43
43
|
|
|
44
|
-
## Commands
|
|
44
|
+
## Commands
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
47
|
npm run dev # development
|
|
@@ -98,7 +98,7 @@ supabase/migrations/ # database changes ONLY here
|
|
|
98
98
|
- Every new table has RLS on, a written policy, and an entry in the `tests/security/rls.mjs` → `TABLES` list (the `rls` job in CI turns red on an unregistered table).
|
|
99
99
|
- On every page, think about `loading.tsx` / empty state / `error.tsx`.
|
|
100
100
|
- Images with `next/image`, links with `next/link`.
|
|
101
|
-
- Texts are in
|
|
101
|
+
- Texts are in the UI language (`docs/product/glossary.md`); format date/money with `Intl` and that locale.
|
|
102
102
|
- Before UI work, read `DESIGN.md` and `docs/design/design-guide.md`; do not go outside the tokens and rules. If a new design decision is needed, use `/design`.
|
|
103
103
|
|
|
104
104
|
## Verification (evidence before a task is done)
|
|
@@ -152,7 +152,7 @@ npx vitest run --reporter=default --reporter=junit --outputFile.junit=test-resul
|
|
|
152
152
|
|
|
153
153
|
- [ ] Vercel prod environment variables entered (Preview and Production separate)
|
|
154
154
|
- [ ] Supabase prod project is separate; prod domain and redirect URLs in Auth → URL Configuration
|
|
155
|
-
- [ ] Supabase email templates in
|
|
155
|
+
- [ ] Supabase email templates in the UI language and a custom SMTP (the default SMTP limit is very low)
|
|
156
156
|
- [ ] Payment: live keys, live webhook endpoint and signing secret
|
|
157
157
|
- [ ] SEO: `metadata` (title, description), Open Graph image, `sitemap.ts`, `robots.ts`, favicon
|
|
158
158
|
- [ ] Lighthouse (mobile): Performance ≥ 90, Accessibility ≥ 90, SEO ≥ 90
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
> Decisions are made with `/design` using **visual previews** (`docs/workflow/decision-protocol.md` §5). If a new color, size or component
|
|
9
9
|
> is needed, it is asked as a decision first, then added to DESIGN.md. No raw colors or arbitrary sizes in code.
|
|
10
10
|
|
|
11
|
-
## 1. Chosen direction
|
|
11
|
+
## 1. Chosen direction
|
|
12
12
|
|
|
13
13
|
| Area | Choice | Skill | Preview | Date |
|
|
14
14
|
|---|---|---|---|---|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kit-
|
|
2
|
+
"kit-session": {
|
|
3
3
|
"SessionStart": [
|
|
4
4
|
{ "type": "command", "command": "node .claude/hooks/session-start.mjs" }
|
|
5
5
|
]
|
|
6
6
|
},
|
|
7
|
-
"kit-
|
|
7
|
+
"kit-file-guards": {
|
|
8
8
|
"PreToolUse": [
|
|
9
9
|
{
|
|
10
10
|
"matcher": "write_to_file|replace_file_content|multi_replace_file_content",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
}
|
|
18
18
|
]
|
|
19
19
|
},
|
|
20
|
-
"kit-
|
|
20
|
+
"kit-edit-rounds": {
|
|
21
21
|
"PostToolUse": [
|
|
22
22
|
{
|
|
23
23
|
"matcher": "write_to_file|replace_file_content|multi_replace_file_content",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
{ "type": "command", "command": "node .claude/hooks/edit-rounds.mjs --inject" }
|
|
31
31
|
]
|
|
32
32
|
},
|
|
33
|
-
"kit-
|
|
33
|
+
"kit-read-guard": {
|
|
34
34
|
"PreToolUse": [
|
|
35
35
|
{
|
|
36
36
|
"matcher": "*",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
}
|
|
41
41
|
]
|
|
42
42
|
},
|
|
43
|
-
"kit-
|
|
43
|
+
"kit-command-guards": {
|
|
44
44
|
"PreToolUse": [
|
|
45
45
|
{
|
|
46
46
|
"matcher": "run_command",
|