@ekoindia/eps-context-mcp 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,12 +39,13 @@ clients can see this programmatically.
39
39
  | --------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
40
40
  | `list_apis` | `category?`, `limit?` | Compact index of EPS endpoints (no request/response bodies). `category` is validated against the bundle's categories; all entries by default. |
41
41
  | `list_topics` | — | Documentation topic ids: `auth`, `errors`, `pricing`, `environments`. |
42
- | `list_recipes` | — | Multi-step recipe ids + names (e.g. `dmt-fino-send-money`, `aeps-cash-withdrawal`). |
42
+ | `list_recipes` | — | Multi-step recipe ids + names (e.g. `dmt-fino-send-money`, `aeps-fingpay-cash-withdrawal`). |
43
43
  | `search` | `query`, `limit?` | Ranked endpoint matches for a query (ids only, no bodies). Top 10 by default; raise `limit` for more. |
44
44
  | `get_api` | `slug` | Full detail for one endpoint (params, response fields, errors, examples). |
45
45
  | `get_topic` | `topic` (`auth` \| `errors` \| `pricing` \| `environments`) | One documentation topic. |
46
46
  | `get_recipe` | `id` | One multi-step recipe (steps + branches). |
47
47
  | `get_signing_snippet` | `language` (`php` \| `java` \| `csharp` \| `javascript` \| `python` \| `go`) | Paste-ready **backend** code to compute the request `secret-key`. |
48
+ | `debug_auth` | `timestamp?`, `secret_key?` | Diagnose a `403`: returns a known-answer test vector to check your signing against, mechanical checks on a supplied timestamp/signature, and ranked causes. **Never takes an `access_key`.** |
48
49
  | `get_meta` | — | Bundle org/version, data source (`baked` or `remote`), this server's `packageVersion`, and `updateAvailable` (whether a newer npm release exists). |
49
50
 
50
51
  **Tiered usage:** start with `list_apis` / `search` (cheap, compact), then call
@@ -206,6 +207,25 @@ languages. **This code is backend-only.**
206
207
  key. This MCP server holds **no credentials** and performs **no signing or API
207
208
  calls** itself; it only provides context and code.
208
209
 
210
+ ### Debugging a 403 without handing over a key
211
+
212
+ `debug_auth` is **secret-free by design** and there is no plan to add an
213
+ `access_key` parameter. Two reasons, both structural:
214
+
215
+ 1. This server is also reachable **anonymously over HTTP**, so a key in a tool
216
+ argument would travel to infrastructure that deliberately holds no secrets.
217
+ 2. A tool argument lands in the **caller's model context** and its transcript.
218
+ Today an agent writes `process.env.EKO_ACCESS_KEY` and never sees the value;
219
+ a signing tool would teach it to read the secret out and paste it into a chat.
220
+
221
+ Instead the tool hands back a **known-answer test vector** — a dummy key, a
222
+ fixed timestamp, and the signature they must produce. Reproduce it with your own
223
+ code and the algorithm is proven; the 403 is then almost always provisioning
224
+ (IP allowlist, inactive key, wrong environment), which `ranked_causes` walks in
225
+ likelihood order. If you want a server that signs with real credentials, that is
226
+ `@ekoindia/eps-transact-mcp`, which takes them from the environment or per-request
227
+ headers — never as tool arguments.
228
+
209
229
  ## License
210
230
 
211
231
  MIT
package/data/eps.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "meta": {
3
3
  "org": "ekoindia",
4
4
  "apiVersion": "v3",
5
- "bundleVersion": "720a494b",
5
+ "bundleVersion": "a5376048",
6
6
  "environments": [
7
7
  {
8
8
  "id": "sandbox",
@@ -70,7 +70,12 @@
70
70
  "Generate the current timestamp in milliseconds (as a string).",
71
71
  "Compute HMAC-SHA256 of the timestamp using the base64-encoded key.",
72
72
  "Base64-encode the resulting signature — this is the secret-key."
73
- ]
73
+ ],
74
+ "testVector": {
75
+ "accessKey": "test-access-key-123",
76
+ "timestamp": "1700000000000",
77
+ "secretKey": "88lqTf9ew69XbVbeczjxVL8/B4vibfp1MvTi1mIj2Xo="
78
+ }
74
79
  },
