@trackunit/serverside-utils 0.0.2

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 ADDED
@@ -0,0 +1,20 @@
1
+ # Trackunit serverside-utils
2
+
3
+ The `@trackunit/serverside-utils` package provides utilities for building serverside extensions in the Iris App SDK.
4
+
5
+ This library is exposed publicly for use in the Trackunit [Iris App SDK](https://www.npmjs.com/package/@trackunit/iris-app).
6
+
7
+ For more info and a full guide on Iris App SDK Development, please visit our [Developer Hub](https://developers.trackunit.com/).
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ yarn add @trackunit/serverside-utils
13
+ ```
14
+
15
+ ## Trackunit
16
+
17
+ This package was developed by Trackunit ApS.
18
+ Trackunit is the leading SaaS-based IoT solution for the construction industry, offering an ecosystem of hardware, fleet management software & telematics.
19
+
20
+ ![The Trackunit logo](https://trackunit.com/wp-content/uploads/2022/03/top-logo.svg)
package/index.cjs.js ADDED
@@ -0,0 +1,251 @@
1
+ 'use strict';
2
+
3
+ var jwtDecode = require('jwt-decode');
4
+ var zod = require('zod');
5
+
6
+ /**
7
+ * Header key for the account admin iris app token.
8
+ * Used by the SDK server to pass admin tokens to serverside functions.
9
+ *
10
+ * Uses vendor prefix "tu-" (Trackunit) per RFC 6648 which deprecated the "X-" convention.
11
+ */
12
+ const ACCOUNT_ADMIN_IRIS_APP_TOKEN_HEADER = "tu-account-admin-iris-app-token";
13
+ /**
14
+ * Header key for the forwarded authorization token.
15
+ * The original user authorization token is moved to this header.
16
+ *
17
+ * Uses vendor prefix "tu-" (Trackunit) per RFC 6648 which deprecated the "X-" convention.
18
+ */
19
+ const FORWARDED_AUTHORIZATION_HEADER = "tu-forwarded-authorization";
20
+
21
+ /**
22
+ * Removes the "Bearer " prefix from a token string if present.
23
+ *
24
+ * @param token - The token string, possibly prefixed with "Bearer "
25
+ * @returns {string} The token without the "Bearer " prefix
26
+ */
27
+ const stripBearerPrefix = (token) => {
28
+ return token.replace(/^(?:Bearer\s+)+/i, "").trim();
29
+ };
30
+
31
+ /**
32
+ * Retrieves the user token from the request headers.
33
+ *
34
+ * @param options - The options object
35
+ * @param options.context - Hono request context
36
+ * @returns {string | undefined} The user token (without "Bearer " prefix), or undefined if not found
37
+ * @example
38
+ * ```typescript
39
+ * import { getUserToken } from "@trackunit/serverside-utils";
40
+ *
41
+ * app.get("/data", async (c) => {
42
+ * const token = getUserToken({ context: c });
43
+ * if (!token) {
44
+ * return c.json({ error: "Unauthorized" }, 401);
45
+ * }
46
+ * // Use token for API calls...
47
+ * });
48
+ * ```
49
+ */
50
+ const getUserToken = ({ context }) => {
51
+ const forwardedAuth = context.req.header(FORWARDED_AUTHORIZATION_HEADER);
52
+ if (forwardedAuth) {
53
+ return stripBearerPrefix(forwardedAuth);
54
+ }
55
+ // Fall back to standard Authorization header
56
+ const authorization = context.req.header("authorization");
57
+ if (authorization) {
58
+ return stripBearerPrefix(authorization);
59
+ }
60
+ return undefined;
61
+ };
62
+
63
+ const JwtAccountPayloadSchema = zod.z.object({
64
+ accountId: zod.z.string().optional(),
65
+ assumedAccountId: zod.z.string().optional(),
66
+ });
67
+ /**
68
+ * Extracts the account ID from the user token in the request.
69
+ *
70
+ * Trackunit tokens may contain both `accountId` (token owner) and `assumedAccountId`
71
+ * (account being acted upon). This function returns the effective account ID,
72
+ * preferring `assumedAccountId` when present.
73
+ *
74
+ * @param options - The options object
75
+ * @param options.context - Hono request context
76
+ * @returns {AccountIdResult} The effective account ID and the raw assumedAccountId (if present)
77
+ * @throws Error if no user token is found or the token has no account ID
78
+ * @example
79
+ * ```typescript
80
+ * // Simple usage — just get the effective account ID:
81
+ * const { accountId } = getAccountId({ context: c });
82
+ *
83
+ * // When you need to know if the user is assuming an account:
84
+ * const { accountId, assumedAccountId } = getAccountId({ context: c });
85
+ * ```
86
+ */
87
+ const getAccountId = ({ context }) => {
88
+ const userToken = getUserToken({ context });
89
+ if (!userToken) {
90
+ throw new Error("Missing user token");
91
+ }
92
+ try {
93
+ const tokenWithoutBearer = stripBearerPrefix(userToken);
94
+ const payload = jwtDecode(tokenWithoutBearer);
95
+ const parseResult = JwtAccountPayloadSchema.safeParse(payload);
96
+ if (!parseResult.success) {
97
+ throw new Error("JWT payload missing accountId");
98
+ }
99
+ const { accountId, assumedAccountId } = parseResult.data;
100
+ const effectiveAccountId = assumedAccountId ?? accountId;
101
+ if (!effectiveAccountId) {
102
+ throw new Error("JWT payload missing accountId");
103
+ }
104
+ return {
105
+ accountId: effectiveAccountId,
106
+ assumedAccountId,
107
+ tokenAccountId: accountId,
108
+ };
109
+ }
110
+ catch (error) {
111
+ if (error instanceof Error && error.message === "JWT payload missing accountId") {
112
+ throw error;
113
+ }
114
+ const message = error instanceof Error ? error.message : "Unknown error";
115
+ throw new Error(`Failed to extract accountId from user token: ${message}`);
116
+ }
117
+ };
118
+
119
+ const SecretsResponseSchema = zod.z.object({
120
+ secrets: zod.z.record(zod.z.string(), zod.z.string()),
121
+ });
122
+ /**
123
+ * Fetches a secret from the SDK Server.
124
+ *
125
+ * By default fetches **app-level** secrets (`GET /secrets`).
126
+ * Pass `accountScoped: true` to fetch **account-scoped** secrets (`GET /secrets/:accountId`).
127
+ *
128
+ * For local development, set the secret as an environment variable with the same name
129
+ * as the secretKey (e.g., `API_KEY=xxx` in your `.env` file).
130
+ *
131
+ * @param options - The options object
132
+ * @param options.context - Hono request context
133
+ * @param options.secretKey - Key name of the secret
134
+ * @param options.accountScoped - When true, fetches account-scoped secrets (requires user token with accountId)
135
+ * @returns {Promise<string>} The secret value
136
+ * @throws Error if required headers or env vars are missing, or secret not found
137
+ * @example
138
+ * ```typescript
139
+ * // App-level secret (default)
140
+ * const apiKey = await getSecret({ context: c, secretKey: "API_KEY" });
141
+ *
142
+ * // Account-scoped secret
143
+ * const accountKey = await getSecret({ context: c, secretKey: "API_KEY", accountScoped: true });
144
+ * ```
145
+ */
146
+ const getSecret = async ({ context, secretKey, accountScoped = false, }) => {
147
+ // Local development: use env var with same name as secretKey
148
+ try {
149
+ const envValue = process.env[secretKey];
150
+ if (envValue) {
151
+ return envValue;
152
+ }
153
+ }
154
+ catch {
155
+ // Ignore error when getting secret from environment variable. Deno blocks access to process.env in serverside functions.
156
+ }
157
+ const adminToken = context.req.header(ACCOUNT_ADMIN_IRIS_APP_TOKEN_HEADER);
158
+ if (!adminToken) {
159
+ throw new Error(`Missing required header: ${ACCOUNT_ADMIN_IRIS_APP_TOKEN_HEADER}`);
160
+ }
161
+ const sdkServerUrl = process.env.SDK_SERVER_URL;
162
+ if (!sdkServerUrl) {
163
+ throw new Error("Missing required environment variable: SDK_SERVER_URL");
164
+ }
165
+ const secretsPath = accountScoped ? `/secrets/${getAccountId({ context }).accountId}` : "/secrets";
166
+ const secretsUrl = new URL(secretsPath, sdkServerUrl).href;
167
+ const res = await fetch(secretsUrl, {
168
+ headers: { Authorization: adminToken },
169
+ });
170
+ if (!res.ok) {
171
+ let errorBody = "";
172
+ try {
173
+ const text = await res.text();
174
+ errorBody = text.length > 200 ? `${text.slice(0, 200)}...` : text;
175
+ }
176
+ catch {
177
+ // Ignore body read errors
178
+ }
179
+ throw new Error(`Failed to fetch secret "${secretKey}" from ${secretsUrl}: ${res.status} ${res.statusText}${errorBody ? ` - ${errorBody}` : ""}`);
180
+ }
181
+ const responseData = await res.json();
182
+ const parseResult = SecretsResponseSchema.safeParse(responseData);
183
+ if (!parseResult.success) {
184
+ throw new Error(`Invalid secrets response from ${secretsUrl}: ${parseResult.error.message}`);
185
+ }
186
+ const secret = parseResult.data.secrets[secretKey];
187
+ if (!secret) {
188
+ throw new Error(`Secret "${secretKey}" not found in response from ${secretsUrl}. Available keys: ${Object.keys(parseResult.data.secrets).join(", ") || "(none)"}`);
189
+ }
190
+ return secret;
191
+ };
192
+
193
+ const DatabricksTokenResponseSchema = zod.z.object({
194
+ access_token: zod.z.string(),
195
+ expires_in: zod.z.number().optional(),
196
+ });
197
+ const buildDatabricksUrl = (host, path) => {
198
+ const trimmed = host.trim();
199
+ const base = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
200
+ return new URL(path, base).toString();
201
+ };
202
+ /**
203
+ * Obtains a Databricks OAuth access token using the client credentials flow.
204
+ *
205
+ * @example
206
+ *
207
+ * const { databricksAccessToken } = await getDatabricksAccessToken({ context: c });
208
+ * @param props - The properties of the request.
209
+ * @param props.context - The context of the request.
210
+ * @param props.databricksHost - The host of the Databricks instance.
211
+ * @param props.databricksClientId - The client ID of the Databricks application.
212
+ * @param props.databricksClientSecret - The client secret of the Databricks application.
213
+ * @param props.accountScoped - When true, fetches account-scoped secrets (requires user token with accountId)
214
+ * @param props.scope - The scope of the request to the Databricks token endpoint. Defaults to `all-apis`.@param options.accountScoped - When true, fetches account-scoped secrets (requires user token with accountId)
215
+ * @returns {Promise<{ databricksAccessToken: string; databricksTokenExpiresIn?: number }>} The access token and optional expiry in seconds.
216
+ */
217
+ const getDatabricksAccessToken = async ({ context, databricksHost, databricksClientId, databricksClientSecret, accountScoped = false, scope = "all-apis", }) => {
218
+ const clientId = databricksClientId ?? (await getSecret({ context, secretKey: "DATABRICKS_CLIENT_ID", accountScoped }));
219
+ const clientSecret = databricksClientSecret ?? (await getSecret({ context, secretKey: "DATABRICKS_CLIENT_SECRET", accountScoped }));
220
+ const rawHost = databricksHost ?? (await getSecret({ context, secretKey: "DATABRICKS_HOST", accountScoped }));
221
+ const tokenUrl = buildDatabricksUrl(rawHost, "/oidc/v1/token");
222
+ const credentials = btoa(`${clientId}:${clientSecret}`);
223
+ const tokenResponse = await fetch(tokenUrl, {
224
+ method: "POST",
225
+ headers: {
226
+ Authorization: `Basic ${credentials}`,
227
+ "Content-Type": "application/x-www-form-urlencoded",
228
+ },
229
+ body: `grant_type=client_credentials&scope=${scope}`,
230
+ });
231
+ if (!tokenResponse.ok) {
232
+ const errorBody = await tokenResponse.text();
233
+ throw new Error(`Failed to obtain Databricks access token: ${tokenResponse.status} ${tokenResponse.statusText} - ${errorBody}`);
234
+ }
235
+ const responseData = await tokenResponse.json();
236
+ const parseResult = DatabricksTokenResponseSchema.safeParse(responseData);
237
+ if (!parseResult.success) {
238
+ throw new Error(`Invalid token response: ${parseResult.error.message}`);
239
+ }
240
+ return {
241
+ databricksAccessToken: parseResult.data.access_token,
242
+ databricksTokenExpiresIn: parseResult.data.expires_in,
243
+ };
244
+ };
245
+
246
+ exports.ACCOUNT_ADMIN_IRIS_APP_TOKEN_HEADER = ACCOUNT_ADMIN_IRIS_APP_TOKEN_HEADER;
247
+ exports.FORWARDED_AUTHORIZATION_HEADER = FORWARDED_AUTHORIZATION_HEADER;
248
+ exports.getAccountId = getAccountId;
249
+ exports.getDatabricksAccessToken = getDatabricksAccessToken;
250
+ exports.getSecret = getSecret;
251
+ exports.getUserToken = getUserToken;
package/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/index";
package/index.esm.js ADDED
@@ -0,0 +1,244 @@
1
+ import jwtDecode from 'jwt-decode';
2
+ import { z } from 'zod';
3
+
4
+ /**
5
+ * Header key for the account admin iris app token.
6
+ * Used by the SDK server to pass admin tokens to serverside functions.
7
+ *
8
+ * Uses vendor prefix "tu-" (Trackunit) per RFC 6648 which deprecated the "X-" convention.
9
+ */
10
+ const ACCOUNT_ADMIN_IRIS_APP_TOKEN_HEADER = "tu-account-admin-iris-app-token";
11
+ /**
12
+ * Header key for the forwarded authorization token.
13
+ * The original user authorization token is moved to this header.
14
+ *
15
+ * Uses vendor prefix "tu-" (Trackunit) per RFC 6648 which deprecated the "X-" convention.
16
+ */
17
+ const FORWARDED_AUTHORIZATION_HEADER = "tu-forwarded-authorization";
18
+
19
+ /**
20
+ * Removes the "Bearer " prefix from a token string if present.
21
+ *
22
+ * @param token - The token string, possibly prefixed with "Bearer "
23
+ * @returns {string} The token without the "Bearer " prefix
24
+ */
25
+ const stripBearerPrefix = (token) => {
26
+ return token.replace(/^(?:Bearer\s+)+/i, "").trim();
27
+ };
28
+
29
+ /**
30
+ * Retrieves the user token from the request headers.
31
+ *
32
+ * @param options - The options object
33
+ * @param options.context - Hono request context
34
+ * @returns {string | undefined} The user token (without "Bearer " prefix), or undefined if not found
35
+ * @example
36
+ * ```typescript
37
+ * import { getUserToken } from "@trackunit/serverside-utils";
38
+ *
39
+ * app.get("/data", async (c) => {
40
+ * const token = getUserToken({ context: c });
41
+ * if (!token) {
42
+ * return c.json({ error: "Unauthorized" }, 401);
43
+ * }
44
+ * // Use token for API calls...
45
+ * });
46
+ * ```
47
+ */
48
+ const getUserToken = ({ context }) => {
49
+ const forwardedAuth = context.req.header(FORWARDED_AUTHORIZATION_HEADER);
50
+ if (forwardedAuth) {
51
+ return stripBearerPrefix(forwardedAuth);
52
+ }
53
+ // Fall back to standard Authorization header
54
+ const authorization = context.req.header("authorization");
55
+ if (authorization) {
56
+ return stripBearerPrefix(authorization);
57
+ }
58
+ return undefined;
59
+ };
60
+
61
+ const JwtAccountPayloadSchema = z.object({
62
+ accountId: z.string().optional(),
63
+ assumedAccountId: z.string().optional(),
64
+ });
65
+ /**
66
+ * Extracts the account ID from the user token in the request.
67
+ *
68
+ * Trackunit tokens may contain both `accountId` (token owner) and `assumedAccountId`
69
+ * (account being acted upon). This function returns the effective account ID,
70
+ * preferring `assumedAccountId` when present.
71
+ *
72
+ * @param options - The options object
73
+ * @param options.context - Hono request context
74
+ * @returns {AccountIdResult} The effective account ID and the raw assumedAccountId (if present)
75
+ * @throws Error if no user token is found or the token has no account ID
76
+ * @example
77
+ * ```typescript
78
+ * // Simple usage — just get the effective account ID:
79
+ * const { accountId } = getAccountId({ context: c });
80
+ *
81
+ * // When you need to know if the user is assuming an account:
82
+ * const { accountId, assumedAccountId } = getAccountId({ context: c });
83
+ * ```
84
+ */
85
+ const getAccountId = ({ context }) => {
86
+ const userToken = getUserToken({ context });
87
+ if (!userToken) {
88
+ throw new Error("Missing user token");
89
+ }
90
+ try {
91
+ const tokenWithoutBearer = stripBearerPrefix(userToken);
92
+ const payload = jwtDecode(tokenWithoutBearer);
93
+ const parseResult = JwtAccountPayloadSchema.safeParse(payload);
94
+ if (!parseResult.success) {
95
+ throw new Error("JWT payload missing accountId");
96
+ }
97
+ const { accountId, assumedAccountId } = parseResult.data;
98
+ const effectiveAccountId = assumedAccountId ?? accountId;
99
+ if (!effectiveAccountId) {
100
+ throw new Error("JWT payload missing accountId");
101
+ }
102
+ return {
103
+ accountId: effectiveAccountId,
104
+ assumedAccountId,
105
+ tokenAccountId: accountId,
106
+ };
107
+ }
108
+ catch (error) {
109
+ if (error instanceof Error && error.message === "JWT payload missing accountId") {
110
+ throw error;
111
+ }
112
+ const message = error instanceof Error ? error.message : "Unknown error";
113
+ throw new Error(`Failed to extract accountId from user token: ${message}`);
114
+ }
115
+ };
116
+
117
+ const SecretsResponseSchema = z.object({
118
+ secrets: z.record(z.string(), z.string()),
119
+ });
120
+ /**
121
+ * Fetches a secret from the SDK Server.
122
+ *
123
+ * By default fetches **app-level** secrets (`GET /secrets`).
124
+ * Pass `accountScoped: true` to fetch **account-scoped** secrets (`GET /secrets/:accountId`).
125
+ *
126
+ * For local development, set the secret as an environment variable with the same name
127
+ * as the secretKey (e.g., `API_KEY=xxx` in your `.env` file).
128
+ *
129
+ * @param options - The options object
130
+ * @param options.context - Hono request context
131
+ * @param options.secretKey - Key name of the secret
132
+ * @param options.accountScoped - When true, fetches account-scoped secrets (requires user token with accountId)
133
+ * @returns {Promise<string>} The secret value
134
+ * @throws Error if required headers or env vars are missing, or secret not found
135
+ * @example
136
+ * ```typescript
137
+ * // App-level secret (default)
138
+ * const apiKey = await getSecret({ context: c, secretKey: "API_KEY" });
139
+ *
140
+ * // Account-scoped secret
141
+ * const accountKey = await getSecret({ context: c, secretKey: "API_KEY", accountScoped: true });
142
+ * ```
143
+ */
144
+ const getSecret = async ({ context, secretKey, accountScoped = false, }) => {
145
+ // Local development: use env var with same name as secretKey
146
+ try {
147
+ const envValue = process.env[secretKey];
148
+ if (envValue) {
149
+ return envValue;
150
+ }
151
+ }
152
+ catch {
153
+ // Ignore error when getting secret from environment variable. Deno blocks access to process.env in serverside functions.
154
+ }
155
+ const adminToken = context.req.header(ACCOUNT_ADMIN_IRIS_APP_TOKEN_HEADER);
156
+ if (!adminToken) {
157
+ throw new Error(`Missing required header: ${ACCOUNT_ADMIN_IRIS_APP_TOKEN_HEADER}`);
158
+ }
159
+ const sdkServerUrl = process.env.SDK_SERVER_URL;
160
+ if (!sdkServerUrl) {
161
+ throw new Error("Missing required environment variable: SDK_SERVER_URL");
162
+ }
163
+ const secretsPath = accountScoped ? `/secrets/${getAccountId({ context }).accountId}` : "/secrets";
164
+ const secretsUrl = new URL(secretsPath, sdkServerUrl).href;
165
+ const res = await fetch(secretsUrl, {
166
+ headers: { Authorization: adminToken },
167
+ });
168
+ if (!res.ok) {
169
+ let errorBody = "";
170
+ try {
171
+ const text = await res.text();
172
+ errorBody = text.length > 200 ? `${text.slice(0, 200)}...` : text;
173
+ }
174
+ catch {
175
+ // Ignore body read errors
176
+ }
177
+ throw new Error(`Failed to fetch secret "${secretKey}" from ${secretsUrl}: ${res.status} ${res.statusText}${errorBody ? ` - ${errorBody}` : ""}`);
178
+ }
179
+ const responseData = await res.json();
180
+ const parseResult = SecretsResponseSchema.safeParse(responseData);
181
+ if (!parseResult.success) {
182
+ throw new Error(`Invalid secrets response from ${secretsUrl}: ${parseResult.error.message}`);
183
+ }
184
+ const secret = parseResult.data.secrets[secretKey];
185
+ if (!secret) {
186
+ throw new Error(`Secret "${secretKey}" not found in response from ${secretsUrl}. Available keys: ${Object.keys(parseResult.data.secrets).join(", ") || "(none)"}`);
187
+ }
188
+ return secret;
189
+ };
190
+
191
+ const DatabricksTokenResponseSchema = z.object({
192
+ access_token: z.string(),
193
+ expires_in: z.number().optional(),
194
+ });
195
+ const buildDatabricksUrl = (host, path) => {
196
+ const trimmed = host.trim();
197
+ const base = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
198
+ return new URL(path, base).toString();
199
+ };
200
+ /**
201
+ * Obtains a Databricks OAuth access token using the client credentials flow.
202
+ *
203
+ * @example
204
+ *
205
+ * const { databricksAccessToken } = await getDatabricksAccessToken({ context: c });
206
+ * @param props - The properties of the request.
207
+ * @param props.context - The context of the request.
208
+ * @param props.databricksHost - The host of the Databricks instance.
209
+ * @param props.databricksClientId - The client ID of the Databricks application.
210
+ * @param props.databricksClientSecret - The client secret of the Databricks application.
211
+ * @param props.accountScoped - When true, fetches account-scoped secrets (requires user token with accountId)
212
+ * @param props.scope - The scope of the request to the Databricks token endpoint. Defaults to `all-apis`.@param options.accountScoped - When true, fetches account-scoped secrets (requires user token with accountId)
213
+ * @returns {Promise<{ databricksAccessToken: string; databricksTokenExpiresIn?: number }>} The access token and optional expiry in seconds.
214
+ */
215
+ const getDatabricksAccessToken = async ({ context, databricksHost, databricksClientId, databricksClientSecret, accountScoped = false, scope = "all-apis", }) => {
216
+ const clientId = databricksClientId ?? (await getSecret({ context, secretKey: "DATABRICKS_CLIENT_ID", accountScoped }));
217
+ const clientSecret = databricksClientSecret ?? (await getSecret({ context, secretKey: "DATABRICKS_CLIENT_SECRET", accountScoped }));
218
+ const rawHost = databricksHost ?? (await getSecret({ context, secretKey: "DATABRICKS_HOST", accountScoped }));
219
+ const tokenUrl = buildDatabricksUrl(rawHost, "/oidc/v1/token");
220
+ const credentials = btoa(`${clientId}:${clientSecret}`);
221
+ const tokenResponse = await fetch(tokenUrl, {
222
+ method: "POST",
223
+ headers: {
224
+ Authorization: `Basic ${credentials}`,
225
+ "Content-Type": "application/x-www-form-urlencoded",
226
+ },
227
+ body: `grant_type=client_credentials&scope=${scope}`,
228
+ });
229
+ if (!tokenResponse.ok) {
230
+ const errorBody = await tokenResponse.text();
231
+ throw new Error(`Failed to obtain Databricks access token: ${tokenResponse.status} ${tokenResponse.statusText} - ${errorBody}`);
232
+ }
233
+ const responseData = await tokenResponse.json();
234
+ const parseResult = DatabricksTokenResponseSchema.safeParse(responseData);
235
+ if (!parseResult.success) {
236
+ throw new Error(`Invalid token response: ${parseResult.error.message}`);
237
+ }
238
+ return {
239
+ databricksAccessToken: parseResult.data.access_token,
240
+ databricksTokenExpiresIn: parseResult.data.expires_in,
241
+ };
242
+ };
243
+
244
+ export { ACCOUNT_ADMIN_IRIS_APP_TOKEN_HEADER, FORWARDED_AUTHORIZATION_HEADER, getAccountId, getDatabricksAccessToken, getSecret, getUserToken };
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@trackunit/serverside-utils",
3
+ "version": "0.0.2",
4
+ "repository": "https://github.com/Trackunit/manager",
5
+ "license": "SEE LICENSE IN LICENSE.txt",
6
+ "engines": {
7
+ "node": ">=24.x"
8
+ },
9
+ "dependencies": {
10
+ "hono": "^4.12.5",
11
+ "jwt-decode": "^3.1.2",
12
+ "zod": "^3.23.8"
13
+ },
14
+ "peerDependencies": {
15
+ "hono": "^4.12.5",
16
+ "jwt-decode": "^3.1.2",
17
+ "zod": "^3.23.8"
18
+ },
19
+ "module": "./index.esm.js",
20
+ "main": "./index.cjs.js",
21
+ "types": "./index.d.ts"
22
+ }
@@ -0,0 +1,35 @@
1
+ import { Context } from "hono";
2
+ type GetAccountIdOptions = {
3
+ /** Hono request context */
4
+ context: Context;
5
+ };
6
+ type AccountIdResult = {
7
+ /** The effective account ID to use (prefers assumedAccountId over accountId) */
8
+ accountId: string;
9
+ /** The assumed account ID from the token, present when acting on behalf of another account */
10
+ assumedAccountId: string | undefined;
11
+ /** The original account ID from the token (the token owner's account) */
12
+ tokenAccountId: string | undefined;
13
+ };
14
+ /**
15
+ * Extracts the account ID from the user token in the request.
16
+ *
17
+ * Trackunit tokens may contain both `accountId` (token owner) and `assumedAccountId`
18
+ * (account being acted upon). This function returns the effective account ID,
19
+ * preferring `assumedAccountId` when present.
20
+ *
21
+ * @param options - The options object
22
+ * @param options.context - Hono request context
23
+ * @returns {AccountIdResult} The effective account ID and the raw assumedAccountId (if present)
24
+ * @throws Error if no user token is found or the token has no account ID
25
+ * @example
26
+ * ```typescript
27
+ * // Simple usage — just get the effective account ID:
28
+ * const { accountId } = getAccountId({ context: c });
29
+ *
30
+ * // When you need to know if the user is assuming an account:
31
+ * const { accountId, assumedAccountId } = getAccountId({ context: c });
32
+ * ```
33
+ */
34
+ export declare const getAccountId: ({ context }: GetAccountIdOptions) => AccountIdResult;
35
+ export {};
@@ -0,0 +1,42 @@
1
+ import { Context } from "hono";
2
+ /**
3
+ * The properties of the request to the Databricks token endpoint.
4
+ */
5
+ export type GetDatabricksAccessTokenProps = {
6
+ /** The context of the request. */
7
+ context: Context;
8
+ /** The host of the Databricks instance. */
9
+ databricksHost?: string;
10
+ /** The client ID of the Databricks application. */
11
+ databricksClientId?: string;
12
+ /** The client secret of the Databricks application. */
13
+ databricksClientSecret?: string;
14
+ /** When true, fetches account-scoped secrets instead of app-level secrets. Requires a valid user token with an accountId. */
15
+ accountScoped?: boolean;
16
+ /**
17
+ * The scope of the request to the Databricks token endpoint.
18
+ * Defaults to all-apis.
19
+ *
20
+ * @see {@link https://docs.databricks.com/aws/en/dev-tools/auth/oauth-u2m#generate-an-account-level-access-token Generate an account-level access token docs}
21
+ */
22
+ scope?: string;
23
+ };
24
+ /**
25
+ * Obtains a Databricks OAuth access token using the client credentials flow.
26
+ *
27
+ * @example
28
+ *
29
+ * const { databricksAccessToken } = await getDatabricksAccessToken({ context: c });
30
+ * @param props - The properties of the request.
31
+ * @param props.context - The context of the request.
32
+ * @param props.databricksHost - The host of the Databricks instance.
33
+ * @param props.databricksClientId - The client ID of the Databricks application.
34
+ * @param props.databricksClientSecret - The client secret of the Databricks application.
35
+ * @param props.accountScoped - When true, fetches account-scoped secrets (requires user token with accountId)
36
+ * @param props.scope - The scope of the request to the Databricks token endpoint. Defaults to `all-apis`.@param options.accountScoped - When true, fetches account-scoped secrets (requires user token with accountId)
37
+ * @returns {Promise<{ databricksAccessToken: string; databricksTokenExpiresIn?: number }>} The access token and optional expiry in seconds.
38
+ */
39
+ export declare const getDatabricksAccessToken: ({ context, databricksHost, databricksClientId, databricksClientSecret, accountScoped, scope, }: GetDatabricksAccessTokenProps) => Promise<{
40
+ databricksAccessToken: string;
41
+ databricksTokenExpiresIn?: number;
42
+ }>;
@@ -0,0 +1,35 @@
1
+ import { Context } from "hono";
2
+ type GetSecretOptions<TSecretKey extends string> = {
3
+ /** Hono request context */
4
+ context: Context;
5
+ /** Key name of the secret (also used as env var name for local dev) */
6
+ secretKey: TSecretKey;
7
+ /** When true, fetches account-scoped secrets instead of app-level secrets. Requires a valid user token with an accountId. */
8
+ accountScoped?: boolean;
9
+ };
10
+ /**
11
+ * Fetches a secret from the SDK Server.
12
+ *
13
+ * By default fetches **app-level** secrets (`GET /secrets`).
14
+ * Pass `accountScoped: true` to fetch **account-scoped** secrets (`GET /secrets/:accountId`).
15
+ *
16
+ * For local development, set the secret as an environment variable with the same name
17
+ * as the secretKey (e.g., `API_KEY=xxx` in your `.env` file).
18
+ *
19
+ * @param options - The options object
20
+ * @param options.context - Hono request context
21
+ * @param options.secretKey - Key name of the secret
22
+ * @param options.accountScoped - When true, fetches account-scoped secrets (requires user token with accountId)
23
+ * @returns {Promise<string>} The secret value
24
+ * @throws Error if required headers or env vars are missing, or secret not found
25
+ * @example
26
+ * ```typescript
27
+ * // App-level secret (default)
28
+ * const apiKey = await getSecret({ context: c, secretKey: "API_KEY" });
29
+ *
30
+ * // Account-scoped secret
31
+ * const accountKey = await getSecret({ context: c, secretKey: "API_KEY", accountScoped: true });
32
+ * ```
33
+ */
34
+ export declare const getSecret: <TSecretKey extends string>({ context, secretKey, accountScoped, }: GetSecretOptions<TSecretKey>) => Promise<string>;
35
+ export {};
@@ -0,0 +1,26 @@
1
+ import { Context } from "hono";
2
+ type GetUserTokenOptions = {
3
+ /** Hono request context */
4
+ context: Context;
5
+ };
6
+ /**
7
+ * Retrieves the user token from the request headers.
8
+ *
9
+ * @param options - The options object
10
+ * @param options.context - Hono request context
11
+ * @returns {string | undefined} The user token (without "Bearer " prefix), or undefined if not found
12
+ * @example
13
+ * ```typescript
14
+ * import { getUserToken } from "@trackunit/serverside-utils";
15
+ *
16
+ * app.get("/data", async (c) => {
17
+ * const token = getUserToken({ context: c });
18
+ * if (!token) {
19
+ * return c.json({ error: "Unauthorized" }, 401);
20
+ * }
21
+ * // Use token for API calls...
22
+ * });
23
+ * ```
24
+ */
25
+ export declare const getUserToken: ({ context }: GetUserTokenOptions) => string | undefined;
26
+ export {};
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Header key for the account admin iris app token.
3
+ * Used by the SDK server to pass admin tokens to serverside functions.
4
+ *
5
+ * Uses vendor prefix "tu-" (Trackunit) per RFC 6648 which deprecated the "X-" convention.
6
+ */
7
+ export declare const ACCOUNT_ADMIN_IRIS_APP_TOKEN_HEADER = "tu-account-admin-iris-app-token";
8
+ /**
9
+ * Header key for the forwarded authorization token.
10
+ * The original user authorization token is moved to this header.
11
+ *
12
+ * Uses vendor prefix "tu-" (Trackunit) per RFC 6648 which deprecated the "X-" convention.
13
+ */
14
+ export declare const FORWARDED_AUTHORIZATION_HEADER = "tu-forwarded-authorization";
package/src/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { getAccountId } from "./get-account-id";
2
+ export { getDatabricksAccessToken, type GetDatabricksAccessTokenProps } from "./get-databricks-access-token";
3
+ export { getSecret } from "./get-secret";
4
+ export { getUserToken } from "./get-user-token";
5
+ export { ACCOUNT_ADMIN_IRIS_APP_TOKEN_HEADER, FORWARDED_AUTHORIZATION_HEADER } from "./headers";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Removes the "Bearer " prefix from a token string if present.
3
+ *
4
+ * @param token - The token string, possibly prefixed with "Bearer "
5
+ * @returns {string} The token without the "Bearer " prefix
6
+ */
7
+ export declare const stripBearerPrefix: (token: string) => string;