@malloydata/db-bigquery 0.0.430 → 0.0.431

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.
@@ -37,6 +37,8 @@ interface BigQueryConnectionOptions extends ConnectionConfig {
37
37
  serviceAccountKey?: {
38
38
  [key: string]: ConnectionParameterValue;
39
39
  };
40
+ /** The key file's contents, as a JSON string or base64-encoded JSON. */
41
+ serviceAccountKeyJson?: string;
40
42
  location?: string;
41
43
  maximumBytesBilled?: string;
42
44
  timeoutMs?: string;
@@ -44,6 +44,74 @@ const googleCommon = __importStar(require("@google-cloud/common"));
44
44
  const gaxios_1 = require("gaxios");
45
45
  const malloy_1 = require("@malloydata/malloy");
46
46
  const connection_1 = require("@malloydata/malloy/connection");
47
+ function isJsonObject(value) {
48
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
49
+ }
50
+ /**
51
+ * The two credential shapes the SDK's `credentials` option accepts: a service
52
+ * account key, and an external account (workload identity federation) config,
53
+ * which carries no key of its own. Anything else — `{}`, a config file, half a
54
+ * key — is rejected here rather than at the first query, where it arrives as
55
+ * "the incoming JSON object does not contain a client_email field" and names
56
+ * nothing that would lead back to this property.
57
+ */
58
+ function isCredentialObject(value) {
59
+ return ((typeof value['client_email'] === 'string' &&
60
+ typeof value['private_key'] === 'string') ||
61
+ value['type'] === 'external_account');
62
+ }
63
+ function isProbablyBase64(text) {
64
+ // `{` is not in the base64 alphabet, so JSON can never be mistaken for an
65
+ // encoding of itself. Everything else is treated as base64 and allowed to
66
+ // fail the parse below, which keeps the check to the one thing it can be
67
+ // certain about.
68
+ return !text.startsWith('{');
69
+ }
70
+ /**
71
+ * A service account key that arrives as a *string* rather than as structured
72
+ * config, in either of the two encodings a deployment can produce.
73
+ *
74
+ * `serviceAccountKey` is a `json`-typed property, and a json-typed slot takes
75
+ * its value literally: an `{env: "..."}` reference is never resolved in one,
76
+ * because reference indirection into structured config is exactly what the
77
+ * config compiler refuses. A deployment whose credentials live in an
78
+ * environment variable — the normal shape for a server — therefore cannot
79
+ * reach that slot at all, and the literal `{env: "..."}` object it does reach
80
+ * the SDK with fails as "the incoming JSON object does not contain a
81
+ * client_email field". This is a string slot, so a reference resolves and the
82
+ * value arrives here to be parsed.
83
+ *
84
+ * Base64 is accepted because a JSON object full of quotes and braces survives
85
+ * a shell, a CI secret editor, and a `.env` file poorly; base64 is one token
86
+ * that survives all three. Which one arrived is detected rather than declared,
87
+ * so a deployment that switches encodings doesn't also have to edit config.
88
+ *
89
+ * Nothing from `text` reaches the error message. It is a private key, and
90
+ * JSON.parse's own SyntaxError quotes the input it choked on.
91
+ */
92
+ function credentialsFromJson(text) {
93
+ // Buffer's base64 decoder drops characters outside the alphabet rather than
94
+ // rejecting them, so a mangled value decodes to garbage instead of throwing.
95
+ // The parse below is what catches that, which is why one message has to
96
+ // cover both encodings: at this point either could have been intended.
97
+ const json = isProbablyBase64(text)
98
+ ? Buffer.from(text, 'base64').toString('utf8')
99
+ : text;
100
+ let parsed;
101
+ try {
102
+ parsed = JSON.parse(json);
103
+ }
104
+ catch {
105
+ throw new Error('serviceAccountKeyJson is neither JSON nor base64-encoded JSON. It ' +
106
+ 'must hold the entire service account key file.');
107
+ }
108
+ if (!isJsonObject(parsed) || !isCredentialObject(parsed)) {
109
+ throw new Error('serviceAccountKeyJson parsed but is not a service account key: it ' +
110
+ 'has no client_email and private_key. It must hold the entire key ' +
111
+ 'file, not a fragment of one.');
112
+ }
113
+ return parsed;
114
+ }
47
115
  // BigQuery label grammar: keys and values are lowercase, <=63 chars, and
48
116
  // [a-z0-9_-]; keys must start with a lowercase letter. Values are transformed
49
117
  // to fit (lowercase, disallowed chars -> '_', truncate); a key that can't be
@@ -294,12 +362,23 @@ class BigQueryConnection extends connection_1.BaseConnection {
294
362
  this.name = arg;
295
363
  }
296
364
  else {
297
- const { name, client_email, private_key, serviceAccountKey, ...args } = arg;
365
+ // Every key-bearing property is destructured out of `args`, so a key
366
+ // never lands in `this.config` — only the credentials object the SDK
367
+ // needs does.
368
+ const { name, client_email, private_key, serviceAccountKey, serviceAccountKeyJson, ...args } = arg;
298
369
  this.name = name;
299
370
  config = args;
371
+ // Trimmed before it is looked at: a value that came through a here-doc,
372
+ // a `$(cat key.json)`, or a secret-store copy tends to carry a trailing
373
+ // newline, and both the encoding sniff and the emptiness check below
374
+ // would otherwise read it as content.
375
+ const keyJson = serviceAccountKeyJson === null || serviceAccountKeyJson === void 0 ? void 0 : serviceAccountKeyJson.trim();
300
376
  if (serviceAccountKey) {
301
377
  config.credentials = serviceAccountKey;
302
378
  }
379
+ else if (keyJson) {
380
+ config.credentials = credentialsFromJson(keyJson);
381
+ }
303
382
  else if (client_email || private_key) {
304
383
  config.credentials = {
305
384
  client_email,
package/dist/index.js CHANGED
@@ -34,6 +34,20 @@ const bigquery_connection_2 = require("./bigquery_connection");
34
34
  type: 'json',
35
35
  optional: true,
36
36
  },
37
+ // The string-typed twin of `serviceAccountKey`. A `json` slot holds its
38
+ // value literally — an `{env: "..."}` reference is never resolved in one —
39
+ // so a deployment holding its key in an environment variable has no way to
40
+ // use the slot above. This is a `secret` string, where references do
41
+ // resolve, and the connection parses what arrives.
42
+ {
43
+ name: 'serviceAccountKeyJson',
44
+ displayName: 'Service Account Key (JSON)',
45
+ type: 'secret',
46
+ optional: true,
47
+ description: 'The entire service account key file, as JSON or base64-encoded ' +
48
+ 'JSON (detected automatically), for supplying the key from an ' +
49
+ 'environment variable or secret manager rather than from disk.',
50
+ },
37
51
  {
38
52
  name: 'location',
39
53
  displayName: 'Location',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/db-bigquery",
3
- "version": "0.0.430",
3
+ "version": "0.0.431",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -26,7 +26,7 @@
26
26
  "@google-cloud/bigquery": "7.9.4",
27
27
  "@google-cloud/common": "5.0.2",
28
28
  "@google-cloud/paginator": "5.0.2",
29
- "@malloydata/malloy": "0.0.430",
29
+ "@malloydata/malloy": "0.0.431",
30
30
  "gaxios": "^4.2.0"
31
31
  }
32
32
  }