@keystrokehq/snowflake 0.0.16 → 0.0.18
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 +1 -1
- package/README.md +9 -233
- package/package.json +39 -171
- package/dist/accounts/index.d.mts +0 -40
- package/dist/accounts/index.mjs +0 -51
- package/dist/bulk/index.d.mts +0 -71
- package/dist/bulk/index.mjs +0 -65
- package/dist/catalog/index.d.mts +0 -144
- package/dist/catalog/index.mjs +0 -158
- package/dist/client/index.d.mts +0 -44
- package/dist/client/index.mjs +0 -273
- package/dist/common-DfpCwJx5.mjs +0 -92
- package/dist/credential-sets/index.d.mts +0 -3
- package/dist/credential-sets/index.mjs +0 -4
- package/dist/databases/index.d.mts +0 -2
- package/dist/databases/index.mjs +0 -3
- package/dist/errors-60AsF7Su.mjs +0 -95
- package/dist/events/index.d.mts +0 -382
- package/dist/events/index.mjs +0 -291
- package/dist/file-formats/index.d.mts +0 -39
- package/dist/file-formats/index.mjs +0 -48
- package/dist/functions/index.d.mts +0 -53
- package/dist/functions/index.mjs +0 -56
- package/dist/grants/index.d.mts +0 -93
- package/dist/grants/index.mjs +0 -102
- package/dist/index.d.mts +0 -4
- package/dist/index.mjs +0 -6
- package/dist/operations/index.d.mts +0 -1
- package/dist/operations/index.mjs +0 -1
- package/dist/org-admin/index.d.mts +0 -80
- package/dist/org-admin/index.mjs +0 -79
- package/dist/pipes/index.d.mts +0 -119
- package/dist/pipes/index.mjs +0 -106
- package/dist/procedures/index.d.mts +0 -61
- package/dist/procedures/index.mjs +0 -79
- package/dist/results/index.d.mts +0 -16
- package/dist/results/index.mjs +0 -64
- package/dist/retry-B4-9MZIp.mjs +0 -164
- package/dist/retry-w7cTp1QL.d.mts +0 -10
- package/dist/roles/index.d.mts +0 -60
- package/dist/roles/index.mjs +0 -74
- package/dist/rows/index.d.mts +0 -106
- package/dist/rows/index.mjs +0 -223
- package/dist/schemas/index.d.mts +0 -2
- package/dist/schemas/index.mjs +0 -4
- package/dist/schemas-catalog/index.d.mts +0 -2
- package/dist/schemas-catalog/index.mjs +0 -3
- package/dist/shares/index.d.mts +0 -56
- package/dist/shares/index.mjs +0 -77
- package/dist/snowflake-s3-sns-webhook.credential-set-D6bLAWy5.d.mts +0 -50
- package/dist/snowflake-s3-sns-webhook.credential-set-DzR7oWNK.mjs +0 -44
- package/dist/snowflake.credential-set-BxT7Gf3A.d.mts +0 -114
- package/dist/snowflake.credential-set-DE3gXf-a.mjs +0 -106
- package/dist/sql/index.d.mts +0 -84
- package/dist/sql/index.mjs +0 -209
- package/dist/sql-options-2k5xQ-oS.d.mts +0 -32
- package/dist/sql-options-D6GEa21F.mjs +0 -79
- package/dist/sql-safety-BZpAPsq6.mjs +0 -56
- package/dist/stages/index.d.mts +0 -64
- package/dist/stages/index.mjs +0 -81
- package/dist/statements-BumhMhN4.mjs +0 -81
- package/dist/statements-DJL0qVNA.d.mts +0 -238
- package/dist/status-page/index.d.mts +0 -510
- package/dist/status-page/index.mjs +0 -261
- package/dist/streaming/index.d.mts +0 -70
- package/dist/streaming/index.mjs +0 -56
- package/dist/streams/index.d.mts +0 -71
- package/dist/streams/index.mjs +0 -78
- package/dist/tables/index.d.mts +0 -2
- package/dist/tables/index.mjs +0 -3
- package/dist/tasks/index.d.mts +0 -79
- package/dist/tasks/index.mjs +0 -104
- package/dist/triggers/index.d.mts +0 -381
- package/dist/triggers/index.mjs +0 -1100
- package/dist/users/index.d.mts +0 -61
- package/dist/users/index.mjs +0 -67
- package/dist/verification/index.d.mts +0 -201
- package/dist/verification/index.mjs +0 -512
- package/dist/views/index.d.mts +0 -2
- package/dist/views/index.mjs +0 -3
- package/dist/warehouses/index.d.mts +0 -68
- package/dist/warehouses/index.mjs +0 -101
|
@@ -1,512 +0,0 @@
|
|
|
1
|
-
import { azureEventGridEventSchema, gcsPubSubPushSchema, snsNotificationSchema } from "../events/index.mjs";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { X509Certificate, createPublicKey, createVerify } from "node:crypto";
|
|
4
|
-
|
|
5
|
-
//#region src/verification/index.ts
|
|
6
|
-
/**
|
|
7
|
-
* Signature / authenticity verification helpers for the three
|
|
8
|
-
* Snowflake auto-ingest webhook triggers. Everything here is
|
|
9
|
-
* credential-free: each function is called from the webhook trigger
|
|
10
|
-
* `verify` callback with request + expected-trust inputs extracted
|
|
11
|
-
* from the relevant `CredentialSet`.
|
|
12
|
-
*
|
|
13
|
-
* All verifiers throw {@link SnowflakeWebhookVerificationError} on
|
|
14
|
-
* failure. Callers should let the error propagate; Keystroke surfaces
|
|
15
|
-
* it to the pipeline audit log and returns 4xx to the caller.
|
|
16
|
-
*/
|
|
17
|
-
/**
|
|
18
|
-
* Thrown whenever a webhook verifier rejects a request. Includes a
|
|
19
|
-
* compact `reason` code so callers can distinguish transport problems
|
|
20
|
-
* (signature mismatch) from policy problems (wrong audience).
|
|
21
|
-
*/
|
|
22
|
-
var SnowflakeWebhookVerificationError = class extends Error {
|
|
23
|
-
name = "SnowflakeWebhookVerificationError";
|
|
24
|
-
reason;
|
|
25
|
-
provider;
|
|
26
|
-
constructor(input) {
|
|
27
|
-
super(input.message, input.cause !== void 0 ? { cause: input.cause } : void 0);
|
|
28
|
-
this.reason = input.reason;
|
|
29
|
-
this.provider = input.provider ?? "unknown";
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
function defaultFetch() {
|
|
33
|
-
if (typeof fetch === "function") return fetch;
|
|
34
|
-
throw new SnowflakeWebhookVerificationError({
|
|
35
|
-
message: "global fetch is not available; pass a custom fetchImpl to the verifier.",
|
|
36
|
-
reason: "no-fetch"
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
function base64UrlDecode(value) {
|
|
40
|
-
const pad = value.length % 4 === 0 ? "" : "=".repeat(4 - value.length % 4);
|
|
41
|
-
return Buffer.from(value.replace(/-/gu, "+").replace(/_/gu, "/") + pad, "base64");
|
|
42
|
-
}
|
|
43
|
-
function readHeader(headers, name) {
|
|
44
|
-
const lower = name.toLowerCase();
|
|
45
|
-
for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === lower && typeof value === "string") return value;
|
|
46
|
-
}
|
|
47
|
-
const DEFAULT_SNS_HOST_PATTERN = /^sns\.[a-z0-9-]+\.amazonaws\.com(\.cn)?$/u;
|
|
48
|
-
/**
|
|
49
|
-
* Parse an SNS HTTP(S) notification body into a typed envelope. Throws
|
|
50
|
-
* if the body is not valid JSON or doesn't match the SNS schema.
|
|
51
|
-
*/
|
|
52
|
-
function parseSnsNotification(rawBody) {
|
|
53
|
-
let json;
|
|
54
|
-
try {
|
|
55
|
-
json = JSON.parse(rawBody);
|
|
56
|
-
} catch (cause) {
|
|
57
|
-
throw new SnowflakeWebhookVerificationError({
|
|
58
|
-
message: "SNS body is not valid JSON.",
|
|
59
|
-
reason: "invalid-json",
|
|
60
|
-
provider: "aws-s3",
|
|
61
|
-
cause
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
const parsed = snsNotificationSchema.safeParse(json);
|
|
65
|
-
if (!parsed.success) throw new SnowflakeWebhookVerificationError({
|
|
66
|
-
message: `SNS body failed schema validation: ${parsed.error.message}`,
|
|
67
|
-
reason: "schema",
|
|
68
|
-
provider: "aws-s3",
|
|
69
|
-
cause: parsed.error
|
|
70
|
-
});
|
|
71
|
-
return parsed.data;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Build the canonical string that SNS signs. The SNS spec fixes the
|
|
75
|
-
* field order per message type — do not re-order keys.
|
|
76
|
-
*
|
|
77
|
-
* @see https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html
|
|
78
|
-
*/
|
|
79
|
-
function canonicalSnsString(message) {
|
|
80
|
-
const lines = [];
|
|
81
|
-
if (message.Type === "Notification") {
|
|
82
|
-
lines.push("Message", message.Message);
|
|
83
|
-
lines.push("MessageId", message.MessageId);
|
|
84
|
-
if (message.Subject !== void 0) lines.push("Subject", message.Subject);
|
|
85
|
-
lines.push("Timestamp", message.Timestamp);
|
|
86
|
-
lines.push("TopicArn", message.TopicArn);
|
|
87
|
-
lines.push("Type", message.Type);
|
|
88
|
-
} else {
|
|
89
|
-
lines.push("Message", message.Message);
|
|
90
|
-
lines.push("MessageId", message.MessageId);
|
|
91
|
-
if (message.SubscribeURL !== void 0) lines.push("SubscribeURL", message.SubscribeURL);
|
|
92
|
-
lines.push("Timestamp", message.Timestamp);
|
|
93
|
-
if (message.Token !== void 0) lines.push("Token", message.Token);
|
|
94
|
-
lines.push("TopicArn", message.TopicArn);
|
|
95
|
-
lines.push("Type", message.Type);
|
|
96
|
-
}
|
|
97
|
-
return `${lines.join("\n")}\n`;
|
|
98
|
-
}
|
|
99
|
-
async function fetchSigningCertPem(signingCertUrl, options) {
|
|
100
|
-
if (options.resolveCert) return options.resolveCert(signingCertUrl);
|
|
101
|
-
const response = await (options.fetchImpl ?? defaultFetch())(signingCertUrl);
|
|
102
|
-
if (!response.ok) throw new SnowflakeWebhookVerificationError({
|
|
103
|
-
message: `Unable to fetch SNS signing certificate: ${response.status}`,
|
|
104
|
-
reason: "cert-fetch",
|
|
105
|
-
provider: "aws-s3"
|
|
106
|
-
});
|
|
107
|
-
return response.text();
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Verify an SNS notification. Performs:
|
|
111
|
-
* 1. Topic-ARN pinning (if provided).
|
|
112
|
-
* 2. SigningCertURL host allowlist (default: AWS SNS hosts only —
|
|
113
|
-
* prevents SSRF against arbitrary URLs).
|
|
114
|
-
* 3. Cert fetch + signature verification (SHA1 or SHA256 per
|
|
115
|
-
* `SignatureVersion`).
|
|
116
|
-
*
|
|
117
|
-
* Returns the parsed notification on success.
|
|
118
|
-
*/
|
|
119
|
-
async function verifySnsMessage(rawBody, options = {}) {
|
|
120
|
-
const message = parseSnsNotification(rawBody);
|
|
121
|
-
if (options.expectedTopicArn && message.TopicArn !== options.expectedTopicArn) throw new SnowflakeWebhookVerificationError({
|
|
122
|
-
message: `SNS TopicArn mismatch: received ${message.TopicArn}, expected ${options.expectedTopicArn}.`,
|
|
123
|
-
reason: "topic-arn-mismatch",
|
|
124
|
-
provider: "aws-s3"
|
|
125
|
-
});
|
|
126
|
-
const hostPattern = options.allowedSigningCertHostPattern ? new RegExp(options.allowedSigningCertHostPattern, "u") : DEFAULT_SNS_HOST_PATTERN;
|
|
127
|
-
let certUrl;
|
|
128
|
-
try {
|
|
129
|
-
certUrl = new URL(message.SigningCertURL);
|
|
130
|
-
} catch (cause) {
|
|
131
|
-
throw new SnowflakeWebhookVerificationError({
|
|
132
|
-
message: "SNS SigningCertURL is not a valid URL.",
|
|
133
|
-
reason: "cert-url-invalid",
|
|
134
|
-
provider: "aws-s3",
|
|
135
|
-
cause
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
if (certUrl.protocol !== "https:" || !hostPattern.test(certUrl.hostname)) throw new SnowflakeWebhookVerificationError({
|
|
139
|
-
message: `SNS SigningCertURL host ${certUrl.hostname} not in allowlist.`,
|
|
140
|
-
reason: "cert-url-host",
|
|
141
|
-
provider: "aws-s3"
|
|
142
|
-
});
|
|
143
|
-
const pem = await fetchSigningCertPem(message.SigningCertURL, options);
|
|
144
|
-
let cert;
|
|
145
|
-
try {
|
|
146
|
-
cert = new X509Certificate(pem);
|
|
147
|
-
} catch (cause) {
|
|
148
|
-
throw new SnowflakeWebhookVerificationError({
|
|
149
|
-
message: "SNS signing certificate is not a valid X.509 PEM.",
|
|
150
|
-
reason: "cert-parse",
|
|
151
|
-
provider: "aws-s3",
|
|
152
|
-
cause
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
const verifier = createVerify(message.SignatureVersion === "2" ? "RSA-SHA256" : "RSA-SHA1");
|
|
156
|
-
verifier.update(canonicalSnsString(message), "utf8");
|
|
157
|
-
if (!verifier.verify(cert.publicKey, message.Signature, "base64")) throw new SnowflakeWebhookVerificationError({
|
|
158
|
-
message: "SNS signature did not verify against the signing certificate.",
|
|
159
|
-
reason: "signature",
|
|
160
|
-
provider: "aws-s3"
|
|
161
|
-
});
|
|
162
|
-
return message;
|
|
163
|
-
}
|
|
164
|
-
const googleJwksKeySchema = z.object({
|
|
165
|
-
kid: z.string(),
|
|
166
|
-
kty: z.string(),
|
|
167
|
-
alg: z.string().optional(),
|
|
168
|
-
use: z.string().optional(),
|
|
169
|
-
n: z.string(),
|
|
170
|
-
e: z.string()
|
|
171
|
-
}).catchall(z.unknown());
|
|
172
|
-
const googleJwksResponseSchema = z.object({ keys: z.array(googleJwksKeySchema) });
|
|
173
|
-
const jwtHeaderSchema = z.object({
|
|
174
|
-
alg: z.string(),
|
|
175
|
-
kid: z.string().optional(),
|
|
176
|
-
typ: z.string().optional()
|
|
177
|
-
}).catchall(z.unknown());
|
|
178
|
-
const googleOidcClaimsSchema = z.object({
|
|
179
|
-
iss: z.string(),
|
|
180
|
-
aud: z.union([z.string(), z.array(z.string())]).optional(),
|
|
181
|
-
sub: z.string().optional(),
|
|
182
|
-
email: z.string().optional(),
|
|
183
|
-
email_verified: z.boolean().optional(),
|
|
184
|
-
exp: z.number(),
|
|
185
|
-
iat: z.number().optional(),
|
|
186
|
-
nbf: z.number().optional()
|
|
187
|
-
}).catchall(z.unknown());
|
|
188
|
-
const jwksCache = /* @__PURE__ */ new Map();
|
|
189
|
-
async function resolveJwksKeys(jwksUri, options) {
|
|
190
|
-
if (options.resolveJwks) return options.resolveJwks(jwksUri);
|
|
191
|
-
if (!options.disableCache) {
|
|
192
|
-
const cached = jwksCache.get(jwksUri);
|
|
193
|
-
const now = options.now ? options.now() : Date.now();
|
|
194
|
-
if (cached && cached.expiresAt > now) return cached.keys;
|
|
195
|
-
}
|
|
196
|
-
const response = await (options.fetchImpl ?? defaultFetch())(jwksUri);
|
|
197
|
-
if (!response.ok) throw new SnowflakeWebhookVerificationError({
|
|
198
|
-
message: `Unable to fetch JWKS ${jwksUri}: ${response.status}`,
|
|
199
|
-
reason: "jwks-fetch",
|
|
200
|
-
provider: "gcp-gcs"
|
|
201
|
-
});
|
|
202
|
-
const json = await response.json();
|
|
203
|
-
const parsed = googleJwksResponseSchema.safeParse(json);
|
|
204
|
-
if (!parsed.success) throw new SnowflakeWebhookVerificationError({
|
|
205
|
-
message: `JWKS payload failed schema validation: ${parsed.error.message}`,
|
|
206
|
-
reason: "jwks-schema",
|
|
207
|
-
provider: "gcp-gcs",
|
|
208
|
-
cause: parsed.error
|
|
209
|
-
});
|
|
210
|
-
if (!options.disableCache) {
|
|
211
|
-
const now = options.now ? options.now() : Date.now();
|
|
212
|
-
jwksCache.set(jwksUri, {
|
|
213
|
-
expiresAt: now + 36e5,
|
|
214
|
-
keys: parsed.data.keys
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
return parsed.data.keys;
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Verify the OIDC JWT that GCP Pub/Sub push sends in `Authorization:
|
|
221
|
-
* Bearer <token>`. Returns the decoded claims on success.
|
|
222
|
-
*/
|
|
223
|
-
async function verifyPubSubPushAuthorization(authorizationHeader, options) {
|
|
224
|
-
if (!authorizationHeader) throw new SnowflakeWebhookVerificationError({
|
|
225
|
-
message: "Missing Authorization header on Pub/Sub push request.",
|
|
226
|
-
reason: "missing-auth-header",
|
|
227
|
-
provider: "gcp-gcs"
|
|
228
|
-
});
|
|
229
|
-
const match = authorizationHeader.match(/^Bearer\s+(\S+)$/u);
|
|
230
|
-
if (!match) throw new SnowflakeWebhookVerificationError({
|
|
231
|
-
message: "Authorization header is not a Bearer token.",
|
|
232
|
-
reason: "bad-auth-header",
|
|
233
|
-
provider: "gcp-gcs"
|
|
234
|
-
});
|
|
235
|
-
const parts = (match[1] ?? "").split(".");
|
|
236
|
-
if (parts.length !== 3) throw new SnowflakeWebhookVerificationError({
|
|
237
|
-
message: "JWT must have three segments.",
|
|
238
|
-
reason: "jwt-shape",
|
|
239
|
-
provider: "gcp-gcs"
|
|
240
|
-
});
|
|
241
|
-
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
|
242
|
-
let header;
|
|
243
|
-
let claims;
|
|
244
|
-
try {
|
|
245
|
-
header = jwtHeaderSchema.parse(JSON.parse(base64UrlDecode(encodedHeader).toString("utf8")));
|
|
246
|
-
claims = googleOidcClaimsSchema.parse(JSON.parse(base64UrlDecode(encodedPayload).toString("utf8")));
|
|
247
|
-
} catch (cause) {
|
|
248
|
-
throw new SnowflakeWebhookVerificationError({
|
|
249
|
-
message: "JWT header or payload did not decode cleanly.",
|
|
250
|
-
reason: "jwt-decode",
|
|
251
|
-
provider: "gcp-gcs",
|
|
252
|
-
cause
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
if (header.alg !== "RS256") throw new SnowflakeWebhookVerificationError({
|
|
256
|
-
message: `Unsupported JWT algorithm ${header.alg}; only RS256 is accepted.`,
|
|
257
|
-
reason: "jwt-alg",
|
|
258
|
-
provider: "gcp-gcs"
|
|
259
|
-
});
|
|
260
|
-
const keys = await resolveJwksKeys(options.jwksUri ?? "https://www.googleapis.com/oauth2/v3/certs", options);
|
|
261
|
-
const jwk = header.kid ? keys.find((k) => k.kid === header.kid) : keys[0];
|
|
262
|
-
if (!jwk) throw new SnowflakeWebhookVerificationError({
|
|
263
|
-
message: `No JWKS key matched kid ${header.kid ?? "(none)"}.`,
|
|
264
|
-
reason: "jwks-kid-miss",
|
|
265
|
-
provider: "gcp-gcs"
|
|
266
|
-
});
|
|
267
|
-
const publicKey = createPublicKey({
|
|
268
|
-
key: jwk,
|
|
269
|
-
format: "jwk"
|
|
270
|
-
});
|
|
271
|
-
const verifier = createVerify("RSA-SHA256");
|
|
272
|
-
verifier.update(`${encodedHeader}.${encodedPayload}`, "utf8");
|
|
273
|
-
if (!verifier.verify(publicKey, base64UrlDecode(encodedSignature))) throw new SnowflakeWebhookVerificationError({
|
|
274
|
-
message: "JWT signature did not verify.",
|
|
275
|
-
reason: "jwt-signature",
|
|
276
|
-
provider: "gcp-gcs"
|
|
277
|
-
});
|
|
278
|
-
const expectedIssuer = options.issuer ?? "https://accounts.google.com";
|
|
279
|
-
if (claims.iss !== expectedIssuer) throw new SnowflakeWebhookVerificationError({
|
|
280
|
-
message: `JWT iss ${claims.iss} != expected ${expectedIssuer}.`,
|
|
281
|
-
reason: "jwt-issuer",
|
|
282
|
-
provider: "gcp-gcs"
|
|
283
|
-
});
|
|
284
|
-
if (!(claims.aud === void 0 ? [] : Array.isArray(claims.aud) ? claims.aud : [claims.aud]).includes(options.expectedAudience)) throw new SnowflakeWebhookVerificationError({
|
|
285
|
-
message: `JWT aud does not include ${options.expectedAudience}.`,
|
|
286
|
-
reason: "jwt-audience",
|
|
287
|
-
provider: "gcp-gcs"
|
|
288
|
-
});
|
|
289
|
-
if (claims.email !== options.expectedServiceAccount) throw new SnowflakeWebhookVerificationError({
|
|
290
|
-
message: `JWT email ${claims.email} != expected service account ${options.expectedServiceAccount}.`,
|
|
291
|
-
reason: "jwt-email",
|
|
292
|
-
provider: "gcp-gcs"
|
|
293
|
-
});
|
|
294
|
-
const now = Math.floor((options.now ? options.now() : Date.now()) / 1e3);
|
|
295
|
-
if (claims.exp < now) throw new SnowflakeWebhookVerificationError({
|
|
296
|
-
message: "JWT is expired.",
|
|
297
|
-
reason: "jwt-expired",
|
|
298
|
-
provider: "gcp-gcs"
|
|
299
|
-
});
|
|
300
|
-
if (claims.nbf !== void 0 && claims.nbf > now + 60) throw new SnowflakeWebhookVerificationError({
|
|
301
|
-
message: "JWT nbf is in the future.",
|
|
302
|
-
reason: "jwt-nbf",
|
|
303
|
-
provider: "gcp-gcs"
|
|
304
|
-
});
|
|
305
|
-
return claims;
|
|
306
|
-
}
|
|
307
|
-
/** Parse a Pub/Sub push envelope. Does not verify anything. */
|
|
308
|
-
function parsePubSubPush(rawBody) {
|
|
309
|
-
let json;
|
|
310
|
-
try {
|
|
311
|
-
json = JSON.parse(rawBody);
|
|
312
|
-
} catch (cause) {
|
|
313
|
-
throw new SnowflakeWebhookVerificationError({
|
|
314
|
-
message: "Pub/Sub body is not valid JSON.",
|
|
315
|
-
reason: "invalid-json",
|
|
316
|
-
provider: "gcp-gcs",
|
|
317
|
-
cause
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
const parsed = gcsPubSubPushSchema.safeParse(json);
|
|
321
|
-
if (!parsed.success) throw new SnowflakeWebhookVerificationError({
|
|
322
|
-
message: `Pub/Sub body failed schema validation: ${parsed.error.message}`,
|
|
323
|
-
reason: "schema",
|
|
324
|
-
provider: "gcp-gcs",
|
|
325
|
-
cause: parsed.error
|
|
326
|
-
});
|
|
327
|
-
return parsed.data;
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Event Grid sends its validation request as a single-event array with
|
|
331
|
-
* `eventType: 'Microsoft.EventGrid.SubscriptionValidationEvent'`. The
|
|
332
|
-
* Keystroke webhook primitive does not support dynamic responses, so
|
|
333
|
-
* operators must use **manual validation** (VSCode extension, portal
|
|
334
|
-
* click-through, or a curl to `validationUrl`) rather than relying on
|
|
335
|
-
* synchronous `validationResponse`.
|
|
336
|
-
*/
|
|
337
|
-
const EVENT_GRID_VALIDATION_EVENT_TYPE = "Microsoft.EventGrid.SubscriptionValidationEvent";
|
|
338
|
-
/** Parse an Event Grid batch. Validates each envelope with Zod. */
|
|
339
|
-
function parseEventGridBatch(rawBody) {
|
|
340
|
-
let json;
|
|
341
|
-
try {
|
|
342
|
-
json = JSON.parse(rawBody);
|
|
343
|
-
} catch (cause) {
|
|
344
|
-
throw new SnowflakeWebhookVerificationError({
|
|
345
|
-
message: "Event Grid body is not valid JSON.",
|
|
346
|
-
reason: "invalid-json",
|
|
347
|
-
provider: "azure-blob",
|
|
348
|
-
cause
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
if (!Array.isArray(json)) throw new SnowflakeWebhookVerificationError({
|
|
352
|
-
message: "Event Grid body must be a JSON array.",
|
|
353
|
-
reason: "shape",
|
|
354
|
-
provider: "azure-blob"
|
|
355
|
-
});
|
|
356
|
-
const events = [];
|
|
357
|
-
const validationEvents = [];
|
|
358
|
-
for (const entry of json) {
|
|
359
|
-
const parsed = azureEventGridEventSchema.safeParse(entry);
|
|
360
|
-
if (!parsed.success) throw new SnowflakeWebhookVerificationError({
|
|
361
|
-
message: `Event Grid event failed schema validation: ${parsed.error.message}`,
|
|
362
|
-
reason: "schema",
|
|
363
|
-
provider: "azure-blob",
|
|
364
|
-
cause: parsed.error
|
|
365
|
-
});
|
|
366
|
-
const event = parsed.data;
|
|
367
|
-
events.push(event);
|
|
368
|
-
if (event.eventType === EVENT_GRID_VALIDATION_EVENT_TYPE) {
|
|
369
|
-
const data = event.data;
|
|
370
|
-
if (typeof data.validationCode === "string") validationEvents.push({
|
|
371
|
-
event,
|
|
372
|
-
data: {
|
|
373
|
-
validationCode: data.validationCode,
|
|
374
|
-
...typeof data.validationUrl === "string" ? { validationUrl: data.validationUrl } : {}
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
return {
|
|
380
|
-
events,
|
|
381
|
-
validationEvents
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Verify an Event Grid request. Event Grid's trust model is
|
|
386
|
-
* primarily the delivery handshake + network ACLs, but operators can
|
|
387
|
-
* wire a shared secret into a custom header. This verifier:
|
|
388
|
-
*
|
|
389
|
-
* - Checks the shared secret (if configured on both ends).
|
|
390
|
-
* - Parses + schema-validates the event batch.
|
|
391
|
-
* - Optionally enforces a topic-ARN pin.
|
|
392
|
-
*/
|
|
393
|
-
function verifyEventGridRequest(input, options = {}) {
|
|
394
|
-
const batch = parseEventGridBatch(input.rawBody);
|
|
395
|
-
if (options.sharedSecret) {
|
|
396
|
-
const headerName = options.sharedSecretHeader ?? "aeg-subscription-secret";
|
|
397
|
-
if (readHeader(input.headers, headerName) !== options.sharedSecret) throw new SnowflakeWebhookVerificationError({
|
|
398
|
-
message: `Event Grid shared-secret header ${headerName} did not match.`,
|
|
399
|
-
reason: "shared-secret",
|
|
400
|
-
provider: "azure-blob"
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
if (options.expectedTopic) {
|
|
404
|
-
for (const event of batch.events) if (event.topic && event.topic !== options.expectedTopic) throw new SnowflakeWebhookVerificationError({
|
|
405
|
-
message: `Event Grid topic ${event.topic} != expected ${options.expectedTopic}.`,
|
|
406
|
-
reason: "topic",
|
|
407
|
-
provider: "azure-blob"
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
return batch;
|
|
411
|
-
}
|
|
412
|
-
const s3EventRecordSchema = z.object({
|
|
413
|
-
eventTime: z.string().optional(),
|
|
414
|
-
eventName: z.string().optional(),
|
|
415
|
-
awsRegion: z.string().optional(),
|
|
416
|
-
requestParameters: z.object({ sourceIPAddress: z.string().optional() }).catchall(z.unknown()).optional(),
|
|
417
|
-
s3: z.object({
|
|
418
|
-
bucket: z.object({ name: z.string() }).catchall(z.unknown()).optional(),
|
|
419
|
-
object: z.object({
|
|
420
|
-
key: z.string().optional(),
|
|
421
|
-
size: z.number().optional(),
|
|
422
|
-
eTag: z.string().optional(),
|
|
423
|
-
sequencer: z.string().optional()
|
|
424
|
-
}).catchall(z.unknown()).optional()
|
|
425
|
-
}).catchall(z.unknown()).optional()
|
|
426
|
-
}).catchall(z.unknown());
|
|
427
|
-
const s3EventEnvelopeSchema = z.object({ Records: z.array(s3EventRecordSchema) }).catchall(z.unknown());
|
|
428
|
-
/**
|
|
429
|
-
* Extract S3 object events out of the inner {@link SnsNotification}
|
|
430
|
-
* message body. SNS forwards the verbatim S3 notification JSON in
|
|
431
|
-
* `Message`.
|
|
432
|
-
*/
|
|
433
|
-
function extractS3Records(snsMessage) {
|
|
434
|
-
if (snsMessage.Type !== "Notification") return [];
|
|
435
|
-
let inner;
|
|
436
|
-
try {
|
|
437
|
-
inner = JSON.parse(snsMessage.Message);
|
|
438
|
-
} catch {
|
|
439
|
-
return [];
|
|
440
|
-
}
|
|
441
|
-
const parsed = s3EventEnvelopeSchema.safeParse(inner);
|
|
442
|
-
if (!parsed.success) return [];
|
|
443
|
-
return parsed.data.Records;
|
|
444
|
-
}
|
|
445
|
-
const gcsObjectPayloadSchema = z.object({
|
|
446
|
-
bucket: z.string().optional(),
|
|
447
|
-
name: z.string().optional(),
|
|
448
|
-
size: z.union([z.string(), z.number()]).optional(),
|
|
449
|
-
etag: z.string().optional(),
|
|
450
|
-
generation: z.string().optional(),
|
|
451
|
-
timeCreated: z.string().optional(),
|
|
452
|
-
updated: z.string().optional()
|
|
453
|
-
}).catchall(z.unknown());
|
|
454
|
-
/**
|
|
455
|
-
* Decode + validate the inner GCS object payload from a Pub/Sub push.
|
|
456
|
-
* Returns `undefined` if the `message.data` field is missing or the
|
|
457
|
-
* decoded body does not shape-match a GCS Object Change notification.
|
|
458
|
-
*/
|
|
459
|
-
function extractGcsObjectPayload(push) {
|
|
460
|
-
const encoded = push.message.data;
|
|
461
|
-
if (typeof encoded !== "string" || encoded.length === 0) return void 0;
|
|
462
|
-
let json;
|
|
463
|
-
try {
|
|
464
|
-
json = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
465
|
-
} catch {
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
const parsed = gcsObjectPayloadSchema.safeParse(json);
|
|
469
|
-
return parsed.success ? parsed.data : void 0;
|
|
470
|
-
}
|
|
471
|
-
const azureBlobDataSchema = z.object({
|
|
472
|
-
api: z.string().optional(),
|
|
473
|
-
blobType: z.string().optional(),
|
|
474
|
-
contentLength: z.number().optional(),
|
|
475
|
-
contentType: z.string().optional(),
|
|
476
|
-
eTag: z.string().optional(),
|
|
477
|
-
url: z.string().optional(),
|
|
478
|
-
sequencer: z.string().optional(),
|
|
479
|
-
storageDiagnostics: z.unknown().optional()
|
|
480
|
-
}).catchall(z.unknown());
|
|
481
|
-
/**
|
|
482
|
-
* Extract `bucket` + `key` from the `url` or `subject` fields of an
|
|
483
|
-
* Event Grid blob event. Event Grid populates `subject` as
|
|
484
|
-
* `/blobServices/default/containers/<container>/blobs/<key>` so we can
|
|
485
|
-
* parse either.
|
|
486
|
-
*/
|
|
487
|
-
function parseAzureBlobLocation(event) {
|
|
488
|
-
const subject = event.subject ?? "";
|
|
489
|
-
const match = subject.match(/\/containers\/([^/]+)\/blobs\/(.+)$/u);
|
|
490
|
-
if (match?.[1] && match[2]) return {
|
|
491
|
-
bucket: match[1],
|
|
492
|
-
key: match[2]
|
|
493
|
-
};
|
|
494
|
-
const data = azureBlobDataSchema.safeParse(event.data);
|
|
495
|
-
if (data.success && typeof data.data.url === "string") try {
|
|
496
|
-
const parts = new URL(data.data.url).pathname.replace(/^\//u, "").split("/");
|
|
497
|
-
if (parts.length >= 2) {
|
|
498
|
-
const [container, ...rest] = parts;
|
|
499
|
-
return {
|
|
500
|
-
bucket: container ?? "",
|
|
501
|
-
key: rest.join("/")
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
} catch {}
|
|
505
|
-
return {
|
|
506
|
-
bucket: "",
|
|
507
|
-
key: subject
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
//#endregion
|
|
512
|
-
export { EVENT_GRID_VALIDATION_EVENT_TYPE, SnowflakeWebhookVerificationError, canonicalSnsString, extractGcsObjectPayload, extractS3Records, parseAzureBlobLocation, parseEventGridBatch, parsePubSubPush, parseSnsNotification, verifyEventGridRequest, verifyPubSubPushAuthorization, verifySnsMessage };
|
package/dist/views/index.d.mts
DELETED
package/dist/views/index.mjs
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { SnowflakeClient } from "../client/index.mjs";
|
|
2
|
-
import { d as HydratedRow } from "../statements-DJL0qVNA.mjs";
|
|
3
|
-
import { t as OptionMap } from "../sql-options-2k5xQ-oS.mjs";
|
|
4
|
-
import { CatalogResult, ShowOptions, showWarehouses } from "../catalog/index.mjs";
|
|
5
|
-
|
|
6
|
-
//#region src/warehouses/index.d.ts
|
|
7
|
-
interface WarehouseContext {
|
|
8
|
-
readonly warehouse?: string;
|
|
9
|
-
readonly role?: string;
|
|
10
|
-
}
|
|
11
|
-
interface CreateWarehouseOptions extends WarehouseContext {
|
|
12
|
-
readonly name: string;
|
|
13
|
-
readonly orReplace?: boolean;
|
|
14
|
-
readonly ifNotExists?: boolean;
|
|
15
|
-
/** `WAREHOUSE_SIZE = 'X-SMALL'`, `AUTO_SUSPEND = 60`, `AUTO_RESUME = TRUE`, etc. */
|
|
16
|
-
readonly options?: OptionMap;
|
|
17
|
-
readonly comment?: string;
|
|
18
|
-
}
|
|
19
|
-
declare function createWarehouse(client: SnowflakeClient, options: CreateWarehouseOptions): Promise<void>;
|
|
20
|
-
interface AlterWarehouseOptions extends WarehouseContext {
|
|
21
|
-
readonly name: string;
|
|
22
|
-
readonly rename?: string;
|
|
23
|
-
readonly set?: OptionMap;
|
|
24
|
-
readonly unset?: readonly string[];
|
|
25
|
-
readonly comment?: string;
|
|
26
|
-
readonly ifExists?: boolean;
|
|
27
|
-
}
|
|
28
|
-
declare function alterWarehouse(client: SnowflakeClient, options: AlterWarehouseOptions): Promise<void>;
|
|
29
|
-
interface DropWarehouseOptions extends WarehouseContext {
|
|
30
|
-
readonly name: string;
|
|
31
|
-
readonly ifExists?: boolean;
|
|
32
|
-
}
|
|
33
|
-
declare function dropWarehouse(client: SnowflakeClient, options: DropWarehouseOptions): Promise<void>;
|
|
34
|
-
interface ResumeWarehouseOptions extends WarehouseContext {
|
|
35
|
-
readonly name: string;
|
|
36
|
-
readonly ifSuspended?: boolean;
|
|
37
|
-
}
|
|
38
|
-
declare function resumeWarehouse(client: SnowflakeClient, options: ResumeWarehouseOptions): Promise<void>;
|
|
39
|
-
interface SuspendWarehouseOptions extends WarehouseContext {
|
|
40
|
-
readonly name: string;
|
|
41
|
-
}
|
|
42
|
-
declare function suspendWarehouse(client: SnowflakeClient, options: SuspendWarehouseOptions): Promise<void>;
|
|
43
|
-
interface DescribeWarehouseOptions extends WarehouseContext {
|
|
44
|
-
readonly name: string;
|
|
45
|
-
}
|
|
46
|
-
declare function describeWarehouse(client: SnowflakeClient, options: DescribeWarehouseOptions): Promise<readonly HydratedRow[]>;
|
|
47
|
-
interface UseWarehouseOptions extends WarehouseContext {
|
|
48
|
-
readonly name: string;
|
|
49
|
-
}
|
|
50
|
-
declare function useWarehouse(client: SnowflakeClient, options: UseWarehouseOptions): Promise<void>;
|
|
51
|
-
interface WarehouseHistoryOptions extends WarehouseContext {
|
|
52
|
-
readonly warehouseName?: string;
|
|
53
|
-
/** ISO-8601 timestamp (passed to `TO_TIMESTAMP_LTZ`). */
|
|
54
|
-
readonly startTime?: string;
|
|
55
|
-
readonly endTime?: string;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* `TABLE(INFORMATION_SCHEMA.WAREHOUSE_LOAD_HISTORY(...))` — queued / running
|
|
59
|
-
* load metrics for a single warehouse.
|
|
60
|
-
*/
|
|
61
|
-
declare function warehouseLoadHistory(client: SnowflakeClient, options?: WarehouseHistoryOptions): Promise<readonly HydratedRow[]>;
|
|
62
|
-
/**
|
|
63
|
-
* `TABLE(INFORMATION_SCHEMA.WAREHOUSE_METERING_HISTORY(...))` — credit usage
|
|
64
|
-
* broken out by hour.
|
|
65
|
-
*/
|
|
66
|
-
declare function warehouseMeteringHistory(client: SnowflakeClient, options?: WarehouseHistoryOptions): Promise<readonly HydratedRow[]>;
|
|
67
|
-
//#endregion
|
|
68
|
-
export { AlterWarehouseOptions, type CatalogResult, CreateWarehouseOptions, DescribeWarehouseOptions, DropWarehouseOptions, ResumeWarehouseOptions, type ShowOptions, SuspendWarehouseOptions, UseWarehouseOptions, WarehouseContext, WarehouseHistoryOptions, alterWarehouse, createWarehouse, describeWarehouse, dropWarehouse, resumeWarehouse, showWarehouses, suspendWarehouse, useWarehouse, warehouseLoadHistory, warehouseMeteringHistory };
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { executeSql } from "../sql/index.mjs";
|
|
2
|
-
import { a as quoteLiteral, i as quoteIdentifier } from "../sql-safety-BZpAPsq6.mjs";
|
|
3
|
-
import { t as formatOptions } from "../sql-options-D6GEa21F.mjs";
|
|
4
|
-
import { showWarehouses } from "../catalog/index.mjs";
|
|
5
|
-
|
|
6
|
-
//#region src/warehouses/index.ts
|
|
7
|
-
async function createWarehouse(client, options) {
|
|
8
|
-
const prefix = `CREATE${options.orReplace ? " OR REPLACE" : ""} WAREHOUSE${options.ifNotExists ? " IF NOT EXISTS" : ""}`;
|
|
9
|
-
const body = options.options && Object.keys(options.options).length > 0 ? ` ${formatOptions(options.options)}` : "";
|
|
10
|
-
const comment = options.comment ? ` COMMENT = ${quoteLiteral(options.comment)}` : "";
|
|
11
|
-
await executeSql(client, {
|
|
12
|
-
statement: `${prefix} ${quoteIdentifier(options.name)}${body}${comment}`,
|
|
13
|
-
warehouse: options.warehouse,
|
|
14
|
-
role: options.role
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
async function alterWarehouse(client, options) {
|
|
18
|
-
let tail;
|
|
19
|
-
if (options.rename) tail = `RENAME TO ${quoteIdentifier(options.rename)}`;
|
|
20
|
-
else if (options.set && Object.keys(options.set).length > 0) tail = `SET ${formatOptions(options.set)}`;
|
|
21
|
-
else if (options.unset && options.unset.length > 0) tail = `UNSET ${options.unset.map((k) => k.toUpperCase()).join(", ")}`;
|
|
22
|
-
else if (options.comment !== void 0) tail = `SET COMMENT = ${quoteLiteral(options.comment)}`;
|
|
23
|
-
else throw new Error("alterWarehouse requires one of: rename / set / unset / comment");
|
|
24
|
-
await executeSql(client, {
|
|
25
|
-
statement: `ALTER WAREHOUSE${options.ifExists ? " IF EXISTS" : ""} ${quoteIdentifier(options.name)} ${tail}`,
|
|
26
|
-
warehouse: options.warehouse,
|
|
27
|
-
role: options.role
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
async function dropWarehouse(client, options) {
|
|
31
|
-
await executeSql(client, {
|
|
32
|
-
statement: `DROP WAREHOUSE${options.ifExists ? " IF EXISTS" : ""} ${quoteIdentifier(options.name)}`,
|
|
33
|
-
warehouse: options.warehouse,
|
|
34
|
-
role: options.role
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
async function resumeWarehouse(client, options) {
|
|
38
|
-
const suffix = options.ifSuspended ? " IF SUSPENDED" : "";
|
|
39
|
-
await executeSql(client, {
|
|
40
|
-
statement: `ALTER WAREHOUSE ${quoteIdentifier(options.name)} RESUME${suffix}`,
|
|
41
|
-
warehouse: options.warehouse,
|
|
42
|
-
role: options.role
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
async function suspendWarehouse(client, options) {
|
|
46
|
-
await executeSql(client, {
|
|
47
|
-
statement: `ALTER WAREHOUSE ${quoteIdentifier(options.name)} SUSPEND`,
|
|
48
|
-
warehouse: options.warehouse,
|
|
49
|
-
role: options.role
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
async function describeWarehouse(client, options) {
|
|
53
|
-
return (await executeSql(client, {
|
|
54
|
-
statement: `DESCRIBE WAREHOUSE ${quoteIdentifier(options.name)}`,
|
|
55
|
-
warehouse: options.warehouse,
|
|
56
|
-
role: options.role
|
|
57
|
-
})).rows;
|
|
58
|
-
}
|
|
59
|
-
async function useWarehouse(client, options) {
|
|
60
|
-
await executeSql(client, {
|
|
61
|
-
statement: `USE WAREHOUSE ${quoteIdentifier(options.name)}`,
|
|
62
|
-
warehouse: options.warehouse,
|
|
63
|
-
role: options.role
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
function renderTimestampArg(value) {
|
|
67
|
-
return value ? `TO_TIMESTAMP_LTZ(${quoteLiteral(value)})` : "NULL";
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* `TABLE(INFORMATION_SCHEMA.WAREHOUSE_LOAD_HISTORY(...))` — queued / running
|
|
71
|
-
* load metrics for a single warehouse.
|
|
72
|
-
*/
|
|
73
|
-
async function warehouseLoadHistory(client, options = {}) {
|
|
74
|
-
const args = [];
|
|
75
|
-
if (options.startTime !== void 0) args.push(`DATE_RANGE_START => ${renderTimestampArg(options.startTime)}`);
|
|
76
|
-
if (options.endTime !== void 0) args.push(`DATE_RANGE_END => ${renderTimestampArg(options.endTime)}`);
|
|
77
|
-
if (options.warehouseName) args.push(`WAREHOUSE_NAME => ${quoteLiteral(options.warehouseName)}`);
|
|
78
|
-
return (await executeSql(client, {
|
|
79
|
-
statement: `SELECT * FROM TABLE(INFORMATION_SCHEMA.WAREHOUSE_LOAD_HISTORY(${args.join(", ")}))`,
|
|
80
|
-
warehouse: options.warehouse,
|
|
81
|
-
role: options.role
|
|
82
|
-
})).rows;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* `TABLE(INFORMATION_SCHEMA.WAREHOUSE_METERING_HISTORY(...))` — credit usage
|
|
86
|
-
* broken out by hour.
|
|
87
|
-
*/
|
|
88
|
-
async function warehouseMeteringHistory(client, options = {}) {
|
|
89
|
-
const args = [];
|
|
90
|
-
if (options.startTime !== void 0) args.push(`DATE_RANGE_START => ${renderTimestampArg(options.startTime)}`);
|
|
91
|
-
if (options.endTime !== void 0) args.push(`DATE_RANGE_END => ${renderTimestampArg(options.endTime)}`);
|
|
92
|
-
if (options.warehouseName) args.push(`WAREHOUSE_NAME => ${quoteLiteral(options.warehouseName)}`);
|
|
93
|
-
return (await executeSql(client, {
|
|
94
|
-
statement: `SELECT * FROM TABLE(INFORMATION_SCHEMA.WAREHOUSE_METERING_HISTORY(${args.join(", ")}))`,
|
|
95
|
-
warehouse: options.warehouse,
|
|
96
|
-
role: options.role
|
|
97
|
-
})).rows;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
//#endregion
|
|
101
|
-
export { alterWarehouse, createWarehouse, describeWarehouse, dropWarehouse, resumeWarehouse, showWarehouses, suspendWarehouse, useWarehouse, warehouseLoadHistory, warehouseMeteringHistory };
|