@keboola/api-client 52.0.0 → 54.0.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/AGENTS.md +33 -6
- package/API-SURFACE.md +5 -1
- package/CHANGELOG.md +39 -0
- package/README.md +1 -1
- package/dist/{admin-Di-MmGFP.cjs → admin-BZ7knTd0.cjs} +18 -2
- package/dist/admin-BZ7knTd0.cjs.map +1 -0
- package/dist/{admin-CbMOsfgs.js → admin-xFfHSZHQ.js} +13 -3
- package/dist/admin-xFfHSZHQ.js.map +1 -0
- package/dist/{componentId-DZP1rBix.js → componentId-Cps6WBy1.js} +1 -2
- package/dist/{componentId-DZP1rBix.js.map → componentId-Cps6WBy1.js.map} +1 -1
- package/dist/{componentId-olp0tltf.cjs → componentId-_tRhEv6W.cjs} +1 -2
- package/dist/{componentId-olp0tltf.cjs.map → componentId-_tRhEv6W.cjs.map} +1 -1
- package/dist/{configurations-WI8H4zLk.cjs → configurations-BM63ep1s.cjs} +2 -2
- package/dist/{configurations-WI8H4zLk.cjs.map → configurations-BM63ep1s.cjs.map} +1 -1
- package/dist/{configurations-Cd6Nsox-.js → configurations-BqvtIQm4.js} +2 -2
- package/dist/{configurations-Cd6Nsox-.js.map → configurations-BqvtIQm4.js.map} +1 -1
- package/dist/constants/index.cjs +4 -3
- package/dist/constants/index.d.cts +1 -1
- package/dist/constants/index.d.ts +1 -1
- package/dist/constants/index.js +3 -2
- package/dist/{constants-Dqi0ewGv.js → constants-DLPkpnGv.js} +4 -32
- package/dist/{constants-Dqi0ewGv.js.map → constants-DLPkpnGv.js.map} +1 -1
- package/dist/{constants-d0Do9NSN.cjs → constants-Day8CU5i.cjs} +3 -37
- package/dist/{constants-d0Do9NSN.cjs.map → constants-Day8CU5i.cjs.map} +1 -1
- package/dist/domain/permissions/index.cjs +4 -3
- package/dist/domain/permissions/index.d.cts +9 -1
- package/dist/domain/permissions/index.d.ts +9 -1
- package/dist/domain/permissions/index.js +4 -4
- package/dist/encryption/index.d.cts +1 -1
- package/dist/encryption/index.d.ts +1 -1
- package/dist/encryption/types.d.cts +1 -1
- package/dist/encryption/types.d.ts +1 -1
- package/dist/{index-Df8lZNSH.d.cts → index-B04rWq_0.d.cts} +2 -2
- package/dist/{index-B7el5pJU.d.cts → index-BtvWJBYh.d.cts} +20 -7
- package/dist/{index-B5rELADq.d.cts → index-Cd0naVic.d.cts} +2 -2
- package/dist/{index-CveF5RFQ.d.ts → index-DCfumWzq.d.ts} +2 -2
- package/dist/{index-81wn1jc3.d.cts → index-DX7qciZA.d.cts} +36 -2
- package/dist/{index-Dhyrg5zd.d.ts → index-DbStyQKc.d.ts} +2 -2
- package/dist/{index-C7JcNvbd.d.ts → index-Df30jjpZ.d.ts} +20 -7
- package/dist/{index-JoKfPQND.d.ts → index-DzqHh0lE.d.ts} +36 -2
- package/dist/index.cjs +9 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -7
- package/dist/management/index.cjs +1 -1
- package/dist/management/index.d.cts +1 -1
- package/dist/management/index.d.ts +1 -1
- package/dist/management/index.js +1 -1
- package/dist/management/types.d.cts +2 -2
- package/dist/management/types.d.ts +2 -2
- package/dist/{management-Cz5ut18k.js → management-BAfg8BmK.js} +45 -4
- package/dist/management-BAfg8BmK.js.map +1 -0
- package/dist/{management-CUXgB2rb.cjs → management-BuNIc7Mr.cjs} +45 -4
- package/dist/management-BuNIc7Mr.cjs.map +1 -0
- package/dist/metaObject-CSGF3d-o.js +53 -0
- package/dist/metaObject-CSGF3d-o.js.map +1 -0
- package/dist/metaObject-Ciyow2gr.cjs +64 -0
- package/dist/metaObject-Ciyow2gr.cjs.map +1 -0
- package/dist/metaObject-ClvW342q.d.ts +35 -0
- package/dist/metaObject-D7oudegV.d.cts +35 -0
- package/dist/oauth/index.cjs +2 -1
- package/dist/oauth/index.d.cts +34 -2
- package/dist/oauth/index.d.ts +34 -2
- package/dist/oauth/index.js +1 -1
- package/dist/oauth/types.d.cts +2 -2
- package/dist/oauth/types.d.ts +2 -2
- package/dist/{oauth-CSBChWuf.cjs → oauth-CXcrilOp.cjs} +47 -1
- package/dist/oauth-CXcrilOp.cjs.map +1 -0
- package/dist/{oauth-DOveZKUv.js → oauth-Ci0qeaS2.js} +43 -3
- package/dist/oauth-Ci0qeaS2.js.map +1 -0
- package/dist/{project-Onf-IcPW.cjs → project-BQ2oD6IW.cjs} +2 -2
- package/dist/{project-Onf-IcPW.cjs.map → project-BQ2oD6IW.cjs.map} +1 -1
- package/dist/{project-DmuC0bMK.js → project-aAVAOcmx.js} +2 -2
- package/dist/{project-DmuC0bMK.js.map → project-aAVAOcmx.js.map} +1 -1
- package/dist/queryService/index.d.cts +1 -1
- package/dist/queryService/index.d.ts +1 -1
- package/dist/queryService/types.d.cts +1 -1
- package/dist/queryService/types.d.ts +1 -1
- package/dist/queryService-B4h1Ez44.cjs +117 -0
- package/dist/queryService-B4h1Ez44.cjs.map +1 -0
- package/dist/queryService-DE_yE_S4.js +106 -0
- package/dist/queryService-DE_yE_S4.js.map +1 -0
- package/dist/queue/index.d.cts +1 -1
- package/dist/queue/index.d.ts +1 -1
- package/dist/readJwtClaims-CMZ3cNC_.js +28 -0
- package/dist/readJwtClaims-CMZ3cNC_.js.map +1 -0
- package/dist/readJwtClaims-SVJgo04x.cjs +33 -0
- package/dist/readJwtClaims-SVJgo04x.cjs.map +1 -0
- package/dist/sdk/configurations/index.cjs +2 -2
- package/dist/sdk/configurations/index.d.cts +2 -2
- package/dist/sdk/configurations/index.d.ts +2 -2
- package/dist/sdk/configurations/index.js +2 -2
- package/dist/sdk/dataAppServer/index.cjs +230 -0
- package/dist/sdk/dataAppServer/index.cjs.map +1 -0
- package/dist/sdk/dataAppServer/index.d.cts +55 -0
- package/dist/sdk/dataAppServer/index.d.ts +55 -0
- package/dist/sdk/dataAppServer/index.js +228 -0
- package/dist/sdk/dataAppServer/index.js.map +1 -0
- package/dist/sdk/developerPortal/index.cjs +3 -15
- package/dist/sdk/developerPortal/index.cjs.map +1 -1
- package/dist/sdk/developerPortal/index.js +3 -15
- package/dist/sdk/developerPortal/index.js.map +1 -1
- package/dist/sdk/queryService/index.cjs +3 -70
- package/dist/sdk/queryService/index.d.cts +2 -2
- package/dist/sdk/queryService/index.d.ts +2 -2
- package/dist/sdk/queryService/index.js +1 -70
- package/dist/sdk/securityPolicy/index.cjs +355 -0
- package/dist/sdk/securityPolicy/index.cjs.map +1 -0
- package/dist/sdk/securityPolicy/index.d.cts +100 -0
- package/dist/sdk/securityPolicy/index.d.ts +100 -0
- package/dist/sdk/securityPolicy/index.js +342 -0
- package/dist/sdk/securityPolicy/index.js.map +1 -0
- package/dist/sdk/securityPolicy/types.cjs +0 -0
- package/dist/sdk/securityPolicy/types.d.cts +2 -0
- package/dist/sdk/securityPolicy/types.d.ts +2 -0
- package/dist/sdk/securityPolicy/types.js +1 -0
- package/dist/sdk/semanticLayer/index.cjs +3 -2
- package/dist/sdk/semanticLayer/index.d.cts +8 -3
- package/dist/sdk/semanticLayer/index.d.ts +8 -3
- package/dist/sdk/semanticLayer/index.js +3 -1
- package/dist/sdk/semanticLayer/types.d.cts +1 -1
- package/dist/sdk/semanticLayer/types.d.ts +1 -1
- package/dist/sdk/vault/index.d.cts +1 -1
- package/dist/sdk/vault/index.d.ts +1 -1
- package/dist/{semanticLayer-djom-J78.cjs → semanticLayer-BVpdWKJn.cjs} +19 -52
- package/dist/semanticLayer-BVpdWKJn.cjs.map +1 -0
- package/dist/{semanticLayer-BdPW_wLo.js → semanticLayer-Btj1Qy9O.js} +20 -47
- package/dist/semanticLayer-Btj1Qy9O.js.map +1 -0
- package/dist/serviceIds-Bg1pWiPE.cjs +37 -0
- package/dist/serviceIds-Bg1pWiPE.cjs.map +1 -0
- package/dist/serviceIds-RAVtP4nK.js +32 -0
- package/dist/serviceIds-RAVtP4nK.js.map +1 -0
- package/dist/storage/index.d.cts +2 -2
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/types.d.cts +2 -2
- package/dist/storage/types.d.ts +2 -2
- package/dist/syncActions/index.cjs +1 -1
- package/dist/syncActions/index.js +1 -1
- package/dist/{syncActions-DToUhyP3.cjs → syncActions-CR7xvuIv.cjs} +2 -2
- package/dist/{syncActions-DToUhyP3.cjs.map → syncActions-CR7xvuIv.cjs.map} +1 -1
- package/dist/{syncActions-DV_aObTQ.js → syncActions-CU4cIGRf.js} +2 -2
- package/dist/{syncActions-DV_aObTQ.js.map → syncActions-CU4cIGRf.js.map} +1 -1
- package/dist/{types-CLtYOszN.d.cts → types-B7FKoyfy.d.cts} +442 -2
- package/dist/{types-iU1Sc1IM.d.ts → types-BDgOF2WN.d.ts} +6 -29
- package/dist/types-BXKL0wTI.d.cts +85 -0
- package/dist/{types-Ey59ZILC.d.ts → types-COb2O9c1.d.ts} +442 -2
- package/dist/{types-DjWubjvP.d.cts → types-CaBcgsT2.d.cts} +121 -53
- package/dist/{types-BljDxuWG.d.cts → types-DCARsJWs.d.cts} +6 -29
- package/dist/{types-CsVRsObb.d.ts → types-DFcj9YyQ.d.ts} +121 -53
- package/dist/types-DK1bSUNr.d.ts +85 -0
- package/package.json +40 -1
- package/surface/management.md +5 -0
- package/surface/sdk.md +2 -0
- package/dist/admin-CbMOsfgs.js.map +0 -1
- package/dist/admin-Di-MmGFP.cjs.map +0 -1
- package/dist/management-CUXgB2rb.cjs.map +0 -1
- package/dist/management-Cz5ut18k.js.map +0 -1
- package/dist/oauth-CSBChWuf.cjs.map +0 -1
- package/dist/oauth-DOveZKUv.js.map +0 -1
- package/dist/sdk/queryService/index.cjs.map +0 -1
- package/dist/sdk/queryService/index.js.map +0 -1
- package/dist/semanticLayer-BdPW_wLo.js.map +0 -1
- package/dist/semanticLayer-djom-J78.cjs.map +0 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { _ as HttpStatus, m as APP_USER_TOKEN_HEADER } from "../../createFetchClient-Dc_ZMuq7.js";
|
|
2
|
+
import { t as createStorageClient } from "../../storage--2FvvLAw.js";
|
|
3
|
+
import { n as isRowLevelSecurityError } from "../../RowLevelSecurityError-CObFqj3E.js";
|
|
4
|
+
import { u as lazy } from "../../transientRetries-BORv47mH.js";
|
|
5
|
+
import { t as isPlainObject } from "../../isPlainObject-BxsT8_t2.js";
|
|
6
|
+
import { t as ServiceId } from "../../serviceIds-RAVtP4nK.js";
|
|
7
|
+
import { t as createQueryServiceClient } from "../../queryService-DQHtHy2-.js";
|
|
8
|
+
import { n as createQueryServiceSdk, t as cellParser } from "../../queryService-DE_yE_S4.js";
|
|
9
|
+
import { t as readJwtClaims } from "../../readJwtClaims-CMZ3cNC_.js";
|
|
10
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
// src/sdks/dataAppServer/express.ts
|
|
13
|
+
/** Answers a row-level security refusal with 403 and passes any other error on. */
|
|
14
|
+
const createExpressErrorHandler = () => (err, _req, res, next) => {
|
|
15
|
+
if (res.headersSent || !isRowLevelSecurityError(err)) {
|
|
16
|
+
next(err);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
res.status(HttpStatus.FORBIDDEN).json({
|
|
20
|
+
error: err.message,
|
|
21
|
+
code: "ROW_LEVEL_SECURITY"
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
// src/sdks/dataAppServer/requestContext.ts
|
|
25
|
+
const isHeadersLike = (headers) => "get" in headers && typeof headers.get === "function";
|
|
26
|
+
const appUserTokenOf = (request) => {
|
|
27
|
+
const headers = "headers" in request ? request.headers : request;
|
|
28
|
+
if (isHeadersLike(headers)) return headers.get("X-Kbc-App-User-Token") || void 0;
|
|
29
|
+
const value = headers[APP_USER_TOKEN_HEADER.toLowerCase()];
|
|
30
|
+
return (Array.isArray(value) ? value[0] : value) || void 0;
|
|
31
|
+
};
|
|
32
|
+
const userOf = (token) => {
|
|
33
|
+
const { idp, sub, email } = readJwtClaims(token) ?? {};
|
|
34
|
+
if (typeof idp !== "string" || typeof sub !== "string" || !idp || !sub) return void 0;
|
|
35
|
+
return typeof email === "string" && email ? {
|
|
36
|
+
idp,
|
|
37
|
+
sub,
|
|
38
|
+
email
|
|
39
|
+
} : {
|
|
40
|
+
idp,
|
|
41
|
+
sub
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The signed-in user of the request being handled, kept across every `await` the request makes.
|
|
46
|
+
*
|
|
47
|
+
* - Work the request starts keeps its user: a timer, a promise cached at module level, a listener.
|
|
48
|
+
* - `currentUser()` reads the token payload unverified: safe only behind Keboola's apps proxy.
|
|
49
|
+
* - `currentUserKey()` is the same for every token of one user (`idp` + `sub`): a per-user cache key.
|
|
50
|
+
*/
|
|
51
|
+
const createRequestContext = () => {
|
|
52
|
+
const storage = new AsyncLocalStorage();
|
|
53
|
+
const current = () => storage.getStore();
|
|
54
|
+
return {
|
|
55
|
+
runWithRequest: (request, fn) => {
|
|
56
|
+
const appUserToken = appUserTokenOf(request);
|
|
57
|
+
return storage.run({
|
|
58
|
+
appUserToken,
|
|
59
|
+
user: appUserToken ? userOf(appUserToken) : void 0
|
|
60
|
+
}, fn);
|
|
61
|
+
},
|
|
62
|
+
current,
|
|
63
|
+
currentUser: () => current()?.user,
|
|
64
|
+
currentUserKey: () => {
|
|
65
|
+
const user = current()?.user;
|
|
66
|
+
return user && JSON.stringify([user.idp, user.sub]);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
// src/sdks/dataAppServer/runQuery.ts
|
|
71
|
+
const PAGE_SIZE = 1e4;
|
|
72
|
+
const TIMEOUT = 6e4;
|
|
73
|
+
const refusalWarning = (scope) => {
|
|
74
|
+
const prefix = "[dataAppServer] row-level security refused";
|
|
75
|
+
if (scope?.appUserToken) return `${prefix} the query for this user`;
|
|
76
|
+
return `${prefix} a query sent without ${APP_USER_TOKEN_HEADER}: ${scope ? "the request has none" : "it ran outside a user request (startup, timer, background job)"}`;
|
|
77
|
+
};
|
|
78
|
+
const toRecords = ({ columns = [], data = [], numberOfRows }, capped) => {
|
|
79
|
+
const parsers = columns.map(({ name, type }) => ({
|
|
80
|
+
key: name.toLowerCase(),
|
|
81
|
+
parse: cellParser(type)
|
|
82
|
+
}));
|
|
83
|
+
const keys = /* @__PURE__ */ new Set();
|
|
84
|
+
for (const { key } of parsers) {
|
|
85
|
+
if (keys.has(key)) throw new Error(`runQuery: two columns are named "${key}"; alias one with AS`);
|
|
86
|
+
keys.add(key);
|
|
87
|
+
}
|
|
88
|
+
const rows = data.map((row) => {
|
|
89
|
+
const record = {};
|
|
90
|
+
parsers.forEach(({ key, parse }, i) => {
|
|
91
|
+
const cell = row[i];
|
|
92
|
+
record[key] = cell == null ? null : parse(cell);
|
|
93
|
+
});
|
|
94
|
+
return record;
|
|
95
|
+
});
|
|
96
|
+
if (!capped && numberOfRows !== void 0 && rows.length < numberOfRows) throw new Error(`runQuery: got ${rows.length} of ${numberOfRows} rows`);
|
|
97
|
+
return rows;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Runs one SQL statement in the app's workspace and resolves every row as a record.
|
|
101
|
+
*
|
|
102
|
+
* - Sends the request's user token with the app's Storage token; without a user, the Storage token.
|
|
103
|
+
* - A repeated column name, or fewer rows than counted (no `maxRows`), throws instead of losing data.
|
|
104
|
+
*/
|
|
105
|
+
const createRunQuery = (settings, requestContext) => {
|
|
106
|
+
const queryService = createQueryServiceSdk({ queryServiceClient: createQueryServiceClient({
|
|
107
|
+
baseUrl: settings.queryServiceUrl,
|
|
108
|
+
auth: async () => {
|
|
109
|
+
const storageToken = await settings.storageToken();
|
|
110
|
+
const appUserToken = requestContext.current()?.appUserToken;
|
|
111
|
+
return appUserToken ? {
|
|
112
|
+
type: "app-user-token",
|
|
113
|
+
token: appUserToken,
|
|
114
|
+
storageToken
|
|
115
|
+
} : {
|
|
116
|
+
type: "sapi-token",
|
|
117
|
+
token: storageToken
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}) });
|
|
121
|
+
return async (sql, options = {}) => {
|
|
122
|
+
const scope = requestContext.current();
|
|
123
|
+
const [result] = await queryService.executeQuery(settings.branchId(), await settings.workspaceId(), {
|
|
124
|
+
statements: [sql],
|
|
125
|
+
transactional: false
|
|
126
|
+
}, {
|
|
127
|
+
...options,
|
|
128
|
+
pageSize: PAGE_SIZE,
|
|
129
|
+
timeout: options.timeout ?? TIMEOUT
|
|
130
|
+
}).catch((error) => {
|
|
131
|
+
if (isRowLevelSecurityError(error)) console.warn(refusalWarning(scope));
|
|
132
|
+
throw error;
|
|
133
|
+
});
|
|
134
|
+
return result ? toRecords(result, options.maxRows !== void 0) : [];
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
// src/sdks/dataAppServer/settings.ts
|
|
138
|
+
/** A setting the app needs is missing or unreadable; the message names the variable or file. */
|
|
139
|
+
var AppConfigError = class extends Error {
|
|
140
|
+
constructor(message, options) {
|
|
141
|
+
super(message, options);
|
|
142
|
+
this.name = "AppConfigError";
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const STACK_INDEX_TIMEOUT = 1e4;
|
|
146
|
+
const required = (env, name) => {
|
|
147
|
+
const value = env[name];
|
|
148
|
+
if (!value) throw new AppConfigError(`Missing environment variable ${name}`);
|
|
149
|
+
return value;
|
|
150
|
+
};
|
|
151
|
+
const storageTokenFrom = async (env) => {
|
|
152
|
+
const file = env.STORAGE_API_TOKEN_PATH;
|
|
153
|
+
if (!file) {
|
|
154
|
+
if (env.KBC_TOKEN) return env.KBC_TOKEN;
|
|
155
|
+
throw new AppConfigError("Missing Storage token: set STORAGE_API_TOKEN_PATH or KBC_TOKEN");
|
|
156
|
+
}
|
|
157
|
+
const token = (await readFile(file, "utf8").catch((error) => {
|
|
158
|
+
throw new AppConfigError(`Cannot read the file in STORAGE_API_TOKEN_PATH: ${file}`, { cause: error });
|
|
159
|
+
})).trim();
|
|
160
|
+
if (!token) throw new AppConfigError(`The token file in STORAGE_API_TOKEN_PATH is empty: ${file}`);
|
|
161
|
+
return token;
|
|
162
|
+
};
|
|
163
|
+
const manifestWorkspaceId = async (file) => {
|
|
164
|
+
let manifest;
|
|
165
|
+
try {
|
|
166
|
+
manifest = JSON.parse(await readFile(file, "utf8"));
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw new AppConfigError(`Cannot read the JSON in KBC_WORKSPACE_MANIFEST_PATH: ${file}`, { cause: error });
|
|
169
|
+
}
|
|
170
|
+
const id = isPlainObject(manifest) ? manifest.workspaceId : void 0;
|
|
171
|
+
if (typeof id === "string" && id || typeof id === "number") return String(id);
|
|
172
|
+
throw new AppConfigError(`The manifest in KBC_WORKSPACE_MANIFEST_PATH has no workspaceId: ${file}`);
|
|
173
|
+
};
|
|
174
|
+
const queryServiceUrlFromIndex = async (kbcUrl, token) => {
|
|
175
|
+
const { services } = await createStorageClient({
|
|
176
|
+
baseUrl: kbcUrl,
|
|
177
|
+
auth: {
|
|
178
|
+
type: "sapi-token",
|
|
179
|
+
token
|
|
180
|
+
}
|
|
181
|
+
}).getStackInfo({ exclude: "components" }, AbortSignal.timeout(STACK_INDEX_TIMEOUT));
|
|
182
|
+
const url = services.find(({ id }) => id === ServiceId.Query)?.url;
|
|
183
|
+
if (!url) throw new AppConfigError("The stack index of KBC_URL lists no query service");
|
|
184
|
+
return url;
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* What the platform provides a data app, read on first use; a missing value throws `AppConfigError`.
|
|
188
|
+
*
|
|
189
|
+
* - The Storage token is read on every call: the platform rotates the file in `STORAGE_API_TOKEN_PATH`.
|
|
190
|
+
* - The workspace ID and the Query Service URL are read once per process.
|
|
191
|
+
* - The Query Service URL is `QUERY_SERVICE_URL`, or the stack index of `KBC_URL` (local runs).
|
|
192
|
+
*/
|
|
193
|
+
const createSettings = (env) => {
|
|
194
|
+
const storageToken = () => storageTokenFrom(env);
|
|
195
|
+
const manifest = env.KBC_WORKSPACE_MANIFEST_PATH;
|
|
196
|
+
return {
|
|
197
|
+
branchId: () => required(env, "BRANCH_ID"),
|
|
198
|
+
storageToken,
|
|
199
|
+
workspaceId: lazy(() => manifest ? manifestWorkspaceId(manifest) : required(env, "WORKSPACE_ID")),
|
|
200
|
+
queryServiceUrl: lazy(async () => env.QUERY_SERVICE_URL || queryServiceUrlFromIndex(required(env, "KBC_URL"), await storageToken())),
|
|
201
|
+
hasWorkspace: Boolean(manifest || env.WORKSPACE_ID)
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
// src/sdks/dataAppServer/initializeApp.ts
|
|
205
|
+
/**
|
|
206
|
+
* Sets up Keboola access for this data app process; call it once.
|
|
207
|
+
*
|
|
208
|
+
* - Synchronous, no network call, never throws: each setting is read on first use.
|
|
209
|
+
* - `workspace` is `undefined` when no workspace is configured (the app has no Storage access).
|
|
210
|
+
* - `server.expressMiddleware()` goes first and `server.expressErrorHandler()` last.
|
|
211
|
+
*/
|
|
212
|
+
const initializeApp = ({ env = process.env } = {}) => {
|
|
213
|
+
const settings = createSettings(env);
|
|
214
|
+
const requestContext = createRequestContext();
|
|
215
|
+
return {
|
|
216
|
+
server: {
|
|
217
|
+
runWithRequest: requestContext.runWithRequest,
|
|
218
|
+
currentUser: requestContext.currentUser,
|
|
219
|
+
currentUserKey: requestContext.currentUserKey,
|
|
220
|
+
expressMiddleware: () => (req, _res, next) => requestContext.runWithRequest(req, next),
|
|
221
|
+
expressErrorHandler: createExpressErrorHandler
|
|
222
|
+
},
|
|
223
|
+
workspace: settings.hasWorkspace ? { runQuery: createRunQuery(settings, requestContext) } : void 0
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
export { AppConfigError, initializeApp };
|
|
227
|
+
|
|
228
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/sdks/dataAppServer/express.ts","../../../src/sdks/dataAppServer/requestContext.ts","../../../src/sdks/dataAppServer/runQuery.ts","../../../src/sdks/dataAppServer/settings.ts","../../../src/sdks/dataAppServer/initializeApp.ts"],"sourcesContent":["import { isRowLevelSecurityError } from '../../errors';\nimport { HttpStatus } from '../../internal';\n\ntype ErrorResponse = {\n headersSent: boolean;\n status(code: number): { json(body: unknown): unknown };\n};\n\n/** Answers a row-level security refusal with 403 and passes any other error on. */\nexport const createExpressErrorHandler =\n () => (err: unknown, _req: unknown, res: ErrorResponse, next: (err?: unknown) => void) => {\n if (res.headersSent || !isRowLevelSecurityError(err)) {\n next(err);\n return;\n }\n res.status(HttpStatus.FORBIDDEN).json({ error: err.message, code: 'ROW_LEVEL_SECURITY' });\n };\n","import { AsyncLocalStorage } from 'node:async_hooks';\n\nimport { APP_USER_TOKEN_HEADER } from '../../internal';\nimport { readJwtClaims } from '../../utils/readJwtClaims';\n\ntype NodeHeaders = Record<string, string | string[] | undefined>;\n\ntype HeadersLike = { get(name: string): string | null };\n\n/** A Fetch `Request` or `Headers`, or a Node request (Express, Fastify) with its lower-case `headers`. */\nexport type AppRequest = HeadersLike | { headers: HeadersLike | NodeHeaders };\n\n/** The signed-in user as the app user token names them; `email` only when the IdP verified it. */\nexport type AppUser = { idp: string; sub: string; email?: string };\n\ntype Scope = { appUserToken: string | undefined; user: AppUser | undefined };\n\nexport type RequestContext = ReturnType<typeof createRequestContext>;\n\nconst isHeadersLike = (headers: object): headers is HeadersLike =>\n 'get' in headers && typeof headers.get === 'function';\n\nconst appUserTokenOf = (request: AppRequest): string | undefined => {\n const headers = 'headers' in request ? request.headers : request;\n if (isHeadersLike(headers)) return headers.get(APP_USER_TOKEN_HEADER) || undefined;\n const value = headers[APP_USER_TOKEN_HEADER.toLowerCase()];\n return (Array.isArray(value) ? value[0] : value) || undefined;\n};\n\nconst userOf = (token: string): AppUser | undefined => {\n const { idp, sub, email } = readJwtClaims(token) ?? {};\n if (typeof idp !== 'string' || typeof sub !== 'string' || !idp || !sub) return undefined;\n return typeof email === 'string' && email ? { idp, sub, email } : { idp, sub };\n};\n\n/**\n * The signed-in user of the request being handled, kept across every `await` the request makes.\n *\n * - Work the request starts keeps its user: a timer, a promise cached at module level, a listener.\n * - `currentUser()` reads the token payload unverified: safe only behind Keboola's apps proxy.\n * - `currentUserKey()` is the same for every token of one user (`idp` + `sub`): a per-user cache key.\n */\nexport const createRequestContext = () => {\n const storage = new AsyncLocalStorage<Scope>();\n const current = () => storage.getStore();\n\n return {\n runWithRequest: <T>(request: AppRequest, fn: () => T): T => {\n const appUserToken = appUserTokenOf(request);\n return storage.run(\n { appUserToken, user: appUserToken ? userOf(appUserToken) : undefined },\n fn,\n );\n },\n current,\n currentUser: () => current()?.user,\n currentUserKey: () => {\n const user = current()?.user;\n return user && JSON.stringify([user.idp, user.sub]);\n },\n };\n};\n","import { createQueryServiceClient, type QueryServiceCredential } from '../../clients/queryService';\nimport type { QueryResult } from '../../clients/queryService/types';\nimport { isRowLevelSecurityError } from '../../errors';\nimport { APP_USER_TOKEN_HEADER } from '../../internal';\nimport { cellParser, createQueryServiceSdk, type ExecuteQueryOptions } from '../queryService';\n\nimport type { RequestContext } from './requestContext';\nimport type { Settings } from './settings';\n\nexport type RunQueryOptions = Pick<ExecuteQueryOptions, 'timeout' | 'maxRows' | 'signal'>;\n\nconst PAGE_SIZE = 10_000;\nconst TIMEOUT = 60_000;\n\nconst refusalWarning = (scope: ReturnType<RequestContext['current']>) => {\n const prefix = '[dataAppServer] row-level security refused';\n if (scope?.appUserToken) return `${prefix} the query for this user`;\n const reason = scope\n ? 'the request has none'\n : 'it ran outside a user request (startup, timer, background job)';\n return `${prefix} a query sent without ${APP_USER_TOKEN_HEADER}: ${reason}`;\n};\n\nconst toRecords = ({ columns = [], data = [], numberOfRows }: QueryResult, capped: boolean) => {\n const parsers = columns.map(({ name, type }) => ({\n key: name.toLowerCase(),\n parse: cellParser(type),\n }));\n const keys = new Set<string>();\n for (const { key } of parsers) {\n if (keys.has(key))\n throw new Error(`runQuery: two columns are named \"${key}\"; alias one with AS`);\n keys.add(key);\n }\n const rows = data.map((row) => {\n const record: Record<string, unknown> = {};\n parsers.forEach(({ key, parse }, i) => {\n const cell = row[i];\n record[key] = cell == null ? null : parse(cell);\n });\n return record;\n });\n if (!capped && numberOfRows !== undefined && rows.length < numberOfRows) {\n throw new Error(`runQuery: got ${rows.length} of ${numberOfRows} rows`);\n }\n return rows;\n};\n\n/**\n * Runs one SQL statement in the app's workspace and resolves every row as a record.\n *\n * - Sends the request's user token with the app's Storage token; without a user, the Storage token.\n * - A repeated column name, or fewer rows than counted (no `maxRows`), throws instead of losing data.\n */\nexport const createRunQuery = (settings: Settings, requestContext: RequestContext) => {\n const queryService = createQueryServiceSdk({\n queryServiceClient: createQueryServiceClient({\n baseUrl: settings.queryServiceUrl,\n auth: async (): Promise<QueryServiceCredential> => {\n const storageToken = await settings.storageToken();\n const appUserToken = requestContext.current()?.appUserToken;\n return appUserToken\n ? { type: 'app-user-token', token: appUserToken, storageToken }\n : { type: 'sapi-token', token: storageToken };\n },\n }),\n });\n\n return async (sql: string, options: RunQueryOptions = {}): Promise<Record<string, unknown>[]> => {\n const scope = requestContext.current();\n const [result] = await queryService\n .executeQuery(\n settings.branchId(),\n await settings.workspaceId(),\n { statements: [sql], transactional: false },\n { ...options, pageSize: PAGE_SIZE, timeout: options.timeout ?? TIMEOUT },\n )\n .catch((error: unknown) => {\n if (isRowLevelSecurityError(error)) console.warn(refusalWarning(scope));\n throw error;\n });\n return result ? toRecords(result, options.maxRows !== undefined) : [];\n };\n};\n","import { readFile } from 'node:fs/promises';\n\nimport { createStorageClient } from '../../clients/storage';\nimport { ServiceId } from '../../constants/serviceIds';\nimport { lazy } from '../../utils/async';\nimport { isPlainObject } from '../../utils/isPlainObject';\n\nexport type Env = Record<string, string | undefined>;\n\n/** A setting the app needs is missing or unreadable; the message names the variable or file. */\nexport class AppConfigError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'AppConfigError';\n }\n}\n\nexport type Settings = ReturnType<typeof createSettings>;\n\nconst STACK_INDEX_TIMEOUT = 10_000;\n\nconst required = (env: Env, name: string) => {\n const value = env[name];\n if (!value) throw new AppConfigError(`Missing environment variable ${name}`);\n return value;\n};\n\nconst storageTokenFrom = async (env: Env) => {\n const file = env.STORAGE_API_TOKEN_PATH;\n if (!file) {\n if (env.KBC_TOKEN) return env.KBC_TOKEN;\n throw new AppConfigError('Missing Storage token: set STORAGE_API_TOKEN_PATH or KBC_TOKEN');\n }\n const content = await readFile(file, 'utf8').catch((error: unknown) => {\n throw new AppConfigError(`Cannot read the file in STORAGE_API_TOKEN_PATH: ${file}`, {\n cause: error,\n });\n });\n const token = content.trim();\n if (!token)\n throw new AppConfigError(`The token file in STORAGE_API_TOKEN_PATH is empty: ${file}`);\n return token;\n};\n\nconst manifestWorkspaceId = async (file: string) => {\n let manifest: unknown;\n try {\n manifest = JSON.parse(await readFile(file, 'utf8'));\n } catch (error) {\n throw new AppConfigError(`Cannot read the JSON in KBC_WORKSPACE_MANIFEST_PATH: ${file}`, {\n cause: error,\n });\n }\n const id = isPlainObject(manifest) ? manifest.workspaceId : undefined;\n if ((typeof id === 'string' && id) || typeof id === 'number') return String(id);\n throw new AppConfigError(\n `The manifest in KBC_WORKSPACE_MANIFEST_PATH has no workspaceId: ${file}`,\n );\n};\n\nconst queryServiceUrlFromIndex = async (kbcUrl: string, token: string) => {\n const { services } = await createStorageClient({\n baseUrl: kbcUrl,\n auth: { type: 'sapi-token', token },\n }).getStackInfo({ exclude: 'components' }, AbortSignal.timeout(STACK_INDEX_TIMEOUT));\n const url = services.find(({ id }) => id === ServiceId.Query)?.url;\n if (!url) throw new AppConfigError('The stack index of KBC_URL lists no query service');\n return url;\n};\n\n/**\n * What the platform provides a data app, read on first use; a missing value throws `AppConfigError`.\n *\n * - The Storage token is read on every call: the platform rotates the file in `STORAGE_API_TOKEN_PATH`.\n * - The workspace ID and the Query Service URL are read once per process.\n * - The Query Service URL is `QUERY_SERVICE_URL`, or the stack index of `KBC_URL` (local runs).\n */\nexport const createSettings = (env: Env) => {\n const storageToken = () => storageTokenFrom(env);\n const manifest = env.KBC_WORKSPACE_MANIFEST_PATH;\n\n return {\n branchId: () => required(env, 'BRANCH_ID'),\n storageToken,\n workspaceId: lazy(() =>\n manifest ? manifestWorkspaceId(manifest) : required(env, 'WORKSPACE_ID'),\n ),\n queryServiceUrl: lazy(\n async () =>\n env.QUERY_SERVICE_URL ||\n queryServiceUrlFromIndex(required(env, 'KBC_URL'), await storageToken()),\n ),\n hasWorkspace: Boolean(manifest || env.WORKSPACE_ID),\n };\n};\n","import { createExpressErrorHandler } from './express';\nimport { type AppRequest, createRequestContext } from './requestContext';\nimport { createRunQuery } from './runQuery';\nimport { createSettings, type Env } from './settings';\n\n/** `env` replaces `process.env` (tests, local runs). */\nexport type DataAppServerOptions = { env?: Env };\n\n/**\n * Sets up Keboola access for this data app process; call it once.\n *\n * - Synchronous, no network call, never throws: each setting is read on first use.\n * - `workspace` is `undefined` when no workspace is configured (the app has no Storage access).\n * - `server.expressMiddleware()` goes first and `server.expressErrorHandler()` last.\n */\nexport const initializeApp = ({ env = process.env }: DataAppServerOptions = {}) => {\n const settings = createSettings(env);\n const requestContext = createRequestContext();\n\n return {\n server: {\n runWithRequest: requestContext.runWithRequest,\n currentUser: requestContext.currentUser,\n currentUserKey: requestContext.currentUserKey,\n expressMiddleware: () => (req: AppRequest, _res: unknown, next: () => void) =>\n requestContext.runWithRequest(req, next),\n expressErrorHandler: createExpressErrorHandler,\n },\n workspace: settings.hasWorkspace\n ? { runQuery: createRunQuery(settings, requestContext) }\n : undefined,\n };\n};\n\nexport type DataAppServer = ReturnType<typeof initializeApp>;\n"],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@ const require_dedupe = require("../../dedupe-Bf0gl9TJ.cjs");
|
|
|
4
4
|
const require_store = require("../../store-2OyH1XOP.cjs");
|
|
5
5
|
const require_middleware = require("../../middleware-vIaPDtQI.cjs");
|
|
6
6
|
const require_developerPortalClient = require("../../developerPortalClient-BcK9F-Fs.cjs");
|
|
7
|
+
const require_readJwtClaims = require("../../readJwtClaims-SVJgo04x.cjs");
|
|
7
8
|
// src/sdks/developerPortal/tokenExpiry.ts
|
|
8
9
|
/**
|
|
9
10
|
* Reads a JWT's `exp` claim as milliseconds, or `null` when unparseable ("expiry unknown", which the
|
|
@@ -13,21 +14,8 @@ const require_developerPortalClient = require("../../developerPortalClient-BcK9F
|
|
|
13
14
|
* proactive-refresh clock honest past the first rotation. The token always carries its own expiry.
|
|
14
15
|
*/
|
|
15
16
|
const readTokenExpiry = (token) => {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const payload = segments[1];
|
|
19
|
-
if (payload === void 0 || payload === "") return null;
|
|
20
|
-
try {
|
|
21
|
-
const base64 = payload.replaceAll("-", "+").replaceAll("_", "/");
|
|
22
|
-
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
|
|
23
|
-
const claims = JSON.parse(atob(padded));
|
|
24
|
-
if (typeof claims !== "object" || claims === null || !("exp" in claims)) return null;
|
|
25
|
-
const { exp } = claims;
|
|
26
|
-
if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
|
|
27
|
-
return exp * 1e3;
|
|
28
|
-
} catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
17
|
+
const exp = require_readJwtClaims.readJwtClaims(token)?.exp;
|
|
18
|
+
return typeof exp === "number" && Number.isFinite(exp) ? exp * 1e3 : null;
|
|
31
19
|
};
|
|
32
20
|
// src/sdks/developerPortal/credential.ts
|
|
33
21
|
const DEFAULT_REFRESH_LEAD_TIME_MS = 3e5;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["isUnauthorized","dedupe","HttpHeader","isUnauthorized","createStore","createDeveloperPortalClient"],"sources":["../../../src/sdks/developerPortal/tokenExpiry.ts","../../../src/sdks/developerPortal/credential.ts","../../../src/sdks/developerPortal/middleware.ts","../../../src/sdks/developerPortal/developerPortalSdk.ts"],"sourcesContent":["/**\n * Reads a JWT's `exp` claim as milliseconds, or `null` when unparseable (\"expiry unknown\", which the\n * caller leaves to the 401 path). A read, not a verification.\n *\n * Needed because apps-api reports `expiresIn` on login but never on refresh, so it cannot keep a\n * proactive-refresh clock honest past the first rotation. The token always carries its own expiry.\n */\nexport const readTokenExpiry = (token: string): number | null => {\n const segments = token.split('.');\n if (segments.length !== 3) return null;\n\n const payload = segments[1];\n if (payload === undefined || payload === '') return null;\n\n try {\n // base64url -> base64, and re-pad: `atob` requires padding, base64url omits it.\n const base64 = payload.replaceAll('-', '+').replaceAll('_', '/');\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');\n const claims: unknown = JSON.parse(atob(padded));\n if (typeof claims !== 'object' || claims === null || !('exp' in claims)) return null;\n const { exp } = claims as { exp: unknown };\n if (typeof exp !== 'number' || !Number.isFinite(exp)) return null;\n return exp * 1000;\n } catch {\n return null;\n }\n};\n","import type { DeveloperPortalTokens } from '../../clients/developerPortal/tokens';\nimport { isUnauthorized } from '../../keboola/bearer/credentials';\nimport { dedupe } from '../../utils';\n\nimport { readTokenExpiry } from './tokenExpiry';\n\ntype Options = {\n refresh: () => Promise<DeveloperPortalTokens>;\n /** Live read from the session, so this holds no token of its own. `null` when signed out. */\n getTokens: () => DeveloperPortalTokens | null;\n /** May fire more than once; the caller owns any fire-once latch. */\n onSessionExpired?: () => void;\n /**\n * An absolute lead time rather than `createBearerCredential`'s fractional threshold: that needs the\n * token's full lifetime, and apps-api reports `expiresIn` on login only, never on refresh.\n */\n refreshLeadTimeMs?: number;\n};\n\nconst DEFAULT_REFRESH_LEAD_TIME_MS = 5 * 60 * 1000;\n\nexport type DeveloperPortalCredential = {\n getIdToken: (options?: { forceRefresh?: boolean }) => Promise<string | null>;\n getTokens: () => DeveloperPortalTokens | null;\n notifyExpired: () => void;\n};\n\n/** Mirrors `createBearerCredential`, over the id token of the trio. */\nexport const createDeveloperPortalCredential = ({\n refresh,\n getTokens,\n onSessionExpired,\n refreshLeadTimeMs = DEFAULT_REFRESH_LEAD_TIME_MS,\n}: Options): DeveloperPortalCredential => {\n const doRefresh = async (): Promise<DeveloperPortalTokens> => {\n try {\n return await refresh();\n } catch (error) {\n // Only a 401 means the refresh token is dead. Signing out over a 5xx would be a self-inflicted\n // logout, so anything else propagates without expiring the session.\n if (isUnauthorized(error)) onSessionExpired?.();\n throw error;\n }\n };\n\n // N concurrent 401s must cause one rotation: each extra one invalidates the tokens the others got.\n const refreshShared = dedupe(doRefresh);\n\n const getIdToken = async ({ forceRefresh = false } = {}): Promise<string | null> => {\n const tokens = getTokens();\n if (tokens === null) return null;\n if (forceRefresh) return (await refreshShared()).token;\n\n const expiresAt = readTokenExpiry(tokens.token);\n // Unknown expiry is left to the 401 path; a `0` here would rotate on every request.\n if (expiresAt === null) return tokens.token;\n if (Date.now() < expiresAt - refreshLeadTimeMs) return tokens.token;\n\n // Proactive rotation is best-effort — a real 401 drives the mandatory one.\n return refreshShared()\n .then((fresh) => fresh.token)\n .catch(() => tokens.token);\n };\n\n return { getIdToken, getTokens, notifyExpired: () => onSessionExpired?.() };\n};\n","import { HttpHeader, type MiddlewareFn } from '../../fetchClient';\nimport { isUnauthorized } from '../../keboola/bearer/credentials';\n\nimport type { DeveloperPortalCredential } from './credential';\n\n/**\n * Stamps the id token, and on a 401 force-refreshes and replays once. Mirrors\n * `createBearerAuthMiddleware`, minus the `Bearer ` prefix apps-api does not use.\n *\n * An anonymous session passes through unstamped, so a failed login is never read as session expiry.\n */\nexport const createDeveloperPortalAuthMiddleware = (\n credential: DeveloperPortalCredential,\n): MiddlewareFn => {\n return (next) => async (request) => {\n const token = await credential.getIdToken();\n if (token === null) return next(request);\n\n // Cloned before the first attempt consumes the body. Cloning the `Request` carries a binary or\n // FormData payload through byte-for-byte.\n const retryRequest = request.request.clone();\n request.request.headers.set(HttpHeader.AUTHORIZATION, token);\n\n try {\n return await next(request);\n } catch (error) {\n if (!isUnauthorized(error)) throw error;\n\n const refreshed = await credential.getIdToken({ forceRefresh: true }).catch(() => null);\n // Nothing to replay with, because a concurrent sign-out landed or the rotation failed. The\n // caller's request failed with a 401, so that is what they see.\n if (refreshed === null) throw error;\n retryRequest.headers.set(HttpHeader.AUTHORIZATION, refreshed);\n\n try {\n return await next({ ...request, request: retryRequest });\n } catch (retryError) {\n if (isUnauthorized(retryError)) credential.notifyExpired();\n throw retryError;\n }\n }\n };\n};\n","import { createDeveloperPortalClient } from '../../clients/developerPortal/developerPortalClient';\nimport type { DeveloperPortalTokens } from '../../clients/developerPortal/tokens';\nimport type { LoginSuccess } from '../../clients/developerPortal/types';\nimport type { MiddlewareFn } from '../../fetchClient';\nimport { createStore } from '../../utils';\n\nimport { createDeveloperPortalCredential } from './credential';\nimport { createDeveloperPortalAuthMiddleware } from './middleware';\n\nexport type DeveloperPortalSdkOptions = {\n baseUrl: string;\n /** Session to restore on construction. */\n tokens?: DeveloperPortalTokens | null;\n /** Persistence hook, fired on sign-in, every rotation, and with `null` on sign-out or expiry. */\n onTokensChange?: (tokens: DeveloperPortalTokens | null) => void;\n /** The session is dead; tokens are already cleared. The consumer redirects to sign-in. */\n onSessionExpired?: () => void;\n refreshLeadTimeMs?: number;\n middlewares?: MiddlewareFn[];\n};\n\n/**\n * `createDeveloperPortalClient` plus the session that keeps it authenticated.\n *\n * - Builds its own client: middleware, credential and session depend on each other in a cycle.\n */\nexport const createDeveloperPortalSdk = ({\n baseUrl,\n tokens: initialTokens = null,\n onTokensChange,\n onSessionExpired,\n refreshLeadTimeMs,\n middlewares = [],\n}: DeveloperPortalSdkOptions) => {\n const store = createStore<DeveloperPortalTokens | null>(initialTokens);\n store.subscribe((value) => onTokensChange?.(value));\n\n const getTokens = () => store.get();\n\n const requireTokens = (action: string): DeveloperPortalTokens => {\n const tokens = store.get();\n if (tokens === null) {\n throw new Error(`Developer Portal: ${action} requires a signed-in session.`);\n }\n return tokens;\n };\n\n // No auth middleware, so a rotation cannot recurse into the middleware that triggered it.\n const refreshClient = createDeveloperPortalClient({ baseUrl, middlewares });\n\n const refresh = async (): Promise<DeveloperPortalTokens> => {\n const before = store.get();\n if (before === null) {\n throw new Error('Cannot refresh the Developer Portal session: signed out.');\n }\n\n const { token, accessToken } = await refreshClient.auth.refreshTokens(before.refreshToken);\n const next: DeveloperPortalTokens = { token, accessToken, refreshToken: before.refreshToken };\n\n // Sign-out wins over an in-flight rotation; otherwise the response signs the user back in.\n const after = store.get();\n if (after === null || after.refreshToken !== before.refreshToken) {\n throw new Error('Developer Portal session changed while refreshing; discarding the result.');\n }\n\n store.set(next);\n return next;\n };\n\n const clear = () => store.set(null);\n\n const credential = createDeveloperPortalCredential({\n refresh,\n getTokens,\n refreshLeadTimeMs,\n onSessionExpired: () => {\n clear();\n onSessionExpired?.();\n },\n });\n\n const client = createDeveloperPortalClient({\n baseUrl,\n middlewares,\n authMiddleware: createDeveloperPortalAuthMiddleware(credential),\n });\n\n // `refreshTokens` is not re-exposed: rotating outside the store leaves stale tokens.\n const { refreshTokens: _storeBypassingRotation, ...auth } = client.auth;\n\n const api = {\n ...client,\n auth: {\n ...auth,\n logout: async (signal?: AbortSignal) =>\n client.auth.logout(requireTokens('sign-out').accessToken, signal),\n // Both MFA calls bypass the reactive 401 path, so rotate proactively first.\n setupMfa: async (signal?: AbortSignal) => {\n await credential.getIdToken();\n const { token, accessToken } = requireTokens('MFA enrolment');\n return client.auth.setupMfa({ idToken: token, accessToken }, signal);\n },\n confirmMfa: async (code: string, signal?: AbortSignal) => {\n await credential.getIdToken();\n const { token, accessToken } = requireTokens('MFA confirmation');\n return client.auth.confirmMfa({ idToken: token, accessToken, code }, signal);\n },\n },\n };\n\n const session = {\n set: (result: LoginSuccess) => {\n store.set({\n token: result.token,\n accessToken: result.accessToken,\n refreshToken: result.refreshToken,\n });\n },\n get: getTokens,\n subscribe: store.subscribe,\n clear,\n /** Rotates if the id token is inside the lead time. Best-effort; the 401 path is the guarantee. */\n ensureFresh: async (): Promise<void> => {\n await credential.getIdToken();\n },\n /** Rotates now, for a change of claims (e.g. leaving a vendor). Propagates failure. */\n refresh: async (): Promise<void> => {\n await credential.getIdToken({ forceRefresh: true });\n },\n isAuthenticated: () => store.get() !== null,\n };\n\n return { api, session };\n};\n\nexport type DeveloperPortalSdk = ReturnType<typeof createDeveloperPortalSdk>;\nexport type DeveloperPortalSession = DeveloperPortalSdk['session'];\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["readJwtClaims","isUnauthorized","dedupe","HttpHeader","isUnauthorized","createStore","createDeveloperPortalClient"],"sources":["../../../src/sdks/developerPortal/tokenExpiry.ts","../../../src/sdks/developerPortal/credential.ts","../../../src/sdks/developerPortal/middleware.ts","../../../src/sdks/developerPortal/developerPortalSdk.ts"],"sourcesContent":["import { readJwtClaims } from '../../utils/readJwtClaims';\n\n/**\n * Reads a JWT's `exp` claim as milliseconds, or `null` when unparseable (\"expiry unknown\", which the\n * caller leaves to the 401 path). A read, not a verification.\n *\n * Needed because apps-api reports `expiresIn` on login but never on refresh, so it cannot keep a\n * proactive-refresh clock honest past the first rotation. The token always carries its own expiry.\n */\nexport const readTokenExpiry = (token: string): number | null => {\n const exp = readJwtClaims(token)?.exp;\n return typeof exp === 'number' && Number.isFinite(exp) ? exp * 1000 : null;\n};\n","import type { DeveloperPortalTokens } from '../../clients/developerPortal/tokens';\nimport { isUnauthorized } from '../../keboola/bearer/credentials';\nimport { dedupe } from '../../utils';\n\nimport { readTokenExpiry } from './tokenExpiry';\n\ntype Options = {\n refresh: () => Promise<DeveloperPortalTokens>;\n /** Live read from the session, so this holds no token of its own. `null` when signed out. */\n getTokens: () => DeveloperPortalTokens | null;\n /** May fire more than once; the caller owns any fire-once latch. */\n onSessionExpired?: () => void;\n /**\n * An absolute lead time rather than `createBearerCredential`'s fractional threshold: that needs the\n * token's full lifetime, and apps-api reports `expiresIn` on login only, never on refresh.\n */\n refreshLeadTimeMs?: number;\n};\n\nconst DEFAULT_REFRESH_LEAD_TIME_MS = 5 * 60 * 1000;\n\nexport type DeveloperPortalCredential = {\n getIdToken: (options?: { forceRefresh?: boolean }) => Promise<string | null>;\n getTokens: () => DeveloperPortalTokens | null;\n notifyExpired: () => void;\n};\n\n/** Mirrors `createBearerCredential`, over the id token of the trio. */\nexport const createDeveloperPortalCredential = ({\n refresh,\n getTokens,\n onSessionExpired,\n refreshLeadTimeMs = DEFAULT_REFRESH_LEAD_TIME_MS,\n}: Options): DeveloperPortalCredential => {\n const doRefresh = async (): Promise<DeveloperPortalTokens> => {\n try {\n return await refresh();\n } catch (error) {\n // Only a 401 means the refresh token is dead. Signing out over a 5xx would be a self-inflicted\n // logout, so anything else propagates without expiring the session.\n if (isUnauthorized(error)) onSessionExpired?.();\n throw error;\n }\n };\n\n // N concurrent 401s must cause one rotation: each extra one invalidates the tokens the others got.\n const refreshShared = dedupe(doRefresh);\n\n const getIdToken = async ({ forceRefresh = false } = {}): Promise<string | null> => {\n const tokens = getTokens();\n if (tokens === null) return null;\n if (forceRefresh) return (await refreshShared()).token;\n\n const expiresAt = readTokenExpiry(tokens.token);\n // Unknown expiry is left to the 401 path; a `0` here would rotate on every request.\n if (expiresAt === null) return tokens.token;\n if (Date.now() < expiresAt - refreshLeadTimeMs) return tokens.token;\n\n // Proactive rotation is best-effort — a real 401 drives the mandatory one.\n return refreshShared()\n .then((fresh) => fresh.token)\n .catch(() => tokens.token);\n };\n\n return { getIdToken, getTokens, notifyExpired: () => onSessionExpired?.() };\n};\n","import { HttpHeader, type MiddlewareFn } from '../../fetchClient';\nimport { isUnauthorized } from '../../keboola/bearer/credentials';\n\nimport type { DeveloperPortalCredential } from './credential';\n\n/**\n * Stamps the id token, and on a 401 force-refreshes and replays once. Mirrors\n * `createBearerAuthMiddleware`, minus the `Bearer ` prefix apps-api does not use.\n *\n * An anonymous session passes through unstamped, so a failed login is never read as session expiry.\n */\nexport const createDeveloperPortalAuthMiddleware = (\n credential: DeveloperPortalCredential,\n): MiddlewareFn => {\n return (next) => async (request) => {\n const token = await credential.getIdToken();\n if (token === null) return next(request);\n\n // Cloned before the first attempt consumes the body. Cloning the `Request` carries a binary or\n // FormData payload through byte-for-byte.\n const retryRequest = request.request.clone();\n request.request.headers.set(HttpHeader.AUTHORIZATION, token);\n\n try {\n return await next(request);\n } catch (error) {\n if (!isUnauthorized(error)) throw error;\n\n const refreshed = await credential.getIdToken({ forceRefresh: true }).catch(() => null);\n // Nothing to replay with, because a concurrent sign-out landed or the rotation failed. The\n // caller's request failed with a 401, so that is what they see.\n if (refreshed === null) throw error;\n retryRequest.headers.set(HttpHeader.AUTHORIZATION, refreshed);\n\n try {\n return await next({ ...request, request: retryRequest });\n } catch (retryError) {\n if (isUnauthorized(retryError)) credential.notifyExpired();\n throw retryError;\n }\n }\n };\n};\n","import { createDeveloperPortalClient } from '../../clients/developerPortal/developerPortalClient';\nimport type { DeveloperPortalTokens } from '../../clients/developerPortal/tokens';\nimport type { LoginSuccess } from '../../clients/developerPortal/types';\nimport type { MiddlewareFn } from '../../fetchClient';\nimport { createStore } from '../../utils';\n\nimport { createDeveloperPortalCredential } from './credential';\nimport { createDeveloperPortalAuthMiddleware } from './middleware';\n\nexport type DeveloperPortalSdkOptions = {\n baseUrl: string;\n /** Session to restore on construction. */\n tokens?: DeveloperPortalTokens | null;\n /** Persistence hook, fired on sign-in, every rotation, and with `null` on sign-out or expiry. */\n onTokensChange?: (tokens: DeveloperPortalTokens | null) => void;\n /** The session is dead; tokens are already cleared. The consumer redirects to sign-in. */\n onSessionExpired?: () => void;\n refreshLeadTimeMs?: number;\n middlewares?: MiddlewareFn[];\n};\n\n/**\n * `createDeveloperPortalClient` plus the session that keeps it authenticated.\n *\n * - Builds its own client: middleware, credential and session depend on each other in a cycle.\n */\nexport const createDeveloperPortalSdk = ({\n baseUrl,\n tokens: initialTokens = null,\n onTokensChange,\n onSessionExpired,\n refreshLeadTimeMs,\n middlewares = [],\n}: DeveloperPortalSdkOptions) => {\n const store = createStore<DeveloperPortalTokens | null>(initialTokens);\n store.subscribe((value) => onTokensChange?.(value));\n\n const getTokens = () => store.get();\n\n const requireTokens = (action: string): DeveloperPortalTokens => {\n const tokens = store.get();\n if (tokens === null) {\n throw new Error(`Developer Portal: ${action} requires a signed-in session.`);\n }\n return tokens;\n };\n\n // No auth middleware, so a rotation cannot recurse into the middleware that triggered it.\n const refreshClient = createDeveloperPortalClient({ baseUrl, middlewares });\n\n const refresh = async (): Promise<DeveloperPortalTokens> => {\n const before = store.get();\n if (before === null) {\n throw new Error('Cannot refresh the Developer Portal session: signed out.');\n }\n\n const { token, accessToken } = await refreshClient.auth.refreshTokens(before.refreshToken);\n const next: DeveloperPortalTokens = { token, accessToken, refreshToken: before.refreshToken };\n\n // Sign-out wins over an in-flight rotation; otherwise the response signs the user back in.\n const after = store.get();\n if (after === null || after.refreshToken !== before.refreshToken) {\n throw new Error('Developer Portal session changed while refreshing; discarding the result.');\n }\n\n store.set(next);\n return next;\n };\n\n const clear = () => store.set(null);\n\n const credential = createDeveloperPortalCredential({\n refresh,\n getTokens,\n refreshLeadTimeMs,\n onSessionExpired: () => {\n clear();\n onSessionExpired?.();\n },\n });\n\n const client = createDeveloperPortalClient({\n baseUrl,\n middlewares,\n authMiddleware: createDeveloperPortalAuthMiddleware(credential),\n });\n\n // `refreshTokens` is not re-exposed: rotating outside the store leaves stale tokens.\n const { refreshTokens: _storeBypassingRotation, ...auth } = client.auth;\n\n const api = {\n ...client,\n auth: {\n ...auth,\n logout: async (signal?: AbortSignal) =>\n client.auth.logout(requireTokens('sign-out').accessToken, signal),\n // Both MFA calls bypass the reactive 401 path, so rotate proactively first.\n setupMfa: async (signal?: AbortSignal) => {\n await credential.getIdToken();\n const { token, accessToken } = requireTokens('MFA enrolment');\n return client.auth.setupMfa({ idToken: token, accessToken }, signal);\n },\n confirmMfa: async (code: string, signal?: AbortSignal) => {\n await credential.getIdToken();\n const { token, accessToken } = requireTokens('MFA confirmation');\n return client.auth.confirmMfa({ idToken: token, accessToken, code }, signal);\n },\n },\n };\n\n const session = {\n set: (result: LoginSuccess) => {\n store.set({\n token: result.token,\n accessToken: result.accessToken,\n refreshToken: result.refreshToken,\n });\n },\n get: getTokens,\n subscribe: store.subscribe,\n clear,\n /** Rotates if the id token is inside the lead time. Best-effort; the 401 path is the guarantee. */\n ensureFresh: async (): Promise<void> => {\n await credential.getIdToken();\n },\n /** Rotates now, for a change of claims (e.g. leaving a vendor). Propagates failure. */\n refresh: async (): Promise<void> => {\n await credential.getIdToken({ forceRefresh: true });\n },\n isAuthenticated: () => store.get() !== null,\n };\n\n return { api, session };\n};\n\nexport type DeveloperPortalSdk = ReturnType<typeof createDeveloperPortalSdk>;\nexport type DeveloperPortalSession = DeveloperPortalSdk['session'];\n"],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@ import { t as dedupe } from "../../dedupe-Bu0d2i_w.js";
|
|
|
3
3
|
import { t as createStore } from "../../store-HP0zRcLz.js";
|
|
4
4
|
import { n as isUnauthorized } from "../../middleware-DxGv7DG8.js";
|
|
5
5
|
import { t as createDeveloperPortalClient } from "../../developerPortalClient-CzCJ5sE3.js";
|
|
6
|
+
import { t as readJwtClaims } from "../../readJwtClaims-CMZ3cNC_.js";
|
|
6
7
|
// src/sdks/developerPortal/tokenExpiry.ts
|
|
7
8
|
/**
|
|
8
9
|
* Reads a JWT's `exp` claim as milliseconds, or `null` when unparseable ("expiry unknown", which the
|
|
@@ -12,21 +13,8 @@ import { t as createDeveloperPortalClient } from "../../developerPortalClient-Cz
|
|
|
12
13
|
* proactive-refresh clock honest past the first rotation. The token always carries its own expiry.
|
|
13
14
|
*/
|
|
14
15
|
const readTokenExpiry = (token) => {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const payload = segments[1];
|
|
18
|
-
if (payload === void 0 || payload === "") return null;
|
|
19
|
-
try {
|
|
20
|
-
const base64 = payload.replaceAll("-", "+").replaceAll("_", "/");
|
|
21
|
-
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
|
|
22
|
-
const claims = JSON.parse(atob(padded));
|
|
23
|
-
if (typeof claims !== "object" || claims === null || !("exp" in claims)) return null;
|
|
24
|
-
const { exp } = claims;
|
|
25
|
-
if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
|
|
26
|
-
return exp * 1e3;
|
|
27
|
-
} catch {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
16
|
+
const exp = readJwtClaims(token)?.exp;
|
|
17
|
+
return typeof exp === "number" && Number.isFinite(exp) ? exp * 1e3 : null;
|
|
30
18
|
};
|
|
31
19
|
// src/sdks/developerPortal/credential.ts
|
|
32
20
|
const DEFAULT_REFRESH_LEAD_TIME_MS = 3e5;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/sdks/developerPortal/tokenExpiry.ts","../../../src/sdks/developerPortal/credential.ts","../../../src/sdks/developerPortal/middleware.ts","../../../src/sdks/developerPortal/developerPortalSdk.ts"],"sourcesContent":["/**\n * Reads a JWT's `exp` claim as milliseconds, or `null` when unparseable (\"expiry unknown\", which the\n * caller leaves to the 401 path). A read, not a verification.\n *\n * Needed because apps-api reports `expiresIn` on login but never on refresh, so it cannot keep a\n * proactive-refresh clock honest past the first rotation. The token always carries its own expiry.\n */\nexport const readTokenExpiry = (token: string): number | null => {\n const segments = token.split('.');\n if (segments.length !== 3) return null;\n\n const payload = segments[1];\n if (payload === undefined || payload === '') return null;\n\n try {\n // base64url -> base64, and re-pad: `atob` requires padding, base64url omits it.\n const base64 = payload.replaceAll('-', '+').replaceAll('_', '/');\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');\n const claims: unknown = JSON.parse(atob(padded));\n if (typeof claims !== 'object' || claims === null || !('exp' in claims)) return null;\n const { exp } = claims as { exp: unknown };\n if (typeof exp !== 'number' || !Number.isFinite(exp)) return null;\n return exp * 1000;\n } catch {\n return null;\n }\n};\n","import type { DeveloperPortalTokens } from '../../clients/developerPortal/tokens';\nimport { isUnauthorized } from '../../keboola/bearer/credentials';\nimport { dedupe } from '../../utils';\n\nimport { readTokenExpiry } from './tokenExpiry';\n\ntype Options = {\n refresh: () => Promise<DeveloperPortalTokens>;\n /** Live read from the session, so this holds no token of its own. `null` when signed out. */\n getTokens: () => DeveloperPortalTokens | null;\n /** May fire more than once; the caller owns any fire-once latch. */\n onSessionExpired?: () => void;\n /**\n * An absolute lead time rather than `createBearerCredential`'s fractional threshold: that needs the\n * token's full lifetime, and apps-api reports `expiresIn` on login only, never on refresh.\n */\n refreshLeadTimeMs?: number;\n};\n\nconst DEFAULT_REFRESH_LEAD_TIME_MS = 5 * 60 * 1000;\n\nexport type DeveloperPortalCredential = {\n getIdToken: (options?: { forceRefresh?: boolean }) => Promise<string | null>;\n getTokens: () => DeveloperPortalTokens | null;\n notifyExpired: () => void;\n};\n\n/** Mirrors `createBearerCredential`, over the id token of the trio. */\nexport const createDeveloperPortalCredential = ({\n refresh,\n getTokens,\n onSessionExpired,\n refreshLeadTimeMs = DEFAULT_REFRESH_LEAD_TIME_MS,\n}: Options): DeveloperPortalCredential => {\n const doRefresh = async (): Promise<DeveloperPortalTokens> => {\n try {\n return await refresh();\n } catch (error) {\n // Only a 401 means the refresh token is dead. Signing out over a 5xx would be a self-inflicted\n // logout, so anything else propagates without expiring the session.\n if (isUnauthorized(error)) onSessionExpired?.();\n throw error;\n }\n };\n\n // N concurrent 401s must cause one rotation: each extra one invalidates the tokens the others got.\n const refreshShared = dedupe(doRefresh);\n\n const getIdToken = async ({ forceRefresh = false } = {}): Promise<string | null> => {\n const tokens = getTokens();\n if (tokens === null) return null;\n if (forceRefresh) return (await refreshShared()).token;\n\n const expiresAt = readTokenExpiry(tokens.token);\n // Unknown expiry is left to the 401 path; a `0` here would rotate on every request.\n if (expiresAt === null) return tokens.token;\n if (Date.now() < expiresAt - refreshLeadTimeMs) return tokens.token;\n\n // Proactive rotation is best-effort — a real 401 drives the mandatory one.\n return refreshShared()\n .then((fresh) => fresh.token)\n .catch(() => tokens.token);\n };\n\n return { getIdToken, getTokens, notifyExpired: () => onSessionExpired?.() };\n};\n","import { HttpHeader, type MiddlewareFn } from '../../fetchClient';\nimport { isUnauthorized } from '../../keboola/bearer/credentials';\n\nimport type { DeveloperPortalCredential } from './credential';\n\n/**\n * Stamps the id token, and on a 401 force-refreshes and replays once. Mirrors\n * `createBearerAuthMiddleware`, minus the `Bearer ` prefix apps-api does not use.\n *\n * An anonymous session passes through unstamped, so a failed login is never read as session expiry.\n */\nexport const createDeveloperPortalAuthMiddleware = (\n credential: DeveloperPortalCredential,\n): MiddlewareFn => {\n return (next) => async (request) => {\n const token = await credential.getIdToken();\n if (token === null) return next(request);\n\n // Cloned before the first attempt consumes the body. Cloning the `Request` carries a binary or\n // FormData payload through byte-for-byte.\n const retryRequest = request.request.clone();\n request.request.headers.set(HttpHeader.AUTHORIZATION, token);\n\n try {\n return await next(request);\n } catch (error) {\n if (!isUnauthorized(error)) throw error;\n\n const refreshed = await credential.getIdToken({ forceRefresh: true }).catch(() => null);\n // Nothing to replay with, because a concurrent sign-out landed or the rotation failed. The\n // caller's request failed with a 401, so that is what they see.\n if (refreshed === null) throw error;\n retryRequest.headers.set(HttpHeader.AUTHORIZATION, refreshed);\n\n try {\n return await next({ ...request, request: retryRequest });\n } catch (retryError) {\n if (isUnauthorized(retryError)) credential.notifyExpired();\n throw retryError;\n }\n }\n };\n};\n","import { createDeveloperPortalClient } from '../../clients/developerPortal/developerPortalClient';\nimport type { DeveloperPortalTokens } from '../../clients/developerPortal/tokens';\nimport type { LoginSuccess } from '../../clients/developerPortal/types';\nimport type { MiddlewareFn } from '../../fetchClient';\nimport { createStore } from '../../utils';\n\nimport { createDeveloperPortalCredential } from './credential';\nimport { createDeveloperPortalAuthMiddleware } from './middleware';\n\nexport type DeveloperPortalSdkOptions = {\n baseUrl: string;\n /** Session to restore on construction. */\n tokens?: DeveloperPortalTokens | null;\n /** Persistence hook, fired on sign-in, every rotation, and with `null` on sign-out or expiry. */\n onTokensChange?: (tokens: DeveloperPortalTokens | null) => void;\n /** The session is dead; tokens are already cleared. The consumer redirects to sign-in. */\n onSessionExpired?: () => void;\n refreshLeadTimeMs?: number;\n middlewares?: MiddlewareFn[];\n};\n\n/**\n * `createDeveloperPortalClient` plus the session that keeps it authenticated.\n *\n * - Builds its own client: middleware, credential and session depend on each other in a cycle.\n */\nexport const createDeveloperPortalSdk = ({\n baseUrl,\n tokens: initialTokens = null,\n onTokensChange,\n onSessionExpired,\n refreshLeadTimeMs,\n middlewares = [],\n}: DeveloperPortalSdkOptions) => {\n const store = createStore<DeveloperPortalTokens | null>(initialTokens);\n store.subscribe((value) => onTokensChange?.(value));\n\n const getTokens = () => store.get();\n\n const requireTokens = (action: string): DeveloperPortalTokens => {\n const tokens = store.get();\n if (tokens === null) {\n throw new Error(`Developer Portal: ${action} requires a signed-in session.`);\n }\n return tokens;\n };\n\n // No auth middleware, so a rotation cannot recurse into the middleware that triggered it.\n const refreshClient = createDeveloperPortalClient({ baseUrl, middlewares });\n\n const refresh = async (): Promise<DeveloperPortalTokens> => {\n const before = store.get();\n if (before === null) {\n throw new Error('Cannot refresh the Developer Portal session: signed out.');\n }\n\n const { token, accessToken } = await refreshClient.auth.refreshTokens(before.refreshToken);\n const next: DeveloperPortalTokens = { token, accessToken, refreshToken: before.refreshToken };\n\n // Sign-out wins over an in-flight rotation; otherwise the response signs the user back in.\n const after = store.get();\n if (after === null || after.refreshToken !== before.refreshToken) {\n throw new Error('Developer Portal session changed while refreshing; discarding the result.');\n }\n\n store.set(next);\n return next;\n };\n\n const clear = () => store.set(null);\n\n const credential = createDeveloperPortalCredential({\n refresh,\n getTokens,\n refreshLeadTimeMs,\n onSessionExpired: () => {\n clear();\n onSessionExpired?.();\n },\n });\n\n const client = createDeveloperPortalClient({\n baseUrl,\n middlewares,\n authMiddleware: createDeveloperPortalAuthMiddleware(credential),\n });\n\n // `refreshTokens` is not re-exposed: rotating outside the store leaves stale tokens.\n const { refreshTokens: _storeBypassingRotation, ...auth } = client.auth;\n\n const api = {\n ...client,\n auth: {\n ...auth,\n logout: async (signal?: AbortSignal) =>\n client.auth.logout(requireTokens('sign-out').accessToken, signal),\n // Both MFA calls bypass the reactive 401 path, so rotate proactively first.\n setupMfa: async (signal?: AbortSignal) => {\n await credential.getIdToken();\n const { token, accessToken } = requireTokens('MFA enrolment');\n return client.auth.setupMfa({ idToken: token, accessToken }, signal);\n },\n confirmMfa: async (code: string, signal?: AbortSignal) => {\n await credential.getIdToken();\n const { token, accessToken } = requireTokens('MFA confirmation');\n return client.auth.confirmMfa({ idToken: token, accessToken, code }, signal);\n },\n },\n };\n\n const session = {\n set: (result: LoginSuccess) => {\n store.set({\n token: result.token,\n accessToken: result.accessToken,\n refreshToken: result.refreshToken,\n });\n },\n get: getTokens,\n subscribe: store.subscribe,\n clear,\n /** Rotates if the id token is inside the lead time. Best-effort; the 401 path is the guarantee. */\n ensureFresh: async (): Promise<void> => {\n await credential.getIdToken();\n },\n /** Rotates now, for a change of claims (e.g. leaving a vendor). Propagates failure. */\n refresh: async (): Promise<void> => {\n await credential.getIdToken({ forceRefresh: true });\n },\n isAuthenticated: () => store.get() !== null,\n };\n\n return { api, session };\n};\n\nexport type DeveloperPortalSdk = ReturnType<typeof createDeveloperPortalSdk>;\nexport type DeveloperPortalSession = DeveloperPortalSdk['session'];\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/sdks/developerPortal/tokenExpiry.ts","../../../src/sdks/developerPortal/credential.ts","../../../src/sdks/developerPortal/middleware.ts","../../../src/sdks/developerPortal/developerPortalSdk.ts"],"sourcesContent":["import { readJwtClaims } from '../../utils/readJwtClaims';\n\n/**\n * Reads a JWT's `exp` claim as milliseconds, or `null` when unparseable (\"expiry unknown\", which the\n * caller leaves to the 401 path). A read, not a verification.\n *\n * Needed because apps-api reports `expiresIn` on login but never on refresh, so it cannot keep a\n * proactive-refresh clock honest past the first rotation. The token always carries its own expiry.\n */\nexport const readTokenExpiry = (token: string): number | null => {\n const exp = readJwtClaims(token)?.exp;\n return typeof exp === 'number' && Number.isFinite(exp) ? exp * 1000 : null;\n};\n","import type { DeveloperPortalTokens } from '../../clients/developerPortal/tokens';\nimport { isUnauthorized } from '../../keboola/bearer/credentials';\nimport { dedupe } from '../../utils';\n\nimport { readTokenExpiry } from './tokenExpiry';\n\ntype Options = {\n refresh: () => Promise<DeveloperPortalTokens>;\n /** Live read from the session, so this holds no token of its own. `null` when signed out. */\n getTokens: () => DeveloperPortalTokens | null;\n /** May fire more than once; the caller owns any fire-once latch. */\n onSessionExpired?: () => void;\n /**\n * An absolute lead time rather than `createBearerCredential`'s fractional threshold: that needs the\n * token's full lifetime, and apps-api reports `expiresIn` on login only, never on refresh.\n */\n refreshLeadTimeMs?: number;\n};\n\nconst DEFAULT_REFRESH_LEAD_TIME_MS = 5 * 60 * 1000;\n\nexport type DeveloperPortalCredential = {\n getIdToken: (options?: { forceRefresh?: boolean }) => Promise<string | null>;\n getTokens: () => DeveloperPortalTokens | null;\n notifyExpired: () => void;\n};\n\n/** Mirrors `createBearerCredential`, over the id token of the trio. */\nexport const createDeveloperPortalCredential = ({\n refresh,\n getTokens,\n onSessionExpired,\n refreshLeadTimeMs = DEFAULT_REFRESH_LEAD_TIME_MS,\n}: Options): DeveloperPortalCredential => {\n const doRefresh = async (): Promise<DeveloperPortalTokens> => {\n try {\n return await refresh();\n } catch (error) {\n // Only a 401 means the refresh token is dead. Signing out over a 5xx would be a self-inflicted\n // logout, so anything else propagates without expiring the session.\n if (isUnauthorized(error)) onSessionExpired?.();\n throw error;\n }\n };\n\n // N concurrent 401s must cause one rotation: each extra one invalidates the tokens the others got.\n const refreshShared = dedupe(doRefresh);\n\n const getIdToken = async ({ forceRefresh = false } = {}): Promise<string | null> => {\n const tokens = getTokens();\n if (tokens === null) return null;\n if (forceRefresh) return (await refreshShared()).token;\n\n const expiresAt = readTokenExpiry(tokens.token);\n // Unknown expiry is left to the 401 path; a `0` here would rotate on every request.\n if (expiresAt === null) return tokens.token;\n if (Date.now() < expiresAt - refreshLeadTimeMs) return tokens.token;\n\n // Proactive rotation is best-effort — a real 401 drives the mandatory one.\n return refreshShared()\n .then((fresh) => fresh.token)\n .catch(() => tokens.token);\n };\n\n return { getIdToken, getTokens, notifyExpired: () => onSessionExpired?.() };\n};\n","import { HttpHeader, type MiddlewareFn } from '../../fetchClient';\nimport { isUnauthorized } from '../../keboola/bearer/credentials';\n\nimport type { DeveloperPortalCredential } from './credential';\n\n/**\n * Stamps the id token, and on a 401 force-refreshes and replays once. Mirrors\n * `createBearerAuthMiddleware`, minus the `Bearer ` prefix apps-api does not use.\n *\n * An anonymous session passes through unstamped, so a failed login is never read as session expiry.\n */\nexport const createDeveloperPortalAuthMiddleware = (\n credential: DeveloperPortalCredential,\n): MiddlewareFn => {\n return (next) => async (request) => {\n const token = await credential.getIdToken();\n if (token === null) return next(request);\n\n // Cloned before the first attempt consumes the body. Cloning the `Request` carries a binary or\n // FormData payload through byte-for-byte.\n const retryRequest = request.request.clone();\n request.request.headers.set(HttpHeader.AUTHORIZATION, token);\n\n try {\n return await next(request);\n } catch (error) {\n if (!isUnauthorized(error)) throw error;\n\n const refreshed = await credential.getIdToken({ forceRefresh: true }).catch(() => null);\n // Nothing to replay with, because a concurrent sign-out landed or the rotation failed. The\n // caller's request failed with a 401, so that is what they see.\n if (refreshed === null) throw error;\n retryRequest.headers.set(HttpHeader.AUTHORIZATION, refreshed);\n\n try {\n return await next({ ...request, request: retryRequest });\n } catch (retryError) {\n if (isUnauthorized(retryError)) credential.notifyExpired();\n throw retryError;\n }\n }\n };\n};\n","import { createDeveloperPortalClient } from '../../clients/developerPortal/developerPortalClient';\nimport type { DeveloperPortalTokens } from '../../clients/developerPortal/tokens';\nimport type { LoginSuccess } from '../../clients/developerPortal/types';\nimport type { MiddlewareFn } from '../../fetchClient';\nimport { createStore } from '../../utils';\n\nimport { createDeveloperPortalCredential } from './credential';\nimport { createDeveloperPortalAuthMiddleware } from './middleware';\n\nexport type DeveloperPortalSdkOptions = {\n baseUrl: string;\n /** Session to restore on construction. */\n tokens?: DeveloperPortalTokens | null;\n /** Persistence hook, fired on sign-in, every rotation, and with `null` on sign-out or expiry. */\n onTokensChange?: (tokens: DeveloperPortalTokens | null) => void;\n /** The session is dead; tokens are already cleared. The consumer redirects to sign-in. */\n onSessionExpired?: () => void;\n refreshLeadTimeMs?: number;\n middlewares?: MiddlewareFn[];\n};\n\n/**\n * `createDeveloperPortalClient` plus the session that keeps it authenticated.\n *\n * - Builds its own client: middleware, credential and session depend on each other in a cycle.\n */\nexport const createDeveloperPortalSdk = ({\n baseUrl,\n tokens: initialTokens = null,\n onTokensChange,\n onSessionExpired,\n refreshLeadTimeMs,\n middlewares = [],\n}: DeveloperPortalSdkOptions) => {\n const store = createStore<DeveloperPortalTokens | null>(initialTokens);\n store.subscribe((value) => onTokensChange?.(value));\n\n const getTokens = () => store.get();\n\n const requireTokens = (action: string): DeveloperPortalTokens => {\n const tokens = store.get();\n if (tokens === null) {\n throw new Error(`Developer Portal: ${action} requires a signed-in session.`);\n }\n return tokens;\n };\n\n // No auth middleware, so a rotation cannot recurse into the middleware that triggered it.\n const refreshClient = createDeveloperPortalClient({ baseUrl, middlewares });\n\n const refresh = async (): Promise<DeveloperPortalTokens> => {\n const before = store.get();\n if (before === null) {\n throw new Error('Cannot refresh the Developer Portal session: signed out.');\n }\n\n const { token, accessToken } = await refreshClient.auth.refreshTokens(before.refreshToken);\n const next: DeveloperPortalTokens = { token, accessToken, refreshToken: before.refreshToken };\n\n // Sign-out wins over an in-flight rotation; otherwise the response signs the user back in.\n const after = store.get();\n if (after === null || after.refreshToken !== before.refreshToken) {\n throw new Error('Developer Portal session changed while refreshing; discarding the result.');\n }\n\n store.set(next);\n return next;\n };\n\n const clear = () => store.set(null);\n\n const credential = createDeveloperPortalCredential({\n refresh,\n getTokens,\n refreshLeadTimeMs,\n onSessionExpired: () => {\n clear();\n onSessionExpired?.();\n },\n });\n\n const client = createDeveloperPortalClient({\n baseUrl,\n middlewares,\n authMiddleware: createDeveloperPortalAuthMiddleware(credential),\n });\n\n // `refreshTokens` is not re-exposed: rotating outside the store leaves stale tokens.\n const { refreshTokens: _storeBypassingRotation, ...auth } = client.auth;\n\n const api = {\n ...client,\n auth: {\n ...auth,\n logout: async (signal?: AbortSignal) =>\n client.auth.logout(requireTokens('sign-out').accessToken, signal),\n // Both MFA calls bypass the reactive 401 path, so rotate proactively first.\n setupMfa: async (signal?: AbortSignal) => {\n await credential.getIdToken();\n const { token, accessToken } = requireTokens('MFA enrolment');\n return client.auth.setupMfa({ idToken: token, accessToken }, signal);\n },\n confirmMfa: async (code: string, signal?: AbortSignal) => {\n await credential.getIdToken();\n const { token, accessToken } = requireTokens('MFA confirmation');\n return client.auth.confirmMfa({ idToken: token, accessToken, code }, signal);\n },\n },\n };\n\n const session = {\n set: (result: LoginSuccess) => {\n store.set({\n token: result.token,\n accessToken: result.accessToken,\n refreshToken: result.refreshToken,\n });\n },\n get: getTokens,\n subscribe: store.subscribe,\n clear,\n /** Rotates if the id token is inside the lead time. Best-effort; the 401 path is the guarantee. */\n ensureFresh: async (): Promise<void> => {\n await credential.getIdToken();\n },\n /** Rotates now, for a change of claims (e.g. leaving a vendor). Propagates failure. */\n refresh: async (): Promise<void> => {\n await credential.getIdToken({ forceRefresh: true });\n },\n isAuthenticated: () => store.get() !== null,\n };\n\n return { api, session };\n};\n\nexport type DeveloperPortalSdk = ReturnType<typeof createDeveloperPortalSdk>;\nexport type DeveloperPortalSession = DeveloperPortalSdk['session'];\n"],"mappings":"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"}
|
|
@@ -1,71 +1,4 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const DEFAULT_EXECUTE_QUERY_PAGE_SIZE = 5e3;
|
|
6
|
-
const DEFAULT_POLL_INTERVAL = 500;
|
|
7
|
-
const DEFAULT_TIMEOUT = 3e5;
|
|
8
|
-
const assertPositiveInteger = (name, value) => {
|
|
9
|
-
if (!Number.isInteger(value) || value <= 0) throw new require_ValidationError.ValidationError(`${name} must be a positive integer, got ${value}`);
|
|
10
|
-
};
|
|
11
|
-
/**
|
|
12
|
-
* Higher-level SDK composing queryService client calls with job polling and result-page
|
|
13
|
-
* auto-pagination for orchestrated workflows.
|
|
14
|
-
*/
|
|
15
|
-
const createQueryServiceSdk = ({ queryServiceClient }) => {
|
|
16
|
-
/**
|
|
17
|
-
* Fetches every page of a statement's results, merging them into a single {@link QueryResult}.
|
|
18
|
-
*
|
|
19
|
-
* - `columns`, `status`, `rowsAffected` and `message` describe the statement, so they are read off
|
|
20
|
-
* the first page; `collectByOffset` hands back only the rows.
|
|
21
|
-
* - A second consumer that needs the envelope back is the cue for a `collectPagesByOffset`
|
|
22
|
-
* sibling resolving `{ first, items }`, not for widening this closure.
|
|
23
|
-
*/
|
|
24
|
-
const fetchAllResultPages = async (queryJobId, statementId, pageSize, maxRows) => {
|
|
25
|
-
let firstPage;
|
|
26
|
-
const data = await require_collectByOffset.collectByOffset({
|
|
27
|
-
pageSize,
|
|
28
|
-
limit: maxRows,
|
|
29
|
-
fetchFrom: async (offset, pageSize) => {
|
|
30
|
-
const page = await queryServiceClient.getQueryResults(queryJobId, statementId, {
|
|
31
|
-
offset,
|
|
32
|
-
pageSize
|
|
33
|
-
});
|
|
34
|
-
firstPage ??= page;
|
|
35
|
-
return page.data ?? [];
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
if (!firstPage) throw new Error(`No result pages returned for statement ${statementId}`);
|
|
39
|
-
return {
|
|
40
|
-
...firstPage,
|
|
41
|
-
data
|
|
42
|
-
};
|
|
43
|
-
};
|
|
44
|
-
/**
|
|
45
|
-
* Submits a query job, waits for completion, and fetches results for every statement —
|
|
46
|
-
* auto-paginating each statement's results so sets larger than `pageSize` come back in full
|
|
47
|
-
* instead of being truncated at the API's default page size.
|
|
48
|
-
*
|
|
49
|
-
* - Throws {@link QueryJobError} when the job ended `failed`, carrying the failed statements.
|
|
50
|
-
* - Throws the poll's failure when the wait timed out, was aborted or ran out of attempts.
|
|
51
|
-
*/
|
|
52
|
-
const executeQuery = async (branchId, workspaceId, body, options = {}) => {
|
|
53
|
-
const { pageSize = DEFAULT_EXECUTE_QUERY_PAGE_SIZE, maxRows, interval = DEFAULT_POLL_INTERVAL, timeout = DEFAULT_TIMEOUT, ...pollOptions } = options;
|
|
54
|
-
assertPositiveInteger("pageSize", pageSize);
|
|
55
|
-
if (maxRows !== void 0) assertPositiveInteger("maxRows", maxRows);
|
|
56
|
-
const { queryJobId } = await queryServiceClient.createQueryJob(branchId, workspaceId, body);
|
|
57
|
-
const status = await queryServiceClient.poller({
|
|
58
|
-
init: { id: queryJobId },
|
|
59
|
-
interval,
|
|
60
|
-
timeout,
|
|
61
|
-
...pollOptions
|
|
62
|
-
}).wait();
|
|
63
|
-
const results = [];
|
|
64
|
-
for (const statement of status.statements) results.push(await fetchAllResultPages(queryJobId, statement.id, pageSize, maxRows));
|
|
65
|
-
return results;
|
|
66
|
-
};
|
|
67
|
-
return { executeQuery };
|
|
68
|
-
};
|
|
69
|
-
exports.createQueryServiceSdk = createQueryServiceSdk;
|
|
70
|
-
|
|
71
|
-
//# sourceMappingURL=index.cjs.map
|
|
2
|
+
const require_queryService = require("../../queryService-B4h1Ez44.cjs");
|
|
3
|
+
exports.cellParser = require_queryService.cellParser;
|
|
4
|
+
exports.createQueryServiceSdk = require_queryService.createQueryServiceSdk;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { b as
|
|
2
|
-
export { ExecuteQueryOptions, createQueryServiceSdk };
|
|
1
|
+
import { S as createQueryServiceSdk, b as cellParser, x as ExecuteQueryOptions } from "../../index-BtvWJBYh.cjs";
|
|
2
|
+
export { ExecuteQueryOptions, cellParser, createQueryServiceSdk };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { b as
|
|
2
|
-
export { ExecuteQueryOptions, createQueryServiceSdk };
|
|
1
|
+
import { S as createQueryServiceSdk, b as cellParser, x as ExecuteQueryOptions } from "../../index-Df30jjpZ.js";
|
|
2
|
+
export { ExecuteQueryOptions, cellParser, createQueryServiceSdk };
|
|
@@ -1,70 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { t as collectByOffset } from "../../collectByOffset-i8Tdoze7.js";
|
|
3
|
-
// src/sdks/queryService/queryServiceSdk.ts
|
|
4
|
-
const DEFAULT_EXECUTE_QUERY_PAGE_SIZE = 5e3;
|
|
5
|
-
const DEFAULT_POLL_INTERVAL = 500;
|
|
6
|
-
const DEFAULT_TIMEOUT = 3e5;
|
|
7
|
-
const assertPositiveInteger = (name, value) => {
|
|
8
|
-
if (!Number.isInteger(value) || value <= 0) throw new ValidationError(`${name} must be a positive integer, got ${value}`);
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* Higher-level SDK composing queryService client calls with job polling and result-page
|
|
12
|
-
* auto-pagination for orchestrated workflows.
|
|
13
|
-
*/
|
|
14
|
-
const createQueryServiceSdk = ({ queryServiceClient }) => {
|
|
15
|
-
/**
|
|
16
|
-
* Fetches every page of a statement's results, merging them into a single {@link QueryResult}.
|
|
17
|
-
*
|
|
18
|
-
* - `columns`, `status`, `rowsAffected` and `message` describe the statement, so they are read off
|
|
19
|
-
* the first page; `collectByOffset` hands back only the rows.
|
|
20
|
-
* - A second consumer that needs the envelope back is the cue for a `collectPagesByOffset`
|
|
21
|
-
* sibling resolving `{ first, items }`, not for widening this closure.
|
|
22
|
-
*/
|
|
23
|
-
const fetchAllResultPages = async (queryJobId, statementId, pageSize, maxRows) => {
|
|
24
|
-
let firstPage;
|
|
25
|
-
const data = await collectByOffset({
|
|
26
|
-
pageSize,
|
|
27
|
-
limit: maxRows,
|
|
28
|
-
fetchFrom: async (offset, pageSize) => {
|
|
29
|
-
const page = await queryServiceClient.getQueryResults(queryJobId, statementId, {
|
|
30
|
-
offset,
|
|
31
|
-
pageSize
|
|
32
|
-
});
|
|
33
|
-
firstPage ??= page;
|
|
34
|
-
return page.data ?? [];
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
if (!firstPage) throw new Error(`No result pages returned for statement ${statementId}`);
|
|
38
|
-
return {
|
|
39
|
-
...firstPage,
|
|
40
|
-
data
|
|
41
|
-
};
|
|
42
|
-
};
|
|
43
|
-
/**
|
|
44
|
-
* Submits a query job, waits for completion, and fetches results for every statement —
|
|
45
|
-
* auto-paginating each statement's results so sets larger than `pageSize` come back in full
|
|
46
|
-
* instead of being truncated at the API's default page size.
|
|
47
|
-
*
|
|
48
|
-
* - Throws {@link QueryJobError} when the job ended `failed`, carrying the failed statements.
|
|
49
|
-
* - Throws the poll's failure when the wait timed out, was aborted or ran out of attempts.
|
|
50
|
-
*/
|
|
51
|
-
const executeQuery = async (branchId, workspaceId, body, options = {}) => {
|
|
52
|
-
const { pageSize = DEFAULT_EXECUTE_QUERY_PAGE_SIZE, maxRows, interval = DEFAULT_POLL_INTERVAL, timeout = DEFAULT_TIMEOUT, ...pollOptions } = options;
|
|
53
|
-
assertPositiveInteger("pageSize", pageSize);
|
|
54
|
-
if (maxRows !== void 0) assertPositiveInteger("maxRows", maxRows);
|
|
55
|
-
const { queryJobId } = await queryServiceClient.createQueryJob(branchId, workspaceId, body);
|
|
56
|
-
const status = await queryServiceClient.poller({
|
|
57
|
-
init: { id: queryJobId },
|
|
58
|
-
interval,
|
|
59
|
-
timeout,
|
|
60
|
-
...pollOptions
|
|
61
|
-
}).wait();
|
|
62
|
-
const results = [];
|
|
63
|
-
for (const statement of status.statements) results.push(await fetchAllResultPages(queryJobId, statement.id, pageSize, maxRows));
|
|
64
|
-
return results;
|
|
65
|
-
};
|
|
66
|
-
return { executeQuery };
|
|
67
|
-
};
|
|
68
|
-
export { createQueryServiceSdk };
|
|
69
|
-
|
|
70
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
export { t as cellParser, n as createQueryServiceSdk } from "../../queryService-DE_yE_S4.js";
|