@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.
- package/dist/data/scope-webhook-map.d.ts +26 -0
- package/dist/data/scope-webhook-map.d.ts.map +1 -0
- package/dist/data/scope-webhook-map.js +117 -0
- package/dist/data/scope-webhook-map.js.map +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -13
- package/dist/index.js.map +1 -1
- package/dist/orchestrated-index.d.ts +4 -9
- package/dist/orchestrated-index.d.ts.map +1 -1
- package/dist/orchestrated-index.js +6 -14
- package/dist/orchestrated-index.js.map +1 -1
- package/dist/orchestrated-server.d.ts +14 -9
- package/dist/orchestrated-server.d.ts.map +1 -1
- package/dist/orchestrated-server.js +86 -48
- package/dist/orchestrated-server.js.map +1 -1
- package/dist/services/session-state.d.ts +50 -16
- package/dist/services/session-state.d.ts.map +1 -1
- package/dist/services/session-state.js +195 -140
- package/dist/services/session-state.js.map +1 -1
- package/dist/services/tool-guard.d.ts +29 -9
- package/dist/services/tool-guard.d.ts.map +1 -1
- package/dist/services/tool-guard.js +83 -60
- package/dist/services/tool-guard.js.map +1 -1
- package/dist/templates/backend/.env.example +3 -0
- package/dist/templates/backend/package.json +2 -1
- package/dist/templates/backend/src/app.module.ts +2 -0
- package/dist/templates/backend/src/oauth/oauth-callback.controller.ts +62 -0
- package/dist/templates/backend/src/oauth/oauth-callback.module.ts +10 -0
- package/dist/templates/backend/src/oauth/oauth-callback.service.ts +158 -0
- package/dist/templates/frontend/.env.example +1 -1
- package/dist/templates/frontend/README.md +16 -0
- package/dist/templates/frontend/eslint.config.js +29 -0
- package/dist/templates/frontend/index.html +4 -4
- package/dist/templates/frontend/jsconfig.json +22 -0
- package/dist/templates/frontend/package.json +14 -11
- package/dist/templates/frontend/public/favicon.svg +1 -0
- package/dist/templates/frontend/public/icons.svg +24 -0
- package/dist/templates/frontend/src/App.css +184 -0
- package/dist/templates/frontend/src/App.jsx +121 -0
- package/dist/templates/frontend/src/assets/hero.png +0 -0
- package/dist/templates/frontend/src/assets/react.svg +1 -0
- package/dist/templates/frontend/src/assets/vite.svg +1 -0
- package/dist/templates/frontend/src/index.css +94 -31
- package/dist/templates/frontend/src/main.jsx +10 -0
- package/dist/templates/frontend/vite.config.js +19 -0
- package/dist/tools/app-management.d.ts.map +1 -1
- package/dist/tools/app-management.js +73 -13
- package/dist/tools/app-management.js.map +1 -1
- package/dist/tools/auth.d.ts.map +1 -1
- package/dist/tools/auth.js +15 -3
- package/dist/tools/auth.js.map +1 -1
- package/dist/tools/codegen.d.ts.map +1 -1
- package/dist/tools/codegen.js +312 -18
- package/dist/tools/codegen.js.map +1 -1
- package/dist/tools/gather-requirements.d.ts.map +1 -1
- package/dist/tools/gather-requirements.js +19 -19
- package/dist/tools/gather-requirements.js.map +1 -1
- package/dist/tools/get-status.d.ts +3 -0
- package/dist/tools/get-status.d.ts.map +1 -0
- package/dist/tools/get-status.js +122 -0
- package/dist/tools/get-status.js.map +1 -0
- package/dist/tools/hookdeck.d.ts +3 -0
- package/dist/tools/hookdeck.d.ts.map +1 -0
- package/dist/tools/hookdeck.js +101 -0
- package/dist/tools/hookdeck.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +7 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/lifecycle.d.ts.map +1 -1
- package/dist/tools/lifecycle.js +22 -11
- package/dist/tools/lifecycle.js.map +1 -1
- package/dist/tools/reference-data.d.ts.map +1 -1
- package/dist/tools/reference-data.js +6 -4
- package/dist/tools/reference-data.js.map +1 -1
- package/dist/tools/requirements.d.ts.map +1 -1
- package/dist/tools/requirements.js +84 -5
- package/dist/tools/requirements.js.map +1 -1
- package/dist/tools/scaffold.d.ts.map +1 -1
- package/dist/tools/scaffold.js +30 -16
- package/dist/tools/scaffold.js.map +1 -1
- package/dist/tools/submission.js +4 -4
- package/dist/tools/submission.js.map +1 -1
- package/dist/tools/webhooks.d.ts +25 -0
- package/dist/tools/webhooks.d.ts.map +1 -1
- package/dist/tools/webhooks.js +143 -93
- package/dist/tools/webhooks.js.map +1 -1
- package/dist/utils/logger.js +17 -17
- package/dist/utils/logger.js.map +1 -1
- package/package.json +2 -2
- package/dist/templates/frontend/src/App.tsx +0 -32
- package/dist/templates/frontend/src/components/Layout.tsx +0 -53
- package/dist/templates/frontend/src/context/AuthContext.tsx +0 -59
- package/dist/templates/frontend/src/main.tsx +0 -16
- package/dist/templates/frontend/src/pages/Callback.tsx +0 -53
- package/dist/templates/frontend/src/pages/Dashboard.tsx +0 -24
- package/dist/templates/frontend/src/pages/Login.tsx +0 -42
- package/dist/templates/frontend/src/services/api.ts +0 -48
- package/dist/templates/frontend/tsconfig.json +0 -21
- 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
|
-
// ──
|
|
6
|
-
|
|
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
|
-
// ──
|
|
31
|
+
// ── Requirements (needs app) ──────────────────────────────────────────
|
|
22
32
|
gather_requirements: () => {
|
|
23
33
|
if (!getSession().context.appId) {
|
|
24
|
-
return blocked('You must create an app first
|
|
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
|
-
// ──
|
|
45
|
+
// ── Webhooks (needs scopes confirmed) ─────────────────────────────────
|
|
39
46
|
get_webhook_topics: () => {
|
|
40
|
-
|
|
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
|
|
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
|
-
// ──
|
|
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
|
-
|
|
73
|
+
validate_build: () => {
|
|
75
74
|
if (!hasCompleted('build_frontend')) {
|
|
76
|
-
return blocked('You must build the frontend first before
|
|
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('
|
|
82
|
-
return blocked('You must
|
|
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 (!
|
|
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
|
-
// ──
|
|
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
|
-
// ──
|
|
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.
|
|
114
|
-
return blocked('You must
|
|
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 {
|
|
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":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"tool-guard.js","sourceRoot":"","sources":["../../src/services/tool-guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
-
<!
|
|
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="/
|
|
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>
|
|
7
|
+
<title>frontend-template</title>
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<div id="root"></div>
|
|
11
|
-
<script type="module" src="/src/main.
|
|
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
|
+
}
|