@ratio-mcp/dev-server 1.2.0 → 1.4.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.
Files changed (100) hide show
  1. package/dist/data/scope-webhook-map.d.ts +26 -0
  2. package/dist/data/scope-webhook-map.d.ts.map +1 -0
  3. package/dist/data/scope-webhook-map.js +117 -0
  4. package/dist/data/scope-webhook-map.js.map +1 -0
  5. package/dist/index.d.ts +5 -4
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +11 -13
  8. package/dist/index.js.map +1 -1
  9. package/dist/orchestrated-index.d.ts +4 -9
  10. package/dist/orchestrated-index.d.ts.map +1 -1
  11. package/dist/orchestrated-index.js +6 -14
  12. package/dist/orchestrated-index.js.map +1 -1
  13. package/dist/orchestrated-server.d.ts +14 -9
  14. package/dist/orchestrated-server.d.ts.map +1 -1
  15. package/dist/orchestrated-server.js +86 -48
  16. package/dist/orchestrated-server.js.map +1 -1
  17. package/dist/services/session-state.d.ts +50 -16
  18. package/dist/services/session-state.d.ts.map +1 -1
  19. package/dist/services/session-state.js +195 -140
  20. package/dist/services/session-state.js.map +1 -1
  21. package/dist/services/tool-guard.d.ts +29 -9
  22. package/dist/services/tool-guard.d.ts.map +1 -1
  23. package/dist/services/tool-guard.js +83 -60
  24. package/dist/services/tool-guard.js.map +1 -1
  25. package/dist/templates/backend/.env.example +3 -0
  26. package/dist/templates/backend/package.json +2 -1
  27. package/dist/templates/backend/src/app.module.ts +2 -0
  28. package/dist/templates/backend/src/oauth/oauth-callback.controller.ts +62 -0
  29. package/dist/templates/backend/src/oauth/oauth-callback.module.ts +10 -0
  30. package/dist/templates/backend/src/oauth/oauth-callback.service.ts +158 -0
  31. package/dist/templates/frontend/.env.example +1 -1
  32. package/dist/templates/frontend/README.md +16 -0
  33. package/dist/templates/frontend/eslint.config.js +29 -0
  34. package/dist/templates/frontend/index.html +4 -4
  35. package/dist/templates/frontend/jsconfig.json +22 -0
  36. package/dist/templates/frontend/package.json +14 -11
  37. package/dist/templates/frontend/public/favicon.svg +1 -0
  38. package/dist/templates/frontend/public/icons.svg +24 -0
  39. package/dist/templates/frontend/src/App.css +184 -0
  40. package/dist/templates/frontend/src/App.jsx +121 -0
  41. package/dist/templates/frontend/src/assets/hero.png +0 -0
  42. package/dist/templates/frontend/src/assets/react.svg +1 -0
  43. package/dist/templates/frontend/src/assets/vite.svg +1 -0
  44. package/dist/templates/frontend/src/index.css +94 -31
  45. package/dist/templates/frontend/src/main.jsx +10 -0
  46. package/dist/templates/frontend/vite.config.js +19 -0
  47. package/dist/tools/app-management.d.ts.map +1 -1
  48. package/dist/tools/app-management.js +73 -13
  49. package/dist/tools/app-management.js.map +1 -1
  50. package/dist/tools/auth.d.ts.map +1 -1
  51. package/dist/tools/auth.js +15 -3
  52. package/dist/tools/auth.js.map +1 -1
  53. package/dist/tools/codegen.d.ts.map +1 -1
  54. package/dist/tools/codegen.js +312 -18
  55. package/dist/tools/codegen.js.map +1 -1
  56. package/dist/tools/gather-requirements.d.ts.map +1 -1
  57. package/dist/tools/gather-requirements.js +19 -19
  58. package/dist/tools/gather-requirements.js.map +1 -1
  59. package/dist/tools/get-status.d.ts +3 -0
  60. package/dist/tools/get-status.d.ts.map +1 -0
  61. package/dist/tools/get-status.js +122 -0
  62. package/dist/tools/get-status.js.map +1 -0
  63. package/dist/tools/hookdeck.d.ts +3 -0
  64. package/dist/tools/hookdeck.d.ts.map +1 -0
  65. package/dist/tools/hookdeck.js +101 -0
  66. package/dist/tools/hookdeck.js.map +1 -0
  67. package/dist/tools/index.d.ts.map +1 -1
  68. package/dist/tools/index.js +7 -3
  69. package/dist/tools/index.js.map +1 -1
  70. package/dist/tools/lifecycle.d.ts.map +1 -1
  71. package/dist/tools/lifecycle.js +22 -11
  72. package/dist/tools/lifecycle.js.map +1 -1
  73. package/dist/tools/reference-data.d.ts.map +1 -1
  74. package/dist/tools/reference-data.js +6 -4
  75. package/dist/tools/reference-data.js.map +1 -1
  76. package/dist/tools/requirements.d.ts.map +1 -1
  77. package/dist/tools/requirements.js +84 -5
  78. package/dist/tools/requirements.js.map +1 -1
  79. package/dist/tools/scaffold.d.ts.map +1 -1
  80. package/dist/tools/scaffold.js +30 -16
  81. package/dist/tools/scaffold.js.map +1 -1
  82. package/dist/tools/submission.js +4 -4
  83. package/dist/tools/submission.js.map +1 -1
  84. package/dist/tools/webhooks.d.ts +25 -0
  85. package/dist/tools/webhooks.d.ts.map +1 -1
  86. package/dist/tools/webhooks.js +143 -93
  87. package/dist/tools/webhooks.js.map +1 -1
  88. package/dist/utils/logger.js +17 -17
  89. package/dist/utils/logger.js.map +1 -1
  90. package/package.json +2 -2
  91. package/dist/templates/frontend/src/App.tsx +0 -32
  92. package/dist/templates/frontend/src/components/Layout.tsx +0 -53
  93. package/dist/templates/frontend/src/context/AuthContext.tsx +0 -59
  94. package/dist/templates/frontend/src/main.tsx +0 -16
  95. package/dist/templates/frontend/src/pages/Callback.tsx +0 -53
  96. package/dist/templates/frontend/src/pages/Dashboard.tsx +0 -24
  97. package/dist/templates/frontend/src/pages/Login.tsx +0 -42
  98. package/dist/templates/frontend/src/services/api.ts +0 -48
  99. package/dist/templates/frontend/tsconfig.json +0 -21
  100. package/dist/templates/frontend/vite.config.ts +0 -15
