@contractspec/lib.contracts-integrations 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +3676 -0
- package/dist/integrations/binding.d.ts +14 -0
- package/dist/integrations/binding.js +1 -0
- package/dist/integrations/connection.d.ts +47 -0
- package/dist/integrations/connection.js +1 -0
- package/dist/integrations/docs/integrations.docblock.d.ts +2 -0
- package/dist/integrations/docs/integrations.docblock.js +110 -0
- package/dist/integrations/health.d.ts +17 -0
- package/dist/integrations/health.js +73 -0
- package/dist/integrations/health.test.d.ts +1 -0
- package/dist/integrations/index.d.ts +11 -0
- package/dist/integrations/index.js +3264 -0
- package/dist/integrations/integrations.capability.d.ts +1 -0
- package/dist/integrations/integrations.capability.js +18 -0
- package/dist/integrations/integrations.feature.d.ts +5 -0
- package/dist/integrations/integrations.feature.js +33 -0
- package/dist/integrations/meeting-recorder/contracts/index.d.ts +7 -0
- package/dist/integrations/meeting-recorder/contracts/index.js +474 -0
- package/dist/integrations/meeting-recorder/contracts/meetings.d.ts +451 -0
- package/dist/integrations/meeting-recorder/contracts/meetings.js +219 -0
- package/dist/integrations/meeting-recorder/contracts/transcripts.d.ts +166 -0
- package/dist/integrations/meeting-recorder/contracts/transcripts.js +287 -0
- package/dist/integrations/meeting-recorder/contracts/webhooks.d.ts +85 -0
- package/dist/integrations/meeting-recorder/contracts/webhooks.js +172 -0
- package/dist/integrations/meeting-recorder/meeting-recorder.capability.d.ts +1 -0
- package/dist/integrations/meeting-recorder/meeting-recorder.capability.js +18 -0
- package/dist/integrations/meeting-recorder/meeting-recorder.feature.d.ts +5 -0
- package/dist/integrations/meeting-recorder/meeting-recorder.feature.js +33 -0
- package/dist/integrations/meeting-recorder/models.d.ts +402 -0
- package/dist/integrations/meeting-recorder/models.js +122 -0
- package/dist/integrations/meeting-recorder/telemetry.d.ts +13 -0
- package/dist/integrations/meeting-recorder/telemetry.js +54 -0
- package/dist/integrations/openbanking/contracts/accounts.d.ts +282 -0
- package/dist/integrations/openbanking/contracts/accounts.js +328 -0
- package/dist/integrations/openbanking/contracts/balances.d.ts +158 -0
- package/dist/integrations/openbanking/contracts/balances.js +292 -0
- package/dist/integrations/openbanking/contracts/index.d.ts +7 -0
- package/dist/integrations/openbanking/contracts/index.js +644 -0
- package/dist/integrations/openbanking/contracts/transactions.d.ts +206 -0
- package/dist/integrations/openbanking/contracts/transactions.js +298 -0
- package/dist/integrations/openbanking/guards.d.ts +8 -0
- package/dist/integrations/openbanking/guards.js +42 -0
- package/dist/integrations/openbanking/guards.test.d.ts +1 -0
- package/dist/integrations/openbanking/models.d.ts +223 -0
- package/dist/integrations/openbanking/models.js +110 -0
- package/dist/integrations/openbanking/openbanking.capability.d.ts +1 -0
- package/dist/integrations/openbanking/openbanking.capability.js +18 -0
- package/dist/integrations/openbanking/openbanking.feature.d.ts +5 -0
- package/dist/integrations/openbanking/openbanking.feature.js +35 -0
- package/dist/integrations/openbanking/telemetry.d.ts +12 -0
- package/dist/integrations/openbanking/telemetry.js +51 -0
- package/dist/integrations/operations.d.ts +430 -0
- package/dist/integrations/operations.js +297 -0
- package/dist/integrations/operations.test.d.ts +1 -0
- package/dist/integrations/providers/analytics-reader.d.ts +103 -0
- package/dist/integrations/providers/analytics-reader.js +1 -0
- package/dist/integrations/providers/analytics-writer.d.ts +6 -0
- package/dist/integrations/providers/analytics-writer.js +1 -0
- package/dist/integrations/providers/analytics.d.ts +47 -0
- package/dist/integrations/providers/analytics.js +1 -0
- package/dist/integrations/providers/calendar.d.ts +75 -0
- package/dist/integrations/providers/calendar.js +1 -0
- package/dist/integrations/providers/database.d.ts +12 -0
- package/dist/integrations/providers/database.js +1 -0
- package/dist/integrations/providers/elevenlabs.d.ts +3 -0
- package/dist/integrations/providers/elevenlabs.js +86 -0
- package/dist/integrations/providers/email.d.ts +83 -0
- package/dist/integrations/providers/email.js +1 -0
- package/dist/integrations/providers/embedding.d.ts +21 -0
- package/dist/integrations/providers/embedding.js +1 -0
- package/dist/integrations/providers/fal.d.ts +3 -0
- package/dist/integrations/providers/fal.js +112 -0
- package/dist/integrations/providers/fathom.d.ts +3 -0
- package/dist/integrations/providers/fathom.js +126 -0
- package/dist/integrations/providers/fireflies.d.ts +3 -0
- package/dist/integrations/providers/fireflies.js +106 -0
- package/dist/integrations/providers/gcs-storage.d.ts +3 -0
- package/dist/integrations/providers/gcs-storage.js +97 -0
- package/dist/integrations/providers/gmail.d.ts +3 -0
- package/dist/integrations/providers/gmail.js +109 -0
- package/dist/integrations/providers/google-calendar.d.ts +3 -0
- package/dist/integrations/providers/google-calendar.js +92 -0
- package/dist/integrations/providers/gradium.d.ts +3 -0
- package/dist/integrations/providers/gradium.js +110 -0
- package/dist/integrations/providers/granola.d.ts +3 -0
- package/dist/integrations/providers/granola.js +107 -0
- package/dist/integrations/providers/index.d.ts +38 -0
- package/dist/integrations/providers/index.js +2094 -0
- package/dist/integrations/providers/jira.d.ts +3 -0
- package/dist/integrations/providers/jira.js +108 -0
- package/dist/integrations/providers/linear.d.ts +3 -0
- package/dist/integrations/providers/linear.js +107 -0
- package/dist/integrations/providers/llm.d.ts +79 -0
- package/dist/integrations/providers/llm.js +1 -0
- package/dist/integrations/providers/meeting-recorder.d.ts +129 -0
- package/dist/integrations/providers/meeting-recorder.js +1 -0
- package/dist/integrations/providers/mistral.d.ts +3 -0
- package/dist/integrations/providers/mistral.js +94 -0
- package/dist/integrations/providers/notion.d.ts +3 -0
- package/dist/integrations/providers/notion.js +113 -0
- package/dist/integrations/providers/openbanking.d.ts +125 -0
- package/dist/integrations/providers/openbanking.js +1 -0
- package/dist/integrations/providers/payments.d.ts +106 -0
- package/dist/integrations/providers/payments.js +1 -0
- package/dist/integrations/providers/posthog-llm-telemetry.d.ts +51 -0
- package/dist/integrations/providers/posthog-llm-telemetry.js +176 -0
- package/dist/integrations/providers/posthog.d.ts +3 -0
- package/dist/integrations/providers/posthog.js +106 -0
- package/dist/integrations/providers/postmark.d.ts +3 -0
- package/dist/integrations/providers/postmark.js +98 -0
- package/dist/integrations/providers/powens.d.ts +3 -0
- package/dist/integrations/providers/powens.js +124 -0
- package/dist/integrations/providers/project-management.d.ts +32 -0
- package/dist/integrations/providers/project-management.js +1 -0
- package/dist/integrations/providers/providers.test.d.ts +1 -0
- package/dist/integrations/providers/qdrant.d.ts +3 -0
- package/dist/integrations/providers/qdrant.js +101 -0
- package/dist/integrations/providers/registry.d.ts +6 -0
- package/dist/integrations/providers/registry.js +1878 -0
- package/dist/integrations/providers/sms.d.ts +31 -0
- package/dist/integrations/providers/sms.js +1 -0
- package/dist/integrations/providers/storage.d.ts +57 -0
- package/dist/integrations/providers/storage.js +1 -0
- package/dist/integrations/providers/stripe.d.ts +3 -0
- package/dist/integrations/providers/stripe.js +105 -0
- package/dist/integrations/providers/supabase-postgres.d.ts +3 -0
- package/dist/integrations/providers/supabase-postgres.js +87 -0
- package/dist/integrations/providers/supabase-vector.d.ts +3 -0
- package/dist/integrations/providers/supabase-vector.js +107 -0
- package/dist/integrations/providers/tldv.d.ts +3 -0
- package/dist/integrations/providers/tldv.js +106 -0
- package/dist/integrations/providers/twilio-sms.d.ts +3 -0
- package/dist/integrations/providers/twilio-sms.js +91 -0
- package/dist/integrations/providers/vector-store.d.ts +39 -0
- package/dist/integrations/providers/vector-store.js +1 -0
- package/dist/integrations/providers/voice.d.ts +31 -0
- package/dist/integrations/providers/voice.js +1 -0
- package/dist/integrations/runtime.d.ts +95 -0
- package/dist/integrations/runtime.js +209 -0
- package/dist/integrations/runtime.test.d.ts +1 -0
- package/dist/integrations/secrets/aws-secret-manager.d.ts +28 -0
- package/dist/integrations/secrets/aws-secret-manager.js +346 -0
- package/dist/integrations/secrets/env-secret-provider.d.ts +28 -0
- package/dist/integrations/secrets/env-secret-provider.js +159 -0
- package/dist/integrations/secrets/gcp-secret-manager.d.ts +29 -0
- package/dist/integrations/secrets/gcp-secret-manager.js +347 -0
- package/dist/integrations/secrets/index.d.ts +6 -0
- package/dist/integrations/secrets/index.js +1129 -0
- package/dist/integrations/secrets/manager.d.ts +44 -0
- package/dist/integrations/secrets/manager.js +183 -0
- package/dist/integrations/secrets/provider.d.ts +49 -0
- package/dist/integrations/secrets/provider.js +74 -0
- package/dist/integrations/secrets/provider.test.d.ts +1 -0
- package/dist/integrations/secrets/scaleway-secret-manager.d.ts +35 -0
- package/dist/integrations/secrets/scaleway-secret-manager.js +375 -0
- package/dist/integrations/secrets-types.d.ts +14 -0
- package/dist/integrations/secrets-types.js +1 -0
- package/dist/integrations/spec.d.ts +72 -0
- package/dist/integrations/spec.js +22 -0
- package/dist/integrations/spec.test.d.ts +1 -0
- package/dist/node/index.js +3675 -0
- package/dist/node/integrations/binding.js +0 -0
- package/dist/node/integrations/connection.js +0 -0
- package/dist/node/integrations/docs/integrations.docblock.js +109 -0
- package/dist/node/integrations/health.js +72 -0
- package/dist/node/integrations/index.js +3263 -0
- package/dist/node/integrations/integrations.capability.js +17 -0
- package/dist/node/integrations/integrations.feature.js +32 -0
- package/dist/node/integrations/meeting-recorder/contracts/index.js +473 -0
- package/dist/node/integrations/meeting-recorder/contracts/meetings.js +218 -0
- package/dist/node/integrations/meeting-recorder/contracts/transcripts.js +286 -0
- package/dist/node/integrations/meeting-recorder/contracts/webhooks.js +171 -0
- package/dist/node/integrations/meeting-recorder/meeting-recorder.capability.js +17 -0
- package/dist/node/integrations/meeting-recorder/meeting-recorder.feature.js +32 -0
- package/dist/node/integrations/meeting-recorder/models.js +121 -0
- package/dist/node/integrations/meeting-recorder/telemetry.js +53 -0
- package/dist/node/integrations/openbanking/contracts/accounts.js +327 -0
- package/dist/node/integrations/openbanking/contracts/balances.js +291 -0
- package/dist/node/integrations/openbanking/contracts/index.js +643 -0
- package/dist/node/integrations/openbanking/contracts/transactions.js +297 -0
- package/dist/node/integrations/openbanking/guards.js +41 -0
- package/dist/node/integrations/openbanking/models.js +109 -0
- package/dist/node/integrations/openbanking/openbanking.capability.js +17 -0
- package/dist/node/integrations/openbanking/openbanking.feature.js +34 -0
- package/dist/node/integrations/openbanking/telemetry.js +50 -0
- package/dist/node/integrations/operations.js +296 -0
- package/dist/node/integrations/providers/analytics-reader.js +0 -0
- package/dist/node/integrations/providers/analytics-writer.js +0 -0
- package/dist/node/integrations/providers/analytics.js +0 -0
- package/dist/node/integrations/providers/calendar.js +0 -0
- package/dist/node/integrations/providers/database.js +0 -0
- package/dist/node/integrations/providers/elevenlabs.js +85 -0
- package/dist/node/integrations/providers/email.js +0 -0
- package/dist/node/integrations/providers/embedding.js +0 -0
- package/dist/node/integrations/providers/fal.js +111 -0
- package/dist/node/integrations/providers/fathom.js +125 -0
- package/dist/node/integrations/providers/fireflies.js +105 -0
- package/dist/node/integrations/providers/gcs-storage.js +96 -0
- package/dist/node/integrations/providers/gmail.js +108 -0
- package/dist/node/integrations/providers/google-calendar.js +91 -0
- package/dist/node/integrations/providers/gradium.js +109 -0
- package/dist/node/integrations/providers/granola.js +106 -0
- package/dist/node/integrations/providers/index.js +2093 -0
- package/dist/node/integrations/providers/jira.js +107 -0
- package/dist/node/integrations/providers/linear.js +106 -0
- package/dist/node/integrations/providers/llm.js +0 -0
- package/dist/node/integrations/providers/meeting-recorder.js +0 -0
- package/dist/node/integrations/providers/mistral.js +93 -0
- package/dist/node/integrations/providers/notion.js +112 -0
- package/dist/node/integrations/providers/openbanking.js +0 -0
- package/dist/node/integrations/providers/payments.js +0 -0
- package/dist/node/integrations/providers/posthog-llm-telemetry.js +175 -0
- package/dist/node/integrations/providers/posthog.js +105 -0
- package/dist/node/integrations/providers/postmark.js +97 -0
- package/dist/node/integrations/providers/powens.js +123 -0
- package/dist/node/integrations/providers/project-management.js +0 -0
- package/dist/node/integrations/providers/qdrant.js +100 -0
- package/dist/node/integrations/providers/registry.js +1877 -0
- package/dist/node/integrations/providers/sms.js +0 -0
- package/dist/node/integrations/providers/storage.js +0 -0
- package/dist/node/integrations/providers/stripe.js +104 -0
- package/dist/node/integrations/providers/supabase-postgres.js +86 -0
- package/dist/node/integrations/providers/supabase-vector.js +106 -0
- package/dist/node/integrations/providers/tldv.js +105 -0
- package/dist/node/integrations/providers/twilio-sms.js +90 -0
- package/dist/node/integrations/providers/vector-store.js +0 -0
- package/dist/node/integrations/providers/voice.js +0 -0
- package/dist/node/integrations/runtime.js +208 -0
- package/dist/node/integrations/secrets/aws-secret-manager.js +345 -0
- package/dist/node/integrations/secrets/env-secret-provider.js +158 -0
- package/dist/node/integrations/secrets/gcp-secret-manager.js +346 -0
- package/dist/node/integrations/secrets/index.js +1128 -0
- package/dist/node/integrations/secrets/manager.js +182 -0
- package/dist/node/integrations/secrets/provider.js +73 -0
- package/dist/node/integrations/secrets/scaleway-secret-manager.js +374 -0
- package/dist/node/integrations/secrets-types.js +0 -0
- package/dist/node/integrations/spec.js +21 -0
- package/package.json +1029 -0
|
@@ -0,0 +1,1129 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/integrations/secrets/provider.ts
|
|
3
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
4
|
+
|
|
5
|
+
class SecretProviderError extends Error {
|
|
6
|
+
provider;
|
|
7
|
+
reference;
|
|
8
|
+
code;
|
|
9
|
+
cause;
|
|
10
|
+
constructor(params) {
|
|
11
|
+
super(params.message);
|
|
12
|
+
this.name = "SecretProviderError";
|
|
13
|
+
this.provider = params.provider;
|
|
14
|
+
this.reference = params.reference;
|
|
15
|
+
this.code = params.code ?? "UNKNOWN";
|
|
16
|
+
this.cause = params.cause;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function parseSecretUri(reference) {
|
|
20
|
+
if (!reference) {
|
|
21
|
+
throw new SecretProviderError({
|
|
22
|
+
message: "Secret reference cannot be empty",
|
|
23
|
+
provider: "unknown",
|
|
24
|
+
reference,
|
|
25
|
+
code: "INVALID"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const [scheme, rest] = reference.split("://");
|
|
29
|
+
if (!scheme || !rest) {
|
|
30
|
+
throw new SecretProviderError({
|
|
31
|
+
message: `Invalid secret reference: ${reference}`,
|
|
32
|
+
provider: "unknown",
|
|
33
|
+
reference,
|
|
34
|
+
code: "INVALID"
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const queryIndex = rest.indexOf("?");
|
|
38
|
+
if (queryIndex === -1) {
|
|
39
|
+
return {
|
|
40
|
+
provider: scheme,
|
|
41
|
+
path: rest
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const path = rest.slice(0, queryIndex);
|
|
45
|
+
const query = rest.slice(queryIndex + 1);
|
|
46
|
+
const extras = Object.fromEntries(query.split("&").filter(Boolean).map((pair) => {
|
|
47
|
+
const [keyRaw, valueRaw] = pair.split("=");
|
|
48
|
+
const key = keyRaw ?? "";
|
|
49
|
+
const value = valueRaw ?? "";
|
|
50
|
+
return [decodeURIComponent(key), decodeURIComponent(value)];
|
|
51
|
+
}));
|
|
52
|
+
return {
|
|
53
|
+
provider: scheme,
|
|
54
|
+
path,
|
|
55
|
+
extras
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function normalizeSecretPayload(payload) {
|
|
59
|
+
if (payload.data instanceof Uint8Array) {
|
|
60
|
+
return payload.data;
|
|
61
|
+
}
|
|
62
|
+
if (payload.encoding === "base64") {
|
|
63
|
+
return Buffer2.from(payload.data, "base64");
|
|
64
|
+
}
|
|
65
|
+
if (payload.encoding === "binary") {
|
|
66
|
+
return Buffer2.from(payload.data, "binary");
|
|
67
|
+
}
|
|
68
|
+
return Buffer2.from(payload.data, "utf-8");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/integrations/secrets/aws-secret-manager.ts
|
|
72
|
+
import { Buffer as Buffer3 } from "buffer";
|
|
73
|
+
import {
|
|
74
|
+
CreateSecretCommand,
|
|
75
|
+
DeleteSecretCommand,
|
|
76
|
+
GetSecretValueCommand,
|
|
77
|
+
PutSecretValueCommand,
|
|
78
|
+
SecretsManagerClient
|
|
79
|
+
} from "@aws-sdk/client-secrets-manager";
|
|
80
|
+
var DEFAULT_DELETE_RECOVERY_DAYS = 7;
|
|
81
|
+
|
|
82
|
+
class AwsSecretsManagerProvider {
|
|
83
|
+
id = "aws-secrets-manager";
|
|
84
|
+
explicitRegion;
|
|
85
|
+
injectedClient;
|
|
86
|
+
clientConfig;
|
|
87
|
+
clientsByRegion = new Map;
|
|
88
|
+
constructor(options = {}) {
|
|
89
|
+
this.explicitRegion = options.region;
|
|
90
|
+
this.injectedClient = options.client;
|
|
91
|
+
this.clientConfig = options.clientConfig;
|
|
92
|
+
}
|
|
93
|
+
canHandle(reference) {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = parseSecretUri(reference);
|
|
96
|
+
return parsed.provider === "aws" && (parsed.path === "secretsmanager" || parsed.path.startsWith("secretsmanager/"));
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async getSecret(reference, options) {
|
|
102
|
+
const location = this.parseReference(reference);
|
|
103
|
+
const client = this.getClient(location.region);
|
|
104
|
+
const requestedVersion = options?.version ?? location.stage ?? location.version;
|
|
105
|
+
const input = {
|
|
106
|
+
SecretId: location.secretId,
|
|
107
|
+
...this.buildVersionSelector(requestedVersion)
|
|
108
|
+
};
|
|
109
|
+
try {
|
|
110
|
+
const result = await client.send(new GetSecretValueCommand(input));
|
|
111
|
+
const data = extractAwsSecretBytes(result, reference, this.id);
|
|
112
|
+
return {
|
|
113
|
+
data,
|
|
114
|
+
version: typeof result.VersionId === "string" && result.VersionId ? result.VersionId : requestedVersion,
|
|
115
|
+
metadata: {
|
|
116
|
+
region: location.region,
|
|
117
|
+
secretId: location.secretId,
|
|
118
|
+
...requestedVersion ? { requestedVersion } : {}
|
|
119
|
+
},
|
|
120
|
+
retrievedAt: new Date
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
throw toAwsSecretProviderError({
|
|
124
|
+
error,
|
|
125
|
+
provider: this.id,
|
|
126
|
+
reference,
|
|
127
|
+
operation: "getSecret"
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async setSecret(reference, payload) {
|
|
132
|
+
const location = this.parseReference(reference);
|
|
133
|
+
const client = this.getClient(location.region);
|
|
134
|
+
const bytes = normalizeSecretPayload(payload);
|
|
135
|
+
try {
|
|
136
|
+
const result = await client.send(new PutSecretValueCommand({
|
|
137
|
+
SecretId: location.secretId,
|
|
138
|
+
SecretBinary: bytes
|
|
139
|
+
}));
|
|
140
|
+
const versionId = typeof result.VersionId === "string" && result.VersionId ? result.VersionId : "latest";
|
|
141
|
+
return {
|
|
142
|
+
reference: this.buildReference(location.region, location.secretId, {
|
|
143
|
+
version: versionId
|
|
144
|
+
}),
|
|
145
|
+
version: versionId
|
|
146
|
+
};
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (!isAwsNotFound(error)) {
|
|
149
|
+
throw toAwsSecretProviderError({
|
|
150
|
+
error,
|
|
151
|
+
provider: this.id,
|
|
152
|
+
reference,
|
|
153
|
+
operation: "putSecretValue"
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (looksLikeAwsArn(location.secretId)) {
|
|
157
|
+
throw new SecretProviderError({
|
|
158
|
+
message: `Secret not found: ${location.secretId}`,
|
|
159
|
+
provider: this.id,
|
|
160
|
+
reference,
|
|
161
|
+
code: "NOT_FOUND",
|
|
162
|
+
cause: error
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const created = await client.send(new CreateSecretCommand({
|
|
167
|
+
Name: location.secretId,
|
|
168
|
+
SecretBinary: bytes
|
|
169
|
+
}));
|
|
170
|
+
const versionId = typeof created.VersionId === "string" && created.VersionId ? created.VersionId : "latest";
|
|
171
|
+
return {
|
|
172
|
+
reference: this.buildReference(location.region, location.secretId, {
|
|
173
|
+
version: versionId
|
|
174
|
+
}),
|
|
175
|
+
version: versionId
|
|
176
|
+
};
|
|
177
|
+
} catch (creationError) {
|
|
178
|
+
throw toAwsSecretProviderError({
|
|
179
|
+
error: creationError,
|
|
180
|
+
provider: this.id,
|
|
181
|
+
reference,
|
|
182
|
+
operation: "createSecret"
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async rotateSecret(reference, payload) {
|
|
188
|
+
return this.setSecret(reference, payload);
|
|
189
|
+
}
|
|
190
|
+
async deleteSecret(reference) {
|
|
191
|
+
const location = this.parseReference(reference);
|
|
192
|
+
const client = this.getClient(location.region);
|
|
193
|
+
try {
|
|
194
|
+
await client.send(new DeleteSecretCommand({
|
|
195
|
+
SecretId: location.secretId,
|
|
196
|
+
RecoveryWindowInDays: DEFAULT_DELETE_RECOVERY_DAYS
|
|
197
|
+
}));
|
|
198
|
+
} catch (error) {
|
|
199
|
+
throw toAwsSecretProviderError({
|
|
200
|
+
error,
|
|
201
|
+
provider: this.id,
|
|
202
|
+
reference,
|
|
203
|
+
operation: "deleteSecret"
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
getClient(region) {
|
|
208
|
+
if (this.injectedClient) {
|
|
209
|
+
return this.injectedClient;
|
|
210
|
+
}
|
|
211
|
+
const cached = this.clientsByRegion.get(region);
|
|
212
|
+
if (cached) {
|
|
213
|
+
return cached;
|
|
214
|
+
}
|
|
215
|
+
const client = new SecretsManagerClient({
|
|
216
|
+
...this.clientConfig ?? {},
|
|
217
|
+
region
|
|
218
|
+
});
|
|
219
|
+
this.clientsByRegion.set(region, client);
|
|
220
|
+
return client;
|
|
221
|
+
}
|
|
222
|
+
parseReference(reference) {
|
|
223
|
+
const parsed = parseSecretUri(reference);
|
|
224
|
+
if (parsed.provider !== "aws") {
|
|
225
|
+
throw new SecretProviderError({
|
|
226
|
+
message: `Unsupported secret provider: ${parsed.provider}`,
|
|
227
|
+
provider: this.id,
|
|
228
|
+
reference,
|
|
229
|
+
code: "INVALID"
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const segments = parsed.path.split("/").filter(Boolean);
|
|
233
|
+
if (segments.length < 3 || segments[0] !== "secretsmanager") {
|
|
234
|
+
throw new SecretProviderError({
|
|
235
|
+
message: "Expected secret reference format aws://secretsmanager/{region}/{secretIdOrArn}[?version=...]",
|
|
236
|
+
provider: this.id,
|
|
237
|
+
reference,
|
|
238
|
+
code: "INVALID"
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const regionCandidate = segments[1];
|
|
242
|
+
const region = this.resolveRegion(regionCandidate);
|
|
243
|
+
const secretId = segments.slice(2).join("/");
|
|
244
|
+
if (!secretId) {
|
|
245
|
+
throw new SecretProviderError({
|
|
246
|
+
message: `Unable to resolve secret id from reference "${parsed.path}"`,
|
|
247
|
+
provider: this.id,
|
|
248
|
+
reference,
|
|
249
|
+
code: "INVALID"
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
region,
|
|
254
|
+
secretId,
|
|
255
|
+
version: parsed.extras?.version,
|
|
256
|
+
stage: parsed.extras?.stage
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
resolveRegion(regionCandidate) {
|
|
260
|
+
const region = regionCandidate ?? this.explicitRegion ?? process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION;
|
|
261
|
+
if (!region) {
|
|
262
|
+
throw new SecretProviderError({
|
|
263
|
+
message: "AWS region must be provided either in reference (aws://secretsmanager/{region}/...) or via AWS_REGION/AWS_DEFAULT_REGION.",
|
|
264
|
+
provider: this.id,
|
|
265
|
+
reference: "aws://secretsmanager//",
|
|
266
|
+
code: "INVALID"
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return region;
|
|
270
|
+
}
|
|
271
|
+
buildVersionSelector(version) {
|
|
272
|
+
if (!version)
|
|
273
|
+
return {};
|
|
274
|
+
if (version === "latest" || version === "current") {
|
|
275
|
+
return { VersionStage: "AWSCURRENT" };
|
|
276
|
+
}
|
|
277
|
+
if (version.startsWith("AWS")) {
|
|
278
|
+
return { VersionStage: version };
|
|
279
|
+
}
|
|
280
|
+
return { VersionId: version };
|
|
281
|
+
}
|
|
282
|
+
buildReference(region, secretId, extras) {
|
|
283
|
+
const base = `aws://secretsmanager/${region}/${secretId}`;
|
|
284
|
+
const query = extras ? Object.entries(extras).filter(([, value]) => Boolean(value)).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&") : "";
|
|
285
|
+
return query ? `${base}?${query}` : base;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function extractAwsSecretBytes(result, reference, provider) {
|
|
289
|
+
if (!result || typeof result !== "object") {
|
|
290
|
+
throw new SecretProviderError({
|
|
291
|
+
message: "Invalid AWS Secrets Manager response",
|
|
292
|
+
provider,
|
|
293
|
+
reference,
|
|
294
|
+
code: "UNKNOWN",
|
|
295
|
+
cause: result
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const record = result;
|
|
299
|
+
if (record.SecretBinary instanceof Uint8Array) {
|
|
300
|
+
return record.SecretBinary;
|
|
301
|
+
}
|
|
302
|
+
if (typeof record.SecretBinary === "string") {
|
|
303
|
+
return Buffer3.from(record.SecretBinary, "base64");
|
|
304
|
+
}
|
|
305
|
+
if (typeof record.SecretString === "string") {
|
|
306
|
+
return Buffer3.from(record.SecretString, "utf-8");
|
|
307
|
+
}
|
|
308
|
+
throw new SecretProviderError({
|
|
309
|
+
message: "AWS secret value is empty",
|
|
310
|
+
provider,
|
|
311
|
+
reference,
|
|
312
|
+
code: "NOT_FOUND",
|
|
313
|
+
cause: result
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function looksLikeAwsArn(secretId) {
|
|
317
|
+
return secretId.startsWith("arn:aws:secretsmanager:");
|
|
318
|
+
}
|
|
319
|
+
function isAwsNotFound(error) {
|
|
320
|
+
if (!error || typeof error !== "object")
|
|
321
|
+
return false;
|
|
322
|
+
const err = error;
|
|
323
|
+
if (typeof err.$metadata?.httpStatusCode === "number") {
|
|
324
|
+
return err.$metadata.httpStatusCode === 404;
|
|
325
|
+
}
|
|
326
|
+
return err.name === "ResourceNotFoundException";
|
|
327
|
+
}
|
|
328
|
+
function toAwsSecretProviderError(params) {
|
|
329
|
+
const { error, provider, reference, operation } = params;
|
|
330
|
+
if (error instanceof SecretProviderError) {
|
|
331
|
+
return error;
|
|
332
|
+
}
|
|
333
|
+
const httpStatusCode = typeof error === "object" && error !== null && "$metadata" in error && typeof error.$metadata === "object" && error.$metadata?.httpStatusCode;
|
|
334
|
+
const code = httpStatusCode === 404 ? "NOT_FOUND" : httpStatusCode === 401 || httpStatusCode === 403 ? "FORBIDDEN" : httpStatusCode === 400 ? "INVALID" : "UNKNOWN";
|
|
335
|
+
const message = error instanceof Error ? error.message : `Unknown error during ${operation}`;
|
|
336
|
+
return new SecretProviderError({
|
|
337
|
+
message,
|
|
338
|
+
provider,
|
|
339
|
+
reference,
|
|
340
|
+
code,
|
|
341
|
+
cause: error
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/integrations/secrets/env-secret-provider.ts
|
|
346
|
+
class EnvSecretProvider {
|
|
347
|
+
id = "env";
|
|
348
|
+
aliases;
|
|
349
|
+
constructor(options = {}) {
|
|
350
|
+
this.aliases = options.aliases ?? {};
|
|
351
|
+
}
|
|
352
|
+
canHandle(reference) {
|
|
353
|
+
const envKey = this.resolveEnvKey(reference);
|
|
354
|
+
return envKey !== undefined && process.env[envKey] !== undefined;
|
|
355
|
+
}
|
|
356
|
+
async getSecret(reference) {
|
|
357
|
+
const envKey = this.resolveEnvKey(reference);
|
|
358
|
+
if (!envKey) {
|
|
359
|
+
throw new SecretProviderError({
|
|
360
|
+
message: `Unable to resolve environment variable for reference "${reference}".`,
|
|
361
|
+
provider: this.id,
|
|
362
|
+
reference,
|
|
363
|
+
code: "INVALID"
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
const value = process.env[envKey];
|
|
367
|
+
if (value === undefined) {
|
|
368
|
+
throw new SecretProviderError({
|
|
369
|
+
message: `Environment variable "${envKey}" not found for reference "${reference}".`,
|
|
370
|
+
provider: this.id,
|
|
371
|
+
reference,
|
|
372
|
+
code: "NOT_FOUND"
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
data: Buffer.from(value, "utf-8"),
|
|
377
|
+
version: "current",
|
|
378
|
+
metadata: {
|
|
379
|
+
source: "env",
|
|
380
|
+
envKey
|
|
381
|
+
},
|
|
382
|
+
retrievedAt: new Date
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
async setSecret(reference, _payload) {
|
|
386
|
+
throw this.forbiddenError("setSecret", reference);
|
|
387
|
+
}
|
|
388
|
+
async rotateSecret(reference, _payload) {
|
|
389
|
+
throw this.forbiddenError("rotateSecret", reference);
|
|
390
|
+
}
|
|
391
|
+
async deleteSecret(reference) {
|
|
392
|
+
throw this.forbiddenError("deleteSecret", reference);
|
|
393
|
+
}
|
|
394
|
+
resolveEnvKey(reference) {
|
|
395
|
+
if (!reference) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (this.aliases[reference]) {
|
|
399
|
+
return this.aliases[reference];
|
|
400
|
+
}
|
|
401
|
+
if (!reference.includes("://")) {
|
|
402
|
+
return reference;
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const parsed = parseSecretUri(reference);
|
|
406
|
+
if (parsed.provider === "env") {
|
|
407
|
+
return parsed.path;
|
|
408
|
+
}
|
|
409
|
+
if (parsed.extras?.env) {
|
|
410
|
+
return parsed.extras.env;
|
|
411
|
+
}
|
|
412
|
+
return this.deriveEnvKey(parsed.path);
|
|
413
|
+
} catch {
|
|
414
|
+
return reference;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
deriveEnvKey(path) {
|
|
418
|
+
if (!path)
|
|
419
|
+
return;
|
|
420
|
+
return path.split(/[\/:\-\.]/).filter(Boolean).map((segment) => segment.replace(/[^a-zA-Z0-9]/g, "_").replace(/_{2,}/g, "_").toUpperCase()).join("_");
|
|
421
|
+
}
|
|
422
|
+
forbiddenError(operation, reference) {
|
|
423
|
+
return new SecretProviderError({
|
|
424
|
+
message: `EnvSecretProvider is read-only. "${operation}" is not allowed for ${reference}.`,
|
|
425
|
+
provider: this.id,
|
|
426
|
+
reference,
|
|
427
|
+
code: "FORBIDDEN"
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/integrations/secrets/gcp-secret-manager.ts
|
|
433
|
+
import {
|
|
434
|
+
SecretManagerServiceClient
|
|
435
|
+
} from "@google-cloud/secret-manager";
|
|
436
|
+
var DEFAULT_REPLICATION = {
|
|
437
|
+
automatic: {}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
class GcpSecretManagerProvider {
|
|
441
|
+
id = "gcp-secret-manager";
|
|
442
|
+
client;
|
|
443
|
+
explicitProjectId;
|
|
444
|
+
replication;
|
|
445
|
+
constructor(options = {}) {
|
|
446
|
+
this.client = options.client ?? new SecretManagerServiceClient(options.clientOptions ?? {});
|
|
447
|
+
this.explicitProjectId = options.projectId;
|
|
448
|
+
this.replication = options.defaultReplication ?? DEFAULT_REPLICATION;
|
|
449
|
+
}
|
|
450
|
+
canHandle(reference) {
|
|
451
|
+
try {
|
|
452
|
+
const parsed = parseSecretUri(reference);
|
|
453
|
+
return parsed.provider === "gcp";
|
|
454
|
+
} catch {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async getSecret(reference, options, callOptions) {
|
|
459
|
+
const location = this.parseReference(reference);
|
|
460
|
+
const secretVersionName = this.buildVersionName(location, options?.version);
|
|
461
|
+
try {
|
|
462
|
+
const response = await this.client.accessSecretVersion({
|
|
463
|
+
name: secretVersionName
|
|
464
|
+
}, callOptions ?? {});
|
|
465
|
+
const [result] = response;
|
|
466
|
+
const payload = result.payload;
|
|
467
|
+
if (!payload?.data) {
|
|
468
|
+
throw new SecretProviderError({
|
|
469
|
+
message: `Secret payload empty for ${secretVersionName}`,
|
|
470
|
+
provider: this.id,
|
|
471
|
+
reference,
|
|
472
|
+
code: "UNKNOWN"
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
const version = extractVersionFromName(result.name ?? secretVersionName);
|
|
476
|
+
return {
|
|
477
|
+
data: payload.data,
|
|
478
|
+
version,
|
|
479
|
+
metadata: payload.dataCrc32c ? { crc32c: payload.dataCrc32c.toString() } : undefined,
|
|
480
|
+
retrievedAt: new Date
|
|
481
|
+
};
|
|
482
|
+
} catch (error) {
|
|
483
|
+
throw toSecretProviderError({
|
|
484
|
+
error,
|
|
485
|
+
provider: this.id,
|
|
486
|
+
reference,
|
|
487
|
+
operation: "access"
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async setSecret(reference, payload) {
|
|
492
|
+
const location = this.parseReference(reference);
|
|
493
|
+
const { secretName } = this.buildNames(location);
|
|
494
|
+
const data = normalizeSecretPayload(payload);
|
|
495
|
+
await this.ensureSecretExists(location, payload);
|
|
496
|
+
try {
|
|
497
|
+
const response = await this.client.addSecretVersion({
|
|
498
|
+
parent: secretName,
|
|
499
|
+
payload: {
|
|
500
|
+
data
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
if (!response) {
|
|
504
|
+
throw new SecretProviderError({
|
|
505
|
+
message: `No version returned when adding secret version for ${secretName}`,
|
|
506
|
+
provider: this.id,
|
|
507
|
+
reference,
|
|
508
|
+
code: "UNKNOWN"
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
const [version] = response;
|
|
512
|
+
const versionName = version?.name ?? `${secretName}/versions/latest`;
|
|
513
|
+
return {
|
|
514
|
+
reference: `gcp://${versionName}`,
|
|
515
|
+
version: extractVersionFromName(versionName) ?? "latest"
|
|
516
|
+
};
|
|
517
|
+
} catch (error) {
|
|
518
|
+
throw toSecretProviderError({
|
|
519
|
+
error,
|
|
520
|
+
provider: this.id,
|
|
521
|
+
reference,
|
|
522
|
+
operation: "addSecretVersion"
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async rotateSecret(reference, payload) {
|
|
527
|
+
return this.setSecret(reference, payload);
|
|
528
|
+
}
|
|
529
|
+
async deleteSecret(reference) {
|
|
530
|
+
const location = this.parseReference(reference);
|
|
531
|
+
const { secretName } = this.buildNames(location);
|
|
532
|
+
try {
|
|
533
|
+
await this.client.deleteSecret({
|
|
534
|
+
name: secretName
|
|
535
|
+
});
|
|
536
|
+
} catch (error) {
|
|
537
|
+
throw toSecretProviderError({
|
|
538
|
+
error,
|
|
539
|
+
provider: this.id,
|
|
540
|
+
reference,
|
|
541
|
+
operation: "delete"
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
parseReference(reference) {
|
|
546
|
+
const parsed = parseSecretUri(reference);
|
|
547
|
+
if (parsed.provider !== "gcp") {
|
|
548
|
+
throw new SecretProviderError({
|
|
549
|
+
message: `Unsupported secret provider: ${parsed.provider}`,
|
|
550
|
+
provider: this.id,
|
|
551
|
+
reference,
|
|
552
|
+
code: "INVALID"
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
const segments = parsed.path.split("/").filter(Boolean);
|
|
556
|
+
if (segments.length < 4 || segments[0] !== "projects") {
|
|
557
|
+
throw new SecretProviderError({
|
|
558
|
+
message: `Expected secret reference format gcp://projects/{project}/secrets/{secret}[(/versions/{version})] but received "${parsed.path}"`,
|
|
559
|
+
provider: this.id,
|
|
560
|
+
reference,
|
|
561
|
+
code: "INVALID"
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
const projectIdCandidate = segments[1] ?? this.explicitProjectId;
|
|
565
|
+
if (!projectIdCandidate) {
|
|
566
|
+
throw new SecretProviderError({
|
|
567
|
+
message: `Unable to resolve project or secret from reference "${parsed.path}"`,
|
|
568
|
+
provider: this.id,
|
|
569
|
+
reference,
|
|
570
|
+
code: "INVALID"
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
const indexOfSecrets = segments.indexOf("secrets");
|
|
574
|
+
if (indexOfSecrets === -1 || indexOfSecrets + 1 >= segments.length) {
|
|
575
|
+
throw new SecretProviderError({
|
|
576
|
+
message: `Unable to resolve project or secret from reference "${parsed.path}"`,
|
|
577
|
+
provider: this.id,
|
|
578
|
+
reference,
|
|
579
|
+
code: "INVALID"
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
const resolvedProjectId = projectIdCandidate;
|
|
583
|
+
const secretIdCandidate = segments[indexOfSecrets + 1];
|
|
584
|
+
if (!secretIdCandidate) {
|
|
585
|
+
throw new SecretProviderError({
|
|
586
|
+
message: `Unable to resolve secret ID from reference "${parsed.path}"`,
|
|
587
|
+
provider: this.id,
|
|
588
|
+
reference,
|
|
589
|
+
code: "INVALID"
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
const secretId = secretIdCandidate;
|
|
593
|
+
const indexOfVersions = segments.indexOf("versions");
|
|
594
|
+
const version = parsed.extras?.version ?? (indexOfVersions !== -1 && indexOfVersions + 1 < segments.length ? segments[indexOfVersions + 1] : undefined);
|
|
595
|
+
return {
|
|
596
|
+
projectId: resolvedProjectId,
|
|
597
|
+
secretId,
|
|
598
|
+
version
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
buildNames(location) {
|
|
602
|
+
const projectId = location.projectId ?? this.explicitProjectId;
|
|
603
|
+
if (!projectId) {
|
|
604
|
+
throw new SecretProviderError({
|
|
605
|
+
message: "Project ID must be provided either in reference or provider configuration",
|
|
606
|
+
provider: this.id,
|
|
607
|
+
reference: `gcp://projects//secrets/${location.secretId}`,
|
|
608
|
+
code: "INVALID"
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
const projectParent = `projects/${projectId}`;
|
|
612
|
+
const secretName = `${projectParent}/secrets/${location.secretId}`;
|
|
613
|
+
return {
|
|
614
|
+
projectParent,
|
|
615
|
+
secretName
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
buildVersionName(location, explicitVersion) {
|
|
619
|
+
const { secretName } = this.buildNames(location);
|
|
620
|
+
const version = explicitVersion ?? location.version ?? "latest";
|
|
621
|
+
return `${secretName}/versions/${version}`;
|
|
622
|
+
}
|
|
623
|
+
async ensureSecretExists(location, payload) {
|
|
624
|
+
const { secretName, projectParent } = this.buildNames(location);
|
|
625
|
+
try {
|
|
626
|
+
await this.client.getSecret({ name: secretName });
|
|
627
|
+
} catch (error) {
|
|
628
|
+
const providerError = toSecretProviderError({
|
|
629
|
+
error,
|
|
630
|
+
provider: this.id,
|
|
631
|
+
reference: `gcp://${secretName}`,
|
|
632
|
+
operation: "getSecret",
|
|
633
|
+
suppressThrow: true
|
|
634
|
+
});
|
|
635
|
+
if (!providerError || providerError.code !== "NOT_FOUND") {
|
|
636
|
+
if (providerError) {
|
|
637
|
+
throw providerError;
|
|
638
|
+
}
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
await this.client.createSecret({
|
|
643
|
+
parent: projectParent,
|
|
644
|
+
secretId: location.secretId,
|
|
645
|
+
secret: {
|
|
646
|
+
replication: this.replication,
|
|
647
|
+
labels: payload.labels
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
} catch (creationError) {
|
|
651
|
+
const creationProviderError = toSecretProviderError({
|
|
652
|
+
error: creationError,
|
|
653
|
+
provider: this.id,
|
|
654
|
+
reference: `gcp://${secretName}`,
|
|
655
|
+
operation: "createSecret"
|
|
656
|
+
});
|
|
657
|
+
throw creationProviderError;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function extractVersionFromName(name) {
|
|
663
|
+
const segments = name.split("/").filter(Boolean);
|
|
664
|
+
const index = segments.indexOf("versions");
|
|
665
|
+
if (index === -1 || index + 1 >= segments.length) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
return segments[index + 1];
|
|
669
|
+
}
|
|
670
|
+
function toSecretProviderError(params) {
|
|
671
|
+
const { error, provider, reference, operation, suppressThrow } = params;
|
|
672
|
+
if (error instanceof SecretProviderError) {
|
|
673
|
+
return error;
|
|
674
|
+
}
|
|
675
|
+
const code = deriveErrorCode(error);
|
|
676
|
+
const message = error instanceof Error ? error.message : `Unknown error during ${operation}`;
|
|
677
|
+
const providerError = new SecretProviderError({
|
|
678
|
+
message,
|
|
679
|
+
provider,
|
|
680
|
+
reference,
|
|
681
|
+
code,
|
|
682
|
+
cause: error
|
|
683
|
+
});
|
|
684
|
+
if (suppressThrow) {
|
|
685
|
+
return providerError;
|
|
686
|
+
}
|
|
687
|
+
throw providerError;
|
|
688
|
+
}
|
|
689
|
+
function deriveErrorCode(error) {
|
|
690
|
+
if (typeof error !== "object" || error === null) {
|
|
691
|
+
return "UNKNOWN";
|
|
692
|
+
}
|
|
693
|
+
const errorAny = error;
|
|
694
|
+
const code = errorAny.code;
|
|
695
|
+
if (code === 5 || code === "NOT_FOUND")
|
|
696
|
+
return "NOT_FOUND";
|
|
697
|
+
if (code === 6 || code === "ALREADY_EXISTS")
|
|
698
|
+
return "INVALID";
|
|
699
|
+
if (code === 7 || code === "PERMISSION_DENIED" || code === 403) {
|
|
700
|
+
return "FORBIDDEN";
|
|
701
|
+
}
|
|
702
|
+
if (code === 3 || code === "INVALID_ARGUMENT")
|
|
703
|
+
return "INVALID";
|
|
704
|
+
return "UNKNOWN";
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/integrations/secrets/scaleway-secret-manager.ts
|
|
708
|
+
import { Buffer as Buffer4 } from "buffer";
|
|
709
|
+
var UUID_V4_LIKE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
710
|
+
|
|
711
|
+
class ScalewaySecretManagerProvider {
|
|
712
|
+
id = "scaleway-secret-manager";
|
|
713
|
+
token;
|
|
714
|
+
defaultRegion;
|
|
715
|
+
defaultProjectId;
|
|
716
|
+
baseUrl;
|
|
717
|
+
fetchFn;
|
|
718
|
+
constructor(options = {}) {
|
|
719
|
+
this.token = options.token ?? process.env.SCW_SECRET_KEY ?? process.env.SCALEWAY_SECRET_KEY ?? "";
|
|
720
|
+
this.defaultRegion = options.defaultRegion ?? process.env.SCW_DEFAULT_REGION ?? process.env.SCW_REGION;
|
|
721
|
+
this.defaultProjectId = options.defaultProjectId ?? process.env.SCW_DEFAULT_PROJECT_ID ?? process.env.SCW_PROJECT_ID;
|
|
722
|
+
this.baseUrl = options.baseUrl ?? "https://api.scaleway.com";
|
|
723
|
+
this.fetchFn = options.fetch ?? fetch;
|
|
724
|
+
}
|
|
725
|
+
canHandle(reference) {
|
|
726
|
+
try {
|
|
727
|
+
const parsed = parseSecretUri(reference);
|
|
728
|
+
return parsed.provider === "scw" && (parsed.path === "secret-manager" || parsed.path.startsWith("secret-manager/"));
|
|
729
|
+
} catch {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async getSecret(reference, options) {
|
|
734
|
+
const location = this.parseReference(reference);
|
|
735
|
+
if (!this.token) {
|
|
736
|
+
throw new SecretProviderError({
|
|
737
|
+
message: "Scaleway secret manager token is missing (set SCW_SECRET_KEY / SCALEWAY_SECRET_KEY).",
|
|
738
|
+
provider: this.id,
|
|
739
|
+
reference,
|
|
740
|
+
code: "FORBIDDEN"
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
if (!UUID_V4_LIKE.test(location.secretIdOrName)) {
|
|
744
|
+
throw new SecretProviderError({
|
|
745
|
+
message: "Scaleway getSecret requires a secretId (uuid) reference, not a secret name.",
|
|
746
|
+
provider: this.id,
|
|
747
|
+
reference,
|
|
748
|
+
code: "INVALID"
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
const revision = options?.version ?? location.revision ?? "latest";
|
|
752
|
+
const url = `${this.baseUrl}/secret-manager/v1beta1/regions/${encodeURIComponent(location.region)}/secrets/${encodeURIComponent(location.secretIdOrName)}/versions/${encodeURIComponent(revision)}/access`;
|
|
753
|
+
const response = await this.fetchFn(url, {
|
|
754
|
+
method: "GET",
|
|
755
|
+
headers: {
|
|
756
|
+
"X-Auth-Token": this.token
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
if (!response.ok) {
|
|
760
|
+
throw await toScalewayError({
|
|
761
|
+
response,
|
|
762
|
+
provider: this.id,
|
|
763
|
+
reference,
|
|
764
|
+
operation: "getSecret"
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
const payload = await response.json();
|
|
768
|
+
const dataB64 = extractScalewayData(payload);
|
|
769
|
+
return {
|
|
770
|
+
data: Buffer4.from(dataB64, "base64"),
|
|
771
|
+
version: revision,
|
|
772
|
+
metadata: {
|
|
773
|
+
region: location.region,
|
|
774
|
+
secretId: location.secretIdOrName
|
|
775
|
+
},
|
|
776
|
+
retrievedAt: new Date
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
async setSecret(reference, payload) {
|
|
780
|
+
const location = this.parseReference(reference);
|
|
781
|
+
if (!this.token) {
|
|
782
|
+
throw new SecretProviderError({
|
|
783
|
+
message: "Scaleway secret manager token is missing (set SCW_SECRET_KEY / SCALEWAY_SECRET_KEY).",
|
|
784
|
+
provider: this.id,
|
|
785
|
+
reference,
|
|
786
|
+
code: "FORBIDDEN"
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
const bytes = normalizeSecretPayload(payload);
|
|
790
|
+
const encoded = Buffer4.from(bytes).toString("base64");
|
|
791
|
+
const secretId = UUID_V4_LIKE.test(location.secretIdOrName) ? location.secretIdOrName : await this.createSecret({
|
|
792
|
+
region: location.region,
|
|
793
|
+
name: location.secretIdOrName,
|
|
794
|
+
reference
|
|
795
|
+
});
|
|
796
|
+
const version = await this.createSecretVersion({
|
|
797
|
+
region: location.region,
|
|
798
|
+
secretId,
|
|
799
|
+
dataB64: encoded,
|
|
800
|
+
reference
|
|
801
|
+
});
|
|
802
|
+
return {
|
|
803
|
+
reference: this.buildReference(location.region, secretId, {
|
|
804
|
+
version
|
|
805
|
+
}),
|
|
806
|
+
version
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
async rotateSecret(reference, payload) {
|
|
810
|
+
return this.setSecret(reference, payload);
|
|
811
|
+
}
|
|
812
|
+
async deleteSecret(reference) {
|
|
813
|
+
const location = this.parseReference(reference);
|
|
814
|
+
if (!this.token) {
|
|
815
|
+
throw new SecretProviderError({
|
|
816
|
+
message: "Scaleway secret manager token is missing (set SCW_SECRET_KEY / SCALEWAY_SECRET_KEY).",
|
|
817
|
+
provider: this.id,
|
|
818
|
+
reference,
|
|
819
|
+
code: "FORBIDDEN"
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
if (!UUID_V4_LIKE.test(location.secretIdOrName)) {
|
|
823
|
+
throw new SecretProviderError({
|
|
824
|
+
message: "Scaleway deleteSecret requires a secretId (uuid) reference, not a secret name.",
|
|
825
|
+
provider: this.id,
|
|
826
|
+
reference,
|
|
827
|
+
code: "INVALID"
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
const url = `${this.baseUrl}/secret-manager/v1beta1/regions/${encodeURIComponent(location.region)}/secrets/${encodeURIComponent(location.secretIdOrName)}`;
|
|
831
|
+
const response = await this.fetchFn(url, {
|
|
832
|
+
method: "DELETE",
|
|
833
|
+
headers: {
|
|
834
|
+
"X-Auth-Token": this.token
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
if (!response.ok) {
|
|
838
|
+
throw await toScalewayError({
|
|
839
|
+
response,
|
|
840
|
+
provider: this.id,
|
|
841
|
+
reference,
|
|
842
|
+
operation: "deleteSecret"
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
parseReference(reference) {
|
|
847
|
+
const parsed = parseSecretUri(reference);
|
|
848
|
+
if (parsed.provider !== "scw") {
|
|
849
|
+
throw new SecretProviderError({
|
|
850
|
+
message: `Unsupported secret provider: ${parsed.provider}`,
|
|
851
|
+
provider: this.id,
|
|
852
|
+
reference,
|
|
853
|
+
code: "INVALID"
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
const segments = parsed.path.split("/").filter(Boolean);
|
|
857
|
+
if (segments.length < 2 || segments[0] !== "secret-manager") {
|
|
858
|
+
throw new SecretProviderError({
|
|
859
|
+
message: "Expected secret reference format scw://secret-manager/{region}/{secretIdOrName}[?version=...]",
|
|
860
|
+
provider: this.id,
|
|
861
|
+
reference,
|
|
862
|
+
code: "INVALID"
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
const region = segments[1] ?? this.defaultRegion;
|
|
866
|
+
if (!region) {
|
|
867
|
+
throw new SecretProviderError({
|
|
868
|
+
message: "Scaleway region must be provided either in reference (scw://secret-manager/{region}/...) or via SCW_DEFAULT_REGION/SCW_REGION.",
|
|
869
|
+
provider: this.id,
|
|
870
|
+
reference,
|
|
871
|
+
code: "INVALID"
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
const secretIdOrName = segments.slice(2).join("/");
|
|
875
|
+
if (!secretIdOrName) {
|
|
876
|
+
throw new SecretProviderError({
|
|
877
|
+
message: `Unable to resolve secret id/name from reference "${parsed.path}"`,
|
|
878
|
+
provider: this.id,
|
|
879
|
+
reference,
|
|
880
|
+
code: "INVALID"
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
return {
|
|
884
|
+
region,
|
|
885
|
+
secretIdOrName,
|
|
886
|
+
revision: parsed.extras?.version
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
async createSecret(params) {
|
|
890
|
+
const projectId = this.defaultProjectId;
|
|
891
|
+
if (!projectId) {
|
|
892
|
+
throw new SecretProviderError({
|
|
893
|
+
message: "Scaleway project id is required to create secrets by name (set SCW_DEFAULT_PROJECT_ID/SCW_PROJECT_ID).",
|
|
894
|
+
provider: this.id,
|
|
895
|
+
reference: params.reference,
|
|
896
|
+
code: "INVALID"
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
const url = `${this.baseUrl}/secret-manager/v1beta1/regions/${encodeURIComponent(params.region)}/secrets`;
|
|
900
|
+
const response = await this.fetchFn(url, {
|
|
901
|
+
method: "POST",
|
|
902
|
+
headers: {
|
|
903
|
+
"Content-Type": "application/json",
|
|
904
|
+
"X-Auth-Token": this.token
|
|
905
|
+
},
|
|
906
|
+
body: JSON.stringify({
|
|
907
|
+
name: params.name,
|
|
908
|
+
project_id: projectId
|
|
909
|
+
})
|
|
910
|
+
});
|
|
911
|
+
if (!response.ok) {
|
|
912
|
+
throw await toScalewayError({
|
|
913
|
+
response,
|
|
914
|
+
provider: this.id,
|
|
915
|
+
reference: params.reference,
|
|
916
|
+
operation: "createSecret"
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
const payload = await response.json();
|
|
920
|
+
const secretId = extractScalewaySecretId(payload);
|
|
921
|
+
return secretId;
|
|
922
|
+
}
|
|
923
|
+
async createSecretVersion(params) {
|
|
924
|
+
const url = `${this.baseUrl}/secret-manager/v1beta1/regions/${encodeURIComponent(params.region)}/secrets/${encodeURIComponent(params.secretId)}/versions`;
|
|
925
|
+
const response = await this.fetchFn(url, {
|
|
926
|
+
method: "POST",
|
|
927
|
+
headers: {
|
|
928
|
+
"Content-Type": "application/json",
|
|
929
|
+
"X-Auth-Token": this.token
|
|
930
|
+
},
|
|
931
|
+
body: JSON.stringify({
|
|
932
|
+
data: params.dataB64
|
|
933
|
+
})
|
|
934
|
+
});
|
|
935
|
+
if (!response.ok) {
|
|
936
|
+
throw await toScalewayError({
|
|
937
|
+
response,
|
|
938
|
+
provider: this.id,
|
|
939
|
+
reference: params.reference,
|
|
940
|
+
operation: "createSecretVersion"
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
const payload = await response.json();
|
|
944
|
+
return extractScalewayRevision(payload) ?? "latest";
|
|
945
|
+
}
|
|
946
|
+
buildReference(region, secretId, extras) {
|
|
947
|
+
const base = `scw://secret-manager/${region}/${secretId}`;
|
|
948
|
+
const query = extras ? Object.entries(extras).filter(([, value]) => Boolean(value)).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&") : "";
|
|
949
|
+
return query ? `${base}?${query}` : base;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
function extractScalewayData(payload) {
|
|
953
|
+
if (!payload || typeof payload !== "object") {
|
|
954
|
+
throw new Error("Invalid scaleway secret payload");
|
|
955
|
+
}
|
|
956
|
+
const record = payload;
|
|
957
|
+
if (typeof record.data === "string" && record.data) {
|
|
958
|
+
return record.data;
|
|
959
|
+
}
|
|
960
|
+
throw new Error("Scaleway secret payload is missing data");
|
|
961
|
+
}
|
|
962
|
+
function extractScalewaySecretId(payload) {
|
|
963
|
+
if (!payload || typeof payload !== "object") {
|
|
964
|
+
throw new Error("Invalid scaleway createSecret payload");
|
|
965
|
+
}
|
|
966
|
+
const record = payload;
|
|
967
|
+
if (typeof record.id === "string" && record.id) {
|
|
968
|
+
return record.id;
|
|
969
|
+
}
|
|
970
|
+
throw new Error("Scaleway createSecret response is missing id");
|
|
971
|
+
}
|
|
972
|
+
function extractScalewayRevision(payload) {
|
|
973
|
+
if (!payload || typeof payload !== "object") {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
const record = payload;
|
|
977
|
+
if (typeof record.revision === "number") {
|
|
978
|
+
return String(record.revision);
|
|
979
|
+
}
|
|
980
|
+
if (typeof record.revision === "string" && record.revision) {
|
|
981
|
+
return record.revision;
|
|
982
|
+
}
|
|
983
|
+
if (typeof record.id === "string" && record.id) {
|
|
984
|
+
return record.id;
|
|
985
|
+
}
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
async function toScalewayError(params) {
|
|
989
|
+
const { response, provider, reference, operation } = params;
|
|
990
|
+
const code = response.status === 404 ? "NOT_FOUND" : response.status === 401 || response.status === 403 ? "FORBIDDEN" : response.status >= 400 && response.status < 500 ? "INVALID" : "UNKNOWN";
|
|
991
|
+
const bodyText = await safeReadBody(response);
|
|
992
|
+
const message = bodyText ? `Scaleway Secret Manager ${operation} failed (${response.status}): ${bodyText}` : `Scaleway Secret Manager ${operation} failed (${response.status})`;
|
|
993
|
+
return new SecretProviderError({
|
|
994
|
+
message,
|
|
995
|
+
provider,
|
|
996
|
+
reference,
|
|
997
|
+
code
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
async function safeReadBody(response) {
|
|
1001
|
+
try {
|
|
1002
|
+
const text = await response.text();
|
|
1003
|
+
const trimmed = text.trim();
|
|
1004
|
+
return trimmed.length ? trimmed : undefined;
|
|
1005
|
+
} catch {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// src/integrations/secrets/manager.ts
|
|
1011
|
+
class SecretProviderManager {
|
|
1012
|
+
id;
|
|
1013
|
+
providers = [];
|
|
1014
|
+
registrationCounter = 0;
|
|
1015
|
+
constructor(options = {}) {
|
|
1016
|
+
this.id = options.id ?? "secret-provider-manager";
|
|
1017
|
+
const initialProviders = options.providers ?? [];
|
|
1018
|
+
for (const entry of initialProviders) {
|
|
1019
|
+
this.register(entry.provider, { priority: entry.priority });
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
register(provider, options = {}) {
|
|
1023
|
+
this.providers.push({
|
|
1024
|
+
provider,
|
|
1025
|
+
priority: options.priority ?? 0,
|
|
1026
|
+
order: this.registrationCounter++
|
|
1027
|
+
});
|
|
1028
|
+
this.providers.sort((a, b) => {
|
|
1029
|
+
if (a.priority !== b.priority) {
|
|
1030
|
+
return b.priority - a.priority;
|
|
1031
|
+
}
|
|
1032
|
+
return a.order - b.order;
|
|
1033
|
+
});
|
|
1034
|
+
return this;
|
|
1035
|
+
}
|
|
1036
|
+
canHandle(reference) {
|
|
1037
|
+
return this.providers.some(({ provider }) => safeCanHandle(provider, reference));
|
|
1038
|
+
}
|
|
1039
|
+
async getSecret(reference, options) {
|
|
1040
|
+
const errors = [];
|
|
1041
|
+
for (const { provider } of this.providers) {
|
|
1042
|
+
if (!safeCanHandle(provider, reference)) {
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
try {
|
|
1046
|
+
return await provider.getSecret(reference, options);
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
if (error instanceof SecretProviderError) {
|
|
1049
|
+
errors.push(error);
|
|
1050
|
+
if (error.code !== "NOT_FOUND") {
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
throw error;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
throw this.composeError("getSecret", reference, errors, options?.version);
|
|
1059
|
+
}
|
|
1060
|
+
async setSecret(reference, payload) {
|
|
1061
|
+
return this.delegateToFirst("setSecret", reference, (provider) => provider.setSecret(reference, payload));
|
|
1062
|
+
}
|
|
1063
|
+
async rotateSecret(reference, payload) {
|
|
1064
|
+
return this.delegateToFirst("rotateSecret", reference, (provider) => provider.rotateSecret(reference, payload));
|
|
1065
|
+
}
|
|
1066
|
+
async deleteSecret(reference) {
|
|
1067
|
+
await this.delegateToFirst("deleteSecret", reference, (provider) => provider.deleteSecret(reference));
|
|
1068
|
+
}
|
|
1069
|
+
async delegateToFirst(operation, reference, invoker) {
|
|
1070
|
+
const errors = [];
|
|
1071
|
+
for (const { provider } of this.providers) {
|
|
1072
|
+
if (!safeCanHandle(provider, reference)) {
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
try {
|
|
1076
|
+
return await invoker(provider);
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
if (error instanceof SecretProviderError) {
|
|
1079
|
+
errors.push(error);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
throw error;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
throw this.composeError(operation, reference, errors);
|
|
1086
|
+
}
|
|
1087
|
+
composeError(operation, reference, errors, version) {
|
|
1088
|
+
if (errors.length === 1) {
|
|
1089
|
+
const [singleError] = errors;
|
|
1090
|
+
if (singleError) {
|
|
1091
|
+
return singleError;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
const messageParts = [
|
|
1095
|
+
`No registered secret provider could ${operation}`,
|
|
1096
|
+
`reference "${reference}"`
|
|
1097
|
+
];
|
|
1098
|
+
if (version) {
|
|
1099
|
+
messageParts.push(`(version: ${version})`);
|
|
1100
|
+
}
|
|
1101
|
+
if (errors.length > 1) {
|
|
1102
|
+
messageParts.push(`Attempts: ${errors.map((error) => `${error.provider}:${error.code}`).join(", ")}`);
|
|
1103
|
+
}
|
|
1104
|
+
return new SecretProviderError({
|
|
1105
|
+
message: messageParts.join(" "),
|
|
1106
|
+
provider: this.id,
|
|
1107
|
+
reference,
|
|
1108
|
+
code: errors.length > 0 ? errors[errors.length - 1]?.code ?? "UNKNOWN" : "UNKNOWN",
|
|
1109
|
+
cause: errors
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function safeCanHandle(provider, reference) {
|
|
1114
|
+
try {
|
|
1115
|
+
return provider.canHandle(reference);
|
|
1116
|
+
} catch {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
export {
|
|
1121
|
+
parseSecretUri,
|
|
1122
|
+
normalizeSecretPayload,
|
|
1123
|
+
SecretProviderManager,
|
|
1124
|
+
SecretProviderError,
|
|
1125
|
+
ScalewaySecretManagerProvider,
|
|
1126
|
+
GcpSecretManagerProvider,
|
|
1127
|
+
EnvSecretProvider,
|
|
1128
|
+
AwsSecretsManagerProvider
|
|
1129
|
+
};
|