@opengeni/vaults-keeper 0.0.0-stage → 3.2.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/LICENSE +190 -0
- package/README.md +147 -2
- package/dist/bootstrap.d.ts +40 -0
- package/dist/config.d.ts +41 -0
- package/dist/connection.d.ts +37 -0
- package/dist/contract.d.ts +6 -0
- package/dist/errors.d.ts +36 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1450 -0
- package/dist/index.js.map +1 -0
- package/dist/mapping.d.ts +59 -0
- package/dist/naming.d.ts +8 -0
- package/dist/operations.d.ts +7 -0
- package/dist/provider.d.ts +81 -0
- package/dist/sdk.d.ts +81 -0
- package/package.json +42 -4
- package/src/bootstrap.ts +182 -0
- package/src/config.ts +161 -0
- package/src/connection.ts +75 -0
- package/src/contract.ts +29 -0
- package/src/errors.ts +196 -0
- package/src/index.ts +45 -0
- package/src/mapping.ts +373 -0
- package/src/naming.ts +44 -0
- package/src/operations.ts +65 -0
- package/src/provider.ts +681 -0
- package/src/sdk.ts +282 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1450 @@
|
|
|
1
|
+
// src/contract.ts
|
|
2
|
+
import {
|
|
3
|
+
VaultProviderError
|
|
4
|
+
} from "@opengeni/vaults";
|
|
5
|
+
import {
|
|
6
|
+
VAULT_LIMITS,
|
|
7
|
+
parseTotpInput
|
|
8
|
+
} from "@opengeni/contracts";
|
|
9
|
+
|
|
10
|
+
// src/config.ts
|
|
11
|
+
var KEEPER_REGION_HOSTS = {
|
|
12
|
+
US: "keepersecurity.com",
|
|
13
|
+
EU: "keepersecurity.eu",
|
|
14
|
+
AU: "keepersecurity.com.au",
|
|
15
|
+
GOV: "govcloud.keepersecurity.us",
|
|
16
|
+
JP: "keepersecurity.jp",
|
|
17
|
+
CA: "keepersecurity.ca",
|
|
18
|
+
IL5: "il5.keepersecurity.us"
|
|
19
|
+
};
|
|
20
|
+
var KEEPER_DEFAULT_ALLOWED_HOSTS = Object.values(KEEPER_REGION_HOSTS);
|
|
21
|
+
var KEEPER_CONFIG_KEYS = [
|
|
22
|
+
"hostname",
|
|
23
|
+
"clientId",
|
|
24
|
+
"clientKey",
|
|
25
|
+
"privateKey",
|
|
26
|
+
"appKey",
|
|
27
|
+
"appOwnerPublicKey",
|
|
28
|
+
"serverPublicKeyId",
|
|
29
|
+
"serverPublicKey"
|
|
30
|
+
];
|
|
31
|
+
var KNOWN = new Set(KEEPER_CONFIG_KEYS);
|
|
32
|
+
function invalid(message) {
|
|
33
|
+
throw new VaultProviderError("provider_auth_invalid", message);
|
|
34
|
+
}
|
|
35
|
+
function parseKeeperConfiguration(input) {
|
|
36
|
+
if (typeof input !== "string") {
|
|
37
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
38
|
+
return invalid("The Keeper configuration is not KSM JSON");
|
|
39
|
+
}
|
|
40
|
+
return valuesOf(input);
|
|
41
|
+
}
|
|
42
|
+
let text = input.trim();
|
|
43
|
+
if (!text.startsWith("{")) {
|
|
44
|
+
try {
|
|
45
|
+
text = Buffer.from(text, "base64").toString("utf8").trim();
|
|
46
|
+
} catch {
|
|
47
|
+
invalid("The Keeper configuration is not KSM JSON");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let parsed;
|
|
51
|
+
try {
|
|
52
|
+
parsed = JSON.parse(text);
|
|
53
|
+
} catch {
|
|
54
|
+
return invalid("The Keeper configuration is not KSM JSON");
|
|
55
|
+
}
|
|
56
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
57
|
+
return invalid("The Keeper configuration is not KSM JSON");
|
|
58
|
+
}
|
|
59
|
+
return valuesOf(parsed);
|
|
60
|
+
}
|
|
61
|
+
function valuesOf(parsed) {
|
|
62
|
+
const values = /* @__PURE__ */ new Map();
|
|
63
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
64
|
+
if (!KNOWN.has(key)) continue;
|
|
65
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 4096) {
|
|
66
|
+
invalid(`The Keeper configuration has an invalid ${key}`);
|
|
67
|
+
}
|
|
68
|
+
values.set(key, value);
|
|
69
|
+
}
|
|
70
|
+
for (const key of ["hostname", "clientId", "privateKey", "appKey"]) {
|
|
71
|
+
if (!values.get(key)) {
|
|
72
|
+
invalid(
|
|
73
|
+
key === "appKey" ? "The Keeper configuration is not bound yet; connect with a one-time token" : `The Keeper configuration has no ${key}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return values;
|
|
78
|
+
}
|
|
79
|
+
function serializeKeeperConfiguration(values) {
|
|
80
|
+
const out = {};
|
|
81
|
+
for (const key of KEEPER_CONFIG_KEYS) {
|
|
82
|
+
const value = values.get(key);
|
|
83
|
+
if (value !== void 0) out[key] = value;
|
|
84
|
+
}
|
|
85
|
+
return JSON.stringify(out);
|
|
86
|
+
}
|
|
87
|
+
function keeperConfigurationRecord(configuration) {
|
|
88
|
+
return JSON.parse(
|
|
89
|
+
serializeKeeperConfiguration(parseKeeperConfiguration(configuration))
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
function assertAllowedHost(hostname, allowed) {
|
|
93
|
+
const host = (hostname ?? "").trim().toLowerCase();
|
|
94
|
+
if (!host || !allowed.includes(host)) {
|
|
95
|
+
throw new VaultProviderError(
|
|
96
|
+
"provider_auth_invalid",
|
|
97
|
+
"The Keeper region host is not one Opengeni connects to"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return host;
|
|
101
|
+
}
|
|
102
|
+
function memoryKeeperStorage(initial = /* @__PURE__ */ new Map()) {
|
|
103
|
+
const values = new Map(initial);
|
|
104
|
+
const before = serializeKeeperConfiguration(initial);
|
|
105
|
+
const set = (key, value) => {
|
|
106
|
+
if (!KNOWN.has(key)) throw new Error(`unexpected Keeper storage key ${key}`);
|
|
107
|
+
values.set(key, value);
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
values,
|
|
111
|
+
changed: () => serializeKeeperConfiguration(values) !== before,
|
|
112
|
+
getString: async (key) => values.get(key),
|
|
113
|
+
saveString: async (key, value) => set(key, value),
|
|
114
|
+
getBytes: async (key) => {
|
|
115
|
+
const value = values.get(key);
|
|
116
|
+
return value === void 0 ? void 0 : new Uint8Array(Buffer.from(value, "base64"));
|
|
117
|
+
},
|
|
118
|
+
saveBytes: async (key, value) => set(key, Buffer.from(value).toString("base64")),
|
|
119
|
+
delete: async (key) => {
|
|
120
|
+
values.delete(key);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/errors.ts
|
|
126
|
+
var AUTH_HINT = /signature|client ?id|clientid|one[- ]?time|token|expired|ip address|ip lock|locked out|not bound|unbound|device|revoked/i;
|
|
127
|
+
var NOT_FOUND_HINT = /not found|does not exist|no such|unknown (?:record|folder)/i;
|
|
128
|
+
var CONFLICT_HINT = /revision|out of (?:date|sync)|conflict|modified/i;
|
|
129
|
+
function keeperErrorBody(error) {
|
|
130
|
+
if (!(error instanceof Error)) return null;
|
|
131
|
+
const text = error.message.trim();
|
|
132
|
+
if (!text.startsWith("{")) return null;
|
|
133
|
+
try {
|
|
134
|
+
const body = JSON.parse(text);
|
|
135
|
+
const code = body.result_code ?? body.error;
|
|
136
|
+
const retry = body.retry_after;
|
|
137
|
+
return {
|
|
138
|
+
code: typeof code === "string" ? code.slice(0, 64) : null,
|
|
139
|
+
message: typeof body.message === "string" ? body.message : "",
|
|
140
|
+
retryAfterSeconds: typeof retry === "number" && retry >= 0 ? retry : null
|
|
141
|
+
};
|
|
142
|
+
} catch {
|
|
143
|
+
const code = /"(?:result_code|error)"\s*:\s*"([a-z_]{1,64})"/.exec(text)?.[1] ?? null;
|
|
144
|
+
return { code, message: "", retryAfterSeconds: null };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function safeCode(code) {
|
|
148
|
+
return code && /^[a-z0-9_]{1,64}$/i.test(code) ? code : "error";
|
|
149
|
+
}
|
|
150
|
+
function isAbortLike(error) {
|
|
151
|
+
if (!(error instanceof Error)) return false;
|
|
152
|
+
return error.name === "AbortError" || error.name === "TimeoutError" || /timed out after \d+ms/.test(error.message);
|
|
153
|
+
}
|
|
154
|
+
function mapKeeperError(error, subject) {
|
|
155
|
+
if (error instanceof VaultProviderError) return error;
|
|
156
|
+
if (error instanceof Error && error.name === "KeeperThrottleError") {
|
|
157
|
+
return new VaultProviderError(
|
|
158
|
+
"provider_rate_limited",
|
|
159
|
+
"Keeper is rate limiting this application",
|
|
160
|
+
6e4
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (isAbortLike(error)) {
|
|
164
|
+
return new VaultProviderError(
|
|
165
|
+
"provider_unavailable",
|
|
166
|
+
`Keeper did not answer in time (${subject})`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const body = keeperErrorBody(error);
|
|
170
|
+
if (body) {
|
|
171
|
+
const { code, message } = body;
|
|
172
|
+
if (code === "throttled") {
|
|
173
|
+
return new VaultProviderError(
|
|
174
|
+
"provider_rate_limited",
|
|
175
|
+
"Keeper is rate limiting this application",
|
|
176
|
+
(body.retryAfterSeconds ?? 60) * 1e3
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (code === "access_denied" || code === "invalid_client" || code === "invalid_client_version") {
|
|
180
|
+
if (code !== "access_denied" || AUTH_HINT.test(message)) {
|
|
181
|
+
return new VaultProviderError(
|
|
182
|
+
"provider_auth_invalid",
|
|
183
|
+
"Keeper rejected this application's credentials; reconnect with a new one-time token"
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (NOT_FOUND_HINT.test(message)) {
|
|
187
|
+
return new VaultProviderError(
|
|
188
|
+
"provider_not_found",
|
|
189
|
+
"No such Keeper shared folder or record"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return new VaultProviderError(
|
|
193
|
+
"provider_permission_denied",
|
|
194
|
+
`The Keeper application may not ${subject}`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (NOT_FOUND_HINT.test(message) || code && /not_found|invalid_record|record_uid/.test(code)) {
|
|
198
|
+
return new VaultProviderError("provider_not_found", "No such Keeper shared folder or record");
|
|
199
|
+
}
|
|
200
|
+
if (CONFLICT_HINT.test(message) || code && /revision|conflict/.test(code)) {
|
|
201
|
+
return new VaultProviderError(
|
|
202
|
+
"provider_conflict",
|
|
203
|
+
"The Keeper record changed since it was read; read it again before writing"
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return new VaultProviderError(
|
|
207
|
+
"provider_unavailable",
|
|
208
|
+
`Keeper rejected the request (${safeCode(code)})`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const status = error instanceof Error ? /unknown ksm error, code (\d{3})/.exec(error.message)?.[1] : void 0;
|
|
212
|
+
if (status) {
|
|
213
|
+
const code = Number(status);
|
|
214
|
+
if (code === 401) {
|
|
215
|
+
return new VaultProviderError(
|
|
216
|
+
"provider_auth_invalid",
|
|
217
|
+
"Keeper rejected this application's credentials"
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (code === 403) {
|
|
221
|
+
return new VaultProviderError(
|
|
222
|
+
"provider_permission_denied",
|
|
223
|
+
`The Keeper application may not ${subject}`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (code === 404)
|
|
227
|
+
return new VaultProviderError("provider_not_found", "No such Keeper shared folder or record");
|
|
228
|
+
if (code === 429)
|
|
229
|
+
return new VaultProviderError(
|
|
230
|
+
"provider_rate_limited",
|
|
231
|
+
"Keeper is rate limiting this application",
|
|
232
|
+
6e4
|
|
233
|
+
);
|
|
234
|
+
return new VaultProviderError("provider_unavailable", `Keeper returned HTTP ${code}`);
|
|
235
|
+
}
|
|
236
|
+
return new VaultProviderError("provider_unavailable", `Keeper could not ${subject}`);
|
|
237
|
+
}
|
|
238
|
+
var KEEPER_DEFINITE_REJECTION_STATUSES = /* @__PURE__ */ new Set([
|
|
239
|
+
400,
|
|
240
|
+
401,
|
|
241
|
+
403,
|
|
242
|
+
404,
|
|
243
|
+
409,
|
|
244
|
+
429
|
|
245
|
+
]);
|
|
246
|
+
function mapKeeperMutationError(error, subject, tracker) {
|
|
247
|
+
const definite = tracker.sent === 0 || tracker.lastStatus !== null && KEEPER_DEFINITE_REJECTION_STATUSES.has(tracker.lastStatus);
|
|
248
|
+
if (!definite) {
|
|
249
|
+
return new VaultProviderError(
|
|
250
|
+
"provider_outcome_unknown",
|
|
251
|
+
`Keeper may or may not have completed "${subject}"; check the record before retrying`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return mapKeeperError(error, subject);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/naming.ts
|
|
258
|
+
var FORBIDDEN_NAME_CHARS = /[/{}#\\\u0000-\u001f\u007f-\u009f]/g;
|
|
259
|
+
function nameKey(name) {
|
|
260
|
+
return name.normalize("NFC").trim().toLowerCase();
|
|
261
|
+
}
|
|
262
|
+
function safeSecretName(raw, fallback) {
|
|
263
|
+
const max = VAULT_LIMITS.nameMaxChars;
|
|
264
|
+
let name = raw.replace(FORBIDDEN_NAME_CHARS, "-").trim().slice(0, max).trim();
|
|
265
|
+
if (!name) name = fallback.replace(FORBIDDEN_NAME_CHARS, "-").trim().slice(0, max) || "item";
|
|
266
|
+
if (/^id:/i.test(name)) name = `id-${name.slice(3)}`;
|
|
267
|
+
return name;
|
|
268
|
+
}
|
|
269
|
+
function safeFieldName(raw, fallback) {
|
|
270
|
+
const name = safeSecretName(raw, fallback);
|
|
271
|
+
return nameKey(name) === "otp" ? "otp-code" : name;
|
|
272
|
+
}
|
|
273
|
+
function uniqueName(name, taken) {
|
|
274
|
+
let candidate = name;
|
|
275
|
+
for (let n = 2; taken.has(nameKey(candidate)); n += 1) {
|
|
276
|
+
const suffix = ` (${n})`;
|
|
277
|
+
candidate = `${name.slice(0, VAULT_LIMITS.nameMaxChars - suffix.length)}${suffix}`;
|
|
278
|
+
}
|
|
279
|
+
taken.add(nameKey(candidate));
|
|
280
|
+
return candidate;
|
|
281
|
+
}
|
|
282
|
+
function totpUri(params, label) {
|
|
283
|
+
const query = new URLSearchParams({
|
|
284
|
+
secret: params.secretBase32,
|
|
285
|
+
algorithm: params.algorithm,
|
|
286
|
+
digits: String(params.digits),
|
|
287
|
+
period: String(params.periodSeconds)
|
|
288
|
+
});
|
|
289
|
+
return `otpauth://totp/${encodeURIComponent(label || "Opengeni")}?${query.toString()}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/mapping.ts
|
|
293
|
+
var NOTES_FIELD_ID = "@notes";
|
|
294
|
+
var FILE_FIELD_PREFIX = "@file:";
|
|
295
|
+
var CONCEALED_TYPES = /* @__PURE__ */ new Set(["password", "secret", "pinCode", "note"]);
|
|
296
|
+
var TOTP_TYPES = /* @__PURE__ */ new Set(["oneTimeCode", "otp"]);
|
|
297
|
+
var CONCEALED_COMPLEX = /* @__PURE__ */ new Set(["paymentCard", "bankAccount", "keyPair", "securityQuestion"]);
|
|
298
|
+
var SKIPPED_TYPES = /* @__PURE__ */ new Set(["fileRef", "passkey", "recordRef", "cardRef", "addressRef"]);
|
|
299
|
+
var DATE_TYPES = /* @__PURE__ */ new Set(["date", "birthDate", "expirationDate"]);
|
|
300
|
+
var STANDARD_NAMES = {
|
|
301
|
+
login: "username",
|
|
302
|
+
password: "password",
|
|
303
|
+
oneTimeCode: "one-time password",
|
|
304
|
+
otp: "one-time password",
|
|
305
|
+
url: "website",
|
|
306
|
+
email: "email",
|
|
307
|
+
note: "note",
|
|
308
|
+
pinCode: "PIN"
|
|
309
|
+
};
|
|
310
|
+
var API_KEY_TYPES = /* @__PURE__ */ new Set([
|
|
311
|
+
"serverCredentials",
|
|
312
|
+
"databaseCredentials",
|
|
313
|
+
"sshKeys",
|
|
314
|
+
"softwareLicense",
|
|
315
|
+
"pamDatabase",
|
|
316
|
+
"pamMachine"
|
|
317
|
+
]);
|
|
318
|
+
function itemTypeForRecordType(type) {
|
|
319
|
+
if (type === "login") return "login";
|
|
320
|
+
if (type === "encryptedNotes") return "note";
|
|
321
|
+
if (type === "file") return "file";
|
|
322
|
+
if (type && API_KEY_TYPES.has(type)) return "api_key";
|
|
323
|
+
return "note";
|
|
324
|
+
}
|
|
325
|
+
function kindFor(entry) {
|
|
326
|
+
if (SKIPPED_TYPES.has(entry.type)) return null;
|
|
327
|
+
if (TOTP_TYPES.has(entry.type)) return "totp";
|
|
328
|
+
if (CONCEALED_TYPES.has(entry.type) || CONCEALED_COMPLEX.has(entry.type)) return "concealed";
|
|
329
|
+
return "text";
|
|
330
|
+
}
|
|
331
|
+
function valueText(entry) {
|
|
332
|
+
const values = Array.isArray(entry.value) ? entry.value : [];
|
|
333
|
+
if (values.length === 0) return "";
|
|
334
|
+
if (DATE_TYPES.has(entry.type) && typeof values[0] === "number") {
|
|
335
|
+
const date = new Date(values[0]);
|
|
336
|
+
return Number.isNaN(date.getTime()) ? String(values[0]) : date.toISOString();
|
|
337
|
+
}
|
|
338
|
+
if (values.length === 1 && (typeof values[0] === "string" || typeof values[0] === "number" || typeof values[0] === "boolean")) {
|
|
339
|
+
return String(values[0]);
|
|
340
|
+
}
|
|
341
|
+
return JSON.stringify(values.length === 1 ? values[0] : values);
|
|
342
|
+
}
|
|
343
|
+
var Collector = class {
|
|
344
|
+
fields = [];
|
|
345
|
+
names = /* @__PURE__ */ new Set();
|
|
346
|
+
ids = /* @__PURE__ */ new Map();
|
|
347
|
+
add(baseId, rawName, kind, source, value) {
|
|
348
|
+
const seen = (this.ids.get(baseId) ?? 0) + 1;
|
|
349
|
+
this.ids.set(baseId, seen);
|
|
350
|
+
this.fields.push({
|
|
351
|
+
externalFieldId: seen === 1 ? baseId : `${baseId}#${seen}`,
|
|
352
|
+
name: uniqueName(safeFieldName(rawName, "field"), this.names),
|
|
353
|
+
kind,
|
|
354
|
+
source,
|
|
355
|
+
value
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
function recordData(record) {
|
|
360
|
+
const data = record.data;
|
|
361
|
+
return data && typeof data === "object" ? data : {};
|
|
362
|
+
}
|
|
363
|
+
function mapRecord(record) {
|
|
364
|
+
const data = recordData(record);
|
|
365
|
+
const collector = new Collector();
|
|
366
|
+
const websites = [];
|
|
367
|
+
const lists = [
|
|
368
|
+
["fields", Array.isArray(data.fields) ? data.fields : []],
|
|
369
|
+
["custom", Array.isArray(data.custom) ? data.custom : []]
|
|
370
|
+
];
|
|
371
|
+
for (const [list, entries] of lists) {
|
|
372
|
+
entries.forEach((entry, index) => {
|
|
373
|
+
if (!entry || typeof entry !== "object" || typeof entry.type !== "string") return;
|
|
374
|
+
const kind = kindFor(entry);
|
|
375
|
+
if (!kind) return;
|
|
376
|
+
const value = valueText(entry);
|
|
377
|
+
if (entry.type === "url") {
|
|
378
|
+
for (const url of Array.isArray(entry.value) ? entry.value : []) {
|
|
379
|
+
if (typeof url === "string" && url.trim()) websites.push(url);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (kind === "totp" && !value) return;
|
|
383
|
+
const label = typeof entry.label === "string" && entry.label.trim() ? entry.label : "";
|
|
384
|
+
const name = list === "fields" ? label || STANDARD_NAMES[entry.type] || entry.type : label || entry.type;
|
|
385
|
+
const baseId = list === "fields" ? `f:${entry.type}` : `c:${label || entry.type}`;
|
|
386
|
+
collector.add(baseId, name, kind, { kind: "field", list, index }, value);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (typeof data.notes === "string" && data.notes) {
|
|
390
|
+
collector.add(NOTES_FIELD_ID, "notes", "concealed", { kind: "notes" }, data.notes);
|
|
391
|
+
}
|
|
392
|
+
for (const file of record.files ?? []) {
|
|
393
|
+
const fileData = file.data ?? {};
|
|
394
|
+
const name = typeof fileData.title === "string" && fileData.title || typeof fileData.name === "string" && fileData.name || "file";
|
|
395
|
+
collector.add(`${FILE_FIELD_PREFIX}${file.fileUid}`, name, "file", { kind: "file", file }, "");
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
meta: {
|
|
399
|
+
externalItemId: record.recordUid,
|
|
400
|
+
name: safeSecretName(typeof data.title === "string" ? data.title : "", record.recordUid),
|
|
401
|
+
type: itemTypeForRecordType(typeof data.type === "string" ? data.type : void 0),
|
|
402
|
+
websites,
|
|
403
|
+
fields: collector.fields.map(({ externalFieldId, name, kind }) => ({
|
|
404
|
+
externalFieldId,
|
|
405
|
+
name,
|
|
406
|
+
kind
|
|
407
|
+
})),
|
|
408
|
+
version: Number.isFinite(record.revision) ? String(record.revision) : null,
|
|
409
|
+
updatedAt: null
|
|
410
|
+
},
|
|
411
|
+
fields: collector.fields
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function fileSize(file) {
|
|
415
|
+
const size = file.data?.size;
|
|
416
|
+
return typeof size === "number" && size >= 0 ? size : null;
|
|
417
|
+
}
|
|
418
|
+
function totpParamsFor(value) {
|
|
419
|
+
try {
|
|
420
|
+
return parseTotpInput(value);
|
|
421
|
+
} catch {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function unavailable(message) {
|
|
426
|
+
throw new VaultProviderError("provider_unavailable", message);
|
|
427
|
+
}
|
|
428
|
+
function textValue(field) {
|
|
429
|
+
const value = field.value ?? "";
|
|
430
|
+
if (Buffer.byteLength(value, "utf8") > VAULT_LIMITS.textValueMaxBytes) {
|
|
431
|
+
unavailable(`The value of "${field.name}" is larger than Opengeni stores`);
|
|
432
|
+
}
|
|
433
|
+
return value;
|
|
434
|
+
}
|
|
435
|
+
function totpValue(field, label) {
|
|
436
|
+
const params = totpParamsFor(textValue(field));
|
|
437
|
+
if (!params) unavailable(`"${field.name}" is not a TOTP seed or otpauth:// URI`);
|
|
438
|
+
return totpUri(params, label);
|
|
439
|
+
}
|
|
440
|
+
function customEntry(field, title) {
|
|
441
|
+
if (field.kind === "file") unavailable("Keeper files cannot be written from Opengeni");
|
|
442
|
+
if (field.kind === "totp")
|
|
443
|
+
return { type: "oneTimeCode", label: field.name, value: [totpValue(field, title)] };
|
|
444
|
+
return {
|
|
445
|
+
type: field.kind === "concealed" ? "secret" : "text",
|
|
446
|
+
label: field.name,
|
|
447
|
+
value: [textValue(field)]
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function buildCreateData(item) {
|
|
451
|
+
const title = item.name;
|
|
452
|
+
for (const field of item.fields) {
|
|
453
|
+
if (field.remove) unavailable("A new Keeper record has no fields to remove");
|
|
454
|
+
}
|
|
455
|
+
if (item.type === "login") {
|
|
456
|
+
let login = null;
|
|
457
|
+
let password = null;
|
|
458
|
+
let otp = null;
|
|
459
|
+
const custom = [];
|
|
460
|
+
const concealed = item.fields.filter((field) => field.kind === "concealed");
|
|
461
|
+
for (const field of item.fields) {
|
|
462
|
+
const key = nameKey(field.name);
|
|
463
|
+
if (field.kind === "text" && key === "username" && login === null) login = textValue(field);
|
|
464
|
+
else if (field.kind === "concealed" && password === null && (key === "password" || concealed.length === 1)) {
|
|
465
|
+
password = textValue(field);
|
|
466
|
+
} else if (field.kind === "totp" && otp === null) otp = totpValue(field, title);
|
|
467
|
+
else custom.push(customEntry(field, title));
|
|
468
|
+
}
|
|
469
|
+
const [first, ...rest] = item.websites.filter((url) => url.trim().length > 0);
|
|
470
|
+
for (const url of rest) custom.push({ type: "url", label: "website", value: [url] });
|
|
471
|
+
return {
|
|
472
|
+
type: "login",
|
|
473
|
+
title,
|
|
474
|
+
notes: "",
|
|
475
|
+
fields: [
|
|
476
|
+
{ type: "login", value: login === null ? [] : [login] },
|
|
477
|
+
{ type: "password", value: password === null ? [] : [password] },
|
|
478
|
+
{ type: "url", value: first ? [first] : [] },
|
|
479
|
+
{ type: "fileRef", value: [] },
|
|
480
|
+
{ type: "oneTimeCode", value: otp === null ? [] : [otp] }
|
|
481
|
+
],
|
|
482
|
+
custom
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (item.type === "note") {
|
|
486
|
+
let note = null;
|
|
487
|
+
const custom = [];
|
|
488
|
+
for (const field of item.fields) {
|
|
489
|
+
if (note === null && (field.kind === "text" || field.kind === "concealed"))
|
|
490
|
+
note = textValue(field);
|
|
491
|
+
else custom.push(customEntry(field, title));
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
type: "encryptedNotes",
|
|
495
|
+
title,
|
|
496
|
+
notes: "",
|
|
497
|
+
fields: [
|
|
498
|
+
{ type: "note", value: note === null ? [] : [note] },
|
|
499
|
+
{ type: "date", value: [] }
|
|
500
|
+
],
|
|
501
|
+
custom
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
return unavailable(
|
|
505
|
+
item.type === "passkey" ? "Passkeys live only in Opengeni vaults" : item.type === "api_key" ? "Keeper has no API key record type; create a login or a secure note" : "Keeper file records cannot be created from Opengeni"
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
function setEntryValue(entry, field, title) {
|
|
509
|
+
if (field.kind === "file") unavailable("Keeper files cannot be written from Opengeni");
|
|
510
|
+
if (TOTP_TYPES.has(entry.type)) {
|
|
511
|
+
entry.value = [totpValue(field, title)];
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const current = Array.isArray(entry.value) ? entry.value : [];
|
|
515
|
+
const scalar = current.length <= 1 && (current.length === 0 || typeof current[0] === "string");
|
|
516
|
+
if (!scalar || CONCEALED_COMPLEX.has(entry.type) || DATE_TYPES.has(entry.type)) {
|
|
517
|
+
unavailable(`"${field.name}" is a structured Keeper field; edit it in Keeper`);
|
|
518
|
+
}
|
|
519
|
+
entry.value = [field.kind === "totp" ? totpValue(field, title) : textValue(field)];
|
|
520
|
+
}
|
|
521
|
+
function applyPatch(record, item) {
|
|
522
|
+
const data = recordData(record);
|
|
523
|
+
data.fields = Array.isArray(data.fields) ? data.fields : [];
|
|
524
|
+
data.custom = Array.isArray(data.custom) ? data.custom : [];
|
|
525
|
+
const title = item.name;
|
|
526
|
+
const mapped = mapRecord(record).fields;
|
|
527
|
+
const removals = [];
|
|
528
|
+
for (const field of item.fields) {
|
|
529
|
+
const target = field.externalFieldId ? mapped.find((candidate) => candidate.externalFieldId === field.externalFieldId) : mapped.find((candidate) => nameKey(candidate.name) === nameKey(field.name));
|
|
530
|
+
if (field.externalFieldId && !target) {
|
|
531
|
+
throw new VaultProviderError(
|
|
532
|
+
"provider_not_found",
|
|
533
|
+
`The Keeper record has no field ${field.externalFieldId}`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
if (!target) {
|
|
537
|
+
if (field.remove) continue;
|
|
538
|
+
data.custom.push(customEntry(field, title));
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const source = target.source;
|
|
542
|
+
if (source.kind === "file") unavailable("Keeper files cannot be changed from Opengeni");
|
|
543
|
+
if (source.kind === "notes") {
|
|
544
|
+
data.notes = field.remove ? "" : textValue(field);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const entry = data[source.list][source.index];
|
|
548
|
+
if (field.remove) {
|
|
549
|
+
if (source.list === "custom") removals.push(source.index);
|
|
550
|
+
else entry.value = [];
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
setEntryValue(entry, field, title);
|
|
554
|
+
}
|
|
555
|
+
for (const index of removals.sort((a, b) => b - a)) data.custom.splice(index, 1);
|
|
556
|
+
data.title = title;
|
|
557
|
+
const websites = item.websites.filter((url) => url.trim().length > 0);
|
|
558
|
+
const current = mapRecord(record).meta.websites;
|
|
559
|
+
if (JSON.stringify(websites) !== JSON.stringify(current)) {
|
|
560
|
+
const standard = data.fields.find((entry) => entry.type === "url");
|
|
561
|
+
data.custom = data.custom.filter(
|
|
562
|
+
(entry) => !(entry.type === "url" && entry.label === "website")
|
|
563
|
+
);
|
|
564
|
+
const [first, ...rest] = websites;
|
|
565
|
+
if (standard) standard.value = first ? [first] : [];
|
|
566
|
+
else if (first) rest.unshift(first);
|
|
567
|
+
for (const url of rest) data.custom.push({ type: "url", label: "website", value: [url] });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/operations.ts
|
|
572
|
+
import { providerOperations } from "@opengeni/vaults";
|
|
573
|
+
function refused(what) {
|
|
574
|
+
throw new VaultProviderError(
|
|
575
|
+
"provider_permission_denied",
|
|
576
|
+
`${what} is not enabled for this vault connection`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
function checkWrite(operations, item) {
|
|
580
|
+
const totp = item.fields.some((field) => field.kind === "totp" && !field.remove);
|
|
581
|
+
if (item.externalItemId !== void 0) {
|
|
582
|
+
if (!operations.updateItem) refused("Updating items");
|
|
583
|
+
if (totp && !operations.updateTotp) refused("Changing one-time password seeds");
|
|
584
|
+
if (item.fields.some((field) => field.remove) && !operations.removeField)
|
|
585
|
+
refused("Removing fields");
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (!operations.createItemTypes.includes(item.type)) refused(`Creating ${item.type} items`);
|
|
589
|
+
if (totp && !operations.createTotp) refused("Adding one-time password seeds");
|
|
590
|
+
}
|
|
591
|
+
function restrictToOperations(provider, verified, fileFieldPrefix) {
|
|
592
|
+
if (!verified) return provider;
|
|
593
|
+
const effective = () => providerOperations(provider.capabilities(), { operations: verified, writeAccess: "unknown" });
|
|
594
|
+
return {
|
|
595
|
+
...provider,
|
|
596
|
+
capabilities() {
|
|
597
|
+
const base = provider.capabilities();
|
|
598
|
+
const operations = effective();
|
|
599
|
+
const writes = operations.createItemTypes.length > 0 || operations.updateItem || operations.archiveItem;
|
|
600
|
+
return { ...base, write: base.write && writes, operations };
|
|
601
|
+
},
|
|
602
|
+
async readFields(ctx, vaultId, itemId, fieldIds) {
|
|
603
|
+
if (!effective().readFiles && fieldIds.some((id) => id.startsWith(fileFieldPrefix)))
|
|
604
|
+
refused("Reading files");
|
|
605
|
+
return provider.readFields(ctx, vaultId, itemId, fieldIds);
|
|
606
|
+
},
|
|
607
|
+
async writeItem(ctx, vaultId, item) {
|
|
608
|
+
checkWrite(effective(), item);
|
|
609
|
+
return provider.writeItem(ctx, vaultId, item);
|
|
610
|
+
},
|
|
611
|
+
async archiveItem(ctx, vaultId, itemId) {
|
|
612
|
+
if (!effective().archiveItem) refused("Archiving items");
|
|
613
|
+
return provider.archiveItem(ctx, vaultId, itemId);
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/sdk.ts
|
|
619
|
+
import * as nodeCrypto from "crypto";
|
|
620
|
+
import { constants as fsConstants, readFileSync } from "fs";
|
|
621
|
+
import { createRequire } from "module";
|
|
622
|
+
import * as nodePath from "path";
|
|
623
|
+
var KEEPER_SDK_VERSION = "17.6.0";
|
|
624
|
+
var KEEPER_SDK_PACKAGE = "@keeper-security/secrets-manager-core";
|
|
625
|
+
var KEEPER_SDK_SHA256 = "94787ce51324f36c1b62d19ac29fbe3b9b206305695e508b76585a4d6e39635c";
|
|
626
|
+
var CapturedConsole = class _CapturedConsole {
|
|
627
|
+
lines = [];
|
|
628
|
+
static MAX_LINES = 200;
|
|
629
|
+
static MAX_CHARS = 500;
|
|
630
|
+
push(args) {
|
|
631
|
+
if (this.lines.length >= _CapturedConsole.MAX_LINES) return;
|
|
632
|
+
const text = args.map((arg) => arg instanceof Error ? `${arg.name}: ${arg.message}` : String(arg)).join(" ");
|
|
633
|
+
this.lines.push(text.slice(0, _CapturedConsole.MAX_CHARS));
|
|
634
|
+
}
|
|
635
|
+
error = (...args) => this.push(args);
|
|
636
|
+
warn = (...args) => this.push(args);
|
|
637
|
+
log = (...args) => this.push(args);
|
|
638
|
+
info = (...args) => this.push(args);
|
|
639
|
+
debug = (...args) => this.push(args);
|
|
640
|
+
/** True when the SDK dropped a record, folder or file it could not decrypt. */
|
|
641
|
+
get skippedSomething() {
|
|
642
|
+
return this.lines.some((line) => /skipped due to error|could not be decrypted/.test(line));
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
function denied(module) {
|
|
646
|
+
throw new Error(`The Keeper SDK may not use ${module} inside Opengeni`);
|
|
647
|
+
}
|
|
648
|
+
var FS_STUB = Object.freeze({ constants: fsConstants });
|
|
649
|
+
var HTTPS_STUB = Object.freeze({});
|
|
650
|
+
var OS_STUB = Object.freeze({});
|
|
651
|
+
function sdkRequire(name) {
|
|
652
|
+
switch (name) {
|
|
653
|
+
case "crypto":
|
|
654
|
+
return nodeCrypto;
|
|
655
|
+
case "path":
|
|
656
|
+
return nodePath;
|
|
657
|
+
case "fs":
|
|
658
|
+
return FS_STUB;
|
|
659
|
+
case "https":
|
|
660
|
+
return HTTPS_STUB;
|
|
661
|
+
case "os":
|
|
662
|
+
return OS_STUB;
|
|
663
|
+
default:
|
|
664
|
+
return denied(name);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
var compiled = null;
|
|
668
|
+
function readInstalledKeeperSdkSource() {
|
|
669
|
+
const require2 = createRequire(import.meta.url);
|
|
670
|
+
let path;
|
|
671
|
+
try {
|
|
672
|
+
path = require2.resolve(KEEPER_SDK_PACKAGE);
|
|
673
|
+
} catch {
|
|
674
|
+
throw new VaultProviderError(
|
|
675
|
+
"provider_unavailable",
|
|
676
|
+
`${KEEPER_SDK_PACKAGE} ${KEEPER_SDK_VERSION} is not installed on this Opengeni server`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
return readFileSync(path, "utf8");
|
|
680
|
+
}
|
|
681
|
+
function factoryFor(source, expectedSha256) {
|
|
682
|
+
const sha256 = nodeCrypto.createHash("sha256").update(source).digest("hex");
|
|
683
|
+
if (compiled && compiled.sha256 === sha256) return compiled.factory;
|
|
684
|
+
if (sha256 !== expectedSha256) {
|
|
685
|
+
throw new VaultProviderError(
|
|
686
|
+
"provider_unavailable",
|
|
687
|
+
`The installed Keeper SDK is not ${KEEPER_SDK_PACKAGE} ${KEEPER_SDK_VERSION}`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
const factory = new Function("module", "exports", "require", "console", source);
|
|
691
|
+
compiled = { sha256, factory };
|
|
692
|
+
return factory;
|
|
693
|
+
}
|
|
694
|
+
function createKeeperSdkLoader(options = {}) {
|
|
695
|
+
let factory = null;
|
|
696
|
+
return {
|
|
697
|
+
instance() {
|
|
698
|
+
if (!factory) {
|
|
699
|
+
const source = typeof options.source === "function" ? options.source() : options.source ?? readInstalledKeeperSdkSource();
|
|
700
|
+
factory = factoryFor(source, options.expectedSha256 ?? KEEPER_SDK_SHA256);
|
|
701
|
+
}
|
|
702
|
+
const captured = new CapturedConsole();
|
|
703
|
+
const module = { exports: {} };
|
|
704
|
+
factory(module, module.exports, sdkRequire, captured);
|
|
705
|
+
return { sdk: module.exports, console: captured };
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
var KEEPER_REQUEST_TIMEOUT_MS = 15e3;
|
|
710
|
+
var KEEPER_MAX_RESPONSE_BYTES = 32 * 1024 * 1024;
|
|
711
|
+
async function readBounded(response, maxBytes) {
|
|
712
|
+
const declared = Number(response.headers.get("content-length") ?? "NaN");
|
|
713
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
714
|
+
await response.body?.cancel().catch(() => {
|
|
715
|
+
});
|
|
716
|
+
throw new VaultProviderError(
|
|
717
|
+
"provider_unavailable",
|
|
718
|
+
"Keeper returned more data than Opengeni reads"
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
if (!response.body) return new Uint8Array();
|
|
722
|
+
const reader = response.body.getReader();
|
|
723
|
+
const chunks = [];
|
|
724
|
+
let total = 0;
|
|
725
|
+
for (; ; ) {
|
|
726
|
+
const { done, value } = await reader.read();
|
|
727
|
+
if (done) break;
|
|
728
|
+
total += value.byteLength;
|
|
729
|
+
if (total > maxBytes) {
|
|
730
|
+
await reader.cancel().catch(() => {
|
|
731
|
+
});
|
|
732
|
+
throw new VaultProviderError(
|
|
733
|
+
"provider_unavailable",
|
|
734
|
+
"Keeper returned more data than Opengeni reads"
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
chunks.push(value);
|
|
738
|
+
}
|
|
739
|
+
const out = new Uint8Array(total);
|
|
740
|
+
let offset = 0;
|
|
741
|
+
for (const chunk of chunks) {
|
|
742
|
+
out.set(chunk, offset);
|
|
743
|
+
offset += chunk.byteLength;
|
|
744
|
+
}
|
|
745
|
+
return out;
|
|
746
|
+
}
|
|
747
|
+
function withDeadline(signal, timeoutMs) {
|
|
748
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
749
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
750
|
+
}
|
|
751
|
+
async function refuseRedirect(response) {
|
|
752
|
+
if (response.status >= 300 && response.status < 400) {
|
|
753
|
+
await response.body?.cancel().catch(() => {
|
|
754
|
+
});
|
|
755
|
+
throw new VaultProviderError("provider_unavailable", "Keeper answered with a redirect");
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function createKeeperFetchTransport(options = {}) {
|
|
759
|
+
const fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
760
|
+
const maxBytes = options.maxResponseBytes ?? KEEPER_MAX_RESPONSE_BYTES;
|
|
761
|
+
return async (url, transmissionKey, payload, _allowUnverifiedCertificate, timeoutMs) => {
|
|
762
|
+
const response = await fetchImpl(url, {
|
|
763
|
+
method: "POST",
|
|
764
|
+
headers: {
|
|
765
|
+
"Content-Type": "application/octet-stream",
|
|
766
|
+
PublicKeyId: String(transmissionKey.publicKeyId),
|
|
767
|
+
TransmissionKey: Buffer.from(transmissionKey.encryptedKey).toString("base64"),
|
|
768
|
+
Authorization: `Signature ${Buffer.from(payload.signature).toString("base64")}`,
|
|
769
|
+
"User-Agent": `Opengeni (KSM JS ${KEEPER_SDK_VERSION})`
|
|
770
|
+
},
|
|
771
|
+
body: payload.payload,
|
|
772
|
+
redirect: "error",
|
|
773
|
+
signal: withDeadline(options.signal, timeoutMs ?? KEEPER_REQUEST_TIMEOUT_MS)
|
|
774
|
+
});
|
|
775
|
+
await refuseRedirect(response);
|
|
776
|
+
const data = await readBounded(response, maxBytes);
|
|
777
|
+
return { statusCode: response.status, headers: Object.fromEntries(response.headers), data };
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function createKeeperFileFetcher(fetchImpl = (input, init) => globalThis.fetch(input, init)) {
|
|
781
|
+
return async (url, options) => {
|
|
782
|
+
const parsed = new URL(url);
|
|
783
|
+
if (parsed.protocol !== "https:") {
|
|
784
|
+
throw new VaultProviderError("provider_unavailable", "Keeper returned a non-HTTPS file URL");
|
|
785
|
+
}
|
|
786
|
+
const response = await fetchImpl(parsed, {
|
|
787
|
+
redirect: "error",
|
|
788
|
+
signal: withDeadline(options.signal, options.timeoutMs)
|
|
789
|
+
});
|
|
790
|
+
await refuseRedirect(response);
|
|
791
|
+
if (!response.ok) {
|
|
792
|
+
await response.body?.cancel().catch(() => {
|
|
793
|
+
});
|
|
794
|
+
throw new VaultProviderError(
|
|
795
|
+
"provider_unavailable",
|
|
796
|
+
`Keeper file storage returned HTTP ${response.status}`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
return readBounded(response, options.maxBytes);
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/provider.ts
|
|
804
|
+
var KEEPER_VALUE_CACHE_MS = VAULT_LIMITS.providerValueCacheMs;
|
|
805
|
+
var KEEPER_OPERATIONS = {
|
|
806
|
+
createItemTypes: ["login", "note"],
|
|
807
|
+
updateItem: true,
|
|
808
|
+
removeField: true,
|
|
809
|
+
archiveItem: false,
|
|
810
|
+
archiveKind: "none",
|
|
811
|
+
deleteItem: false,
|
|
812
|
+
restoreItem: false,
|
|
813
|
+
readFiles: true,
|
|
814
|
+
writeFiles: false,
|
|
815
|
+
createTotp: true,
|
|
816
|
+
updateTotp: true,
|
|
817
|
+
createVault: false,
|
|
818
|
+
restoreFieldHistory: false
|
|
819
|
+
};
|
|
820
|
+
var KEEPER_UID = /^[A-Za-z0-9_-]{16,64}$/;
|
|
821
|
+
function assertUids(...uids) {
|
|
822
|
+
for (const uid of uids) {
|
|
823
|
+
if (!KEEPER_UID.test(uid)) {
|
|
824
|
+
throw new VaultProviderError("provider_not_found", "No such Keeper shared folder or record");
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
async function runKeeperOperation(input) {
|
|
829
|
+
const allowed = (input.runtime.allowedHosts ?? KEEPER_DEFAULT_ALLOWED_HOSTS).map(
|
|
830
|
+
(host) => host.toLowerCase()
|
|
831
|
+
);
|
|
832
|
+
const { sdk, console } = input.loader.instance();
|
|
833
|
+
const storage = memoryKeeperStorage(input.config);
|
|
834
|
+
const transport = input.runtime.queryFunction ?? createKeeperFetchTransport({
|
|
835
|
+
...input.signal ? { signal: input.signal } : {},
|
|
836
|
+
...input.runtime.fetch ? { fetch: input.runtime.fetch } : {}
|
|
837
|
+
});
|
|
838
|
+
const tracker = { sent: 0, lastStatus: null, mutating: false };
|
|
839
|
+
const queryFunction = async (url, transmissionKey, payload, allow, timeoutMs) => {
|
|
840
|
+
input.signal?.throwIfAborted();
|
|
841
|
+
const parsed = new URL(url);
|
|
842
|
+
if (parsed.protocol !== "https:" || !parsed.pathname.startsWith("/api/rest/sm/v1/")) {
|
|
843
|
+
throw new VaultProviderError("provider_unavailable", "Refused an unexpected Keeper request");
|
|
844
|
+
}
|
|
845
|
+
assertAllowedHost(parsed.hostname, allowed);
|
|
846
|
+
if (tracker.mutating) {
|
|
847
|
+
tracker.sent += 1;
|
|
848
|
+
tracker.lastStatus = null;
|
|
849
|
+
}
|
|
850
|
+
const response = await transport(url, transmissionKey, payload, false, timeoutMs);
|
|
851
|
+
if (tracker.mutating) tracker.lastStatus = response.statusCode;
|
|
852
|
+
return response;
|
|
853
|
+
};
|
|
854
|
+
const options = {
|
|
855
|
+
storage,
|
|
856
|
+
queryFunction,
|
|
857
|
+
requestTimeoutMs: input.runtime.requestTimeoutMs ?? KEEPER_REQUEST_TIMEOUT_MS,
|
|
858
|
+
// Never sleep inside a request: surface the server's back-off to the caller.
|
|
859
|
+
throttleSleep: (ms) => Promise.reject(
|
|
860
|
+
new VaultProviderError(
|
|
861
|
+
"provider_rate_limited",
|
|
862
|
+
"Keeper is rate limiting this application",
|
|
863
|
+
ms
|
|
864
|
+
)
|
|
865
|
+
),
|
|
866
|
+
onDecryptionError: () => {
|
|
867
|
+
throw new VaultProviderError(
|
|
868
|
+
"provider_inventory_incomplete",
|
|
869
|
+
"Keeper returned a folder Opengeni could not decrypt"
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
input.beforeNetwork?.(storage);
|
|
874
|
+
if (input.hostInConfig !== false) assertAllowedHost(storage.values.get("hostname"), allowed);
|
|
875
|
+
const result = await input.work({ sdk, options, console, tracker }, storage);
|
|
876
|
+
return { result, storage };
|
|
877
|
+
}
|
|
878
|
+
function cacheFor(map, budget) {
|
|
879
|
+
let cache = map.get(budget);
|
|
880
|
+
if (!cache) {
|
|
881
|
+
cache = /* @__PURE__ */ new Map();
|
|
882
|
+
map.set(budget, cache);
|
|
883
|
+
}
|
|
884
|
+
return cache;
|
|
885
|
+
}
|
|
886
|
+
function createKeeperVaultProvider(options) {
|
|
887
|
+
let config = parseKeeperConfiguration(options.configuration);
|
|
888
|
+
let persisted = serializeKeeperConfiguration(config);
|
|
889
|
+
let pending = null;
|
|
890
|
+
const loader = options.sdk ?? createKeeperSdkLoader();
|
|
891
|
+
const now = options.now ?? Date.now;
|
|
892
|
+
const valueCacheMs = options.valueCacheMs ?? KEEPER_VALUE_CACHE_MS;
|
|
893
|
+
const fetchFile = options.fetchFile ?? createKeeperFileFetcher(options.fetch);
|
|
894
|
+
let queue = Promise.resolve();
|
|
895
|
+
const attemptListCaches = /* @__PURE__ */ new WeakMap();
|
|
896
|
+
const attemptRecordCaches = /* @__PURE__ */ new WeakMap();
|
|
897
|
+
async function flushPending() {
|
|
898
|
+
if (pending === null) return;
|
|
899
|
+
const next = pending;
|
|
900
|
+
try {
|
|
901
|
+
await options.persistConfiguration(next, { previous: persisted, reason: "rotation" });
|
|
902
|
+
} catch {
|
|
903
|
+
throw new VaultProviderError(
|
|
904
|
+
"provider_unavailable",
|
|
905
|
+
"Opengeni could not save the updated Keeper configuration"
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
persisted = next;
|
|
909
|
+
if (pending === next) pending = null;
|
|
910
|
+
}
|
|
911
|
+
function serialized(work) {
|
|
912
|
+
const run = queue.then(work, work);
|
|
913
|
+
queue = run.then(
|
|
914
|
+
() => void 0,
|
|
915
|
+
() => void 0
|
|
916
|
+
);
|
|
917
|
+
return run;
|
|
918
|
+
}
|
|
919
|
+
function operation(ctx, op, subject, work) {
|
|
920
|
+
ctx.signal?.throwIfAborted();
|
|
921
|
+
ctx.budget.take(op);
|
|
922
|
+
return serialized(async () => {
|
|
923
|
+
ctx.signal?.throwIfAborted();
|
|
924
|
+
await flushPending();
|
|
925
|
+
let outcome = null;
|
|
926
|
+
let failure = null;
|
|
927
|
+
let storageRef = null;
|
|
928
|
+
let tracker = null;
|
|
929
|
+
try {
|
|
930
|
+
outcome = await runKeeperOperation({
|
|
931
|
+
runtime: options,
|
|
932
|
+
loader,
|
|
933
|
+
config,
|
|
934
|
+
...ctx.signal ? { signal: ctx.signal } : {},
|
|
935
|
+
beforeNetwork: (storage2) => {
|
|
936
|
+
storageRef = storage2;
|
|
937
|
+
},
|
|
938
|
+
work: (keeper) => {
|
|
939
|
+
tracker = keeper.tracker;
|
|
940
|
+
return work(keeper);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
} catch (error) {
|
|
944
|
+
failure = error;
|
|
945
|
+
}
|
|
946
|
+
const storage = outcome?.storage ?? storageRef;
|
|
947
|
+
let persistError = null;
|
|
948
|
+
if (storage?.changed()) {
|
|
949
|
+
config = new Map(storage.values);
|
|
950
|
+
pending = serializeKeeperConfiguration(config);
|
|
951
|
+
try {
|
|
952
|
+
await flushPending();
|
|
953
|
+
} catch (error) {
|
|
954
|
+
persistError = error;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
if (failure !== null) {
|
|
958
|
+
const usedTracker = tracker;
|
|
959
|
+
if (op === "write" && usedTracker && (usedTracker.sent > 0 || usedTracker.mutating)) {
|
|
960
|
+
throw mapKeeperMutationError(failure, subject, usedTracker);
|
|
961
|
+
}
|
|
962
|
+
if (op !== "write" && failure instanceof Error && failure.name === "AbortError") {
|
|
963
|
+
ctx.signal?.throwIfAborted();
|
|
964
|
+
}
|
|
965
|
+
throw mapKeeperError(failure, subject);
|
|
966
|
+
}
|
|
967
|
+
if (persistError && op !== "write") throw persistError;
|
|
968
|
+
return outcome.result;
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
function reportFresh(ctx) {
|
|
972
|
+
const at = new Date(now()).toISOString();
|
|
973
|
+
ctx.onFreshness?.({
|
|
974
|
+
state: "current",
|
|
975
|
+
lastCheckedAt: at,
|
|
976
|
+
sourceSyncedAt: at,
|
|
977
|
+
source: "live",
|
|
978
|
+
errorCode: null,
|
|
979
|
+
retryAt: null
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
function incomplete() {
|
|
983
|
+
throw new VaultProviderError(
|
|
984
|
+
"provider_inventory_incomplete",
|
|
985
|
+
"Keeper returned records Opengeni could not decrypt; the listing is incomplete"
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
function allRecords(ctx) {
|
|
989
|
+
const cache = cacheFor(attemptListCaches, ctx.budget);
|
|
990
|
+
const key = ctx.cacheEpoch ?? "";
|
|
991
|
+
const cached = cache.get(key);
|
|
992
|
+
if (cached && !ctx.forceRefresh && now() - cached.at < valueCacheMs) return cached.records;
|
|
993
|
+
const records = operation(
|
|
994
|
+
ctx,
|
|
995
|
+
"list",
|
|
996
|
+
"list records",
|
|
997
|
+
async ({ sdk, options: sdkOptions, console }) => {
|
|
998
|
+
const secrets = await sdk.getSecrets2(sdkOptions);
|
|
999
|
+
if (console.skippedSomething) incomplete();
|
|
1000
|
+
return secrets.records;
|
|
1001
|
+
}
|
|
1002
|
+
);
|
|
1003
|
+
const entry = { at: now(), records };
|
|
1004
|
+
cache.set(key, entry);
|
|
1005
|
+
records.catch(() => {
|
|
1006
|
+
if (cache.get(key) === entry) cache.delete(key);
|
|
1007
|
+
});
|
|
1008
|
+
return records;
|
|
1009
|
+
}
|
|
1010
|
+
async function fetchRecord(keeper, externalVaultId, externalItemId) {
|
|
1011
|
+
const secrets = await keeper.sdk.getSecrets2(keeper.options, {
|
|
1012
|
+
recordsFilter: [externalItemId]
|
|
1013
|
+
});
|
|
1014
|
+
const record = secrets.records.find((candidate) => candidate.recordUid === externalItemId);
|
|
1015
|
+
if (!record) {
|
|
1016
|
+
if (keeper.console.skippedSomething) {
|
|
1017
|
+
throw new VaultProviderError(
|
|
1018
|
+
"provider_unavailable",
|
|
1019
|
+
"Opengeni could not decrypt this Keeper record"
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
throw new VaultProviderError("provider_not_found", "No such Keeper record");
|
|
1023
|
+
}
|
|
1024
|
+
if (record.folderUid !== externalVaultId) {
|
|
1025
|
+
throw new VaultProviderError(
|
|
1026
|
+
"provider_not_found",
|
|
1027
|
+
"No such Keeper record in this shared folder"
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
return record;
|
|
1031
|
+
}
|
|
1032
|
+
function materialFor(mapped, field) {
|
|
1033
|
+
if (field.kind === "totp") {
|
|
1034
|
+
const params = totpParamsFor(field.value);
|
|
1035
|
+
if (!params) {
|
|
1036
|
+
throw new VaultProviderError(
|
|
1037
|
+
"provider_unavailable",
|
|
1038
|
+
`The one-time code in Keeper record "${mapped.meta.name}" is not a valid TOTP seed`
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
return { type: "totp", params };
|
|
1042
|
+
}
|
|
1043
|
+
return { type: "text", value: field.value };
|
|
1044
|
+
}
|
|
1045
|
+
async function readFile(keeper, field, signal) {
|
|
1046
|
+
if (field.source.kind !== "file") throw new Error("not a file field");
|
|
1047
|
+
const file = field.source.file;
|
|
1048
|
+
const limit = VAULT_LIMITS.fileValueMaxBytes;
|
|
1049
|
+
const size = fileSize(file);
|
|
1050
|
+
if (size === null || size > limit) {
|
|
1051
|
+
throw new VaultProviderError(
|
|
1052
|
+
"provider_unavailable",
|
|
1053
|
+
`"${field.name}" is larger than Opengeni reads from Keeper`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
if (!file.url)
|
|
1057
|
+
throw new VaultProviderError("provider_unavailable", "Keeper returned no download URL");
|
|
1058
|
+
const encrypted = await fetchFile(file.url, {
|
|
1059
|
+
...signal ? { signal } : {},
|
|
1060
|
+
// AES-GCM adds a 12-byte nonce and a 16-byte tag.
|
|
1061
|
+
maxBytes: limit + 28,
|
|
1062
|
+
timeoutMs: options.requestTimeoutMs ?? KEEPER_REQUEST_TIMEOUT_MS
|
|
1063
|
+
});
|
|
1064
|
+
const plain = await keeper.sdk.platform.decrypt(encrypted, file.fileUid);
|
|
1065
|
+
if (plain.byteLength > limit) {
|
|
1066
|
+
throw new VaultProviderError(
|
|
1067
|
+
"provider_unavailable",
|
|
1068
|
+
`"${field.name}" is larger than Opengeni reads from Keeper`
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
return new Uint8Array(plain);
|
|
1072
|
+
}
|
|
1073
|
+
const provider = {
|
|
1074
|
+
id: "keeper",
|
|
1075
|
+
capabilities() {
|
|
1076
|
+
return {
|
|
1077
|
+
write: true,
|
|
1078
|
+
createVault: false,
|
|
1079
|
+
totp: true,
|
|
1080
|
+
// Legacy flag means file writes; reads are declared in operations.readFiles.
|
|
1081
|
+
files: false,
|
|
1082
|
+
groupGrants: false,
|
|
1083
|
+
passkeys: false,
|
|
1084
|
+
operations: {
|
|
1085
|
+
...KEEPER_OPERATIONS,
|
|
1086
|
+
createItemTypes: [...KEEPER_OPERATIONS.createItemTypes]
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
},
|
|
1090
|
+
async validate(ctx) {
|
|
1091
|
+
const name = await operation(
|
|
1092
|
+
ctx,
|
|
1093
|
+
"list",
|
|
1094
|
+
"validate",
|
|
1095
|
+
async ({ sdk, options: sdkOptions }) => {
|
|
1096
|
+
const secrets = await sdk.getSecrets2(sdkOptions);
|
|
1097
|
+
const title = secrets.appData?.title;
|
|
1098
|
+
return typeof title === "string" && title.trim() ? title.trim().slice(0, 200) : null;
|
|
1099
|
+
}
|
|
1100
|
+
);
|
|
1101
|
+
return { accountName: name };
|
|
1102
|
+
},
|
|
1103
|
+
async listVaults(ctx) {
|
|
1104
|
+
const folders = await operation(
|
|
1105
|
+
ctx,
|
|
1106
|
+
"list",
|
|
1107
|
+
"list shared folders",
|
|
1108
|
+
async ({ sdk, options: sdkOptions, console }) => {
|
|
1109
|
+
const result = await sdk.getFolders(sdkOptions);
|
|
1110
|
+
if (console.skippedSomething) incomplete();
|
|
1111
|
+
return result;
|
|
1112
|
+
}
|
|
1113
|
+
);
|
|
1114
|
+
reportFresh(ctx);
|
|
1115
|
+
return folders.filter((folder) => !folder.parentUid).map((folder) => ({
|
|
1116
|
+
externalVaultId: folder.folderUid,
|
|
1117
|
+
name: safeSecretName(folder.name ?? "", folder.folderUid),
|
|
1118
|
+
contentVersion: null,
|
|
1119
|
+
itemCount: null,
|
|
1120
|
+
// KSM does not tell an application whether a shared folder is editable.
|
|
1121
|
+
writeAccess: "unknown"
|
|
1122
|
+
}));
|
|
1123
|
+
},
|
|
1124
|
+
async listItems(ctx, externalVaultId) {
|
|
1125
|
+
assertUids(externalVaultId);
|
|
1126
|
+
const records = await allRecords(ctx);
|
|
1127
|
+
const metas = records.filter((record) => record.folderUid === externalVaultId).map((record) => mapRecord(record).meta);
|
|
1128
|
+
if (metas.length > VAULT_LIMITS.itemsPerVault) {
|
|
1129
|
+
throw new VaultProviderError(
|
|
1130
|
+
"provider_inventory_incomplete",
|
|
1131
|
+
"The Keeper shared folder has more records than Opengeni mirrors at once"
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
for (const meta of metas) assertMirrorable(meta);
|
|
1135
|
+
reportFresh(ctx);
|
|
1136
|
+
return metas;
|
|
1137
|
+
},
|
|
1138
|
+
async readFields(ctx, externalVaultId, externalItemId, externalFieldIds) {
|
|
1139
|
+
assertUids(externalVaultId, externalItemId);
|
|
1140
|
+
const wantsFile = externalFieldIds.some((id) => id.startsWith("@file:"));
|
|
1141
|
+
const cache = cacheFor(attemptRecordCaches, ctx.budget);
|
|
1142
|
+
const key = `${ctx.cacheEpoch ?? ""}|${externalVaultId}/${externalItemId}`;
|
|
1143
|
+
const cached = cache.get(key);
|
|
1144
|
+
if (!wantsFile && cached && !ctx.forceRefresh && now() - cached.at < valueCacheMs) {
|
|
1145
|
+
const mapped = await cached.record;
|
|
1146
|
+
return externalFieldIds.map((externalFieldId) => {
|
|
1147
|
+
const field = mapped.fields.find(
|
|
1148
|
+
(candidate) => candidate.externalFieldId === externalFieldId
|
|
1149
|
+
);
|
|
1150
|
+
if (!field)
|
|
1151
|
+
throw new VaultProviderError(
|
|
1152
|
+
"provider_not_found",
|
|
1153
|
+
`Keeper record has no field ${externalFieldId}`
|
|
1154
|
+
);
|
|
1155
|
+
return { externalFieldId, material: materialFor(mapped, field) };
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
let resolveMapped;
|
|
1159
|
+
let rejectMapped;
|
|
1160
|
+
const mappedPromise = new Promise((resolve, reject) => {
|
|
1161
|
+
resolveMapped = resolve;
|
|
1162
|
+
rejectMapped = reject;
|
|
1163
|
+
});
|
|
1164
|
+
mappedPromise.catch(() => {
|
|
1165
|
+
});
|
|
1166
|
+
const entry = { at: now(), record: mappedPromise };
|
|
1167
|
+
cache.set(key, entry);
|
|
1168
|
+
try {
|
|
1169
|
+
return await operation(ctx, "read", "read record", async (keeper) => {
|
|
1170
|
+
const record = await fetchRecord(keeper, externalVaultId, externalItemId);
|
|
1171
|
+
const mapped = mapRecord(record);
|
|
1172
|
+
resolveMapped({
|
|
1173
|
+
...mapped,
|
|
1174
|
+
fields: mapped.fields.filter((field) => field.kind !== "file")
|
|
1175
|
+
});
|
|
1176
|
+
const values = [];
|
|
1177
|
+
for (const externalFieldId of externalFieldIds) {
|
|
1178
|
+
const field = mapped.fields.find(
|
|
1179
|
+
(candidate) => candidate.externalFieldId === externalFieldId
|
|
1180
|
+
);
|
|
1181
|
+
if (!field) {
|
|
1182
|
+
throw new VaultProviderError(
|
|
1183
|
+
"provider_not_found",
|
|
1184
|
+
`Keeper record "${mapped.meta.name}" has no field ${externalFieldId}`
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
values.push({
|
|
1188
|
+
externalFieldId,
|
|
1189
|
+
material: field.kind === "file" ? { type: "bytes", value: await readFile(keeper, field, ctx.signal) } : materialFor(mapped, field)
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
return values;
|
|
1193
|
+
});
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
rejectMapped(error);
|
|
1196
|
+
if (cache.get(key) === entry) cache.delete(key);
|
|
1197
|
+
throw error;
|
|
1198
|
+
}
|
|
1199
|
+
},
|
|
1200
|
+
async writeItem(ctx, externalVaultId, item) {
|
|
1201
|
+
assertUids(externalVaultId);
|
|
1202
|
+
const meta = await writeRecord(ctx, externalVaultId, item);
|
|
1203
|
+
attemptRecordCaches.get(ctx.budget)?.clear();
|
|
1204
|
+
attemptListCaches.get(ctx.budget)?.clear();
|
|
1205
|
+
return meta;
|
|
1206
|
+
},
|
|
1207
|
+
async archiveItem() {
|
|
1208
|
+
throw new VaultProviderError(
|
|
1209
|
+
"provider_unavailable",
|
|
1210
|
+
"Keeper Secrets Manager applications cannot archive or trash records; remove it in Keeper"
|
|
1211
|
+
);
|
|
1212
|
+
},
|
|
1213
|
+
currentConfiguration: () => serializeKeeperConfiguration(config),
|
|
1214
|
+
close: async () => {
|
|
1215
|
+
await queue.catch(() => {
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
function writeRecord(ctx, externalVaultId, item) {
|
|
1220
|
+
if (item.externalItemId === void 0) {
|
|
1221
|
+
const data = buildCreateData(item);
|
|
1222
|
+
return operation(ctx, "write", "create record", async (keeper) => {
|
|
1223
|
+
const folders = await keeper.sdk.getFolders(keeper.options);
|
|
1224
|
+
const folder = folders.find((candidate) => candidate.folderUid === externalVaultId);
|
|
1225
|
+
if (!folder || folder.parentUid) {
|
|
1226
|
+
throw new VaultProviderError("provider_not_found", "No such Keeper shared folder");
|
|
1227
|
+
}
|
|
1228
|
+
keeper.tracker.mutating = true;
|
|
1229
|
+
const recordUid2 = await keeper.sdk.createSecret2(
|
|
1230
|
+
keeper.options,
|
|
1231
|
+
{ folderUid: externalVaultId },
|
|
1232
|
+
data
|
|
1233
|
+
);
|
|
1234
|
+
keeper.tracker.mutating = false;
|
|
1235
|
+
try {
|
|
1236
|
+
return mapRecord(await fetchRecord(keeper, externalVaultId, recordUid2)).meta;
|
|
1237
|
+
} catch {
|
|
1238
|
+
const meta = mapRecord({
|
|
1239
|
+
recordUid: recordUid2,
|
|
1240
|
+
folderUid: externalVaultId,
|
|
1241
|
+
data,
|
|
1242
|
+
revision: Number.NaN
|
|
1243
|
+
}).meta;
|
|
1244
|
+
return { ...meta, version: null };
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
const recordUid = item.externalItemId;
|
|
1249
|
+
assertUids(recordUid);
|
|
1250
|
+
return operation(ctx, "write", "update record", async (keeper) => {
|
|
1251
|
+
const record = await fetchRecord(keeper, externalVaultId, recordUid);
|
|
1252
|
+
if (item.expectedVersion !== void 0 && item.expectedVersion !== null && String(record.revision) !== item.expectedVersion) {
|
|
1253
|
+
throw new VaultProviderError(
|
|
1254
|
+
"provider_conflict",
|
|
1255
|
+
"The Keeper record changed since it was read; read it again before writing"
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
applyPatch(record, item);
|
|
1259
|
+
keeper.tracker.mutating = true;
|
|
1260
|
+
await keeper.sdk.updateSecret2(keeper.options, record);
|
|
1261
|
+
keeper.tracker.mutating = false;
|
|
1262
|
+
try {
|
|
1263
|
+
return mapRecord(await fetchRecord(keeper, externalVaultId, recordUid)).meta;
|
|
1264
|
+
} catch {
|
|
1265
|
+
return { ...mapRecord(record).meta, version: null };
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
return restrictToOperations(provider, options.operations, FILE_FIELD_PREFIX);
|
|
1270
|
+
}
|
|
1271
|
+
function assertMirrorable(meta) {
|
|
1272
|
+
if (meta.fields.length > VAULT_LIMITS.fieldsPerItem || meta.websites.length > VAULT_LIMITS.websitesPerItem || meta.websites.some((website) => website.length > VAULT_LIMITS.websiteMaxChars)) {
|
|
1273
|
+
throw new VaultProviderError(
|
|
1274
|
+
"provider_inventory_incomplete",
|
|
1275
|
+
`A Keeper record has more than ${VAULT_LIMITS.fieldsPerItem} fields or ${VAULT_LIMITS.websitesPerItem} websites (or a website over ${VAULT_LIMITS.websiteMaxChars} characters), more than Opengeni mirrors`
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// src/bootstrap.ts
|
|
1281
|
+
var TOKEN_PART = /^[A-Za-z0-9_-]{16,128}$/;
|
|
1282
|
+
function invalidToken() {
|
|
1283
|
+
throw new VaultProviderError(
|
|
1284
|
+
"provider_auth_invalid",
|
|
1285
|
+
"That is not a Keeper Secrets Manager one-time access token"
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
function keeperTokenHost(oneTimeToken, hostname) {
|
|
1289
|
+
const parts = oneTimeToken.trim().split(":");
|
|
1290
|
+
if (parts.length === 1) {
|
|
1291
|
+
if (!TOKEN_PART.test(parts[0])) invalidToken();
|
|
1292
|
+
if (!hostname) {
|
|
1293
|
+
throw new VaultProviderError(
|
|
1294
|
+
"provider_auth_invalid",
|
|
1295
|
+
"This one-time token has no region prefix; choose the Keeper region"
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
return hostname;
|
|
1299
|
+
}
|
|
1300
|
+
if (parts.length !== 2 && !(parts.length === 4 && parts[0].toUpperCase() === "IL5"))
|
|
1301
|
+
invalidToken();
|
|
1302
|
+
if (!TOKEN_PART.test(parts[1])) invalidToken();
|
|
1303
|
+
const region = parts[0].toUpperCase();
|
|
1304
|
+
return KEEPER_REGION_HOSTS[region] ?? parts[0];
|
|
1305
|
+
}
|
|
1306
|
+
async function bootstrapKeeperConfiguration(options) {
|
|
1307
|
+
const token = options.oneTimeToken.trim();
|
|
1308
|
+
const allowed = (options.allowedHosts ?? KEEPER_DEFAULT_ALLOWED_HOSTS).map(
|
|
1309
|
+
(host) => host.toLowerCase()
|
|
1310
|
+
);
|
|
1311
|
+
assertAllowedHost(keeperTokenHost(token, options.hostname), allowed);
|
|
1312
|
+
const loader = options.sdk ?? createKeeperSdkLoader();
|
|
1313
|
+
let tracker = null;
|
|
1314
|
+
let bindStorage = null;
|
|
1315
|
+
let accountName = null;
|
|
1316
|
+
let failure = null;
|
|
1317
|
+
try {
|
|
1318
|
+
await runKeeperOperation({
|
|
1319
|
+
runtime: options,
|
|
1320
|
+
loader,
|
|
1321
|
+
config: /* @__PURE__ */ new Map(),
|
|
1322
|
+
hostInConfig: false,
|
|
1323
|
+
...options.signal ? { signal: options.signal } : {},
|
|
1324
|
+
work: async (keeper, storage) => {
|
|
1325
|
+
tracker = keeper.tracker;
|
|
1326
|
+
bindStorage = storage;
|
|
1327
|
+
await keeper.sdk.initializeStorage(storage, token, options.hostname);
|
|
1328
|
+
assertAllowedHost(storage.values.get("hostname"), allowed);
|
|
1329
|
+
keeper.tracker.mutating = true;
|
|
1330
|
+
const secrets = await keeper.sdk.getSecrets2(keeper.options);
|
|
1331
|
+
keeper.tracker.mutating = false;
|
|
1332
|
+
const title = secrets.appData?.title;
|
|
1333
|
+
accountName = typeof title === "string" && title.trim() ? title.trim().slice(0, 200) : null;
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
failure = error;
|
|
1338
|
+
}
|
|
1339
|
+
const bound = bindStorage;
|
|
1340
|
+
if (bound?.values.get("appKey")) {
|
|
1341
|
+
bound.values.delete("clientKey");
|
|
1342
|
+
const configuration = serializeKeeperConfiguration(bound.values);
|
|
1343
|
+
parseKeeperConfiguration(configuration);
|
|
1344
|
+
if (options.persistConfiguration) {
|
|
1345
|
+
try {
|
|
1346
|
+
await options.persistConfiguration(configuration, { previous: null, reason: "bind" });
|
|
1347
|
+
} catch {
|
|
1348
|
+
throw new VaultProviderError(
|
|
1349
|
+
"provider_unavailable",
|
|
1350
|
+
"Keeper accepted the one-time token but Opengeni could not save the connection; generate a new token and connect again"
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return { configuration, accountName };
|
|
1355
|
+
}
|
|
1356
|
+
try {
|
|
1357
|
+
if (failure === null) {
|
|
1358
|
+
throw new VaultProviderError(
|
|
1359
|
+
"provider_outcome_unknown",
|
|
1360
|
+
"Keeper did not complete binding; generate a new one-time token"
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
throw failure;
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
if (error instanceof VaultProviderError && error.code === "provider_outcome_unknown")
|
|
1366
|
+
throw error;
|
|
1367
|
+
const used = tracker;
|
|
1368
|
+
const mapped = used ? mapKeeperMutationError(error, "redeem the one-time token", used) : error instanceof VaultProviderError ? error : new VaultProviderError(
|
|
1369
|
+
"provider_auth_invalid",
|
|
1370
|
+
"Keeper could not use that one-time token"
|
|
1371
|
+
);
|
|
1372
|
+
if (mapped.code === "provider_outcome_unknown") {
|
|
1373
|
+
throw new VaultProviderError(
|
|
1374
|
+
"provider_outcome_unknown",
|
|
1375
|
+
"Keeper may have redeemed the one-time token; generate a new one and connect again"
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
if (mapped.code === "provider_auth_invalid" || mapped.code === "provider_permission_denied") {
|
|
1379
|
+
throw new VaultProviderError(
|
|
1380
|
+
"provider_auth_invalid",
|
|
1381
|
+
"Keeper rejected the one-time token (expired, already used, or IP-locked); generate a new one"
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
throw mapped;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
async function bootstrapKeeperVaultProvider(options) {
|
|
1388
|
+
const { oneTimeToken: _token, hostname: _hostname, signal: _signal, ...runtime } = options;
|
|
1389
|
+
const result = await bootstrapKeeperConfiguration(options);
|
|
1390
|
+
const provider = createKeeperVaultProvider({ ...runtime, configuration: result.configuration });
|
|
1391
|
+
return { ...result, provider };
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// src/connection.ts
|
|
1395
|
+
async function openKeeperConnection(options) {
|
|
1396
|
+
const persist = options.persistConfiguration;
|
|
1397
|
+
if (!persist) {
|
|
1398
|
+
throw new VaultProviderError(
|
|
1399
|
+
"provider_unavailable",
|
|
1400
|
+
"Keeper connections need durable configuration storage on this server"
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
const { credentials, persistConfiguration: _persist, operations, signal, ...runtime } = options;
|
|
1404
|
+
let configuration;
|
|
1405
|
+
if ("oneTimeToken" in credentials) {
|
|
1406
|
+
const bound = await bootstrapKeeperConfiguration({
|
|
1407
|
+
...runtime,
|
|
1408
|
+
oneTimeToken: credentials.oneTimeToken,
|
|
1409
|
+
...credentials.hostname ? { hostname: credentials.hostname } : {},
|
|
1410
|
+
...signal ? { signal } : {},
|
|
1411
|
+
persistConfiguration: (next, context) => persist(keeperConfigurationRecord(next), { reason: context.reason })
|
|
1412
|
+
});
|
|
1413
|
+
configuration = keeperConfigurationRecord(bound.configuration);
|
|
1414
|
+
} else {
|
|
1415
|
+
configuration = keeperConfigurationRecord(credentials.configuration);
|
|
1416
|
+
}
|
|
1417
|
+
const provider = createKeeperVaultProvider({
|
|
1418
|
+
...runtime,
|
|
1419
|
+
configuration,
|
|
1420
|
+
...operations ? { operations } : {},
|
|
1421
|
+
persistConfiguration: (next, context) => persist(keeperConfigurationRecord(next), { reason: context.reason })
|
|
1422
|
+
});
|
|
1423
|
+
return { provider, configuration };
|
|
1424
|
+
}
|
|
1425
|
+
export {
|
|
1426
|
+
FILE_FIELD_PREFIX,
|
|
1427
|
+
KEEPER_CONFIG_KEYS,
|
|
1428
|
+
KEEPER_DEFAULT_ALLOWED_HOSTS,
|
|
1429
|
+
KEEPER_DEFINITE_REJECTION_STATUSES,
|
|
1430
|
+
KEEPER_MAX_RESPONSE_BYTES,
|
|
1431
|
+
KEEPER_OPERATIONS,
|
|
1432
|
+
KEEPER_REGION_HOSTS,
|
|
1433
|
+
KEEPER_REQUEST_TIMEOUT_MS,
|
|
1434
|
+
KEEPER_SDK_PACKAGE,
|
|
1435
|
+
KEEPER_SDK_SHA256,
|
|
1436
|
+
KEEPER_SDK_VERSION,
|
|
1437
|
+
KEEPER_VALUE_CACHE_MS,
|
|
1438
|
+
NOTES_FIELD_ID,
|
|
1439
|
+
bootstrapKeeperConfiguration,
|
|
1440
|
+
bootstrapKeeperVaultProvider,
|
|
1441
|
+
createKeeperSdkLoader,
|
|
1442
|
+
createKeeperVaultProvider,
|
|
1443
|
+
keeperConfigurationRecord,
|
|
1444
|
+
keeperTokenHost,
|
|
1445
|
+
openKeeperConnection,
|
|
1446
|
+
parseKeeperConfiguration,
|
|
1447
|
+
restrictToOperations,
|
|
1448
|
+
serializeKeeperConfiguration
|
|
1449
|
+
};
|
|
1450
|
+
//# sourceMappingURL=index.js.map
|