@superdurable/connectors-sdk 0.0.0-stage → 0.1.1
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 +21 -0
- package/README.md +264 -2
- package/dist/chunk-2IDXFKMY.js +251 -0
- package/dist/chunk-2NBM7P4T.js +42 -0
- package/dist/chunk-KD5DYYSU.js +93 -0
- package/dist/chunk-SOXYPIMO.js +179 -0
- package/dist/connectormock/index.d.ts +181 -0
- package/dist/connectormock/index.js +350 -0
- package/dist/credentials-vrvW0d4j.d.ts +445 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +376 -0
- package/dist/oauthtoken/index.d.ts +90 -0
- package/dist/oauthtoken/index.js +236 -0
- package/dist/operation-Ca8rfacf.d.ts +34 -0
- package/dist/projectconfig/index.d.ts +569 -0
- package/dist/projectconfig/index.js +1433 -0
- package/dist/providerhttp/index.d.ts +57 -0
- package/dist/providerhttp/index.js +26 -0
- package/dist/singledispatch/index.d.ts +72 -0
- package/dist/singledispatch/index.js +178 -0
- package/package.json +73 -4
|
@@ -0,0 +1,1433 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BodyTooLargeError,
|
|
3
|
+
isLocalStorageHost,
|
|
4
|
+
isPrivateBrokerHost,
|
|
5
|
+
readBoundedBody
|
|
6
|
+
} from "../chunk-2IDXFKMY.js";
|
|
7
|
+
import {
|
|
8
|
+
OperationRefSchema,
|
|
9
|
+
ReauthorizationRequiredError
|
|
10
|
+
} from "../chunk-SOXYPIMO.js";
|
|
11
|
+
|
|
12
|
+
// src/projectconfig/model.ts
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
var ConfigurationSchemaVersion = "connectors.dex.dev/project-configuration/v1alpha3";
|
|
15
|
+
var LocalConnectionsSchemaVersion = "connectors.dex.dev/local-connections/v1alpha1";
|
|
16
|
+
var RFC3339NanoSchema = z.iso.datetime({ offset: true }).refine((value) => {
|
|
17
|
+
const fraction = /\.(\d+)(?:Z|[+-]\d{2}:\d{2})$/.exec(value)?.[1];
|
|
18
|
+
return fraction === void 0 || fraction.length <= 9;
|
|
19
|
+
}, "timestamp precision exceeds nanoseconds");
|
|
20
|
+
var ScopeSchema = z.strictObject({ projectId: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/), kind: z.enum(["live", "preview"]) });
|
|
21
|
+
function scopePrefix(scope) {
|
|
22
|
+
const parsed = ScopeSchema.parse(scope);
|
|
23
|
+
return `projects/${parsed.projectId}/${parsed.kind}`;
|
|
24
|
+
}
|
|
25
|
+
var ApplicationSecretRefSchema = z.strictObject({ key: z.string().min(1), version: z.string().min(1), digest: z.string().regex(/^sha256:[a-f0-9]{64}$/) });
|
|
26
|
+
var EnvironmentValueSchema = z.strictObject({ value: z.string().optional(), secretRef: ApplicationSecretRefSchema.optional() }).refine((value) => value.value === void 0 !== (value.secretRef === void 0));
|
|
27
|
+
var ConnectionConfigurationSchema = z.strictObject({
|
|
28
|
+
connectorId: z.string().regex(/^[a-z][a-z0-9-]{1,62}$/),
|
|
29
|
+
connectionName: z.string().min(1),
|
|
30
|
+
modulePath: z.string().min(1),
|
|
31
|
+
provider: z.string().min(1),
|
|
32
|
+
authMethodId: z.string().optional(),
|
|
33
|
+
sharedConnectionId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/).optional(),
|
|
34
|
+
configuration: z.record(z.string(), z.unknown())
|
|
35
|
+
});
|
|
36
|
+
var TriggerConfigurationSchema = z.strictObject({
|
|
37
|
+
connectorId: z.string().min(1),
|
|
38
|
+
connectionName: z.string().min(1),
|
|
39
|
+
triggerName: z.string().min(1),
|
|
40
|
+
bindingName: z.string().min(1),
|
|
41
|
+
configuration: z.record(z.string(), z.unknown())
|
|
42
|
+
});
|
|
43
|
+
var OperationConfigurationSchema = z.strictObject({
|
|
44
|
+
connectorId: z.string().min(1),
|
|
45
|
+
connectionName: z.string().min(1),
|
|
46
|
+
operationId: z.string().min(1),
|
|
47
|
+
flowType: z.string().min(1),
|
|
48
|
+
stepType: z.string().min(1),
|
|
49
|
+
configuration: z.record(z.string(), z.unknown())
|
|
50
|
+
});
|
|
51
|
+
var CredentialBrokerConfigurationSchema = z.strictObject({ endpoint: z.url(), tokenSecretRef: ApplicationSecretRefSchema });
|
|
52
|
+
var ConfigurationSchema = z.strictObject({
|
|
53
|
+
schemaVersion: z.enum(["connectors.dex.dev/project-configuration/v1alpha2", ConfigurationSchemaVersion]),
|
|
54
|
+
scope: ScopeSchema,
|
|
55
|
+
revision: z.number().int().positive(),
|
|
56
|
+
connections: z.array(ConnectionConfigurationSchema),
|
|
57
|
+
triggerBindings: z.array(TriggerConfigurationSchema),
|
|
58
|
+
operationConfigurations: z.array(OperationConfigurationSchema),
|
|
59
|
+
environment: z.record(z.string(), EnvironmentValueSchema).optional(),
|
|
60
|
+
credentialBroker: CredentialBrokerConfigurationSchema.optional()
|
|
61
|
+
}).superRefine((configuration, context) => {
|
|
62
|
+
const hasShared = configuration.connections.some((connection) => connection.sharedConnectionId !== void 0);
|
|
63
|
+
if (hasShared && !configuration.credentialBroker) context.addIssue({ code: "custom", message: "shared connections require a credential broker" });
|
|
64
|
+
if (configuration.schemaVersion.endsWith("v1alpha2") && (hasShared || configuration.credentialBroker)) context.addIssue({ code: "custom", message: "shared connections require v1alpha3" });
|
|
65
|
+
if (configuration.credentialBroker) {
|
|
66
|
+
const broker = configuration.credentialBroker;
|
|
67
|
+
const expectedPrefix = `${scopePrefix(configuration.scope)}/app-secrets/DEX_CREDENTIAL_BROKER_TOKEN/`;
|
|
68
|
+
if (!broker.tokenSecretRef.key.startsWith(expectedPrefix) || !/^[a-f0-9]{32}$/.test(broker.tokenSecretRef.key.slice(expectedPrefix.length))) context.addIssue({ code: "custom", message: "broker token reference differs from scope" });
|
|
69
|
+
const endpoint = new URL(broker.endpoint);
|
|
70
|
+
if (!["http:", "https:"].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.search || endpoint.hash || endpoint.protocol === "http:" && !isPrivateBrokerHost(endpoint.hostname)) context.addIssue({ code: "custom", message: "broker endpoint is invalid" });
|
|
71
|
+
}
|
|
72
|
+
const connections = configuration.connections.map((item) => `${item.connectorId}\0${item.connectionName}`);
|
|
73
|
+
const triggers = configuration.triggerBindings.map((item) => `${item.connectorId}\0${item.connectionName}\0${item.triggerName}\0${item.bindingName}`);
|
|
74
|
+
const operations = configuration.operationConfigurations.map((item) => `${item.connectorId}\0${item.connectionName}\0${item.operationId}\0${item.flowType}\0${item.stepType}`);
|
|
75
|
+
for (const keys of [connections, triggers, operations]) {
|
|
76
|
+
if (new Set(keys).size !== keys.length) context.addIssue({ code: "custom", message: "duplicate configuration identity" });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
var SnapshotRefSchema = z.strictObject({ key: z.string().min(1), version: z.string().min(1), digest: z.string().regex(/^sha256:[a-f0-9]{64}$/), mediaType: z.literal("application/json") });
|
|
80
|
+
var LocalConnectionSchema = z.object({
|
|
81
|
+
connectorId: z.string().min(1),
|
|
82
|
+
connectionName: z.string().min(1),
|
|
83
|
+
modulePath: z.string().min(1),
|
|
84
|
+
provider: z.string().min(1),
|
|
85
|
+
authMethodId: z.string().optional(),
|
|
86
|
+
authMethodIds: z.array(z.string()).nullable().optional(),
|
|
87
|
+
configuration: z.record(z.string(), z.unknown()),
|
|
88
|
+
credentials: z.record(z.string(), z.unknown()),
|
|
89
|
+
credentialExpiresAt: RFC3339NanoSchema.nullable().optional()
|
|
90
|
+
});
|
|
91
|
+
var LocalConnectionsFileSchema = z.object({
|
|
92
|
+
schemaVersion: z.literal(LocalConnectionsSchemaVersion),
|
|
93
|
+
connections: z.array(LocalConnectionSchema),
|
|
94
|
+
triggerBindings: z.array(TriggerConfigurationSchema).optional()
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// src/projectconfig/storage.ts
|
|
98
|
+
import { createHash } from "crypto";
|
|
99
|
+
import { inspect } from "util";
|
|
100
|
+
import { GetBucketVersioningCommand, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
101
|
+
var ObjectNotFoundError = class extends Error {
|
|
102
|
+
constructor() {
|
|
103
|
+
super("project configuration object is absent");
|
|
104
|
+
this.name = "ObjectNotFoundError";
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
var ConflictError = class extends Error {
|
|
108
|
+
constructor() {
|
|
109
|
+
super("project configuration revision changed");
|
|
110
|
+
this.name = "ConflictError";
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var OutcomeUnknownError = class extends Error {
|
|
114
|
+
constructor() {
|
|
115
|
+
super("project configuration write outcome is unknown");
|
|
116
|
+
this.name = "OutcomeUnknownError";
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var ProjectObject = class {
|
|
120
|
+
/** Keep private bytes outside Node inspection; key, version, and ETag identify the exact object. */
|
|
121
|
+
constructor(key, version, etag, contents) {
|
|
122
|
+
this.key = key;
|
|
123
|
+
this.version = version;
|
|
124
|
+
this.etag = etag;
|
|
125
|
+
this.#contents = contents;
|
|
126
|
+
}
|
|
127
|
+
key;
|
|
128
|
+
version;
|
|
129
|
+
etag;
|
|
130
|
+
#contents;
|
|
131
|
+
/** Exact private bytes; callers must not log or persist them. */
|
|
132
|
+
get contents() {
|
|
133
|
+
return this.#contents;
|
|
134
|
+
}
|
|
135
|
+
/** Never format private contents. */
|
|
136
|
+
toString() {
|
|
137
|
+
return `ProjectObject{${this.key} version=${this.version}}`;
|
|
138
|
+
}
|
|
139
|
+
/** Keep Node inspection and console output secret-safe. */
|
|
140
|
+
[inspect.custom]() {
|
|
141
|
+
return this.toString();
|
|
142
|
+
}
|
|
143
|
+
/** Reject accidental JSON transport. */
|
|
144
|
+
toJSON() {
|
|
145
|
+
throw new TypeError("private project objects cannot be serialized");
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
var InMemoryObjectStore = class {
|
|
149
|
+
#versions = /* @__PURE__ */ new Map();
|
|
150
|
+
/** Read the latest object or an exact immutable version. */
|
|
151
|
+
async readObject(key, version = "") {
|
|
152
|
+
validateObjectKey(key);
|
|
153
|
+
const versions = this.#versions.get(key) ?? [];
|
|
154
|
+
const found = version ? versions.find((object) => object.version === version) : versions.at(-1);
|
|
155
|
+
if (!found) throw new ObjectNotFoundError();
|
|
156
|
+
return found;
|
|
157
|
+
}
|
|
158
|
+
/** Create only when no object exists under this key. */
|
|
159
|
+
async createObject(key, contents) {
|
|
160
|
+
validateObjectKey(key);
|
|
161
|
+
validateContents(contents);
|
|
162
|
+
if (this.#versions.has(key)) throw new ConflictError();
|
|
163
|
+
return this.#write(key, contents);
|
|
164
|
+
}
|
|
165
|
+
/** Compare and swap the latest object by ETag. */
|
|
166
|
+
async compareAndSwapObject(key, expectedETag, contents) {
|
|
167
|
+
validateObjectKey(key);
|
|
168
|
+
validateContents(contents);
|
|
169
|
+
const current = (this.#versions.get(key) ?? []).at(-1);
|
|
170
|
+
if (!current || current.etag !== expectedETag) throw new ConflictError();
|
|
171
|
+
return this.#write(key, contents);
|
|
172
|
+
}
|
|
173
|
+
#write(key, contents) {
|
|
174
|
+
const version = String((this.#versions.get(key)?.length ?? 0) + 1);
|
|
175
|
+
const object = new ProjectObject(key, version, `${digestBytes(contents).slice(7)}-${version}`, new Uint8Array(contents));
|
|
176
|
+
this.#versions.set(key, [...this.#versions.get(key) ?? [], object]);
|
|
177
|
+
return object;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
var S3ObjectStore = class _S3ObjectStore {
|
|
181
|
+
constructor(configuration, prefix) {
|
|
182
|
+
this.configuration = configuration;
|
|
183
|
+
this.prefix = prefix;
|
|
184
|
+
}
|
|
185
|
+
configuration;
|
|
186
|
+
prefix;
|
|
187
|
+
/** Verify bucket versioning before any application request. */
|
|
188
|
+
static async create(configuration) {
|
|
189
|
+
if (!configuration.client || !configuration.bucket || /[/\\\0]/.test(configuration.bucket)) throw new TypeError("versioned project storage is required");
|
|
190
|
+
const hasExactKmsKey = /^arn:[a-z0-9-]+:kms:[a-z0-9-]+:[0-9]{12}:key\/[a-zA-Z0-9-]+$/.test(configuration.kmsKeyId ?? "");
|
|
191
|
+
if (!hasExactKmsKey && !(configuration.allowUnencrypted && !configuration.kmsKeyId)) throw new TypeError("exact hosted KMS key ARN is required");
|
|
192
|
+
const prefix = configuration.prefix?.replace(/\/$/, "") ?? "";
|
|
193
|
+
if (prefix) validateObjectKey(prefix);
|
|
194
|
+
if (await configuration.client.config.maxAttempts() !== 1) throw new TypeError("S3 client must disable automatic write retries with maxAttempts: 1");
|
|
195
|
+
const versioning = await configuration.client.send(new GetBucketVersioningCommand({ Bucket: configuration.bucket }));
|
|
196
|
+
if (versioning.Status !== "Enabled") throw new Error("project object bucket versioning must be enabled");
|
|
197
|
+
return new _S3ObjectStore(configuration, prefix ? `${prefix}/` : "");
|
|
198
|
+
}
|
|
199
|
+
/** Read bounded private bytes and validate exact version and KMS key. */
|
|
200
|
+
async readObject(key, version = "") {
|
|
201
|
+
validateObjectKey(key);
|
|
202
|
+
let result;
|
|
203
|
+
try {
|
|
204
|
+
result = await this.configuration.client.send(new GetObjectCommand({ Bucket: this.configuration.bucket, Key: this.prefix + key, ...version ? { VersionId: version } : {} }));
|
|
205
|
+
} catch (error) {
|
|
206
|
+
throw classifyStorageError(error, false);
|
|
207
|
+
}
|
|
208
|
+
if (!result.VersionId || result.VersionId === "null" || !result.ETag || version && result.VersionId !== version) throw new Error("project object lacks exact version identity");
|
|
209
|
+
if (this.configuration.kmsKeyId && (result.ServerSideEncryption !== "aws:kms" || result.SSEKMSKeyId !== this.configuration.kmsKeyId)) throw new Error("project object encryption boundary differs");
|
|
210
|
+
if (!result.Body) throw new Error("project object body is unavailable");
|
|
211
|
+
const contents = await readBoundedBody(result.Body, 4 << 20);
|
|
212
|
+
validateContents(contents);
|
|
213
|
+
return new ProjectObject(key, result.VersionId, result.ETag, contents);
|
|
214
|
+
}
|
|
215
|
+
/** Write once with If-None-Match. */
|
|
216
|
+
async createObject(key, contents) {
|
|
217
|
+
return this.writeObject(key, "", contents);
|
|
218
|
+
}
|
|
219
|
+
/** Write once with If-Match; ambiguous outcomes stay unknown. */
|
|
220
|
+
async compareAndSwapObject(key, expectedETag, contents) {
|
|
221
|
+
if (!expectedETag) throw new TypeError("expected ETag is required");
|
|
222
|
+
return this.writeObject(key, expectedETag, contents);
|
|
223
|
+
}
|
|
224
|
+
async writeObject(key, etag, contents) {
|
|
225
|
+
validateObjectKey(key);
|
|
226
|
+
validateContents(contents);
|
|
227
|
+
let result;
|
|
228
|
+
try {
|
|
229
|
+
result = await this.configuration.client.send(new PutObjectCommand({
|
|
230
|
+
Bucket: this.configuration.bucket,
|
|
231
|
+
Key: this.prefix + key,
|
|
232
|
+
Body: contents,
|
|
233
|
+
ContentType: "application/json",
|
|
234
|
+
...etag ? { IfMatch: etag } : { IfNoneMatch: "*" },
|
|
235
|
+
...this.configuration.kmsKeyId ? { ServerSideEncryption: "aws:kms", SSEKMSKeyId: this.configuration.kmsKeyId } : {}
|
|
236
|
+
}));
|
|
237
|
+
} catch (error) {
|
|
238
|
+
throw classifyStorageError(error, true);
|
|
239
|
+
}
|
|
240
|
+
if (!result.VersionId || result.VersionId === "null" || !result.ETag) throw new OutcomeUnknownError();
|
|
241
|
+
return new ProjectObject(key, result.VersionId, result.ETag, contents);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
var ConfigurationStore = class {
|
|
245
|
+
/** Bind a private object store to one validated project scope. */
|
|
246
|
+
constructor(objects, scope) {
|
|
247
|
+
this.objects = objects;
|
|
248
|
+
this.scope = scope;
|
|
249
|
+
if (!objects) throw new TypeError("configuration object store is required");
|
|
250
|
+
this.#key = `${scopePrefix(scope)}/configuration/head`;
|
|
251
|
+
}
|
|
252
|
+
objects;
|
|
253
|
+
scope;
|
|
254
|
+
#key;
|
|
255
|
+
/** Read and validate current ordinary configuration. */
|
|
256
|
+
async readConfiguration() {
|
|
257
|
+
const object = await this.objects.readObject(this.#key);
|
|
258
|
+
return { configuration: this.decodeConfiguration(object), object };
|
|
259
|
+
}
|
|
260
|
+
/** Conditionally replace ordinary settings using an expected revision. */
|
|
261
|
+
async writeConfiguration(expectedRevision, document) {
|
|
262
|
+
let previous;
|
|
263
|
+
let priorObject;
|
|
264
|
+
try {
|
|
265
|
+
const current = await this.readConfiguration();
|
|
266
|
+
previous = current.configuration;
|
|
267
|
+
priorObject = current.object;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (!(error instanceof ObjectNotFoundError && expectedRevision === 0)) throw error;
|
|
270
|
+
}
|
|
271
|
+
if ((previous?.revision ?? 0) !== expectedRevision) throw new ConflictError();
|
|
272
|
+
const schemaVersion = document.credentialBroker || document.connections.some((connection) => connection.sharedConnectionId) ? ConfigurationSchemaVersion : "connectors.dex.dev/project-configuration/v1alpha2";
|
|
273
|
+
const configuration = ConfigurationSchema.parse({ schemaVersion, scope: this.scope, revision: expectedRevision + 1, ...document });
|
|
274
|
+
const contents = Buffer.from(encodeGoCompatibleJSON(configuration));
|
|
275
|
+
try {
|
|
276
|
+
if (!priorObject) await this.objects.createObject(this.#key, contents);
|
|
277
|
+
else await this.objects.compareAndSwapObject(this.#key, priorObject.etag, contents);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (!(error instanceof OutcomeUnknownError)) throw error;
|
|
280
|
+
try {
|
|
281
|
+
const observed = await this.objects.readObject(this.#key);
|
|
282
|
+
if (!Buffer.from(observed.contents).equals(contents)) throw error;
|
|
283
|
+
} catch {
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return configuration;
|
|
288
|
+
}
|
|
289
|
+
/** Read an exact pinned snapshot after checking its digest. */
|
|
290
|
+
async readSnapshot(reference) {
|
|
291
|
+
if (reference.key !== this.#key || !reference.version || reference.version === "null" || reference.mediaType !== "application/json") throw new TypeError("invalid configuration snapshot reference");
|
|
292
|
+
const object = await this.objects.readObject(reference.key, reference.version);
|
|
293
|
+
if (digestBytes(object.contents) !== reference.digest) throw new Error("configuration snapshot digest differs");
|
|
294
|
+
return this.decodeConfiguration(object);
|
|
295
|
+
}
|
|
296
|
+
/** Pin the current version when its revision is still expected. */
|
|
297
|
+
async freezeConfiguration(expectedRevision) {
|
|
298
|
+
const { configuration, object } = await this.readConfiguration();
|
|
299
|
+
if (configuration.revision !== expectedRevision) throw new ConflictError();
|
|
300
|
+
return { key: object.key, version: object.version, digest: digestBytes(object.contents), mediaType: "application/json" };
|
|
301
|
+
}
|
|
302
|
+
decodeConfiguration(object) {
|
|
303
|
+
if (object.key !== this.#key || !object.version || !object.etag) throw new Error("configuration document identity is invalid");
|
|
304
|
+
let decoded;
|
|
305
|
+
try {
|
|
306
|
+
decoded = JSON.parse(Buffer.from(object.contents).toString("utf8"));
|
|
307
|
+
} catch {
|
|
308
|
+
throw new Error("configuration document is not valid JSON");
|
|
309
|
+
}
|
|
310
|
+
const configuration = ConfigurationSchema.parse(decoded);
|
|
311
|
+
if (configuration.scope.projectId !== this.scope.projectId || configuration.scope.kind !== this.scope.kind) throw new Error("configuration scope differs");
|
|
312
|
+
return configuration;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
function validateObjectKey(key) {
|
|
316
|
+
if (!key || key.length > 1024 || key.startsWith("/") || /[\\\0\r\n]/.test(key) || key.split("/").some((segment) => !segment || segment === "." || segment === "..")) throw new TypeError("invalid project object key");
|
|
317
|
+
}
|
|
318
|
+
function connectionHeadKey(scope, connectorId, connectionName) {
|
|
319
|
+
if (!/^[a-z][a-z0-9-]{1,62}$/.test(connectorId) || !connectionName.trim()) throw new TypeError("connection identity is invalid");
|
|
320
|
+
return `${scopePrefix(scope)}/connections/${connectorId}/${Buffer.from(connectionName).toString("base64url")}/head`;
|
|
321
|
+
}
|
|
322
|
+
function digestBytes(contents) {
|
|
323
|
+
return `sha256:${createHash("sha256").update(contents).digest("hex")}`;
|
|
324
|
+
}
|
|
325
|
+
function encodeGoCompatibleJSON(document) {
|
|
326
|
+
if (document === null || typeof document !== "object") throw new TypeError("project document must be a JSON object");
|
|
327
|
+
return encodeCanonicalValue(document);
|
|
328
|
+
}
|
|
329
|
+
function encodeCanonicalValue(value, context = "structured") {
|
|
330
|
+
if (typeof value === "number" && !Number.isFinite(value)) throw new TypeError("project document contains a non-finite number");
|
|
331
|
+
if (value === null || typeof value === "boolean" || typeof value === "number") return JSON.stringify(value);
|
|
332
|
+
if (typeof value === "string") return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (character) => {
|
|
333
|
+
const escapes = { "<": "\\u003c", ">": "\\u003e", "&": "\\u0026", "\u2028": "\\u2028", "\u2029": "\\u2029" };
|
|
334
|
+
return escapes[character] ?? character;
|
|
335
|
+
});
|
|
336
|
+
if (Array.isArray(value)) return `[${value.map((item) => encodeCanonicalValue(item, context)).join(",")}]`;
|
|
337
|
+
if (typeof value !== "object") throw new TypeError("project document contains a non-JSON value");
|
|
338
|
+
const object = value;
|
|
339
|
+
const isStructured = context === "structured";
|
|
340
|
+
const keys = isStructured ? canonicalFieldOrder(object) : Object.keys(object).sort((left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right)));
|
|
341
|
+
return `{${keys.filter((key) => object[key] !== void 0 && (!isStructured || !shouldOmitGoField(object, key))).map((key) => {
|
|
342
|
+
const childContext = context === "environment" ? "structured" : context === "map" ? "map" : key === "environment" ? "environment" : ["configuration", "credentials", "event"].includes(key) ? "map" : "structured";
|
|
343
|
+
const fieldValue = isStructured && ["expiresAt", "deadline", "issuedAt", "credentialExpiresAt", "observedAt"].includes(key) && typeof object[key] === "string" ? canonicalRFC3339Nano(object[key]) : object[key];
|
|
344
|
+
return `${encodeCanonicalValue(key)}:${encodeCanonicalValue(fieldValue, childContext)}`;
|
|
345
|
+
}).join(",")}}`;
|
|
346
|
+
}
|
|
347
|
+
function canonicalRFC3339Nano(value) {
|
|
348
|
+
const parts = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/.exec(value);
|
|
349
|
+
if (!parts || Number.isNaN(Date.parse(value))) throw new TypeError("project timestamp must be RFC3339Nano with at most nine fractional digits");
|
|
350
|
+
const fraction = (parts[2] ?? "").replace(/0+$/, "");
|
|
351
|
+
const offset = parts[3] === "+00:00" || parts[3] === "-00:00" ? "Z" : parts[3];
|
|
352
|
+
return `${parts[1]}${fraction ? `.${fraction}` : ""}${offset}`;
|
|
353
|
+
}
|
|
354
|
+
function shouldOmitGoField(object, key) {
|
|
355
|
+
const value = object[key];
|
|
356
|
+
if (value === void 0) return true;
|
|
357
|
+
if (["expiresAt", "credential", "mutation", "credentialBroker", "secretRef"].includes(key)) return value === null;
|
|
358
|
+
if (["authMethodId", "sharedConnectionId", "attemptId"].includes(key)) return value === "";
|
|
359
|
+
if (key === "environment") return value === null || typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
function canonicalFieldOrder(object) {
|
|
363
|
+
const keys = Object.keys(object);
|
|
364
|
+
let preferred = [];
|
|
365
|
+
if ("schemaVersion" in object && "operationConfigurations" in object) preferred = ["schemaVersion", "scope", "revision", "connections", "triggerBindings", "operationConfigurations", "environment", "credentialBroker"];
|
|
366
|
+
else if ("schemaVersion" in object && "status" in object) preferred = ["schemaVersion", "connection", "scope", "revision", "fence", "status", "expiresAt", "authMethod", "credential", "mutation"];
|
|
367
|
+
else if ("schemaVersion" in object && "credentials" in object) preferred = ["schemaVersion", "scope", "connection", "fence", "attemptId", "credentials", "expiresAt", "authMethod"];
|
|
368
|
+
else if ("projectId" in object && "kind" in object) preferred = ["projectId", "kind"];
|
|
369
|
+
else if ("connectorId" in object && "modulePath" in object) preferred = ["connectorId", "connectionName", "modulePath", "provider", "authMethodId", "sharedConnectionId", "configuration"];
|
|
370
|
+
else if ("connectorId" in object && "triggerName" in object) preferred = ["connectorId", "connectionName", "triggerName", "bindingName", "configuration"];
|
|
371
|
+
else if ("connectorId" in object && "flowType" in object) preferred = ["connectorId", "connectionName", "operationId", "flowType", "stepType", "configuration"];
|
|
372
|
+
else if ("connectorId" in object && "connectionName" in object) preferred = ["connectorId", "connectionName"];
|
|
373
|
+
else if ("kind" in object && "invocationId" in object) preferred = ["kind", "invocationId", "id", "fence", "baseRevision", "deadline", "resultKey"];
|
|
374
|
+
else if ("endpoint" in object && "tokenSecretRef" in object) preferred = ["endpoint", "tokenSecretRef"];
|
|
375
|
+
else if ("value" in object || "secretRef" in object) preferred = ["value", "secretRef"];
|
|
376
|
+
else if ("key" in object && "version" in object && "digest" in object) preferred = ["key", "version", "digest"];
|
|
377
|
+
const selected = preferred.filter((key) => keys.includes(key));
|
|
378
|
+
return selected.concat(keys.filter((key) => !selected.includes(key)).sort((left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right))));
|
|
379
|
+
}
|
|
380
|
+
function validateContents(contents) {
|
|
381
|
+
if (!contents.length || contents.length > 4 << 20) throw new TypeError("project object body is empty or oversized");
|
|
382
|
+
}
|
|
383
|
+
function classifyStorageError(error, isWrite) {
|
|
384
|
+
const code = error !== null && typeof error === "object" && "name" in error ? String(error.name) : "";
|
|
385
|
+
if (["NoSuchKey", "NoSuchVersion", "NotFound"].includes(code)) return new ObjectNotFoundError();
|
|
386
|
+
if (["PreconditionFailed", "ConditionalRequestConflict", "OperationAborted"].includes(code)) return new ConflictError();
|
|
387
|
+
return isWrite ? new OutcomeUnknownError() : new Error("project object read failed");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/projectconfig/environment.ts
|
|
391
|
+
import { readFile as readFile2, lstat as lstat2 } from "fs/promises";
|
|
392
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
393
|
+
import { S3Client as S3Client2 } from "@aws-sdk/client-s3";
|
|
394
|
+
import { z as z4 } from "zod";
|
|
395
|
+
|
|
396
|
+
// src/projectconfig/connection_schema.ts
|
|
397
|
+
import { z as z2 } from "zod";
|
|
398
|
+
var ConnectionRecordSchema = z2.strictObject({
|
|
399
|
+
schemaVersion: z2.literal("connectors.dex.dev/project-connection/v1alpha2"),
|
|
400
|
+
connection: z2.strictObject({ connectorId: z2.string(), connectionName: z2.string() }),
|
|
401
|
+
scope: ScopeSchema,
|
|
402
|
+
revision: z2.number().int().positive(),
|
|
403
|
+
fence: z2.number().int().positive(),
|
|
404
|
+
status: z2.enum(["READY", "AUTHORIZING", "REFRESHING", "REAUTHORIZATION_REQUIRED", "REVOKED"]),
|
|
405
|
+
authMethod: z2.string(),
|
|
406
|
+
expiresAt: RFC3339NanoSchema.nullable().optional(),
|
|
407
|
+
credential: z2.strictObject({ key: z2.string(), version: z2.string(), digest: z2.string() }).optional(),
|
|
408
|
+
mutation: z2.strictObject({ kind: z2.string(), invocationId: z2.string(), id: z2.string(), fence: z2.number(), baseRevision: z2.number(), deadline: RFC3339NanoSchema, resultKey: z2.string() }).optional()
|
|
409
|
+
});
|
|
410
|
+
var CredentialEnvelopeSchema = z2.strictObject({
|
|
411
|
+
schemaVersion: z2.literal("connectors.dex.dev/project-connection/v1alpha2"),
|
|
412
|
+
scope: ScopeSchema,
|
|
413
|
+
connection: z2.strictObject({ connectorId: z2.string(), connectionName: z2.string() }),
|
|
414
|
+
fence: z2.number().int().positive(),
|
|
415
|
+
attemptId: z2.string().optional(),
|
|
416
|
+
credentials: z2.record(z2.string(), z2.unknown()),
|
|
417
|
+
expiresAt: RFC3339NanoSchema.nullable().optional(),
|
|
418
|
+
authMethod: z2.string()
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// src/projectconfig/connections.ts
|
|
422
|
+
import { inspect as inspect2 } from "util";
|
|
423
|
+
function connectionPath(scope, key) {
|
|
424
|
+
if (!/^[a-z][a-z0-9-]{1,62}$/.test(key.connectorId) || !key.connectionName || key.connectionName.trim() !== key.connectionName || key.connectionName.length > 256 || /[\0\r\n]/.test(key.connectionName)) throw new TypeError("invalid project connection key");
|
|
425
|
+
return `${scopePrefix(scope)}/connections/${key.connectorId}/${Buffer.from(key.connectionName).toString("base64url")}`;
|
|
426
|
+
}
|
|
427
|
+
var CredentialMaterial = class {
|
|
428
|
+
/** Capture complete private credentials and optional access expiry. */
|
|
429
|
+
constructor(credentials, expiresAt, authMethod) {
|
|
430
|
+
this.credentials = credentials;
|
|
431
|
+
this.expiresAt = expiresAt;
|
|
432
|
+
this.authMethod = authMethod;
|
|
433
|
+
}
|
|
434
|
+
credentials;
|
|
435
|
+
expiresAt;
|
|
436
|
+
authMethod;
|
|
437
|
+
/** Reject accidental durable JSON serialization. */
|
|
438
|
+
toJSON() {
|
|
439
|
+
throw new TypeError("credential material cannot be serialized");
|
|
440
|
+
}
|
|
441
|
+
/** Redact private material in logs. */
|
|
442
|
+
[inspect2.custom]() {
|
|
443
|
+
return "CredentialMaterial([REDACTED])";
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
var ConnectionStore = class {
|
|
447
|
+
/** Bind one object store and validated project scope. */
|
|
448
|
+
constructor(objects, scope, shared) {
|
|
449
|
+
this.objects = objects;
|
|
450
|
+
this.scope = scope;
|
|
451
|
+
this.shared = shared;
|
|
452
|
+
if (!objects) throw new TypeError("project connection object store is required");
|
|
453
|
+
scopePrefix(scope);
|
|
454
|
+
}
|
|
455
|
+
objects;
|
|
456
|
+
scope;
|
|
457
|
+
shared;
|
|
458
|
+
/** Read safe connection metadata without credential material. */
|
|
459
|
+
async readConnection(key) {
|
|
460
|
+
const { record } = await this.readRecord(key);
|
|
461
|
+
return this.#connection(key, record);
|
|
462
|
+
}
|
|
463
|
+
/** Read exact current private material from a READY connection. */
|
|
464
|
+
async readCredentialMaterial(key) {
|
|
465
|
+
const { record } = await this.readRecord(key);
|
|
466
|
+
const connection = this.#connection(key, record);
|
|
467
|
+
if (record.status !== "READY") throw new Error("project credential is unavailable");
|
|
468
|
+
return { material: await this.readMaterial(key, record), connection };
|
|
469
|
+
}
|
|
470
|
+
/** Read and validate a current head for refresh admission. */
|
|
471
|
+
async readRecord(key) {
|
|
472
|
+
const path = connectionPath(this.scope, key);
|
|
473
|
+
const object = await this.objects.readObject(`${path}/head`);
|
|
474
|
+
if (!object.version || !object.etag) throw new Error("project connection head lacks version identity");
|
|
475
|
+
const parsed = ConnectionRecordSchema.safeParse(parsePrivateJSON(object.contents));
|
|
476
|
+
if (!parsed.success) throw new Error("project connection identity is invalid");
|
|
477
|
+
const record = parsed.data;
|
|
478
|
+
if (record.scope.projectId !== this.scope.projectId || record.scope.kind !== this.scope.kind || record.connection.connectorId !== key.connectorId || record.connection.connectionName !== key.connectionName) throw new Error("project connection identity is invalid");
|
|
479
|
+
if (record.status === "REVOKED" && (record.credential || record.mutation)) throw new Error("revoked connection retains active material");
|
|
480
|
+
if (["READY", "REFRESHING"].includes(record.status) && !record.credential) throw new Error("project credential reference is absent");
|
|
481
|
+
if (record.credential && (!record.credential.key.startsWith(`${path}/`) || !record.credential.version || !/^[a-f0-9]{64}$/.test(record.credential.digest))) throw new Error("project credential reference is invalid");
|
|
482
|
+
if (record.status === "REFRESHING" || record.status === "AUTHORIZING") {
|
|
483
|
+
const mutation = record.mutation;
|
|
484
|
+
if (!mutation || mutation.kind !== record.status || mutation.fence !== record.fence || mutation.baseRevision !== record.revision - 1 || !/^[A-Za-z0-9_-]{1,128}$/.test(mutation.id) || !mutation.invocationId || mutation.resultKey !== `${path}/exchange-results/${mutation.id}`) throw new Error("credential exchange admission is invalid");
|
|
485
|
+
} else if (record.mutation) throw new Error("inactive credential exchange admission is invalid");
|
|
486
|
+
return { record, object };
|
|
487
|
+
}
|
|
488
|
+
/** Read private material by an already validated head. */
|
|
489
|
+
async readMaterial(key, record) {
|
|
490
|
+
const reference = record.credential;
|
|
491
|
+
if (!reference) throw new Error("project credential reference is absent");
|
|
492
|
+
const object = await this.objects.readObject(reference.key, reference.version);
|
|
493
|
+
if (digestBytes(object.contents).slice(7) !== reference.digest) throw new Error("project credential integrity differs");
|
|
494
|
+
const parsed = CredentialEnvelopeSchema.safeParse(parsePrivateJSON(object.contents));
|
|
495
|
+
if (!parsed.success) throw new Error("project credential envelope violates its schema");
|
|
496
|
+
const envelope = parsed.data;
|
|
497
|
+
if (envelope.scope.projectId !== this.scope.projectId || envelope.scope.kind !== this.scope.kind || envelope.connection.connectorId !== key.connectorId || envelope.connection.connectionName !== key.connectionName || envelope.authMethod !== record.authMethod || !sameExpiry(envelope.expiresAt, record.expiresAt)) throw new Error("project credential identity differs");
|
|
498
|
+
return new CredentialMaterial(envelope.credentials, parseExpiry(envelope.expiresAt), envelope.authMethod);
|
|
499
|
+
}
|
|
500
|
+
/** Fence prior work and publish one complete immutable credential version. */
|
|
501
|
+
async replaceCredential(key, expectedRevision, material) {
|
|
502
|
+
if (!material.authMethod || !material.credentials || typeof material.credentials !== "object" || Array.isArray(material.credentials)) throw new TypeError("invalid credential material");
|
|
503
|
+
const path = connectionPath(this.scope, key);
|
|
504
|
+
let previous;
|
|
505
|
+
try {
|
|
506
|
+
previous = await this.readRecord(key);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (!(error instanceof ObjectNotFoundError && expectedRevision === 0)) throw error;
|
|
509
|
+
}
|
|
510
|
+
if ((previous?.record.revision ?? 0) !== expectedRevision) throw new ConflictError();
|
|
511
|
+
const fence = (previous?.record.fence ?? 0) + 1;
|
|
512
|
+
const envelope = credentialEnvelope(this.scope, key, fence, material);
|
|
513
|
+
const contents = Buffer.from(encodeGoCompatibleJSON(envelope));
|
|
514
|
+
const immutableKey = `${path}/credentials/${digestBytes(contents).slice(7)}`;
|
|
515
|
+
let created;
|
|
516
|
+
try {
|
|
517
|
+
created = await this.objects.createObject(immutableKey, contents);
|
|
518
|
+
} catch (error) {
|
|
519
|
+
if (!(error instanceof ConflictError || error instanceof OutcomeUnknownError)) throw error;
|
|
520
|
+
created = await this.objects.readObject(immutableKey);
|
|
521
|
+
if (!Buffer.from(created.contents).equals(contents)) throw new Error("immutable credential content differs");
|
|
522
|
+
}
|
|
523
|
+
const reference = { key: immutableKey, version: created.version, digest: digestBytes(contents).slice(7) };
|
|
524
|
+
const record = connectionRecord(this.scope, key, expectedRevision + 1, fence, "READY", material.expiresAt, material.authMethod, reference);
|
|
525
|
+
await this.writeHead(key, previous?.object, record);
|
|
526
|
+
return this.#connection(key, record);
|
|
527
|
+
}
|
|
528
|
+
/** Conditionally publish a new head and reconcile an unknown write outcome. */
|
|
529
|
+
async writeHead(key, previous, record) {
|
|
530
|
+
const path = connectionPath(this.scope, key);
|
|
531
|
+
const contents = Buffer.from(encodeGoCompatibleJSON(record));
|
|
532
|
+
try {
|
|
533
|
+
return previous ? await this.objects.compareAndSwapObject(`${path}/head`, previous.etag, contents) : await this.objects.createObject(`${path}/head`, contents);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
if (!(error instanceof OutcomeUnknownError)) throw error;
|
|
536
|
+
const observed = await this.objects.readObject(`${path}/head`);
|
|
537
|
+
if (!Buffer.from(observed.contents).equals(contents)) throw error;
|
|
538
|
+
return observed;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
#connection(key, record) {
|
|
542
|
+
const expiresAt = parseExpiry(record.expiresAt);
|
|
543
|
+
return { key, scope: this.scope, revision: record.revision, fence: record.fence, status: record.status, authMethod: record.authMethod, ...expiresAt ? { expiresAt } : {} };
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
function credentialEnvelope(scope, key, fence, material, attemptId) {
|
|
547
|
+
return { schemaVersion: "connectors.dex.dev/project-connection/v1alpha2", scope, connection: key, fence, ...attemptId ? { attemptId } : {}, credentials: material.credentials, ...material.expiresAt ? { expiresAt: material.expiresAt.toISOString() } : {}, authMethod: material.authMethod };
|
|
548
|
+
}
|
|
549
|
+
function connectionRecord(scope, key, revision, fence, status, expiresAt, authMethod, credential, mutation) {
|
|
550
|
+
return ConnectionRecordSchema.parse({ schemaVersion: "connectors.dex.dev/project-connection/v1alpha2", connection: key, scope, revision, fence, status, ...expiresAt ? { expiresAt: expiresAt.toISOString() } : {}, authMethod, ...credential ? { credential } : {}, ...mutation ? { mutation } : {} });
|
|
551
|
+
}
|
|
552
|
+
function parsePrivateJSON(contents) {
|
|
553
|
+
try {
|
|
554
|
+
return JSON.parse(Buffer.from(contents).toString("utf8"));
|
|
555
|
+
} catch {
|
|
556
|
+
throw new Error("project credential is not valid JSON");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function parseExpiry(value) {
|
|
560
|
+
return value ? new Date(value) : void 0;
|
|
561
|
+
}
|
|
562
|
+
function sameExpiry(left, right) {
|
|
563
|
+
return left && right ? rfc3339Nanoseconds(left) === rfc3339Nanoseconds(right) : !left && !right;
|
|
564
|
+
}
|
|
565
|
+
function rfc3339Nanoseconds(value) {
|
|
566
|
+
const parts = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/.exec(value);
|
|
567
|
+
if (!parts) throw new Error("project credential expiry is invalid");
|
|
568
|
+
const milliseconds = Date.parse(`${parts[1]}${parts[3]}`);
|
|
569
|
+
if (!Number.isFinite(milliseconds)) throw new Error("project credential expiry is invalid");
|
|
570
|
+
return BigInt(milliseconds) * 1000000n + BigInt((parts[2] ?? "").padEnd(9, "0") || "0");
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/projectconfig/dexweb_store.ts
|
|
574
|
+
import { randomUUID, createHash as createHash2 } from "crypto";
|
|
575
|
+
import { lstat, open, readFile, rename, unlink } from "fs/promises";
|
|
576
|
+
import { dirname, join } from "path";
|
|
577
|
+
var DexWebObjectStore = class {
|
|
578
|
+
/** Bind a mode-0600 connections.json file and local project scope. */
|
|
579
|
+
constructor(path, scope) {
|
|
580
|
+
this.path = path;
|
|
581
|
+
this.#connections = new ConnectionStore(this.#memory, scope);
|
|
582
|
+
}
|
|
583
|
+
path;
|
|
584
|
+
#memory = new InMemoryObjectStore();
|
|
585
|
+
#connections;
|
|
586
|
+
#imported = /* @__PURE__ */ new Map();
|
|
587
|
+
#lockTail = Promise.resolve();
|
|
588
|
+
/** Import current Dex Web credentials before application registration. */
|
|
589
|
+
async initialize() {
|
|
590
|
+
await this.#withLock(() => this.#sync());
|
|
591
|
+
}
|
|
592
|
+
/** Adopt Dex Web updates before reading a connection head. */
|
|
593
|
+
async readObject(key, version = "") {
|
|
594
|
+
if (isConnectionHead(key)) return this.#withLock(async () => {
|
|
595
|
+
await this.#sync();
|
|
596
|
+
return this.#memory.readObject(key, version);
|
|
597
|
+
});
|
|
598
|
+
return this.#memory.readObject(key, version);
|
|
599
|
+
}
|
|
600
|
+
/** Create an immutable private object in process memory. */
|
|
601
|
+
async createObject(key, contents) {
|
|
602
|
+
return this.#memory.createObject(key, contents);
|
|
603
|
+
}
|
|
604
|
+
/** CAS a head, then write back a refreshed credential if the file is still current. */
|
|
605
|
+
async compareAndSwapObject(key, expectedETag, contents) {
|
|
606
|
+
if (!isConnectionHead(key)) return this.#memory.compareAndSwapObject(key, expectedETag, contents);
|
|
607
|
+
return this.#withLock(async () => {
|
|
608
|
+
await this.#sync();
|
|
609
|
+
const object = await this.#memory.compareAndSwapObject(key, expectedETag, contents);
|
|
610
|
+
await this.#writeBack(object);
|
|
611
|
+
return object;
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
async #sync() {
|
|
615
|
+
const file = await readDexWebFile(this.path);
|
|
616
|
+
for (const connection of file.connections) {
|
|
617
|
+
const key = { connectorId: connection.connectorId, connectionName: connection.connectionName };
|
|
618
|
+
const expiresAt = connection.credentialExpiresAt ? new Date(connection.credentialExpiresAt) : void 0;
|
|
619
|
+
const authMethod = connection.authMethodId || (connection.authMethodIds?.length === 1 ? connection.authMethodIds[0] : "local");
|
|
620
|
+
const material = new CredentialMaterial(connection.credentials, expiresAt, authMethod);
|
|
621
|
+
const digest = credentialDigest(connection.credentials, connection.credentialExpiresAt);
|
|
622
|
+
if (this.#imported.get(keyString(key)) === digest) continue;
|
|
623
|
+
let revision = 0;
|
|
624
|
+
try {
|
|
625
|
+
revision = (await this.#connections.readConnection(key)).revision;
|
|
626
|
+
} catch (error) {
|
|
627
|
+
if (!(error instanceof ObjectNotFoundError)) throw error;
|
|
628
|
+
}
|
|
629
|
+
await this.#connections.replaceCredential(key, revision, material);
|
|
630
|
+
this.#imported.set(keyString(key), digest);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async #writeBack(head) {
|
|
634
|
+
const parsed = ConnectionRecordSchema.safeParse(parseJSON(head.contents));
|
|
635
|
+
if (!parsed.success || parsed.data.status !== "READY" || !parsed.data.credential) return;
|
|
636
|
+
const key = parsed.data.connection;
|
|
637
|
+
try {
|
|
638
|
+
const material = await this.#connections.readMaterial(key, parsed.data);
|
|
639
|
+
const digest = credentialDigest(material.credentials, material.expiresAt?.toISOString());
|
|
640
|
+
if (digest === this.#imported.get(keyString(key))) return;
|
|
641
|
+
await this.#replaceInFile(key, material);
|
|
642
|
+
this.#imported.set(keyString(key), digest);
|
|
643
|
+
} catch {
|
|
644
|
+
console.warn("local connector credential write-back skipped");
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async #replaceInFile(key, material) {
|
|
648
|
+
const file = await readDexWebFile(this.path);
|
|
649
|
+
const record = file.connections.find((connection) => connection.connectorId === key.connectorId && connection.connectionName === key.connectionName);
|
|
650
|
+
if (!record) throw new Error("the connection is no longer in the file");
|
|
651
|
+
if (credentialDigest(record.credentials, record.credentialExpiresAt) !== this.#imported.get(keyString(key))) throw new Error("the file holds a newer credential");
|
|
652
|
+
record.credentials = material.credentials;
|
|
653
|
+
if (material.expiresAt) record.credentialExpiresAt = material.expiresAt.toISOString();
|
|
654
|
+
else delete record.credentialExpiresAt;
|
|
655
|
+
await writePrivateFile(this.path, Buffer.from(`${JSON.stringify(file, null, 2)}
|
|
656
|
+
`));
|
|
657
|
+
}
|
|
658
|
+
async #withLock(action) {
|
|
659
|
+
const previous = this.#lockTail;
|
|
660
|
+
let release;
|
|
661
|
+
this.#lockTail = new Promise((resolve) => {
|
|
662
|
+
release = resolve;
|
|
663
|
+
});
|
|
664
|
+
await previous;
|
|
665
|
+
try {
|
|
666
|
+
return await action();
|
|
667
|
+
} finally {
|
|
668
|
+
release();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
function isConnectionHead(key) {
|
|
673
|
+
return key.includes("/connections/") && key.endsWith("/head");
|
|
674
|
+
}
|
|
675
|
+
function keyString(key) {
|
|
676
|
+
return `${key.connectorId}\0${key.connectionName}`;
|
|
677
|
+
}
|
|
678
|
+
function credentialDigest(credentials, expiresAt) {
|
|
679
|
+
const canonical = encodeGoCompatibleJSON(credentials);
|
|
680
|
+
const expiry = expiresAt ? JSON.parse(encodeGoCompatibleJSON({ expiry: expiresAt })).expiry : "";
|
|
681
|
+
return createHash2("sha256").update(`${canonical}${expiry ? `|${expiry}` : ""}`).digest("hex");
|
|
682
|
+
}
|
|
683
|
+
async function readDexWebFile(path) {
|
|
684
|
+
const stat = await lstat(path);
|
|
685
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 511) !== 384) throw new Error("connector configuration file must be a regular mode-0600 file");
|
|
686
|
+
let raw;
|
|
687
|
+
try {
|
|
688
|
+
raw = JSON.parse(await readFile(path, "utf8"));
|
|
689
|
+
} catch {
|
|
690
|
+
throw new Error("connector configuration file is not valid JSON or not private");
|
|
691
|
+
}
|
|
692
|
+
const validated = LocalConnectionsFileSchema.safeParse(raw);
|
|
693
|
+
if (!validated.success || !raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("connector configuration file violates its schema");
|
|
694
|
+
return raw;
|
|
695
|
+
}
|
|
696
|
+
function parseJSON(contents) {
|
|
697
|
+
try {
|
|
698
|
+
return JSON.parse(Buffer.from(contents).toString("utf8"));
|
|
699
|
+
} catch {
|
|
700
|
+
return void 0;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
async function writePrivateFile(path, contents) {
|
|
704
|
+
const temporary = join(dirname(path), `.write-${randomUUID()}`);
|
|
705
|
+
const handle = await open(temporary, "wx", 384);
|
|
706
|
+
let isClosed = false;
|
|
707
|
+
try {
|
|
708
|
+
await handle.writeFile(contents);
|
|
709
|
+
await handle.sync();
|
|
710
|
+
await handle.close();
|
|
711
|
+
isClosed = true;
|
|
712
|
+
await rename(temporary, path);
|
|
713
|
+
} finally {
|
|
714
|
+
if (!isClosed) await handle.close();
|
|
715
|
+
await unlink(temporary).catch(() => void 0);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/projectconfig/broker.ts
|
|
720
|
+
import { z as z3 } from "zod";
|
|
721
|
+
var CredentialBrokerUnavailableError = class extends Error {
|
|
722
|
+
constructor() {
|
|
723
|
+
super("credential broker is unavailable");
|
|
724
|
+
this.name = "CredentialBrokerUnavailableError";
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
var CredentialBrokerRejectedError = class extends Error {
|
|
728
|
+
constructor() {
|
|
729
|
+
super("credential broker rejected the request or answered invalidly");
|
|
730
|
+
this.name = "CredentialBrokerRejectedError";
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
var ConnectionNotSelectedError = class extends Error {
|
|
734
|
+
constructor() {
|
|
735
|
+
super("project connection is not selected by this scope");
|
|
736
|
+
this.name = "ConnectionNotSelectedError";
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
var RejectedCredentialNotRefreshedError = class extends Error {
|
|
740
|
+
constructor() {
|
|
741
|
+
super("the rejected credential was not refreshed");
|
|
742
|
+
this.name = "RejectedCredentialNotRefreshedError";
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
var BrokerAnswerSchema = z3.object({
|
|
746
|
+
status: z3.enum(["READY", "REAUTHORIZATION_REQUIRED", "CONNECTION_NOT_SELECTED"]),
|
|
747
|
+
credential: z3.record(z3.string(), z3.unknown()).nullable().optional(),
|
|
748
|
+
revision: z3.number().int().nonnegative().optional(),
|
|
749
|
+
issuedAt: RFC3339NanoSchema.nullable().optional(),
|
|
750
|
+
expiresAt: RFC3339NanoSchema.nullable().optional()
|
|
751
|
+
});
|
|
752
|
+
var CredentialBrokerClient = class {
|
|
753
|
+
/** Bind a trusted endpoint, token source, and optional transport; tokens renew only after 401. */
|
|
754
|
+
constructor(endpoint, readToken, transport = fetch) {
|
|
755
|
+
this.endpoint = endpoint;
|
|
756
|
+
this.readToken = readToken;
|
|
757
|
+
this.transport = transport;
|
|
758
|
+
let parsed;
|
|
759
|
+
try {
|
|
760
|
+
parsed = new URL(endpoint);
|
|
761
|
+
} catch {
|
|
762
|
+
throw new TypeError("credential broker endpoint is invalid");
|
|
763
|
+
}
|
|
764
|
+
if (!["https:", "http:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash || parsed.protocol === "http:" && !isPrivateBrokerHost(parsed.hostname)) throw new TypeError("credential broker endpoint is invalid");
|
|
765
|
+
}
|
|
766
|
+
endpoint;
|
|
767
|
+
readToken;
|
|
768
|
+
transport;
|
|
769
|
+
#token;
|
|
770
|
+
/** Read a shared credential; rejectedRevision is sent only when positive. */
|
|
771
|
+
async read(sharedConnectionId, rejectedRevision = 0, signal) {
|
|
772
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(sharedConnectionId)) throw new TypeError("shared connection ID is invalid");
|
|
773
|
+
const timeoutSignal = AbortSignal.timeout(4e4);
|
|
774
|
+
const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
775
|
+
return new Promise((resolve, reject) => {
|
|
776
|
+
const onAbort = () => reject(new CredentialBrokerUnavailableError());
|
|
777
|
+
if (requestSignal.aborted) {
|
|
778
|
+
onAbort();
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
requestSignal.addEventListener("abort", onAbort, { once: true });
|
|
782
|
+
void this.readWithinDeadline(sharedConnectionId, rejectedRevision, requestSignal).then(resolve, reject).finally(() => requestSignal.removeEventListener("abort", onAbort));
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
async readWithinDeadline(sharedConnectionId, rejectedRevision, requestSignal) {
|
|
786
|
+
let token = this.#token ?? await this.readToken();
|
|
787
|
+
if (!isHeaderSafeBrokerToken(token)) throw new CredentialBrokerRejectedError();
|
|
788
|
+
this.#token = token;
|
|
789
|
+
const payload = JSON.stringify({ sharedConnectionId, ...rejectedRevision > 0 ? { rejectedRevision } : {} });
|
|
790
|
+
let response;
|
|
791
|
+
response = await this.post(token, payload, requestSignal);
|
|
792
|
+
if (response.status === 401) {
|
|
793
|
+
const renewedToken = await this.readToken(true);
|
|
794
|
+
if (!isHeaderSafeBrokerToken(renewedToken) || renewedToken === token) throw new CredentialBrokerRejectedError();
|
|
795
|
+
token = renewedToken;
|
|
796
|
+
this.#token = token;
|
|
797
|
+
response = await this.post(token, payload, requestSignal);
|
|
798
|
+
}
|
|
799
|
+
if (response.status !== 200) {
|
|
800
|
+
if ([408, 425, 429].includes(response.status) || response.status >= 500) throw new CredentialBrokerUnavailableError();
|
|
801
|
+
throw new CredentialBrokerRejectedError();
|
|
802
|
+
}
|
|
803
|
+
let body;
|
|
804
|
+
try {
|
|
805
|
+
body = await readBoundedBody(responseChunks(response.body), 1 << 20);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
throw error instanceof BodyTooLargeError || error instanceof CredentialBrokerRejectedError ? new CredentialBrokerRejectedError() : new CredentialBrokerUnavailableError();
|
|
808
|
+
}
|
|
809
|
+
let decoded;
|
|
810
|
+
try {
|
|
811
|
+
decoded = JSON.parse(Buffer.from(body).toString("utf8"));
|
|
812
|
+
} catch {
|
|
813
|
+
throw new CredentialBrokerRejectedError();
|
|
814
|
+
}
|
|
815
|
+
const parsed = BrokerAnswerSchema.safeParse(decoded);
|
|
816
|
+
if (!parsed.success) throw new CredentialBrokerRejectedError();
|
|
817
|
+
if (parsed.data.status === "REAUTHORIZATION_REQUIRED") throw new ReauthorizationRequiredError();
|
|
818
|
+
if (parsed.data.status === "CONNECTION_NOT_SELECTED") throw new ConnectionNotSelectedError();
|
|
819
|
+
if (!parsed.data.credential || !parsed.data.revision || Buffer.byteLength(JSON.stringify(parsed.data.credential)) > 64 << 10) throw new CredentialBrokerRejectedError();
|
|
820
|
+
if (parsed.data.expiresAt && Date.parse(parsed.data.expiresAt) <= Date.now()) throw new CredentialBrokerUnavailableError();
|
|
821
|
+
return {
|
|
822
|
+
credential: parsed.data.credential,
|
|
823
|
+
revision: parsed.data.revision,
|
|
824
|
+
...parsed.data.issuedAt ? { issuedAt: parsed.data.issuedAt } : {},
|
|
825
|
+
...parsed.data.expiresAt !== void 0 ? { expiresAt: parsed.data.expiresAt } : {}
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
async post(token, payload, signal) {
|
|
829
|
+
try {
|
|
830
|
+
return await this.transport(this.endpoint, {
|
|
831
|
+
method: "POST",
|
|
832
|
+
redirect: "manual",
|
|
833
|
+
signal: signal ?? null,
|
|
834
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", Accept: "application/json" },
|
|
835
|
+
body: payload
|
|
836
|
+
});
|
|
837
|
+
} catch {
|
|
838
|
+
throw new CredentialBrokerUnavailableError();
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
function isHeaderSafeBrokerToken(token) {
|
|
843
|
+
return Boolean(token) && ![...token].some((character) => {
|
|
844
|
+
const code = character.charCodeAt(0);
|
|
845
|
+
return code < 33 || code > 126;
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
async function* responseChunks(body) {
|
|
849
|
+
if (!body) throw new CredentialBrokerRejectedError();
|
|
850
|
+
const reader = body.getReader();
|
|
851
|
+
try {
|
|
852
|
+
while (true) {
|
|
853
|
+
const next = await reader.read();
|
|
854
|
+
if (next.done) return;
|
|
855
|
+
yield next.value;
|
|
856
|
+
}
|
|
857
|
+
} finally {
|
|
858
|
+
try {
|
|
859
|
+
await reader.cancel();
|
|
860
|
+
} catch {
|
|
861
|
+
}
|
|
862
|
+
reader.releaseLock();
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/projectconfig/shared_connections.ts
|
|
867
|
+
var SharedConnections = class {
|
|
868
|
+
/** Bind shared entries to the project's trusted credential broker. */
|
|
869
|
+
constructor(configuration, broker) {
|
|
870
|
+
this.broker = broker;
|
|
871
|
+
for (const connection of configuration.connections) {
|
|
872
|
+
if (connection.sharedConnectionId) this.#entries.set(keyString2(connection), {
|
|
873
|
+
id: connection.sharedConnectionId,
|
|
874
|
+
cached: void 0,
|
|
875
|
+
readAt: 0,
|
|
876
|
+
notBefore: 0,
|
|
877
|
+
issuedByCall: /* @__PURE__ */ new Map(),
|
|
878
|
+
pending: void 0
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
broker;
|
|
883
|
+
#entries = /* @__PURE__ */ new Map();
|
|
884
|
+
/** Whether a loaded connection resolves through the broker. */
|
|
885
|
+
has(key) {
|
|
886
|
+
return this.#entries.has(keyString2(key));
|
|
887
|
+
}
|
|
888
|
+
/** Read a current shared credential and remember the revision issued to this call. */
|
|
889
|
+
async read(key, callId, signal) {
|
|
890
|
+
const entry = this.#entry(key);
|
|
891
|
+
const now = Date.now();
|
|
892
|
+
let credential;
|
|
893
|
+
if (entry.cached && isFresh(entry, now)) credential = entry.cached;
|
|
894
|
+
else if (entry.pending && entry.cached && isUnexpired(entry.cached, now)) credential = entry.cached;
|
|
895
|
+
else {
|
|
896
|
+
try {
|
|
897
|
+
credential = await this.#readBroker(entry, 0, signal);
|
|
898
|
+
} catch (error) {
|
|
899
|
+
if (error instanceof CredentialBrokerUnavailableError && entry.cached && isUnexpired(entry.cached, Date.now())) {
|
|
900
|
+
entry.notBefore = Date.now() + 1e4;
|
|
901
|
+
credential = entry.cached;
|
|
902
|
+
} else throw error;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
rememberIssued(entry, callId, credential.revision);
|
|
906
|
+
return credential;
|
|
907
|
+
}
|
|
908
|
+
/** Ask the broker for a revision newer than the one issued to this call. */
|
|
909
|
+
async readAfterRejection(key, callId, signal) {
|
|
910
|
+
const entry = this.#entry(key);
|
|
911
|
+
const issued = entry.issuedByCall.get(callId) ?? 0;
|
|
912
|
+
if (entry.pending) await entry.pending;
|
|
913
|
+
if (entry.cached && entry.cached.revision > issued && isUnexpired(entry.cached, Date.now())) {
|
|
914
|
+
rememberIssued(entry, callId, entry.cached.revision);
|
|
915
|
+
return entry.cached;
|
|
916
|
+
}
|
|
917
|
+
const credential = await this.#readBroker(entry, issued, signal);
|
|
918
|
+
if (issued > 0 && credential.revision === issued) throw new RejectedCredentialNotRefreshedError();
|
|
919
|
+
rememberIssued(entry, callId, credential.revision);
|
|
920
|
+
return credential;
|
|
921
|
+
}
|
|
922
|
+
#entry(key) {
|
|
923
|
+
const entry = this.#entries.get(keyString2(key));
|
|
924
|
+
if (!entry) throw new TypeError("connection is not a loaded shared selection");
|
|
925
|
+
return entry;
|
|
926
|
+
}
|
|
927
|
+
async #readBroker(entry, rejectedRevision, signal) {
|
|
928
|
+
if (entry.pending) return entry.pending;
|
|
929
|
+
const pending = this.broker.read(entry.id, rejectedRevision, signal).then((credential) => {
|
|
930
|
+
entry.cached = credential;
|
|
931
|
+
entry.readAt = Date.now();
|
|
932
|
+
entry.notBefore = entry.readAt + 1e3;
|
|
933
|
+
return credential;
|
|
934
|
+
}, (error) => {
|
|
935
|
+
if (error instanceof ReauthorizationRequiredError || error instanceof ConnectionNotSelectedError) entry.cached = void 0;
|
|
936
|
+
throw error;
|
|
937
|
+
});
|
|
938
|
+
entry.pending = pending;
|
|
939
|
+
try {
|
|
940
|
+
return await pending;
|
|
941
|
+
} finally {
|
|
942
|
+
if (entry.pending === pending) entry.pending = void 0;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
function keyString2(key) {
|
|
947
|
+
return `${key.connectorId}\0${key.connectionName}`;
|
|
948
|
+
}
|
|
949
|
+
function isUnexpired(credential, now) {
|
|
950
|
+
return !credential.expiresAt || Date.parse(credential.expiresAt) > now;
|
|
951
|
+
}
|
|
952
|
+
function isFresh(entry, now) {
|
|
953
|
+
const credential = entry.cached;
|
|
954
|
+
if (!credential || !isUnexpired(credential, now)) return false;
|
|
955
|
+
if (now < entry.notBefore) return true;
|
|
956
|
+
if (!credential.expiresAt) return now - entry.readAt < 15 * 6e4;
|
|
957
|
+
const lifetime = credential.issuedAt ? Date.parse(credential.expiresAt) - Date.parse(credential.issuedAt) : 0;
|
|
958
|
+
const skew = lifetime > 0 ? Math.min(5 * 6e4, lifetime / 4) : 5 * 6e4;
|
|
959
|
+
return Date.parse(credential.expiresAt) > now + skew;
|
|
960
|
+
}
|
|
961
|
+
function rememberIssued(entry, callId, revision) {
|
|
962
|
+
entry.issuedByCall.set(callId, revision);
|
|
963
|
+
if (entry.issuedByCall.size > 4096) entry.issuedByCall.delete(entry.issuedByCall.keys().next().value);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/projectconfig/environment.ts
|
|
967
|
+
var LocalConnectionsFileEnvironment = "DEX_CONNECTOR_CONFIG_FILE";
|
|
968
|
+
var LoadedProject = class {
|
|
969
|
+
/** Bind startup-pinned configuration to exactly one private credential source. */
|
|
970
|
+
constructor(configuration, snapshot, objects, _localFilePath, broker) {
|
|
971
|
+
this.configuration = configuration;
|
|
972
|
+
this.snapshot = snapshot;
|
|
973
|
+
this.broker = broker;
|
|
974
|
+
ConfigurationSchema.parse(configuration);
|
|
975
|
+
this.connections = new ConnectionStore(objects ?? new InMemoryObjectStore(), configuration.scope, broker ? new SharedConnections(configuration, broker) : void 0);
|
|
976
|
+
}
|
|
977
|
+
configuration;
|
|
978
|
+
snapshot;
|
|
979
|
+
broker;
|
|
980
|
+
#sharedCredentials = /* @__PURE__ */ new Map();
|
|
981
|
+
/** Versioned private connection store for generated provider adapters. */
|
|
982
|
+
connections;
|
|
983
|
+
/** Read a current private credential, never returning it through a Flow result. */
|
|
984
|
+
async resolveCredential(connectorId, connectionName, signal) {
|
|
985
|
+
const connection = this.configuration.connections.find((entry) => entry.connectorId === connectorId && entry.connectionName === connectionName);
|
|
986
|
+
if (!connection) throw new Error("project connection is not configured");
|
|
987
|
+
if (connection.sharedConnectionId) {
|
|
988
|
+
if (!this.broker) throw new Error("credential broker is not configured");
|
|
989
|
+
const cached = this.#sharedCredentials.get(connection.sharedConnectionId);
|
|
990
|
+
if (cached && isSharedCredentialFresh(cached, Date.now())) return cached.result.credential;
|
|
991
|
+
try {
|
|
992
|
+
const result = await this.broker.read(connection.sharedConnectionId, 0, signal);
|
|
993
|
+
this.#sharedCredentials.set(connection.sharedConnectionId, { result, readAt: Date.now(), notBefore: Date.now() + 1e3 });
|
|
994
|
+
return result.credential;
|
|
995
|
+
} catch (error) {
|
|
996
|
+
if (error instanceof CredentialBrokerUnavailableError && cached && isSharedCredentialUnexpired(cached.result, Date.now())) {
|
|
997
|
+
cached.notBefore = Date.now() + 1e4;
|
|
998
|
+
return cached.result.credential;
|
|
999
|
+
}
|
|
1000
|
+
throw error;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const key = { connectorId, connectionName };
|
|
1004
|
+
const connectionState = await this.connections.readConnection(key);
|
|
1005
|
+
if (connectionState.status === "REAUTHORIZATION_REQUIRED" || connectionState.status === "REVOKED") throw new ReauthorizationRequiredError();
|
|
1006
|
+
const { material } = await this.connections.readCredentialMaterial(key);
|
|
1007
|
+
if (material.expiresAt && material.expiresAt.getTime() <= Date.now()) throw new Error("expired project credential requires refresh");
|
|
1008
|
+
return material.credentials;
|
|
1009
|
+
}
|
|
1010
|
+
/** Resolve a newer shared credential after the provider rejected one revision. */
|
|
1011
|
+
async resolveCredentialAfterRejection(connectorId, connectionName, rejectedRevision, signal) {
|
|
1012
|
+
const connection = this.configuration.connections.find((entry) => entry.connectorId === connectorId && entry.connectionName === connectionName);
|
|
1013
|
+
if (!connection?.sharedConnectionId || !this.broker || !Number.isSafeInteger(rejectedRevision) || rejectedRevision < 1) throw new TypeError("shared credential rejection identity is invalid");
|
|
1014
|
+
const cached = this.#sharedCredentials.get(connection.sharedConnectionId);
|
|
1015
|
+
if (cached && cached.result.revision > rejectedRevision && isSharedCredentialUnexpired(cached.result, Date.now())) return cached.result.credential;
|
|
1016
|
+
const result = await this.broker.read(connection.sharedConnectionId, rejectedRevision, signal);
|
|
1017
|
+
if (result.revision === rejectedRevision) throw new RejectedCredentialNotRefreshedError();
|
|
1018
|
+
this.#sharedCredentials.set(connection.sharedConnectionId, { result, readAt: Date.now(), notBefore: Date.now() + 1e3 });
|
|
1019
|
+
return result.credential;
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
function isSharedCredentialUnexpired(result, now) {
|
|
1023
|
+
return !result.expiresAt || Date.parse(result.expiresAt) > now;
|
|
1024
|
+
}
|
|
1025
|
+
function isSharedCredentialFresh(cached, now) {
|
|
1026
|
+
if (!isSharedCredentialUnexpired(cached.result, now)) return false;
|
|
1027
|
+
if (now < cached.notBefore) return true;
|
|
1028
|
+
if (!cached.result.expiresAt) return now - cached.readAt < 15 * 60 * 1e3;
|
|
1029
|
+
const lifetime = cached.result.issuedAt ? Date.parse(cached.result.expiresAt) - Date.parse(cached.result.issuedAt) : 0;
|
|
1030
|
+
const skew = lifetime > 0 ? Math.min(5 * 60 * 1e3, lifetime / 4) : 5 * 60 * 1e3;
|
|
1031
|
+
return Date.parse(cached.result.expiresAt) > now + skew;
|
|
1032
|
+
}
|
|
1033
|
+
async function loadFromEnvironment(environment = process.env) {
|
|
1034
|
+
const localFilePath = environment[LocalConnectionsFileEnvironment];
|
|
1035
|
+
if (localFilePath) {
|
|
1036
|
+
for (const name of ["DEX_PROJECT_ID", "DEX_PROJECT_SCOPE", "DEX_PROJECT_CONFIG_KEY", "DEX_PROJECT_STORAGE_BUCKET"]) {
|
|
1037
|
+
if (environment[name]) throw new Error(`${LocalConnectionsFileEnvironment} and ${name} select different project configurations`);
|
|
1038
|
+
}
|
|
1039
|
+
const file = await readLocalConnectionsFile(localFilePath);
|
|
1040
|
+
const scope2 = { projectId: "local", kind: "preview" };
|
|
1041
|
+
const useFilePath = join2(dirname2(localFilePath), "use-configurations.json");
|
|
1042
|
+
let operationConfigurations = [];
|
|
1043
|
+
try {
|
|
1044
|
+
const useFile = z4.object({ schemaVersion: z4.literal("connectors.dex.dev/local-use-configurations/v1alpha1"), operationConfigurations: z4.array(z4.object({ connectorId: z4.string(), connectionName: z4.string(), operationId: z4.string(), flowType: z4.string(), stepType: z4.string(), configuration: z4.record(z4.string(), z4.unknown()) })) }).parse(JSON.parse(await readPrivateFile(useFilePath)));
|
|
1045
|
+
operationConfigurations = useFile.operationConfigurations;
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
if (!(error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT")) throw new Error("connector use configuration file is invalid");
|
|
1048
|
+
}
|
|
1049
|
+
const configuration2 = {
|
|
1050
|
+
schemaVersion: "connectors.dex.dev/project-configuration/v1alpha2",
|
|
1051
|
+
scope: scope2,
|
|
1052
|
+
revision: 1,
|
|
1053
|
+
connections: file.connections.map(localConnectionConfiguration),
|
|
1054
|
+
triggerBindings: file.triggerBindings ?? [],
|
|
1055
|
+
operationConfigurations
|
|
1056
|
+
};
|
|
1057
|
+
const localObjects = new DexWebObjectStore(localFilePath, scope2);
|
|
1058
|
+
await localObjects.initialize();
|
|
1059
|
+
return new LoadedProject(configuration2, void 0, localObjects, localFilePath, void 0);
|
|
1060
|
+
}
|
|
1061
|
+
const scope = { projectId: environment.DEX_PROJECT_ID ?? "", kind: environment.DEX_PROJECT_SCOPE ?? "" };
|
|
1062
|
+
scopePrefix(scope);
|
|
1063
|
+
const allowLocalStorage = environment.DEX_PROJECT_ALLOW_LOCAL_STORAGE === "true";
|
|
1064
|
+
if (!["", "true", "false"].includes(environment.DEX_PROJECT_ALLOW_LOCAL_STORAGE ?? "")) throw new Error("project local storage opt-in must be true or false");
|
|
1065
|
+
const endpoint = environment.DEX_PROJECT_STORAGE_ENDPOINT;
|
|
1066
|
+
if (Boolean(endpoint) !== allowLocalStorage) throw new Error("local project storage requires explicit local endpoint and opt-in");
|
|
1067
|
+
if (endpoint && !isLocalStorageEndpoint(endpoint)) throw new Error("custom project storage endpoint requires a local fixture boundary");
|
|
1068
|
+
const bucket = environment.DEX_PROJECT_STORAGE_BUCKET;
|
|
1069
|
+
if (!bucket) throw new Error("project storage bucket is required");
|
|
1070
|
+
const client = new S3Client2(endpoint ? { endpoint, forcePathStyle: true, maxAttempts: 1 } : { maxAttempts: 1 });
|
|
1071
|
+
const objects = await S3ObjectStore.create({
|
|
1072
|
+
client,
|
|
1073
|
+
bucket,
|
|
1074
|
+
allowUnencrypted: allowLocalStorage,
|
|
1075
|
+
...environment.DEX_PROJECT_STORAGE_PREFIX ? { prefix: environment.DEX_PROJECT_STORAGE_PREFIX } : {},
|
|
1076
|
+
...environment.DEX_PROJECT_STORAGE_KMS_KEY_ARN ? { kmsKeyId: environment.DEX_PROJECT_STORAGE_KMS_KEY_ARN } : {}
|
|
1077
|
+
});
|
|
1078
|
+
const snapshot = {
|
|
1079
|
+
key: environment.DEX_PROJECT_CONFIG_KEY ?? "",
|
|
1080
|
+
version: environment.DEX_PROJECT_CONFIG_VERSION ?? "",
|
|
1081
|
+
digest: environment.DEX_PROJECT_CONFIG_DIGEST ?? "",
|
|
1082
|
+
mediaType: "application/json"
|
|
1083
|
+
};
|
|
1084
|
+
const configuration = await new ConfigurationStore(objects, scope).readSnapshot(snapshot);
|
|
1085
|
+
let tokenReference = configuration.credentialBroker?.tokenSecretRef;
|
|
1086
|
+
const broker = configuration.credentialBroker ? new CredentialBrokerClient(configuration.credentialBroker.endpoint, async (renew = false) => {
|
|
1087
|
+
if (renew) tokenReference = (await new ConfigurationStore(objects, scope).readConfiguration()).configuration.credentialBroker?.tokenSecretRef;
|
|
1088
|
+
if (!tokenReference) throw new Error("credential broker token reference is unavailable");
|
|
1089
|
+
const tokenObject = await objects.readObject(tokenReference.key, tokenReference.version);
|
|
1090
|
+
if (digestBytes(tokenObject.contents) !== tokenReference.digest) throw new Error("credential broker token integrity differs");
|
|
1091
|
+
return Buffer.from(tokenObject.contents).toString("utf8");
|
|
1092
|
+
}) : void 0;
|
|
1093
|
+
return new LoadedProject(configuration, snapshot, objects, void 0, broker);
|
|
1094
|
+
}
|
|
1095
|
+
async function readLocalConnectionsFile(path) {
|
|
1096
|
+
let parsed;
|
|
1097
|
+
try {
|
|
1098
|
+
parsed = JSON.parse(await readPrivateFile(path));
|
|
1099
|
+
} catch {
|
|
1100
|
+
throw new Error("connector configuration file is not valid JSON or not private");
|
|
1101
|
+
}
|
|
1102
|
+
const result = LocalConnectionsFileSchema.safeParse(parsed);
|
|
1103
|
+
if (!result.success) throw new Error("connector configuration file violates its schema");
|
|
1104
|
+
return { connections: result.data.connections, ...result.data.triggerBindings ? { triggerBindings: result.data.triggerBindings } : {} };
|
|
1105
|
+
}
|
|
1106
|
+
async function readPrivateFile(path) {
|
|
1107
|
+
const info = await lstat2(path);
|
|
1108
|
+
if (!info.isFile() || info.isSymbolicLink() || (info.mode & 511) !== 384) throw new Error("connector configuration file must be a regular mode-0600 file");
|
|
1109
|
+
return readFile2(path, "utf8");
|
|
1110
|
+
}
|
|
1111
|
+
function localConnectionConfiguration(entry) {
|
|
1112
|
+
return {
|
|
1113
|
+
connectorId: entry.connectorId,
|
|
1114
|
+
connectionName: entry.connectionName,
|
|
1115
|
+
modulePath: entry.modulePath,
|
|
1116
|
+
provider: entry.provider,
|
|
1117
|
+
configuration: entry.configuration,
|
|
1118
|
+
...entry.authMethodId ? { authMethodId: entry.authMethodId } : {}
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
function isLocalStorageEndpoint(value) {
|
|
1122
|
+
try {
|
|
1123
|
+
const endpoint = new URL(value);
|
|
1124
|
+
const hostname = endpoint.hostname;
|
|
1125
|
+
return ["http:", "https:"].includes(endpoint.protocol) && !endpoint.username && !endpoint.password && !endpoint.search && !endpoint.hash && (!endpoint.pathname || endpoint.pathname === "/") && isLocalStorageHost(hostname);
|
|
1126
|
+
} catch {
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/projectconfig/provider.ts
|
|
1132
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1133
|
+
import { setTimeout as wait } from "timers/promises";
|
|
1134
|
+
var ExchangePendingError = class extends Error {
|
|
1135
|
+
constructor() {
|
|
1136
|
+
super("credential exchange is pending");
|
|
1137
|
+
this.name = "ExchangePendingError";
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
var RefreshFailedError = class extends Error {
|
|
1141
|
+
constructor(message = "project credential refresh failed; current authorization is kept") {
|
|
1142
|
+
super(message);
|
|
1143
|
+
this.name = "RefreshFailedError";
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
var CredentialProvider = class {
|
|
1147
|
+
/** Bind one validated project connection and a credential decoder. */
|
|
1148
|
+
constructor(store, key, decode, decodeShared) {
|
|
1149
|
+
this.store = store;
|
|
1150
|
+
this.key = key;
|
|
1151
|
+
this.decode = decode;
|
|
1152
|
+
this.decodeShared = decodeShared;
|
|
1153
|
+
if (!store || !decode) throw new TypeError("project credential store and decoder are required");
|
|
1154
|
+
connectionPath(store.scope, key);
|
|
1155
|
+
}
|
|
1156
|
+
store;
|
|
1157
|
+
key;
|
|
1158
|
+
decode;
|
|
1159
|
+
decodeShared;
|
|
1160
|
+
/** Read current private material without refreshing known-expired credentials. */
|
|
1161
|
+
async resolve(call, signal) {
|
|
1162
|
+
this.validateCall(call);
|
|
1163
|
+
const effectiveSignal = signal ?? call.context.cancellationSignal;
|
|
1164
|
+
throwIfAborted(effectiveSignal);
|
|
1165
|
+
if (this.store.shared?.has(this.key)) return this.decodeSharedMaterial((await this.store.shared.read(this.key, call.id, effectiveSignal)).credential);
|
|
1166
|
+
const { material, connection } = await this.store.readCredentialMaterial(this.key);
|
|
1167
|
+
if (connection.expiresAt && connection.expiresAt.getTime() <= Date.now()) throw new Error("expired project credential requires refresh");
|
|
1168
|
+
return this.decodeMaterial(material);
|
|
1169
|
+
}
|
|
1170
|
+
/** Read current material under an explicit cancellation signal. */
|
|
1171
|
+
async resolveContext(call, signal) {
|
|
1172
|
+
return this.resolve(call, signal);
|
|
1173
|
+
}
|
|
1174
|
+
/** Reject a call aimed at another configured connection. */
|
|
1175
|
+
validateCall(call) {
|
|
1176
|
+
if (!call || call.operation.connectorId !== this.key.connectorId || call.connection.name !== this.key.connectionName) throw new TypeError("connector call differs from configured project connection");
|
|
1177
|
+
OperationRefSchema.parse(call.operation);
|
|
1178
|
+
}
|
|
1179
|
+
/** Decode private material without leaking invalid values in errors. */
|
|
1180
|
+
decodeMaterial(material) {
|
|
1181
|
+
try {
|
|
1182
|
+
return this.decode(material.credentials);
|
|
1183
|
+
} catch {
|
|
1184
|
+
throw new TypeError("project credential decoding failed");
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
/** Decode a broker response with the connector's shared credential rules. */
|
|
1188
|
+
decodeSharedMaterial(document) {
|
|
1189
|
+
try {
|
|
1190
|
+
return (this.decodeShared ?? this.decode)(document);
|
|
1191
|
+
} catch {
|
|
1192
|
+
throw new TypeError(this.decodeShared ? "shared project credential decoding failed" : "project credential decoding failed: this connector version does not support shared credentials");
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
var RefreshingCredentialProvider = class extends CredentialProvider {
|
|
1197
|
+
#encode;
|
|
1198
|
+
/** Bind a complete replacement encoder in addition to the decoder. */
|
|
1199
|
+
constructor(store, key, decode, encode, decodeShared) {
|
|
1200
|
+
super(store, key, decode, decodeShared);
|
|
1201
|
+
if (!encode) throw new TypeError("project credential encoder is required");
|
|
1202
|
+
this.#encode = encode;
|
|
1203
|
+
}
|
|
1204
|
+
/** Admit a durable refresh before one provider request; observers recover its result. */
|
|
1205
|
+
async resolveWithRefresh(call, driver, signal) {
|
|
1206
|
+
this.validateCall(call);
|
|
1207
|
+
if (!driver) throw new TypeError("credential refresh driver is required");
|
|
1208
|
+
const effectiveSignal = signal ?? call.context.cancellationSignal;
|
|
1209
|
+
if (this.store.shared?.has(this.key)) return this.decodeSharedMaterial((await this.store.shared.read(this.key, call.id, effectiveSignal)).credential);
|
|
1210
|
+
for (; ; ) {
|
|
1211
|
+
throwIfAborted(effectiveSignal);
|
|
1212
|
+
const { record, object } = await this.store.readRecord(this.key);
|
|
1213
|
+
if (record.status === "REAUTHORIZATION_REQUIRED" || record.status === "REVOKED") throw new ReauthorizationRequiredError();
|
|
1214
|
+
if (record.status === "AUTHORIZING" || record.status === "REFRESHING") {
|
|
1215
|
+
if (record.status === "AUTHORIZING" && record.mutation && Date.now() < new Date(record.mutation.deadline).getTime()) throw new ExchangePendingError();
|
|
1216
|
+
try {
|
|
1217
|
+
await this.#recoverOrWait(record, effectiveSignal);
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
if (error instanceof ConflictError) continue;
|
|
1220
|
+
throw error;
|
|
1221
|
+
}
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
if (record.status !== "READY") throw new TypeError("project credential status is invalid");
|
|
1225
|
+
const material = await this.store.readMaterial(this.key, record);
|
|
1226
|
+
const credentials = this.decodeMaterial(material);
|
|
1227
|
+
const now = /* @__PURE__ */ new Date();
|
|
1228
|
+
const state = { credentials, now, ...material.expiresAt ? { expiresAt: material.expiresAt } : {} };
|
|
1229
|
+
if ((!material.expiresAt || material.expiresAt.getTime() > now.getTime()) && !driver.refreshRequired(state)) return credentials;
|
|
1230
|
+
let admission;
|
|
1231
|
+
try {
|
|
1232
|
+
admission = await this.#admitRefresh(record, object, now);
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
if (error instanceof ConflictError) continue;
|
|
1235
|
+
throw error;
|
|
1236
|
+
}
|
|
1237
|
+
return this.#refreshAdmitted(admission, material, state, driver, effectiveSignal);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
/** Force refresh only when stored expiry proves the rejected token expired. */
|
|
1241
|
+
async resolveAfterRejection(call, driver, signal) {
|
|
1242
|
+
this.validateCall(call);
|
|
1243
|
+
if (!driver) throw new TypeError("credential refresh driver is required");
|
|
1244
|
+
if (this.store.shared?.has(this.key)) return this.decodeSharedMaterial((await this.store.shared.readAfterRejection(this.key, call.id, signal ?? call.context.cancellationSignal)).credential);
|
|
1245
|
+
const connection = await this.store.readConnection(this.key);
|
|
1246
|
+
if (!connection.expiresAt || connection.expiresAt.getTime() > Date.now()) throw new RejectedCredentialNotRefreshedError();
|
|
1247
|
+
return this.resolveWithRefresh(call, driver, signal);
|
|
1248
|
+
}
|
|
1249
|
+
async #admitRefresh(record, object, now) {
|
|
1250
|
+
const attemptId = randomUUID2().replaceAll("-", "");
|
|
1251
|
+
const deadline = new Date(now.getTime() + 4e4);
|
|
1252
|
+
const resultKey = `${connectionPath(this.store.scope, this.key)}/exchange-results/${attemptId}`;
|
|
1253
|
+
const mutation = { kind: "REFRESHING", invocationId: randomUUID2().replaceAll("-", ""), id: attemptId, fence: record.fence + 1, baseRevision: record.revision, deadline: deadline.toISOString(), resultKey };
|
|
1254
|
+
const updated = connectionRecord(this.store.scope, this.key, record.revision + 1, record.fence + 1, "REFRESHING", parseExpiry2(record.expiresAt), record.authMethod, record.credential, mutation);
|
|
1255
|
+
await this.store.writeHead(this.key, object, updated);
|
|
1256
|
+
return { key: this.key, kind: "REFRESHING", attemptId, fence: updated.fence, deadline, resultKey };
|
|
1257
|
+
}
|
|
1258
|
+
async #refreshAdmitted(admission, prior, state, driver, signal) {
|
|
1259
|
+
let result;
|
|
1260
|
+
try {
|
|
1261
|
+
result = await refreshWithinTimeout(driver, state, signal);
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
if (error instanceof ReauthorizationRequiredError) {
|
|
1264
|
+
await this.#finishExchange(admission, "REAUTHORIZATION_REQUIRED");
|
|
1265
|
+
throw error;
|
|
1266
|
+
}
|
|
1267
|
+
await this.#finishExchange(admission, "READY");
|
|
1268
|
+
throw new RefreshFailedError();
|
|
1269
|
+
}
|
|
1270
|
+
if (!(result.expiresAt instanceof Date) || !Number.isFinite(result.expiresAt.getTime()) || result.expiresAt.getTime() <= Date.now()) {
|
|
1271
|
+
await this.#finishExchange(admission, "READY");
|
|
1272
|
+
throw new RefreshFailedError("refreshed credential has no future expiry");
|
|
1273
|
+
}
|
|
1274
|
+
let encoded;
|
|
1275
|
+
try {
|
|
1276
|
+
encoded = this.#encode(result.credentials);
|
|
1277
|
+
if (!encoded || typeof encoded !== "object" || Array.isArray(encoded) || Object.keys(encoded).length === 0) throw new TypeError();
|
|
1278
|
+
} catch {
|
|
1279
|
+
await this.#finishExchange(admission, "READY");
|
|
1280
|
+
throw new RefreshFailedError("refreshed credential encoding failed");
|
|
1281
|
+
}
|
|
1282
|
+
await this.#commitRefresh(admission, new CredentialMaterial(encoded, result.expiresAt, prior.authMethod));
|
|
1283
|
+
return result.credentials;
|
|
1284
|
+
}
|
|
1285
|
+
async #recoverOrWait(record, signal) {
|
|
1286
|
+
const mutation = record.mutation;
|
|
1287
|
+
if (!mutation) throw new TypeError("credential exchange admission is absent");
|
|
1288
|
+
const admission = { key: this.key, kind: record.status === "AUTHORIZING" ? "AUTHORIZING" : "REFRESHING", attemptId: mutation.id, fence: record.fence, deadline: new Date(mutation.deadline), resultKey: mutation.resultKey };
|
|
1289
|
+
let result;
|
|
1290
|
+
try {
|
|
1291
|
+
result = await this.store.objects.readObject(admission.resultKey);
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
if (!(error instanceof ObjectNotFoundError)) throw error;
|
|
1294
|
+
if (Date.now() < admission.deadline.getTime()) {
|
|
1295
|
+
await wait(100, void 0, signal ? { signal } : void 0);
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
await this.#finishExchange(admission, admission.kind === "AUTHORIZING" ? "REAUTHORIZATION_REQUIRED" : "READY");
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
await this.#publishResult(admission, result);
|
|
1302
|
+
}
|
|
1303
|
+
async #commitRefresh(admission, material) {
|
|
1304
|
+
const envelope = credentialEnvelope(this.store.scope, this.key, admission.fence, material, admission.attemptId);
|
|
1305
|
+
const contents = Buffer.from(encodeGoCompatibleJSON(envelope));
|
|
1306
|
+
let result;
|
|
1307
|
+
try {
|
|
1308
|
+
result = await this.store.objects.createObject(admission.resultKey, contents);
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
if (!(error instanceof ConflictError || error instanceof OutcomeUnknownError)) throw error;
|
|
1311
|
+
result = await this.store.objects.readObject(admission.resultKey);
|
|
1312
|
+
if (!Buffer.from(result.contents).equals(contents)) throw new Error("immutable credential result differs");
|
|
1313
|
+
}
|
|
1314
|
+
await this.#publishResult(admission, result);
|
|
1315
|
+
}
|
|
1316
|
+
async #publishResult(admission, result) {
|
|
1317
|
+
const { record, object } = await this.store.readRecord(this.key);
|
|
1318
|
+
if (record.status === "READY" && record.credential?.key === result.key) return;
|
|
1319
|
+
requireAdmission(record, admission);
|
|
1320
|
+
const parsed = CredentialEnvelopeSchema.safeParse(parsePrivateJSON2(result.contents));
|
|
1321
|
+
if (!parsed.success) throw new TypeError("credential exchange result is invalid");
|
|
1322
|
+
const envelope = parsed.data;
|
|
1323
|
+
if (envelope.scope.projectId !== this.store.scope.projectId || envelope.scope.kind !== this.store.scope.kind || envelope.connection.connectorId !== this.key.connectorId || envelope.connection.connectionName !== this.key.connectionName || envelope.fence !== admission.fence || envelope.attemptId !== admission.attemptId) throw new TypeError("credential exchange result identity is invalid");
|
|
1324
|
+
const reference = { key: result.key, version: result.version, digest: digestBytes(result.contents).slice(7) };
|
|
1325
|
+
const updated = connectionRecord(this.store.scope, this.key, record.revision + 1, record.fence, "READY", parseExpiry2(envelope.expiresAt), envelope.authMethod, reference);
|
|
1326
|
+
try {
|
|
1327
|
+
await this.store.writeHead(this.key, object, updated);
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
if (!(error instanceof ConflictError)) throw error;
|
|
1330
|
+
const observed = await this.store.readRecord(this.key);
|
|
1331
|
+
if (observed.record.status !== "READY" || observed.record.credential?.key !== result.key) throw error;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
async #finishExchange(admission, status) {
|
|
1335
|
+
const { record, object } = await this.store.readRecord(this.key);
|
|
1336
|
+
requireAdmission(record, admission);
|
|
1337
|
+
const updated = connectionRecord(this.store.scope, this.key, record.revision + 1, record.fence + 1, status, parseExpiry2(record.expiresAt), record.authMethod, record.credential);
|
|
1338
|
+
await this.store.writeHead(this.key, object, updated);
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
function newCredentialProvider(store, key, decode, decodeShared) {
|
|
1342
|
+
return new CredentialProvider(store, key, decode, decodeShared);
|
|
1343
|
+
}
|
|
1344
|
+
function newRefreshingCredentialProvider(store, key, decode, encode, decodeShared) {
|
|
1345
|
+
return new RefreshingCredentialProvider(store, key, decode, encode, decodeShared);
|
|
1346
|
+
}
|
|
1347
|
+
function requireAdmission(record, admission) {
|
|
1348
|
+
if (record.status !== admission.kind || record.fence !== admission.fence || record.mutation?.id !== admission.attemptId || record.mutation.kind !== admission.kind) throw new ConflictError();
|
|
1349
|
+
}
|
|
1350
|
+
function parsePrivateJSON2(contents) {
|
|
1351
|
+
try {
|
|
1352
|
+
return JSON.parse(Buffer.from(contents).toString("utf8"));
|
|
1353
|
+
} catch {
|
|
1354
|
+
throw new TypeError("project credential is not valid JSON");
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
function parseExpiry2(value) {
|
|
1358
|
+
return value ? new Date(value) : void 0;
|
|
1359
|
+
}
|
|
1360
|
+
function throwIfAborted(signal) {
|
|
1361
|
+
if (signal?.aborted) throw signal.reason instanceof Error ? signal.reason : new Error("credential operation cancelled");
|
|
1362
|
+
}
|
|
1363
|
+
async function refreshWithinTimeout(driver, state, signal) {
|
|
1364
|
+
const timeoutController = new AbortController();
|
|
1365
|
+
const timer = setTimeout(() => timeoutController.abort(new Error("credential refresh timed out")), 3e4);
|
|
1366
|
+
const refreshSignal = signal ? AbortSignal.any([signal, timeoutController.signal]) : timeoutController.signal;
|
|
1367
|
+
let rejectInterruption;
|
|
1368
|
+
const interrupted = new Promise((_resolve, reject) => {
|
|
1369
|
+
rejectInterruption = reject;
|
|
1370
|
+
});
|
|
1371
|
+
const onAbort = () => rejectInterruption(new Error("credential refresh interrupted"));
|
|
1372
|
+
if (refreshSignal.aborted) onAbort();
|
|
1373
|
+
else refreshSignal.addEventListener("abort", onAbort, { once: true });
|
|
1374
|
+
try {
|
|
1375
|
+
return await Promise.race([driver.refresh(state, refreshSignal), interrupted]);
|
|
1376
|
+
} finally {
|
|
1377
|
+
clearTimeout(timer);
|
|
1378
|
+
refreshSignal.removeEventListener("abort", onAbort);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
export {
|
|
1382
|
+
ApplicationSecretRefSchema,
|
|
1383
|
+
BrokerAnswerSchema,
|
|
1384
|
+
ConfigurationSchema,
|
|
1385
|
+
ConfigurationSchemaVersion,
|
|
1386
|
+
ConfigurationStore,
|
|
1387
|
+
ConflictError,
|
|
1388
|
+
ConnectionConfigurationSchema,
|
|
1389
|
+
ConnectionNotSelectedError,
|
|
1390
|
+
ConnectionRecordSchema,
|
|
1391
|
+
ConnectionStore,
|
|
1392
|
+
CredentialBrokerClient,
|
|
1393
|
+
CredentialBrokerConfigurationSchema,
|
|
1394
|
+
CredentialBrokerRejectedError,
|
|
1395
|
+
CredentialBrokerUnavailableError,
|
|
1396
|
+
CredentialEnvelopeSchema,
|
|
1397
|
+
CredentialMaterial,
|
|
1398
|
+
CredentialProvider,
|
|
1399
|
+
DexWebObjectStore,
|
|
1400
|
+
EnvironmentValueSchema,
|
|
1401
|
+
ExchangePendingError,
|
|
1402
|
+
InMemoryObjectStore,
|
|
1403
|
+
LoadedProject,
|
|
1404
|
+
LocalConnectionSchema,
|
|
1405
|
+
LocalConnectionsFileEnvironment,
|
|
1406
|
+
LocalConnectionsFileSchema,
|
|
1407
|
+
LocalConnectionsSchemaVersion,
|
|
1408
|
+
ObjectNotFoundError,
|
|
1409
|
+
OperationConfigurationSchema,
|
|
1410
|
+
OutcomeUnknownError,
|
|
1411
|
+
ProjectObject,
|
|
1412
|
+
RFC3339NanoSchema,
|
|
1413
|
+
RefreshFailedError,
|
|
1414
|
+
RefreshingCredentialProvider,
|
|
1415
|
+
RejectedCredentialNotRefreshedError,
|
|
1416
|
+
S3ObjectStore,
|
|
1417
|
+
ScopeSchema,
|
|
1418
|
+
SharedConnections,
|
|
1419
|
+
SnapshotRefSchema,
|
|
1420
|
+
TriggerConfigurationSchema,
|
|
1421
|
+
connectionHeadKey,
|
|
1422
|
+
connectionPath,
|
|
1423
|
+
connectionRecord,
|
|
1424
|
+
credentialEnvelope,
|
|
1425
|
+
digestBytes,
|
|
1426
|
+
encodeGoCompatibleJSON,
|
|
1427
|
+
loadFromEnvironment,
|
|
1428
|
+
newCredentialProvider,
|
|
1429
|
+
newRefreshingCredentialProvider,
|
|
1430
|
+
readLocalConnectionsFile,
|
|
1431
|
+
scopePrefix,
|
|
1432
|
+
validateObjectKey
|
|
1433
|
+
};
|