@thegraphitelab/n8n-nodes-servicetitan 0.1.0 → 0.3.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/README.md CHANGED
@@ -12,13 +12,15 @@ More resources (Job, Invoice, Estimate, Technician, Booking, etc.) get added per
12
12
 
13
13
  ## Credentials
14
14
 
15
- Required fields:
15
+ Required UI fields (per-client, set when creating a credential in n8n):
16
16
  - **Environment** — `Integration (Sandbox)` or `Production`
17
17
  - **Tenant ID** — found in ServiceTitan → Settings → API Application Access
18
- - **App Key** — `ST-App-Key`. TGL-owned application identifier. Same across all TGL clients (controlled by TGL's ServiceTitan developer portal app).
19
18
  - **Client ID** — OAuth client ID, per-client
20
19
  - **Client Secret** — OAuth client secret, per-client
21
20
 
21
+ Required env var (set once per n8n instance, on **both** primary and worker services):
22
+ - **`TGL_SERVICETITAN_APP_KEY`** — the `ST-App-Key`. TGL-owned application identifier, same across all TGL clients. Sourced from the TGL ServiceTitan developer portal app. Read from the env at request time, not stored per-credential.
23
+
22
24
  The credential's "Test" button hits `/settings/v2/tenant/{tenantId}/user-roles?page=1&pageSize=1` to verify the full auth chain works.
23
25
 
24
26
  ## Auth flow
@@ -8,6 +8,7 @@ export declare class ServiceTitanApi implements ICredentialType {
8
8
  properties: INodeProperties[];
9
9
  preAuthentication(this: IHttpRequestHelper, credentials: ICredentialDataDecryptedObject): Promise<{
10
10
  accessToken: string;
11
+ appKey: string;
11
12
  }>;
12
13
  authenticate: IAuthenticateGeneric;
13
14
  test: ICredentialTestRequest;
@@ -42,15 +42,6 @@ class ServiceTitanApi {
42
42
  required: true,
43
43
  description: 'Found in ServiceTitan → Settings → API Application Access. Treated as a string in URL paths.',
44
44
  },
45
- {
46
- displayName: 'App Key',
47
- name: 'appKey',
48
- type: 'string',
49
- typeOptions: { password: true },
50
- default: '',
51
- required: true,
52
- description: 'The ST-App-Key. TGL-owned application identifier — the same value is used across all TGL clients. Get this from the TGL ServiceTitan developer portal application.',
53
- },
54
45
  {
55
46
  displayName: 'Client ID',
56
47
  name: 'clientId',
@@ -68,7 +59,8 @@ class ServiceTitanApi {
68
59
  required: true,
69
60
  description: "Per-client OAuth client secret from ServiceTitan → Settings → API Application Access.",
70
61
  },
71
- // accessToken is populated by preAuthentication and consumed by authenticate
62
+ // Both fields below are populated by preAuthentication, not user input.
63
+ // appKey is read from the TGL_SERVICETITAN_APP_KEY env var on the n8n service.
72
64
  {
73
65
  displayName: 'Access Token',
74
66
  name: 'accessToken',
@@ -76,25 +68,77 @@ class ServiceTitanApi {
76
68
  typeOptions: { password: true },
77
69
  default: '',
78
70
  },
71
+ {
72
+ displayName: 'App Key',
73
+ name: 'appKey',
74
+ type: 'hidden',
75
+ typeOptions: { password: true },
76
+ default: '',
77
+ },
79
78
  ];
80
79
  async preAuthentication(credentials) {
80
+ // The ST-App-Key is TGL-owned and identical across all clients, so it
81
+ // belongs at the n8n instance level, not on every credential.
82
+ // eslint-disable-next-line @n8n/community-nodes/no-restricted-globals
83
+ const appKey = process.env.TGL_SERVICETITAN_APP_KEY;
84
+ if (!appKey) {
85
+ throw new Error('[ServiceTitan auth] TGL_SERVICETITAN_APP_KEY env var is not set on this n8n service. Set it on BOTH the n8n primary and worker services, then redeploy.');
86
+ }
81
87
  const environment = credentials.environment ?? 'integration';
82
88
  const clientId = credentials.clientId;
83
89
  const clientSecret = credentials.clientSecret;
90
+ const tenantId = credentials.tenantId;
91
+ const missing = [];
92
+ if (!clientId)
93
+ missing.push('Client ID');
94
+ if (!clientSecret)
95
+ missing.push('Client Secret');
96
+ if (!tenantId)
97
+ missing.push('Tenant ID');
98
+ if (missing.length > 0) {
99
+ throw new Error(`[ServiceTitan auth] Missing required credential field(s): ${missing.join(', ')}. Fill them in on the n8n credential and save.`);
100
+ }
101
+ const authHost = getAuthHost(environment);
102
+ const tokenUrl = `https://${authHost}/connect/token`;
103
+ // Diagnostics — env, host, clientId prefix (NOT secret), App Key prefix
104
+ const clientIdPreview = `${clientId.slice(0, 6)}…${clientId.slice(-4)}`;
105
+ const appKeyPreview = `${appKey.slice(0, 6)}…${appKey.slice(-4)}`;
106
+ // eslint-disable-next-line no-console
107
+ console.log(`[ServiceTitan auth] env=${environment} tokenUrl=${tokenUrl} tenantId=${tenantId} clientId=${clientIdPreview} appKey=${appKeyPreview}`);
84
108
  const body = new URLSearchParams({
85
109
  grant_type: 'client_credentials',
86
110
  client_id: clientId,
87
111
  client_secret: clientSecret,
88
112
  }).toString();
89
- const response = (await this.helpers.httpRequest({
90
- method: 'POST',
91
- url: `https://${getAuthHost(environment)}/connect/token`,
92
- headers: {
93
- 'Content-Type': 'application/x-www-form-urlencoded',
94
- },
95
- body,
96
- }));
97
- return { accessToken: response.access_token };
113
+ let response;
114
+ try {
115
+ response = (await this.helpers.httpRequest({
116
+ method: 'POST',
117
+ url: tokenUrl,
118
+ headers: {
119
+ 'Content-Type': 'application/x-www-form-urlencoded',
120
+ },
121
+ body,
122
+ }));
123
+ }
124
+ catch (err) {
125
+ // Surface ServiceTitan's actual error response so users can fix it
126
+ const e = err;
127
+ const status = e.response?.status;
128
+ const statusText = e.response?.statusText;
129
+ const data = e.response?.data;
130
+ const bodyStr = typeof data === 'string' ? data : JSON.stringify(data);
131
+ const detail = bodyStr ? ` body=${bodyStr.slice(0, 500)}` : '';
132
+ throw new Error(`[ServiceTitan auth] Token fetch failed: ${status ?? '?'} ${statusText ?? ''}${detail}. ` +
133
+ `URL=${tokenUrl} env=${environment} clientId=${clientIdPreview}. ` +
134
+ `Verify Client ID/Secret are correct for the ${environment} environment.`);
135
+ }
136
+ if (!response?.access_token) {
137
+ throw new Error(`[ServiceTitan auth] Token endpoint returned 2xx but no access_token. Response: ${JSON.stringify(response).slice(0, 500)}`);
138
+ }
139
+ // eslint-disable-next-line no-console
140
+ console.log(`[ServiceTitan auth] Token fetch OK. expires_in=${response.expires_in}s`);
141
+ return { accessToken: response.access_token, appKey };
98
142
  }
99
143
  authenticate = {
100
144
  type: 'generic',
@@ -1 +1 @@
1
- {"version":3,"file":"ServiceTitanApi.credentials.js","sourceRoot":"","sources":["../../credentials/ServiceTitanApi.credentials.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ServiceTitanApi.credentials.js","sourceRoot":"","sources":["../../credentials/ServiceTitanApi.credentials.ts"],"names":[],"mappings":"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"}
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thegraphitelab/n8n-nodes-servicetitan",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "n8n custom node for ServiceTitan — TGL internal use.",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thegraphitelab/n8n-nodes-servicetitan",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "n8n custom node for ServiceTitan — TGL internal use.",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",