@@ -2,126 +2,124 @@ import { getSession, hasCompleted, hasCompletedAny } from './session-state.js';
2
2
  import { logger } from '../utils/logger.js';
3
3
  // ── Prerequisite rules ──────────────────────────────────────────────────
4
4
  const TOOL_PREREQUISITES = {
5
- // ── Phase 1: App Creation (auth categories create_app) ──────────
6
- get_categories: () => {
5
+ // ── Auth (entry points no prerequisites) ────────────────────────────
6
+ // developer_login, developer_signup: no entry needed (always allowed)
7
+ developer_verify_otp: () => {
8
+ if (!hasCompleted('developer_signup')) {
9
+ return blocked('You must sign up first before verifying OTP.', ['developer_signup'], ['developer_signup', 'developer_verify_otp']);
10
+ }
11
+ return allowed();
12
+ },
13
+ // ── Read-only tools (no auth needed) ──────────────────────────────────
14
+ // get_categories: no entry needed (always allowed — read-only, anyone can browse)
15
+ // get_status: no entry needed (always allowed)
16
+ // get_app_lifecycle: no entry needed (always allowed)
17
+ // ── Hookdeck Setup (needs auth) ───────────────────────────────────────
18
+ setup_hookdeck: () => {
7
19
  if (!isAuthenticated()) {
8
- return blocked('You must authenticate first. Call developer_login.', ['developer_login']);
20
+ return blocked('You must authenticate first. Call developer_login.', ['developer_login'], ['developer_login', 'setup_hookdeck']);
9
21
  }
10
22
  return allowed();
11
23
  },
24
+ // ── App Setup (needs auth) ────────────────────────────────────────────
12
25
  create_app: () => {
13
26
  if (!isAuthenticated()) {
14
- return blocked('You must authenticate first. Call developer_login.', ['developer_login']);
15
- }
16
- if (!hasCompleted('get_categories')) {
17
- return blocked('You must call get_categories first so the developer can choose app categories.', ['get_categories']);
27
+ return blocked('You must authenticate first. Call developer_login.', ['developer_login'], ['developer_login', 'create_app']);
18
28
  }
19
29
  return allowed();
20
30
  },
21
- // ── Phase 2: Scope Selection (gather requirements → define scopes) ──
31
+ // ── Requirements (needs app) ──────────────────────────────────────────
22
32
  gather_requirements: () => {
23
33
  if (!getSession().context.appId) {
24
- return blocked('You must create an app first (create_app) before gathering requirements.', ['get_categories', 'create_app']);
34
+ return blocked('You must create an app first before gathering requirements.', ['create_app'], ['developer_login', 'create_app', 'gather_requirements']);
25
35
  }
26
36
  return allowed();
27
37
  },
28
38
  define_app_requirements: () => {
29
- if (!getSession().context.appId) {
30
- return blocked('You must create an app first (create_app) before defining scopes.', ['get_categories', 'create_app']);
31
- }
32
39
  if (!hasCompleted('gather_requirements')) {
33
40
  return blocked('You must call gather_requirements first to analyze what the developer wants to build, ' +
34
- 'then use the recommended scopes with define_app_requirements.', ['gather_requirements']);
41
+ 'then use the recommended scopes with define_app_requirements.', ['gather_requirements'], ['gather_requirements', 'define_app_requirements']);
35
42
  }
36
43
  return allowed();
37
44
  },
38
- // ── Phase 3: Webhooks (needs scopes confirmed) ─────────────────────
45
+ // ── Webhooks (needs scopes confirmed) ─────────────────────────────────
39
46
  get_webhook_topics: () => {
40
- if (!isAuthenticated()) {
41
- return blocked('You must authenticate first.', ['developer_login']);
42
- }
43
- if (!getSession().context.scopesConfirmed) {
44
- return blocked('You must confirm scopes first (define_app_requirements) before configuring webhooks. ' +
45
- 'Webhook events are suggested based on the selected scopes.', ['define_app_requirements']);
46
- }
47
+ // Read-only browsing of webhook topics — no prerequisites
47
48
  return allowed();
48
49
  },
49
50
  create_webhooks: () => {
50
51
  if (!getSession().context.appId) {
51
- return blocked('You must create an app first before configuring webhooks.', ['create_app']);
52
+ return blocked('You must create an app first before configuring webhooks.', ['create_app'], ['developer_login', 'create_app', 'create_webhooks']);
52
53
  }
53
54
  if (!getSession().context.scopesConfirmed) {
54
- return blocked('You must confirm scopes first. Webhook events are based on selected scopes.', ['define_app_requirements']);
55
+ return blocked('You must confirm scopes first (define_app_requirements) before configuring webhooks. ' +
56
+ 'Webhook events should be based on the selected scopes.', ['define_app_requirements'], ['gather_requirements', 'define_app_requirements', 'create_webhooks']);
55
57
  }
56
58
  return allowed();
57
59
  },
58
- // ── Phase 4: Scaffold & Build ───────────────────────────────────────
60
+ // ── Frontend Build Pipeline (needs scopes confirmed) ──────────────────
59
61
  scaffold_frontend: () => {
60
- if (!getSession().context.appId) {
61
- return blocked('You must create an app first before scaffolding.', ['create_app']);
62
- }
63
62
  if (!getSession().context.scopesConfirmed) {
64
- return blocked('You must define and confirm app scopes before scaffolding.', ['define_app_requirements']);
63
+ return blocked('You must define and confirm app scopes (define_app_requirements) before scaffolding the frontend.', ['define_app_requirements'], ['gather_requirements', 'define_app_requirements', 'scaffold_frontend']);
65
64
  }
66
65
  return allowed();
67
66
  },
68
67
  build_frontend: () => {
69
68
  if (!hasCompleted('scaffold_frontend')) {
70
- return blocked('You must scaffold the frontend first before building.', ['scaffold_frontend']);
69
+ return blocked('You must scaffold the frontend first before building.', ['scaffold_frontend'], ['scaffold_frontend', 'build_frontend']);
71
70
  }
72
71
  return allowed();
73
72
  },
74
- create_submission_zip: () => {
73
+ validate_build: () => {
75
74
  if (!hasCompleted('build_frontend')) {
76
- return blocked('You must build the frontend first before creating a submission zip.', ['build_frontend']);
75
+ return blocked('You must build the frontend first before validating.', ['build_frontend'], ['scaffold_frontend', 'build_frontend', 'validate_build']);
76
+ }
77
+ return allowed();
78
+ },
79
+ create_submission_zip: () => {
80
+ if (!hasCompleted('validate_build')) {
81
+ return blocked('You must validate the build first before creating a submission zip.', ['validate_build'], ['scaffold_frontend', 'build_frontend', 'validate_build', 'create_submission_zip']);
77
82
  }
78
83
  return allowed();
79
84
  },
80
85
  upload_build: () => {
81
- if (!hasCompleted('build_frontend')) {
82
- return blocked('You must build the frontend first before uploading.', ['build_frontend']);
86
+ if (!hasCompleted('create_submission_zip')) {
87
+ return blocked('You must create a submission zip first before uploading.', ['create_submission_zip'], ['scaffold_frontend', 'build_frontend', 'validate_build', 'create_submission_zip', 'upload_build']);
83
88
  }
84
89
  return allowed();
85
90
  },
86
91
  submit_for_review: () => {
87
- if (!getSession().context.buildUploaded && !hasCompleted('upload_build')) {
88
- return blocked('You must upload the build first before submitting for review.', ['upload_build']);
92
+ if (!hasCompleted('upload_build')) {
93
+ return blocked('You must upload the build first before submitting for review.', ['upload_build'], ['upload_build', 'submit_for_review']);
89
94
  }
90
95
  return allowed();
91
96
  },
92
- // ── Phase 5: Publish & OAuth ────────────────────────────────────────
97
+ // ── Review & Publish ──────────────────────────────────────────────────
98
+ get_app_status: () => requireAuth(),
93
99
  publish_app: () => {
94
100
  if (!getSession().context.appApproved) {
95
- return blocked('The app must be approved by admin before publishing. Call get_app_status to check.', ['get_app_status']);
96
- }
97
- return allowed();
98
- },
99
- request_auth_code: () => {
100
- if (!getSession().context.appPublished && !hasCompleted('publish_app')) {
101
- return blocked('You must publish the app first before starting the OAuth flow.', ['publish_app']);
102
- }
103
- return allowed();
104
- },
105
- get_access_token: () => {
106
- if (!hasCompleted('request_auth_code')) {
107
- return blocked('You must request an authorization code first.', ['request_auth_code']);
101
+ return blocked('The app must be approved by admin before publishing. Call get_app_status to check review status.', ['get_app_status'], ['submit_for_review', 'get_app_status', 'publish_app']);
108
102
  }
109
103
  return allowed();
110
104
  },
111
- // ── Phase 6: Backend Codegen ────────────────────────────────────────
105
+ // ── OAuth tools removed from main flow ────────────────────────────────
106
+ // request_auth_code and get_access_token no longer needed — OAuth happens
107
+ // via merchant install callback (Hookdeck → localhost → token exchange)
108
+ // ── Backend Codegen (needs app published) ─────────────────────────────
112
109
  scaffold_backend: () => {
113
- if (!getSession().context.oauthCompleted) {
114
- return blocked('You must complete the OAuth flow (get_access_token) before scaffolding the backend.', ['get_access_token']);
110
+ if (!getSession().context.appPublished && !hasCompleted('publish_app')) {
111
+ return blocked('You must publish the app first before scaffolding the backend. ' +
112
+ 'The backend includes an OAuth callback handler that receives merchant install tokens automatically.', ['publish_app'], ['publish_app', 'scaffold_backend']);
115
113
  }
116
114
  return allowed();
117
115
  },
118
116
  generate_api_routes: () => {
119
117
  if (!hasCompleted('scaffold_backend')) {
120
- return blocked('You must scaffold the backend first before generating API routes.', ['scaffold_backend']);
118
+ return blocked('You must scaffold the backend first before generating API routes.', ['scaffold_backend'], ['scaffold_backend', 'generate_api_routes']);
121
119
  }
122
120
  return allowed();
123
121
  },
124
- // ── Management tools (auth only) ───────────────────────────────────
122
+ // ── Management tools (auth only) ──────────────────────────────────────
125
123
  update_app: () => requireAuth(),
126
124
  delete_app: () => requireAuth(),
127
125
  list_apps: () => requireAuth(),
@@ -129,36 +127,61 @@ const TOOL_PREREQUISITES = {
129
127
  list_webhooks: () => requireAuth(),
130
128
  toggle_webhook: () => requireAuth(),
131
129
  delete_webhook: () => requireAuth(),
132
- get_app_status: () => requireAuth(),
133
130
  refresh_access_token: () => requireAuth(),
134
131
  check_token_status: () => requireAuth(),
135
- validate_build: () => requireAuth(),
136
132
  };
137
133
  // ── Helpers ─────────────────────────────────────────────────────────────
138
134
  function allowed() {
139
135
  return { allowed: true };
140
136
  }
141
- function blocked(message, requiredTools) {
142
- return { allowed: false, message, requiredTools };
137
+ function blocked(message, requiredTools, chain) {
138
+ return {
139
+ allowed: false,
140
+ message,
141
+ requiredTools,
142
+ chain,
143
+ hint: `Please call ${requiredTools.join(' → ')} first, then retry.`,
144
+ };
143
145
  }
144
146
  function isAuthenticated() {
145
147
  return hasCompletedAny(['developer_login', 'developer_verify_otp']);
146
148
  }
147
149
  function requireAuth() {
148
150
  if (!isAuthenticated()) {
149
- return blocked('You must authenticate first. Call developer_login or developer_signup.', ['developer_login']);
151
+ return blocked('You must authenticate first. Call developer_login or developer_signup + developer_verify_otp.', ['developer_login'], ['developer_login']);
150
152
  }
151
153
  return allowed();
152
154
  }
153
155
  // ── Public API ──────────────────────────────────────────────────────────
156
+ /**
157
+ * Check whether a tool's prerequisites are met.
158
+ * Returns structured result with error details if blocked.
159
+ */
154
160
  export function checkToolPrerequisites(toolName) {
155
161
  const check = TOOL_PREREQUISITES[toolName];
156
162
  if (!check)
157
- return allowed();
163
+ return allowed(); // No prerequisites defined → always allowed
158
164
  const result = check();
159
165
  if (!result.allowed) {
160
166
  logger.info(`GUARD BLOCKED: ${toolName} — ${result.message} (need: [${result.requiredTools?.join(', ')}])`);
161
167
  }
162
168
  return result;
163
169
  }
170
+ /**
171
+ * Get the full prerequisite chain for a tool (for status display).
172
+ * Returns tool names that must be completed before this tool can run.
173
+ */
174
+ export function getToolPrerequisiteNames(toolName) {
175
+ const check = TOOL_PREREQUISITES[toolName];
176
+ if (!check)
177
+ return [];
178
+ const result = check();
179
+ return result.chain || result.requiredTools || [];
180
+ }
181
+ /**
182
+ * Get all tool names that have prerequisites defined.
183
+ */
184
+ export function getGuardedToolNames() {
185
+ return Object.keys(TOOL_PREREQUISITES);
186
+ }
164
187
  //# sourceMappingURL=tool-guard.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tool-guard.js","sourceRoot":"","sources":["../../src/services/tool-guard.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tool-guard.js","sourceRoot":"","sources":["../../src/services/tool-guard.ts"],"names":[],"mappings":"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"}
@@ -20,3 +20,6 @@ RATIO_SCOPES=
20
20
  # JWT
21
21
  JWT_SECRET=your_jwt_secret_change_this
22
22
  JWT_EXPIRES_IN=7d
23
+
24
+ # Hookdeck (managed by MCP — do not edit)
25
+ HOOKDECK_URL=
@@ -6,7 +6,8 @@
6
6
  "build": "nest build",
7
7
  "start": "nest start",
8
8
  "start:dev": "nest start --watch",
9
- "start:prod": "node dist/main"
9
+ "start:prod": "node dist/main",
10
+ "dev": "nest start --watch"
10
11
  },
11
12
  "dependencies": {
12
13
  "@nestjs/common": "^10.3.0",
@@ -1,9 +1,11 @@
1
1
  import { Module } from '@nestjs/common';
2
2
  import { ConfigModule } from '@nestjs/config';
3
+ import { OAuthCallbackModule } from './oauth/oauth-callback.module';
3
4
 
4
5
  @Module({
5
6
  imports: [
6
7
  ConfigModule.forRoot({ isGlobal: true }),
8
+ OAuthCallbackModule,
7
9
  // Resource modules will be added here by generate_api_routes
8
10
  ],
9
11
  })
@@ -0,0 +1,62 @@
1
+ import { Controller, Get, Query, Logger, Res } from '@nestjs/common';
2
+ import { Response } from 'express';
3
+ import { OAuthCallbackService } from './oauth-callback.service';
4
+
5
+ @Controller()
6
+ export class OAuthCallbackController {
7
+ private readonly logger = new Logger(OAuthCallbackController.name);
8
+
9
+ constructor(private readonly oauthService: OAuthCallbackService) {}
10
+
11
+ /**
12
+ * OAuth callback endpoint.
13
+ * Receives the authorization code from the merchant store redirect:
14
+ * GET /callback?code=xxx&state=yyy
15
+ *
16
+ * Exchanges the code for an access token and stores it per merchant.
17
+ */
18
+ @Get('callback')
19
+ async handleCallback(
20
+ @Query('code') code: string,
21
+ @Query('state') state: string,
22
+ @Res() res: Response,
23
+ ) {
24
+ this.logger.log(`OAuth callback received — code=${code?.substring(0, 12)}..., state=${state?.substring(0, 12)}...`);
25
+
26
+ if (!code) {
27
+ this.logger.warn('OAuth callback received without code');
28
+ return res.status(400).send(`
29
+ <html><body>
30
+ <h1>OAuth Error</h1>
31
+ <p>No authorization code received. The merchant may have denied the request.</p>
32
+ </body></html>
33
+ `);
34
+ }
35
+
36
+ try {
37
+ const result = await this.oauthService.exchangeCodeForToken(code);
38
+
39
+ this.logger.log(`App installed successfully for merchant: ${result.merchant_id}`);
40
+
41
+ return res.status(200).send(`
42
+ <html><body>
43
+ <h1>App Installed Successfully!</h1>
44
+ <p>Merchant <strong>${result.merchant_id}</strong> has installed your app.</p>
45
+ <p>Access token injected. Your API routes are now live.</p>
46
+ <p>Scopes granted: <code>${result.scopes}</code></p>
47
+ <p style="color: #666; font-size: 0.9em;">You can close this window.</p>
48
+ </body></html>
49
+ `);
50
+ } catch (error: any) {
51
+ this.logger.error(`OAuth callback failed: ${error.message}`);
52
+
53
+ return res.status(500).send(`
54
+ <html><body>
55
+ <h1>Installation Failed</h1>
56
+ <p>Error: ${error.message}</p>
57
+ <p>Please try installing the app again.</p>
58
+ </body></html>
59
+ `);
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,10 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { OAuthCallbackController } from './oauth-callback.controller';
3
+ import { OAuthCallbackService } from './oauth-callback.service';
4
+
5
+ @Module({
6
+ controllers: [OAuthCallbackController],
7
+ providers: [OAuthCallbackService],
8
+ exports: [OAuthCallbackService],
9
+ })
10
+ export class OAuthCallbackModule {}
@@ -0,0 +1,158 @@
1
+ import { Injectable, Logger } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import axios from 'axios';
4
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
5
+ import { join } from 'path';
6
+
7
+ @Injectable()
8
+ export class OAuthCallbackService {
9
+ private readonly logger = new Logger(OAuthCallbackService.name);
10
+ private readonly apiBaseUrl: string;
11
+ private readonly clientId: string;
12
+ private readonly clientSecret: string;
13
+ private readonly redirectUri: string;
14
+
15
+ constructor(private configService: ConfigService) {
16
+ this.apiBaseUrl = this.configService.get<string>('RATIO_API_BASE_URL');
17
+ this.clientId = this.configService.get<string>('RATIO_APP_ID', '');
18
+ this.clientSecret = this.configService.get<string>('RATIO_APP_SECRET', '');
19
+ this.redirectUri = this.configService.get<string>('RATIO_REDIRECT_URI', 'http://localhost:3000/callback');
20
+ }
21
+
22
+ /**
23
+ * Exchange authorization code for access token.
24
+ * Writes token to process.env (immediate) + .env file (persists across restarts).
25
+ */
26
+ async exchangeCodeForToken(code: string) {
27
+ this.logger.log(`Exchanging authorization code for access token...`);
28
+
29
+ try {
30
+ const response = await axios.post(`${this.apiBaseUrl}/api/v1/oauth/token`, {
31
+ grant_type: 'authorization_code',
32
+ code,
33
+ clientId: this.clientId,
34
+ clientSecret: this.clientSecret,
35
+ redirectUri: this.redirectUri,
36
+ });
37
+
38
+ const data = response.data;
39
+
40
+ // Update process.env immediately (hot-reload — no restart needed)
41
+ process.env.RATIO_ACCESS_TOKEN = data.access_token;
42
+ process.env.RATIO_REFRESH_TOKEN = data.refresh_token;
43
+ process.env.RATIO_MERCHANT_ID = data.merchant_id;
44
+ process.env.RATIO_SCOPES = data.scope;
45
+
46
+ // Persist to .env file so it survives restarts
47
+ this.writeToEnvFile({
48
+ RATIO_ACCESS_TOKEN: data.access_token,
49
+ RATIO_REFRESH_TOKEN: data.refresh_token,
50
+ RATIO_MERCHANT_ID: data.merchant_id,
51
+ RATIO_SCOPES: data.scope,
52
+ });
53
+
54
+ // Console output so the developer sees the token info
55
+ console.log('\n' + '='.repeat(60));
56
+ console.log('NEW MERCHANT INSTALLED YOUR APP!');
57
+ console.log('='.repeat(60));
58
+ console.log(`Merchant ID: ${data.merchant_id}`);
59
+ console.log(`Access Token: ${data.access_token}`);
60
+ console.log(`Refresh Token: ${data.refresh_token}`);
61
+ console.log(`Scopes: ${data.scope}`);
62
+ console.log('='.repeat(60));
63
+ console.log('Token injected into process.env and .env file.');
64
+ console.log('Your API routes are now live — no restart needed.');
65
+ console.log('='.repeat(60) + '\n');
66
+
67
+ this.logger.log(`Token obtained for merchant: ${data.merchant_id}`);
68
+
69
+ return {
70
+ access_token: data.access_token,
71
+ merchant_id: data.merchant_id,
72
+ scopes: data.scope,
73
+ };
74
+ } catch (error: any) {
75
+ const errMsg = error.response?.data?.message || error.message;
76
+ this.logger.error(`Token exchange failed: ${errMsg}`);
77
+ throw new Error(`OAuth token exchange failed: ${errMsg}`);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Refresh an expired access token using the refresh token from process.env.
83
+ */
84
+ async refreshToken(): Promise<void> {
85
+ const refreshToken = process.env.RATIO_REFRESH_TOKEN;
86
+ if (!refreshToken) {
87
+ throw new Error('No refresh token available. A merchant must install the app first.');
88
+ }
89
+
90
+ this.logger.log('Refreshing access token...');
91
+
92
+ const response = await axios.post(`${this.apiBaseUrl}/api/v1/oauth/token`, {
93
+ grant_type: 'refresh_token',
94
+ refresh_token: refreshToken,
95
+ clientId: this.clientId,
96
+ clientSecret: this.clientSecret,
97
+ });
98
+
99
+ const data = response.data;
100
+
101
+ // Update process.env immediately
102
+ process.env.RATIO_ACCESS_TOKEN = data.access_token;
103
+ process.env.RATIO_REFRESH_TOKEN = data.refresh_token;
104
+ if (data.merchant_id) process.env.RATIO_MERCHANT_ID = data.merchant_id;
105
+ if (data.scope) process.env.RATIO_SCOPES = data.scope;
106
+
107
+ // Persist to .env file
108
+ this.writeToEnvFile({
109
+ RATIO_ACCESS_TOKEN: data.access_token,
110
+ RATIO_REFRESH_TOKEN: data.refresh_token,
111
+ ...(data.merchant_id && { RATIO_MERCHANT_ID: data.merchant_id }),
112
+ ...(data.scope && { RATIO_SCOPES: data.scope }),
113
+ });
114
+
115
+ this.logger.log('Access token refreshed successfully.');
116
+ }
117
+
118
+ /**
119
+ * Get the current access token from process.env.
120
+ * Returns null if no merchant has installed yet.
121
+ */
122
+ getAccessToken(): string | null {
123
+ return process.env.RATIO_ACCESS_TOKEN || null;
124
+ }
125
+
126
+ /**
127
+ * Get the current merchant ID from process.env.
128
+ */
129
+ getMerchantId(): string | null {
130
+ return process.env.RATIO_MERCHANT_ID || null;
131
+ }
132
+
133
+ /**
134
+ * Write key-value pairs to the .env file. Updates existing keys, appends new ones.
135
+ */
136
+ private writeToEnvFile(values: Record<string, string>): void {
137
+ const envPath = join(process.cwd(), '.env');
138
+ if (!existsSync(envPath)) return;
139
+
140
+ try {
141
+ let envContent = readFileSync(envPath, 'utf-8');
142
+
143
+ for (const [key, value] of Object.entries(values)) {
144
+ const regex = new RegExp(`^${key}=.*$`, 'm');
145
+ if (regex.test(envContent)) {
146
+ envContent = envContent.replace(regex, `${key}=${value}`);
147
+ } else {
148
+ envContent += `\n${key}=${value}`;
149
+ }
150
+ }
151
+
152
+ writeFileSync(envPath, envContent);
153
+ this.logger.log('Updated .env file with new token values.');
154
+ } catch (error: any) {
155
+ this.logger.warn(`Failed to update .env file: ${error.message}`);
156
+ }
157
+ }
158
+ }
@@ -3,4 +3,4 @@ VITE_APP_NAME=My Ratio App
3
3
  VITE_APP_VERSION=1.0.0
4
4
  VITE_RATIO_CLIENT_ID=your_client_id
5
5
  VITE_RATIO_REDIRECT_URI=http://localhost:5173/callback
6
- VITE_RATIO_DASHBOARD_URL=https://adm-apps.gokwik.co
6
+ VITE_RATIO_DASHBOARD_URL=https://dev-developers.dev.gokwik.in
@@ -0,0 +1,16 @@
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
@@ -0,0 +1,29 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
@@ -1,13 +1,13 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Ratio App</title>
7
+ <title>frontend-template</title>
8
8
  </head>
9
9
  <body>
10
10
  <div id="root"></div>
11
- <script type="module" src="/src/main.tsx"></script>
11
+ <script type="module" src="/src/main.jsx"></script>
12
12
  </body>
13
13
  </html>
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "paths": {
5
+ "@/*": ["./src/*"],
6
+ "@components/*": ["./src/components/*"],
7
+ "@assets/*": ["./src/assets/*"],
8
+ "@hooks/*": ["./src/hooks/*"],
9
+ "@utils/*": ["./src/utils/*"],
10
+ "@pages/*": ["./src/pages/*"]
11
+ },
12
+ "jsx": "react-jsx",
13
+ "jsxImportSource": "react",
14
+ "target": "ES2020",
15
+ "useDefineForClassFields": true,
16
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
17
+ "module": "ESNext",
18
+ "skipLibCheck": true
19
+ },
20
+ "include": ["src"],
21
+ "references": [{ "path": "./tsconfig.app.json" }]
22
+ }