75
80
  "errors": {
76
81
  "id": "errors",
@@ -8050,394 +8055,6 @@
8050
8055
  ],
8051
8056
  "responseTypes": []
8052
8057
  },
8053
- {
8054
- "slug": "aadhaar-dmt-levin-validate",
8055
- "productId": "dmt",
8056
- "productName": "Domestic Money Transfer (DMT)",
8057
- "name": "Validate Aadhaar & Generate OTP",
8058
- "method": "POST",
8059
- "path": "/customer/payment/dmt-levin/sender/{customer_id}/aadhaar/otp",
8060
- "summary": "Validate a sender's Aadhaar number and trigger an OTP to the linked mobile for DMT Levin onboarding.",
8061
- "category": "bc",
8062
- "relevance": "M",
8063
- "description": "Submits the sender's Aadhaar number via the DMT Levin channel to initiate OTP-based Aadhaar validation. The `otp_ref_id` from the sender's existing profile plus the Aadhaar number are required. Returns a new `otp_ref_id` for the validation session. Part of the Levin DMT sender onboarding flow.",
8064
- "bestFor": "DMT Levin sender onboarding flows that require Aadhaar number validation as a step before completing identity verification.",
8065
- "docsUrl": "https://eps.eko.in/docs/aadhaar-dmt-levin-validate",
8066
- "headers": [
8067
- {
8068
- "name": "developer_key",
8069
- "in": "header",
8070
- "type": "string",
8071
- "required": true,
8072
- "description": "Static API key issued to your account after KYC."
8073
- },
8074
- {
8075
- "name": "secret-key",
8076
- "in": "header",
8077
- "type": "string",
8078
- "required": true,
8079
- "description": "Dynamic per-request signature: base64(HMAC-SHA256(timestamp, base64(access_key)))."
8080
- },
8081
- {
8082
- "name": "secret-key-timestamp",
8083
- "in": "header",
8084
- "type": "string",
8085
- "required": true,
8086
- "description": "Current time in milliseconds since UNIX epoch, used to compute secret-key. Must match server time."
8087
- },
8088
- {
8089
- "name": "content-type",
8090
- "in": "header",
8091
- "type": "string",
8092
- "required": true,
8093
- "description": "application/json",
8094
- "example": "application/json"
8095
- }
8096
- ],
8097
- "requestParams": [
8098
- {
8099
- "name": "initiator_id",
8100
- "type": "string",
8101
- "required": true,
8102
- "description": "Registered mobile number of the API user (see Platform Credentials).",
8103
- "example": "9962981729",
8104
- "in": "body"
8105
- },
8106
- {
8107
- "name": "client_ref_id",
8108
- "type": "string",
8109
- "required": false,
8110
- "description": "Unique reference ID per API call, generated by your system (max 20 characters).",
8111
- "example": "2026010100123456789",
8112
- "in": "body"
8113
- },
8114
- {
8115
- "name": "customer_id",
8116
- "type": "integer",
8117
- "required": true,
8118
- "description": "Sender's mobile number.",
8119
- "example": 9876543210,
8120
- "in": "path"
8121
- },
8122
- {
8123
- "name": "aadhar",
8124
- "type": "string",
8125
- "required": true,
8126
- "description": "12-digit Aadhaar number of the sender.",
8127
- "example": "123456789012",
8128
- "in": "body"
8129
- },
8130
- {
8131
- "name": "otp_ref_id",
8132
- "type": "string",
8133
- "required": true,
8134
- "description": "Reference ID returned from the Get Sender Info (or Create Sender) API for this customer.",
8135
- "example": "73849201",
8136
- "in": "body"
8137
- },
8138
- {
8139
- "name": "additional_info",
8140
- "type": "string",
8141
- "required": true,
8142
- "description": "Additional info flag. Defaults to 1.",
8143
- "example": "1",
8144
- "in": "body"
8145
- }
8146
- ],
8147
- "sampleRequest": {
8148
- "initiator_id": "9962981729",
8149
- "client_ref_id": "2026010100123456789",
8150
- "aadhar": "123456789012",
8151
- "otp_ref_id": "73849201",
8152
- "additional_info": "1"
8153
- },
8154
- "responseFields": [
8155
- {
8156
- "name": "status",
8157
- "type": "number",
8158
- "description": "Primary success indicator (0 = success).",
8159
- "example": 0
8160
- },
8161
- {
8162
- "name": "message",
8163
- "type": "string",
8164
- "description": "Human-readable response / error message.",
8165
- "example": "Verification successful"
8166
- },
8167
- {
8168
- "name": "response_status_id",
8169
- "type": "number",
8170
- "description": "Granular status id; see the shared error-codes table.",
8171
- "example": 0
8172
- },
8173
- {
8174
- "name": "response_type_id",
8175
- "type": "number",
8176
- "description": "A unique id for every possible response shape (success or error) — useful for client logic branching and analytics.",
8177
- "example": 1388
8178
- },
8179
- {
8180
- "name": "data",
8181
- "type": "object",
8182
- "description": "API-specific response payload.",
8183
- "children": [
8184
- {
8185
- "name": "otp_ref_id",
8186
- "type": "string",
8187
- "description": "New OTP reference ID for the Aadhaar validation session. Pass to Validate Sender Aadhaar OTP.",
8188
- "example": "93847201"
8189
- },
8190
- {
8191
- "name": "mobile_hint",
8192
- "type": "string",
8193
- "description": "Masked mobile number to which the OTP was dispatched.",
8194
- "example": "******3210"
8195
- }
8196
- ]
8197
- }
8198
- ],
8199
- "sampleSuccessResponse": {
8200
- "status": 0,
8201
- "response_status_id": 0,
8202
- "message": "Aadhaar OTP dispatched successfully",
8203
- "response_type_id": 1388,
8204
- "data": {
8205
- "otp_ref_id": "93847201",
8206
- "mobile_hint": "******3210"
8207
- }
8208
- },
8209
- "errorScenarios": [
8210
- {
8211
- "scenario": "Invalid Aadhaar number",
8212
- "statusCode": 200,
8213
- "example": {
8214
- "status": 1,
8215
- "response_status_id": 301,
8216
- "message": "Invalid Aadhaar number provided.",
8217
- "response_type_id": 1388,
8218
- "data": {}
8219
- }
8220
- }
8221
- ],
8222
- "responseTypes": []
8223
- },
8224
- {
8225
- "slug": "aadhaar-dmt-levin-verify-otp",
8226
- "productId": "dmt",
8227
- "productName": "Domestic Money Transfer (DMT)",
8228
- "name": "Validate Sender Aadhaar OTP",
8229
- "method": "POST",
8230
- "path": "/customer/payment/dmt-levin/sender/{customer_id}/aadhaar/otp/verify",
8231
- "summary": "Verify the Aadhaar OTP for a DMT Levin sender to complete identity validation.",
8232
- "category": "bc",
8233
- "relevance": "M",
8234
- "description": "Validates the OTP received on the sender's Aadhaar-linked mobile number in the DMT Levin flow. Use `intent_id: 20` for Aadhaar validation (as opposed to `intent_id: 19` for sender onboarding). On success, the sender's identity is confirmed and their DMT Levin wallet profile is updated.",
8235
- "bestFor": "Completing OTP-based Aadhaar validation in the DMT Levin sender onboarding flow.",
8236
- "docsUrl": "https://eps.eko.in/docs/aadhaar-dmt-levin-verify-otp",
8237
- "headers": [
8238
- {
8239
- "name": "developer_key",
8240
- "in": "header",
8241
- "type": "string",
8242
- "required": true,
8243
- "description": "Static API key issued to your account after KYC."
8244
- },
8245
- {
8246
- "name": "secret-key",
8247
- "in": "header",
8248
- "type": "string",
8249
- "required": true,
8250
- "description": "Dynamic per-request signature: base64(HMAC-SHA256(timestamp, base64(access_key)))."
8251
- },
8252
- {
8253
- "name": "secret-key-timestamp",
8254
- "in": "header",
8255
- "type": "string",
8256
- "required": true,
8257
- "description": "Current time in milliseconds since UNIX epoch, used to compute secret-key. Must match server time."
8258
- },
8259
- {
8260
- "name": "content-type",
8261
- "in": "header",
8262
- "type": "string",
8263
- "required": true,
8264
- "description": "application/json",
8265
- "example": "application/json"
8266
- }
8267
- ],
8268
- "requestParams": [
8269
- {
8270
- "name": "initiator_id",
8271
- "type": "string",
8272
- "required": true,
8273
- "description": "Registered mobile number of the API user (see Platform Credentials).",
8274
- "example": "9962981729",
8275
- "in": "body"
8276
- },
8277
- {
8278
- "name": "client_ref_id",
8279
- "type": "string",
8280
- "required": false,
8281
- "description": "Unique reference ID per API call, generated by your system (max 20 characters).",
8282
- "example": "2026010100123456789",
8283
- "in": "body"
8284
- },
8285
- {
8286
- "name": "customer_id",
8287
- "type": "string",
8288
- "required": true,
8289
- "description": "Sender's mobile number.",
8290
- "example": "9876543210",
8291
- "in": "path"
8292
- },
8293
- {
8294
- "name": "otp",
8295
- "type": "integer",
8296
- "required": false,
8297
- "description": "OTP received on the Aadhaar-linked mobile number.",
8298
- "example": 123456,
8299
- "in": "body"
8300
- },
8301
- {
8302
- "name": "otp_ref_id",
8303
- "type": "integer",
8304
- "required": true,
8305
- "description": "Reference ID from the Validate Aadhaar (DMT Levin) response.",
8306
- "example": 93847201,
8307
- "in": "body"
8308
- },
8309
- {
8310
- "name": "intent_id",
8311
- "type": "string",
8312
- "required": false,
8313
- "description": "Set to \"19\" for sender onboarding or \"20\" for Aadhaar validation.",
8314
- "example": "20",
8315
- "in": "body"
8316
- },
8317
- {
8318
- "name": "additional_info",
8319
- "type": "string",
8320
- "required": false,
8321
- "description": "Additional info flag. Defaults to 1.",
8322
- "example": "1",
8323
- "in": "body"
8324
- }
8325
- ],
8326
- "sampleRequest": {
8327
- "initiator_id": "9962981729",
8328
- "client_ref_id": "2026010100123456789",
8329
- "otp": 123456,
8330
- "otp_ref_id": 93847201,
8331
- "intent_id": "20",
8332
- "additional_info": "1"
8333
- },
8334
- "responseFields": [
8335
- {
8336
- "name": "status",
8337
- "type": "number",
8338
- "description": "Primary success indicator (0 = success).",
8339
- "example": 0
8340
- },
8341
- {
8342
- "name": "message",
8343
- "type": "string",
8344
- "description": "Human-readable response / error message.",
8345
- "example": "Verification successful"
8346
- },
8347
- {
8348
- "name": "response_status_id",
8349
- "type": "number",
8350
- "description": "Granular status id; see the shared error-codes table.",
8351
- "example": 0
8352
- },
8353
- {
8354
- "name": "response_type_id",
8355
- "type": "number",
8356
- "description": "A unique id for every possible response shape (success or error) — useful for client logic branching and analytics.",
8357
- "example": 1388
8358
- },
8359
- {
8360
- "name": "data",
8361
- "type": "object",
8362
- "description": "API-specific response payload.",
8363
- "children": [
8364
- {
8365
- "name": "verified",
8366
- "type": "boolean",
8367
- "description": "True if Aadhaar OTP validation was successful.",
8368
- "imp": true,
8369
- "example": true
8370
- },
8371
- {
8372
- "name": "name",
8373
- "type": "string",
8374
- "description": "Verified sender name from Aadhaar.",
8375
- "imp": true,
8376
- "example": "Arjun Mehta"
8377
- },
8378
- {
8379
- "name": "gender",
8380
- "type": "string",
8381
- "description": "Gender from Aadhaar record.",
8382
- "imp": true,
8383
- "example": "M"
8384
- },
8385
- {
8386
- "name": "dob",
8387
- "type": "string",
8388
- "description": "Date of birth from Aadhaar.",
8389
- "imp": true,
8390
- "example": "10-01-1988"
8391
- },
8392
- {
8393
- "name": "masked_aadhaar",
8394
- "type": "string",
8395
- "description": "Aadhaar number with first 8 digits masked.",
8396
- "imp": true,
8397
- "example": "XXXX-XXXX-3456"
8398
- }
8399
- ]
8400
- }
8401
- ],
8402
- "sampleSuccessResponse": {
8403
- "status": 0,
8404
- "response_status_id": 0,
8405
- "message": "Aadhaar OTP verified successfully",
8406
- "response_type_id": 1388,
8407
- "data": {
8408
- "verified": true,
8409
- "name": "Arjun Mehta",
8410
- "gender": "M",
8411
- "dob": "10-01-1988",
8412
- "masked_aadhaar": "XXXX-XXXX-3456"
8413
- }
8414
- },
8415
- "errorScenarios": [
8416
- {
8417
- "scenario": "Wrong OTP",
8418
- "statusCode": 200,
8419
- "example": {
8420
- "status": 1,
8421
- "response_status_id": 302,
8422
- "message": "Wrong OTP. Please check and retry.",
8423
- "response_type_id": 1388,
8424
- "data": {}
8425
- }
8426
- },
8427
- {
8428
- "scenario": "OTP expired",
8429
- "statusCode": 200,
8430
- "example": {
8431
- "status": 1,
8432
- "response_status_id": 303,
8433
- "message": "OTP expired. Please regenerate.",
8434
- "response_type_id": 1388,
8435
- "data": {}
8436
- }
8437
- }
8438
- ],
8439
- "responseTypes": []
8440
- },
8441
8058
  {
8442
8059
  "slug": "aadhaar-ppi-levin-validate",
8443
8060
  "productId": "ppi",
@@ -23050,38 +22667,84 @@
23050
22667
  "id": "dmt-fino-send-money",
23051
22668
  "slug": "dmt-fino-send-money",
23052
22669
  "name": "DMT (Fino) — Send Money",
23053
- "summary": "Full Fino DMT money-transfer flow: look up the sender, onboard them if new, add the recipient, then send an OTP-verified transfer.",
22670
+ "summary": "Full Fino DMT money-transfer flow: look up the sender, onboard and biometric-eKYC them if new, pick or add the recipient, then send an OTP-verified transfer.",
23054
22671
  "productId": "dmt",
23055
22672
  "steps": [
23056
22673
  {
23057
22674
  "specSlug": "dmt-get-sender",
23058
- "purpose": "Check whether the customer is already a registered DMT sender.",
22675
+ "purpose": "Check whether the customer is already a registered DMT sender, and which stage of onboarding they are at. The `response_type_id` decides where the flow enters.",
23059
22676
  "branches": [
23060
22677
  {
23061
22678
  "onResponseTypeId": 308,
23062
22679
  "goto": "dmt-onboard-sender",
23063
22680
  "note": "Sender not found — onboard them before continuing."
22681
+ },
22682
+ {
22683
+ "onResponseTypeId": 2134,
22684
+ "goto": "dmt-fino-sender-ekyc",
22685
+ "note": "Sender found but biometric eKYC is pending — capture their fingerprint."
22686
+ },
22687
+ {
22688
+ "onResponseTypeId": 2129,
22689
+ "goto": "dmt-fino-validate-ekyc-otp",
22690
+ "note": "Sender found mid-eKYC — only the OTP validation is left."
22691
+ },
22692
+ {
22693
+ "onResponseTypeId": 309,
22694
+ "goto": "dmt-get-recipients",
22695
+ "note": "Sender found and KYC complete — skip onboarding and go straight to recipients."
23064
22696
  }
23065
22697
  ]
23066
22698
  },
23067
22699
  {
23068
22700
  "specSlug": "dmt-onboard-sender",
23069
- "purpose": "Register a new sender when Get Sender API returns `response_type_id=308`."
22701
+ "purpose": "Register a new sender with name, date of birth and residence address. This opens the sender on Eko but leaves KYC pending on Fino (`response_type_id=2134`) — they cannot transact yet.",
22702
+ "branches": [
22703
+ {
22704
+ "onResponseTypeId": 309,
22705
+ "goto": "dmt-get-recipients",
22706
+ "note": "Already onboarded on Fino externally, KYC complete — no eKYC needed."
22707
+ }
22708
+ ]
22709
+ },
22710
+ {
22711
+ "specSlug": "dmt-fino-sender-ekyc",
22712
+ "purpose": "Biometric Aadhaar eKYC — one-time per sender. Capture the PID block from an RD-service fingerprint scanner and submit it with the sender's Aadhaar number; the response returns the `kyc_request_id` and `otp_ref_id` the next step needs. Completing eKYC raises the sender's monthly limit from ₹5,000 to ₹25,000."
22713
+ },
22714
+ {
22715
+ "specSlug": "dmt-fino-validate-ekyc-otp",
22716
+ "purpose": "Confirm the eKYC by submitting the OTP sent to the sender's Aadhaar-linked mobile, along with the `kyc_request_id` and `otp_ref_id` from the biometric step. The sender is fully KYC-verified on success."
22717
+ },
22718
+ {
22719
+ "specSlug": "dmt-get-recipients",
22720
+ "purpose": "List the sender's saved beneficiaries. If the one they want is already there, reuse its `recipient_id` and skip Add Recipient.",
22721
+ "branches": [
22722
+ {
22723
+ "onResponseTypeId": 22,
22724
+ "goto": "dmt-add-recipient",
22725
+ "note": "No recipients saved yet — add one before transacting."
22726
+ },
22727
+ {
22728
+ "onResponseTypeId": 23,
22729
+ "goto": "dmt-send-otp",
22730
+ "note": "Recipient already saved — reuse its `recipient_id`."
22731
+ }
22732
+ ]
23070
22733
  },
23071
22734
  {
23072
22735
  "specSlug": "dmt-add-recipient",
23073
- "purpose": "Add the beneficiary the sender wants to transfer to."
22736
+ "purpose": "Add the beneficiary the sender wants to transfer to; returns the `recipient_id` used by the two transaction steps."
23074
22737
  },
23075
22738
  {
23076
22739
  "specSlug": "dmt-send-otp",
23077
- "purpose": "Trigger the transaction OTP sent to the sender."
22740
+ "purpose": "Pre-authorise the transfer: sends an OTP to the sender's registered mobile and returns the `otp_ref_id`. Required before every transfer — request a fresh one per attempt."
23078
22741
  },
23079
22742
  {
23080
22743
  "specSlug": "dmt-initiate-transfer",
23081
- "purpose": "Submit the OTP-verified transfer to complete the flow.",
22744
+ "purpose": "Submit the transfer with the customer-entered OTP, its `otp_ref_id`, and a `client_ref_id` unique to this attempt. The only money-debit step — persist `tid` and `bank_ref_num` and reconcile before any retry.",
23082
22745
  "branches": [
23083
22746
  {
23084
- "onResponseStatusId": 0,
22747
+ "onStatus": 0,
23085
22748
  "goto": "done"
23086
22749
  }
23087
22750
  ]
@@ -23125,7 +22788,7 @@
23125
22788
  "purpose": "Perform the biometric Aadhaar-enabled cash withdrawal.",
23126
22789
  "branches": [
23127
22790
  {
23128
- "onResponseStatusId": 0,
22791
+ "onStatus": 0,
23129
22792
  "goto": "done"
23130
22793
  }
23131
22794
  ]
package/dist/index.js CHANGED
@@ -67,6 +67,152 @@ var getApi = (bundle, slug) => bundle.apis.find((a) => a.slug === slug);
67
67
  var getTopic = (bundle, topic) => bundle.topics[topic];
68
68
  var getRecipe = (bundle, id) => bundle.recipes.find((r) => r.id === id);
69
69
 
70
+ // src/auth-debug.ts
71
+ var MS_DIGITS = 13;
72
+ var SECONDS_DIGITS = 10;
73
+ var DRIFT_WARN_MS = 5 * 60 * 1e3;
74
+ var supplied = (value) => {
75
+ const trimmed = value?.trim();
76
+ return trimmed ? trimmed : void 0;
77
+ };
78
+ var checkTimestamp = (value, nowMs) => {
79
+ const timestamp = supplied(value);
80
+ if (!timestamp)
81
+ return [
82
+ {
83
+ name: "timestamp",
84
+ ok: null,
85
+ detail: "No timestamp supplied. Pass the exact secret-key-timestamp you sent."
86
+ }
87
+ ];
88
+ if (!/^\d+$/.test(timestamp))
89
+ return [
90
+ {
91
+ name: "timestamp_format",
92
+ ok: false,
93
+ detail: "Not a plain digit string. The timestamp is signed verbatim, so quotes, a decimal point, a '+' or whitespace all change the signature."
94
+ }
95
+ ];
96
+ if (timestamp.length === SECONDS_DIGITS)
97
+ return [
98
+ {
99
+ name: "timestamp_unit",
100
+ ok: false,
101
+ detail: "10 digits \u2014 these are epoch SECONDS. Eko expects MILLISECONDS (13 digits): use Date.now(), time.time()*1000, or System.currentTimeMillis()."
102
+ }
103
+ ];
104
+ if (timestamp.length !== MS_DIGITS)
105
+ return [
106
+ {
107
+ name: "timestamp_unit",
108
+ ok: false,
109
+ detail: `${timestamp.length} digits \u2014 epoch milliseconds is ${MS_DIGITS}. Check for a truncated or padded value.`
110
+ }
111
+ ];
112
+ const driftMs = Number(timestamp) - nowMs;
113
+ const magnitude = Math.abs(driftMs);
114
+ const direction = driftMs > 0 ? "in the future" : "in the past";
115
+ return [
116
+ { name: "timestamp_unit", ok: true, detail: "13 digits \u2014 milliseconds." },
117
+ {
118
+ name: "clock_drift",
119
+ ok: magnitude <= DRIFT_WARN_MS,
120
+ detail: magnitude <= DRIFT_WARN_MS ? `${driftMs} ms from this server's clock \u2014 within the ${DRIFT_WARN_MS} ms heuristic.` : `${magnitude} ms ${direction} vs this server's clock (heuristic threshold ${DRIFT_WARN_MS} ms). Either the machine's clock is wrong (check NTP) or the timestamp is being reused instead of regenerated per request.`
121
+ }
122
+ ];
123
+ };
124
+ var checkSignatureShape = (value) => {
125
+ const signature = supplied(value);
126
+ if (!signature)
127
+ return [
128
+ {
129
+ name: "signature_shape",
130
+ ok: null,
131
+ detail: "No secret-key supplied. Pass the signature your code produced \u2014 never the access_key it was derived from."
132
+ }
133
+ ];
134
+ if (value !== signature)
135
+ return [
136
+ {
137
+ name: "signature_shape",
138
+ ok: false,
139
+ detail: "Leading/trailing whitespace or a trailing newline. Header values are sent verbatim \u2014 strip it (a common artefact of reading the key from a file)."
140
+ }
141
+ ];
142
+ if (/^[0-9a-f]{64}$/i.test(signature))
143
+ return [
144
+ {
145
+ name: "signature_shape",
146
+ ok: false,
147
+ detail: "64 hex characters \u2014 this is the raw HMAC digest. The final base64 step was skipped: base64-encode the digest bytes."
148
+ }
149
+ ];
150
+ if (/[-_]/.test(signature))
151
+ return [
152
+ {
153
+ name: "signature_shape",
154
+ ok: false,
155
+ detail: "Contains '-' or '_' \u2014 URL-safe base64. Eko expects the standard alphabet ('+' and '/'), padded with '='."
156
+ }
157
+ ];
158
+ const decoded = Buffer.from(signature, "base64");
159
+ if (decoded.toString("base64") !== signature)
160
+ return [
161
+ {
162
+ name: "signature_shape",
163
+ ok: false,
164
+ detail: "Not canonical base64 \u2014 wrong padding or an out-of-alphabet character."
165
+ }
166
+ ];
167
+ if (decoded.length !== 32)
168
+ return [
169
+ {
170
+ name: "signature_shape",
171
+ ok: false,
172
+ detail: `Decodes to ${decoded.length} bytes; SHA-256 is 32. Check the hash algorithm \u2014 SHA-1 gives 20, SHA-512 gives 64.`
173
+ }
174
+ ];
175
+ return [
176
+ {
177
+ name: "signature_shape",
178
+ ok: true,
179
+ detail: "Canonical base64 of 32 bytes \u2014 the right shape for HMAC-SHA256. Shape alone cannot prove the value: run the test vector."
180
+ }
181
+ ];
182
+ };
183
+ var RANKED_403_CAUSES = [
184
+ {
185
+ id: "ip_not_allowlisted",
186
+ cause: "The calling server's public IP is not allowlisted for that key.",
187
+ fix: "Send your egress IP to Eko support. Note that serverless/dynamic egress (Vercel, Lambda) cannot be allowlisted \u2014 call from a fixed-IP host."
188
+ },
189
+ {
190
+ id: "key_inactive",
191
+ cause: "The key is not active, or not provisioned for that environment.",
192
+ fix: "Confirm with Eko that the keypair is live for the environment you are calling."
193
+ },
194
+ {
195
+ id: "environment_mismatch",
196
+ cause: "UAT credentials sent to the production base URL, or the reverse \u2014 the keys are environment-specific.",
197
+ fix: "Check the base URL against the environments topic; the developer_key and access_key must come from the same environment."
198
+ },
199
+ {
200
+ id: "header_name_typo",
201
+ cause: "Header spelled wrongly: `secret_key`/`secretKey` instead of `secret-key`, or `developer-key` instead of `developer_key`.",
202
+ fix: "Header names are exactly: developer_key, secret-key, secret-key-timestamp, content-type."
203
+ },
204
+ {
205
+ id: "timestamp_mismatch",
206
+ cause: "The signed timestamp is not the one sent in `secret-key-timestamp` \u2014 often a second Date.now() call, or a value cached across requests.",
207
+ fix: "Compute the timestamp once, sign that exact string, and send the same string."
208
+ },
209
+ {
210
+ id: "key_decoded_before_signing",
211
+ cause: "The base64 of the access_key was decoded back to bytes before being used as the HMAC key.",
212
+ fix: "The HMAC key is the base64 STRING itself, used as-is. Confirm with the test vector."
213
+ }
214
+ ];
215
+
70
216
  // src/signing-snippets.ts
71
217
  var SIGNING_LANGUAGES = [
72
218
  "php",
@@ -290,6 +436,28 @@ var createEpsServer = (bundle, source, versionState) => {
290
436
  content: [{ type: "text", text: getSigningSnippet(language) }]
291
437
  })
292
438
  );
439
+ server.registerTool(
440
+ "debug_auth",
441
+ {
442
+ title: "Debug auth / 403",
443
+ description: "Diagnose a 403 from an EPS API. Returns a known-answer TEST VECTOR: run your own signing code over test_vector.accessKey + test_vector.timestamp \u2014 if you reproduce test_vector.secretKey, your HMAC is correct, so stop debugging the algorithm and work through ranked_causes instead. Optionally pass the timestamp and secret-key from the failing request and they are checked for the mechanical faults (seconds instead of milliseconds, clock drift, wrong digest length, stray newline). SECRET-FREE BY DESIGN: there is no access_key parameter and there never will be \u2014 never paste an access_key into a tool call; it is a server-side secret.",
444
+ inputSchema: {
445
+ timestamp: z.string().optional().describe("The secret-key-timestamp sent on the failing request."),
446
+ secret_key: z.string().optional().describe("The secret-key your code produced. Never the access_key.")
447
+ },
448
+ annotations: READ_ONLY
449
+ },
450
+ async ({ timestamp, secret_key }) => json({
451
+ test_vector: bundle.topics.auth.testVector,
452
+ how_to_use_test_vector: "secret-key = base64(HMAC_SHA256(key = base64(access_key) AS A STRING, message = timestamp)). Reproduce test_vector.secretKey from the vector's inputs to prove your implementation.",
453
+ checks: [
454
+ ...checkTimestamp(timestamp, Date.now()),
455
+ ...checkSignatureShape(secret_key)
456
+ ],
457
+ ranked_causes: RANKED_403_CAUSES,
458
+ docs_url: bundle.topics.auth.docsUrl
459
+ })
460
+ );
293
461
  server.registerTool(
294
462
  "get_meta",
295
463
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekoindia/eps-context-mcp",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Local MCP server giving AI coding agents context for Eko Platform Services (EPS) APIs.",
5
5
  "license": "MIT",
6
6
  "type": "module",