@aifabrix/builder 2.47.4 → 2.48.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.
Files changed (42) hide show
  1. package/jest.isolated-projects.js +4 -0
  2. package/jest.projects.js +4 -0
  3. package/lib/api/types/dev.types.js +1 -1
  4. package/lib/app/register.js +20 -18
  5. package/lib/app/rotate-secret.js +39 -16
  6. package/lib/build/index.js +12 -49
  7. package/lib/build/integration-docker-staging.js +35 -0
  8. package/lib/build/standard-docker-build.js +113 -0
  9. package/lib/cli/setup-app.js +4 -0
  10. package/lib/commands/dev-init.js +18 -0
  11. package/lib/commands/setup-prompts-platform-mode.js +11 -1
  12. package/lib/commands/setup.js +1 -0
  13. package/lib/commands/up-dataplane-credentials.js +88 -5
  14. package/lib/core/setup-platform-mode.js +16 -9
  15. package/lib/parameters/infra-kv-discovery.js +4 -3
  16. package/lib/utils/aifabrix-runtime-config-dir.js +2 -1
  17. package/lib/utils/compose-generate-docker-compose.js +2 -1
  18. package/lib/utils/config-paths.js +2 -2
  19. package/lib/utils/dev-ca-docker-augment.js +54 -0
  20. package/lib/utils/docker-daemon-tls-ca.js +326 -0
  21. package/lib/utils/ensure-dev-certs-for-remote-docker.js +5 -0
  22. package/lib/utils/promote-dev-docker-image.js +170 -0
  23. package/lib/utils/remote-docker-env.js +22 -7
  24. package/lib/utils/secrets-helpers.js +9 -14
  25. package/lib/utils/secrets-kv-refs.js +47 -3
  26. package/lib/utils/secrets-missing-error.js +1 -9
  27. package/lib/utils/token-manager-client-credentials.js +11 -1
  28. package/lib/utils/worker-sidecars.js +28 -1
  29. package/package.json +1 -1
  30. package/.temp/jest-protection-convert/run-IFN2nv/.protection/hubspot-companies.json +0 -1
  31. package/.temp/jest-protection-convert/run-IvmKLO/.protection/hubspot-companies.yaml +0 -17
  32. package/.temp/jest-protection-convert/run-SdvblF/.protection/hubspot-companies.json +0 -1
  33. package/.temp/jest-protection-convert/run-b51TsW/.protection/hubspot-companies.json +0 -1
  34. package/.temp/jest-protection-convert/run-fiLmb1/.protection/hubspot-companies.json +0 -1
  35. package/.temp/jest-protection-convert/run-qYGHMU/.protection/hubspot-companies.yaml +0 -17
  36. package/.temp/jest-protection-fixtures/hubspot-Mq5re8/hubspot-companies.yaml +0 -17
  37. package/.temp/jest-protection-fixtures/hubspot-f3u4YL/hubspot-companies.yaml +0 -17
  38. package/.temp/jest-protection-fixtures/hubspot-iQtd1O/hubspot-companies.yaml +0 -17
  39. package/.temp/jest-protection-fixtures/hubspot-mdmz5M/hubspot-companies.yaml +0 -17
  40. package/.temp/jest-protection-fixtures/hubspot-qbKYxQ/hubspot-companies.yaml +0 -17
  41. package/.temp/jest-protection-fixtures/hubspot-yzlJy6/hubspot-companies.yaml +0 -17
  42. package/.temp/validation/summary.txt +0 -5
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Promote a developer-scoped local Docker image to the manifest base repository (tag only).
3
+ *
4
+ * @fileoverview Used by `aifabrix build --base --from-dev` and dev-promote workflows
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const config = require('../core/config');
12
+ const { loadAndValidateConfig } = require('./build-helpers');
13
+ const { resolveBuildDockerTagNames } = require('./build-resolve-image');
14
+ const { execWithDockerEnv } = require('./docker-exec');
15
+ const logger = require('./logger');
16
+ const { formatSuccessLine } = require('./cli-test-layout-chalk');
17
+
18
+ /**
19
+ * @param {string} value
20
+ * @returns {string}
21
+ */
22
+ function shellQuote(value) {
23
+ return `'${String(value).replace(/'/g, '\'\\\'\'')}'`;
24
+ }
25
+
26
+ /**
27
+ * @param {string} imageRef - repository:tag
28
+ * @returns {Promise<boolean>}
29
+ */
30
+ async function dockerImageRefExists(imageRef) {
31
+ try {
32
+ await execWithDockerEnv(`docker image inspect ${shellQuote(imageRef)}`);
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * @param {string} imageRef
41
+ * @returns {Promise<string|null>}
42
+ */
43
+ async function readDockerImageId(imageRef) {
44
+ try {
45
+ const { stdout } = await execWithDockerEnv(
46
+ `docker image inspect --format '{{.Id}}' ${shellQuote(imageRef)}`
47
+ );
48
+ const id = stdout.trim();
49
+ return id || null;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * @param {string} sourceRef
57
+ * @param {string} targetRef
58
+ * @returns {Promise<void>}
59
+ */
60
+ async function dockerTagImageRef(sourceRef, targetRef) {
61
+ await execWithDockerEnv(`docker tag ${shellQuote(sourceRef)} ${shellQuote(targetRef)}`);
62
+ }
63
+
64
+ /**
65
+ * @param {string} appName
66
+ * @param {Object} variables
67
+ * @param {string|number} developerId
68
+ * @param {Object} options - `tag` (base target), `fromDevTag` (dev source; default `latest`)
69
+ * @returns {Promise<{ devRef: string, baseRef: string, devTag: string, baseTag: string }>}
70
+ */
71
+ async function resolveDevAndBaseImageRefs(appName, variables, developerId, options) {
72
+ const baseTag = options.tag || 'latest';
73
+ const devTag = options.fromDevTag || 'latest';
74
+ const { buildImageName: baseName } = await resolveBuildDockerTagNames(appName, variables, developerId, {
75
+ ...options,
76
+ base: true
77
+ });
78
+ const { buildImageName: devName } = await resolveBuildDockerTagNames(appName, variables, developerId, {
79
+ ...options,
80
+ base: false
81
+ });
82
+ return {
83
+ devRef: `${devName}:${devTag}`,
84
+ baseRef: `${baseName}:${baseTag}`,
85
+ devTag,
86
+ baseTag
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Find a local developer-scoped image to promote (dev-build usually tags `latest`).
92
+ * @param {string} appName
93
+ * @param {Object} appConfig
94
+ * @param {string|number} developerId
95
+ * @param {Object} options
96
+ * @returns {Promise<{ devRef: string, baseRef: string }|null>}
97
+ */
98
+ async function resolvePromoteSourceAndTargetRefs(appName, appConfig, developerId, options) {
99
+ const baseTag = options.tag || 'latest';
100
+ const devTagCandidates = ['latest'];
101
+ if (baseTag !== 'latest' && !devTagCandidates.includes(baseTag)) {
102
+ devTagCandidates.push(baseTag);
103
+ }
104
+ for (const fromDevTag of devTagCandidates) {
105
+ const refs = await resolveDevAndBaseImageRefs(appName, appConfig, developerId, {
106
+ ...options,
107
+ fromDevTag
108
+ });
109
+ if (await dockerImageRefExists(refs.devRef)) {
110
+ return { devRef: refs.devRef, baseRef: refs.baseRef };
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+
116
+ /**
117
+ * Tag developer-scoped image as manifest base when present locally.
118
+ * @param {string} appName
119
+ * @param {Object} options - build options (`base`, `tag`, `registry`, `fromDev`)
120
+ * @returns {Promise<string|null>} base image ref when promoted/skipped; null when dev image missing
121
+ */
122
+ async function tryPromoteDevImageToBase(appName, options = {}) {
123
+ if (options.base !== true) {
124
+ throw new Error('Promote-from-dev requires --base.');
125
+ }
126
+
127
+ const developerId = await config.getDeveloperId();
128
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : Number(developerId);
129
+ if (!Number.isFinite(idNum) || idNum <= 0) {
130
+ return null;
131
+ }
132
+
133
+ const { config: appConfig } = await loadAndValidateConfig(appName);
134
+ const resolved = await resolvePromoteSourceAndTargetRefs(appName, appConfig, developerId, options);
135
+ if (!resolved) {
136
+ return null;
137
+ }
138
+ const { devRef, baseRef } = resolved;
139
+
140
+ const devId = await readDockerImageId(devRef);
141
+ if (await dockerImageRefExists(baseRef)) {
142
+ const baseId = await readDockerImageId(baseRef);
143
+ if (devId && baseId && devId === baseId) {
144
+ logger.log(formatSuccessLine(`Base image already matches dev build: ${baseRef}`));
145
+ return baseRef;
146
+ }
147
+ }
148
+
149
+ await dockerTagImageRef(devRef, baseRef);
150
+ logger.log(formatSuccessLine(`Promoted dev image to base (tag only): ${baseRef}`));
151
+ return baseRef;
152
+ }
153
+
154
+ /**
155
+ * @param {Object} options
156
+ * @returns {boolean}
157
+ */
158
+ function shouldAttemptDevPromote(options) {
159
+ return options.fromDev === true && options.base === true;
160
+ }
161
+
162
+ module.exports = {
163
+ tryPromoteDevImageToBase,
164
+ shouldAttemptDevPromote,
165
+ resolveDevAndBaseImageRefs,
166
+ resolvePromoteSourceAndTargetRefs,
167
+ dockerImageRefExists,
168
+ readDockerImageId,
169
+ dockerTagImageRef
170
+ };
@@ -6,10 +6,12 @@
6
6
  * @version 2.0.0
7
7
  */
8
8
 
9
+ const fs = require('fs');
9
10
  const path = require('path');
10
11
  const config = require('../core/config');
11
- const { getCertDir } = require('./dev-cert-helper');
12
+ const { getCertDir, readServerCaPem } = require('./dev-cert-helper');
12
13
  const { getConfigDirForPaths } = require('./paths');
14
+ const { resolveRemoteDockerDaemonVerify } = require('./docker-daemon-tls-ca');
13
15
 
14
16
  function devTlsCertPaths(certDir) {
15
17
  return {
@@ -43,10 +45,13 @@ function missingCaError(trimmed, certDir) {
43
45
  * When docker-endpoint is set, we do not fall back to the local daemon without that endpoint
44
46
  * (avoids accidentally using Docker Desktop while the dev profile targets a remote engine).
45
47
  *
46
- * When **TLS skip-verify** is enabled (config or env) and **ca.pem is missing**, client cert/key are
47
- * optional: Docker can use DOCKER_TLS_VERIFY=0 with no client certs if the daemon allows it.
48
- * If **ca.pem is present** (e.g. from Builder Server issue-cert), the daemon certificate is always
49
- * verified (DOCKER_TLS_VERIFY=1) even when skip-verify is set — better security once a trust anchor exists.
48
+ * When **TLS skip-verify** is enabled (config or `AIFABRIX_DOCKER_TLS_SKIP_VERIFY`), Docker uses
49
+ * DOCKER_TLS_VERIFY=0 (client cert/key still sent when present). Skip-verify applies even when ca.pem
50
+ * exists — ca.pem may hold the internal issue-cert CA while the Docker daemon uses a public/wildcard cert.
51
+ *
52
+ * When skip-verify is off and ca.pem exists, the CLI verifies the daemon only if ca.pem includes the
53
+ * Docker daemon trust chain (merged at dev init/refresh or lazily on first Docker use). Otherwise it
54
+ * falls back to DOCKER_TLS_VERIFY=0 with client mTLS (public CA daemon on a trusted builder host).
50
55
  *
51
56
  * Without skip-verify, cert.pem, key.pem, and ca.pem are required in the dev cert directory.
52
57
  *
@@ -61,7 +66,6 @@ async function getRemoteDockerEnv() {
61
66
  const trimmed = endpoint.trim();
62
67
  const certDir = getCertDir(getConfigDirForPaths(), await config.getDeveloperId());
63
68
  const { certPath, keyPath, caPath } = devTlsCertPaths(certDir);
64
- const fs = require('fs');
65
69
  const skipVerify = await config.getDockerTlsSkipVerify();
66
70
  const hasClient = fs.existsSync(certPath) && fs.existsSync(keyPath);
67
71
  const hasCa = fs.existsSync(caPath);
@@ -71,7 +75,18 @@ async function getRemoteDockerEnv() {
71
75
  return { DOCKER_HOST: trimmed, DOCKER_TLS_VERIFY: '0' };
72
76
  }
73
77
  if (!hasCa && !skipVerify) throw missingCaError(trimmed, certDir);
74
- const verifyDaemon = hasCa;
78
+
79
+ let verifyDaemon = false;
80
+ if (!skipVerify && hasCa) {
81
+ verifyDaemon = await resolveRemoteDockerDaemonVerify({
82
+ certDir,
83
+ existingCaPem: readServerCaPem(certDir),
84
+ dockerEndpoint: trimmed,
85
+ remoteServerUrl: await config.getRemoteServer(),
86
+ skipVerify: false
87
+ });
88
+ }
89
+
75
90
  return {
76
91
  DOCKER_HOST: trimmed,
77
92
  DOCKER_TLS_VERIFY: verifyDaemon ? '1' : '0',
@@ -19,7 +19,7 @@ const { updateContainerPortInEnvFile } = require('./env-ports');
19
19
  const { buildEnvVarMap } = require('./env-map');
20
20
  const { getLocalPortFromPath } = require('./port-resolver');
21
21
  const { readYamlAtPath, applyCanonicalSecretsOverride } = require('./secrets-canonical');
22
- const { collectUniqueKvPathStrings } = require('./secrets-kv-refs');
22
+ const { collectUniqueKvPathStrings, isCommentOrEmptyLine, activeEnvLineForKvScan, envLineWithoutInlineComment } = require('./secrets-kv-refs');
23
23
 
24
24
  /**
25
25
  * Interpolate ${VAR} occurrences with values from envVars map
@@ -34,16 +34,6 @@ function interpolateEnvVars(content, envVars) {
34
34
  });
35
35
  }
36
36
 
37
- /**
38
- * Returns true if the line is a comment or empty (should be skipped for kv:// resolution)
39
- * @param {string} line - Single line
40
- * @returns {boolean}
41
- */
42
- function isCommentOrEmptyLine(line) {
43
- const t = line.trim();
44
- return t === '' || t.startsWith('#');
45
- }
46
-
47
37
  /** Regex for kv:// path (allows slashes, e.g. kv://hubspot/clientId) */
48
38
  const KV_REF_PATTERN = /kv:\/\/([a-zA-Z0-9_\-/]+)/g;
49
39
 
@@ -162,10 +152,11 @@ function collectMissingSecrets(content, secrets, scopedKv = null) {
162
152
  const seen = new Set();
163
153
  const missing = [];
164
154
  for (const line of content.split('\n')) {
165
- if (isCommentOrEmptyLine(line)) continue;
155
+ const scanLine = activeEnvLineForKvScan(line);
156
+ if (!scanLine) continue;
166
157
  let match;
167
158
  KV_REF_PATTERN.lastIndex = 0;
168
- while ((match = KV_REF_PATTERN.exec(line)) !== null) {
159
+ while ((match = KV_REF_PATTERN.exec(scanLine)) !== null) {
169
160
  const pathStr = match[1];
170
161
  if (seen.has(pathStr)) continue;
171
162
  seen.add(pathStr);
@@ -222,7 +213,10 @@ function replaceKvInContent(content, secrets, envVars, scopedKv = null) {
222
213
  const lines = content.split('\n');
223
214
  const result = lines.map(line => {
224
215
  if (isCommentOrEmptyLine(line)) return line;
225
- return line.replace(KV_REF_PATTERN, (match, pathStr) => {
216
+ const scanLine = envLineWithoutInlineComment(line);
217
+ if (!scanLine.trim()) return line;
218
+ const suffix = line.slice(scanLine.length);
219
+ const replaced = scanLine.replace(KV_REF_PATTERN, (match, pathStr) => {
226
220
  let value = resolveKvRefValue(secretsMap, pathStr, scopedKv?.envKey, effective);
227
221
  if ((value === undefined || value === null) && isKvKeyAllowedEmptyWhenAbsent(pathStr)) {
228
222
  value = '';
@@ -234,6 +228,7 @@ function replaceKvInContent(content, secrets, envVars, scopedKv = null) {
234
228
  }
235
229
  return value !== null && value !== undefined ? String(value) : match;
236
230
  });
231
+ return replaced + suffix;
237
232
  });
238
233
  return result.join('\n');
239
234
  }
@@ -7,11 +7,51 @@
7
7
 
8
8
  const KV_REF_PATTERN = /kv:\/\/([a-zA-Z0-9_\-/]+)/g;
9
9
 
10
+ /**
11
+ * True when the line is empty or a full-line comment (# after trim).
12
+ *
13
+ * @param {string} line
14
+ * @returns {boolean}
15
+ */
10
16
  function isCommentOrEmptyLine(line) {
11
- const t = line.trim();
17
+ const t = String(line).trim();
12
18
  return t === '' || t.startsWith('#');
13
19
  }
14
20
 
21
+ /**
22
+ * Active env line for kv:// scanning: drop inline `#` comments (not inside quotes).
23
+ *
24
+ * @param {string} line
25
+ * @returns {string}
26
+ */
27
+ function envLineWithoutInlineComment(line) {
28
+ if (isCommentOrEmptyLine(line)) {
29
+ return '';
30
+ }
31
+ let inSingle = false;
32
+ let inDouble = false;
33
+ const raw = String(line);
34
+ for (let i = 0; i < raw.length; i++) {
35
+ const c = raw[i];
36
+ if (c === '\'' && !inDouble) {
37
+ inSingle = !inSingle;
38
+ } else if (c === '"' && !inSingle) {
39
+ inDouble = !inDouble;
40
+ } else if (c === '#' && !inSingle && !inDouble) {
41
+ return raw.slice(0, i);
42
+ }
43
+ }
44
+ return raw;
45
+ }
46
+
47
+ /**
48
+ * @param {string} line
49
+ * @returns {string}
50
+ */
51
+ function activeEnvLineForKvScan(line) {
52
+ return envLineWithoutInlineComment(line).trim();
53
+ }
54
+
15
55
  /**
16
56
  * @param {string} content
17
57
  * @returns {string[]}
@@ -23,10 +63,11 @@ function collectUniqueKvPathStrings(content) {
23
63
  return out;
24
64
  }
25
65
  for (const line of content.split('\n')) {
26
- if (isCommentOrEmptyLine(line)) continue;
66
+ const scanLine = activeEnvLineForKvScan(line);
67
+ if (!scanLine) continue;
27
68
  let match;
28
69
  KV_REF_PATTERN.lastIndex = 0;
29
- while ((match = KV_REF_PATTERN.exec(line)) !== null) {
70
+ while ((match = KV_REF_PATTERN.exec(scanLine)) !== null) {
30
71
  const pathStr = match[1];
31
72
  if (!seen.has(pathStr)) {
32
73
  seen.add(pathStr);
@@ -38,5 +79,8 @@ function collectUniqueKvPathStrings(content) {
38
79
  }
39
80
 
40
81
  module.exports = {
82
+ isCommentOrEmptyLine,
83
+ envLineWithoutInlineComment,
84
+ activeEnvLineForKvScan,
41
85
  collectUniqueKvPathStrings
42
86
  };
@@ -9,15 +9,7 @@
9
9
  'use strict';
10
10
 
11
11
  const { formatMissingSecretsFileInfo } = require('./secrets-helpers');
12
-
13
- /**
14
- * @param {string} line
15
- * @returns {boolean}
16
- */
17
- function isCommentOrEmptyLine(line) {
18
- const t = String(line).trim();
19
- return t === '' || t.startsWith('#');
20
- }
12
+ const { isCommentOrEmptyLine } = require('./secrets-kv-refs');
21
13
 
22
14
  /**
23
15
  * @param {string} kvRef - e.g. kv://hubspot-demo/clientSecret
@@ -183,12 +183,22 @@ async function resolveCredentialAppForDeployment(appName) {
183
183
 
184
184
  /**
185
185
  * @param {string} appName
186
+ * @param {{ secretsFileOnly?: boolean }} [options]
186
187
  * @returns {Promise<{ clientId: string, clientSecret: string }|null>}
187
188
  */
188
- async function loadClientCredentials(appName) {
189
+ async function loadClientCredentials(appName, options = {}) {
189
190
  if (!appName || typeof appName !== 'string') {
190
191
  throw new Error('App name is required and must be a string');
191
192
  }
193
+ if (options.secretsFileOnly === true) {
194
+ for (const key of credentialAppKeysToTry(appName)) {
195
+ const fromFile = await readClientCredentialsFromSecretsFileDecrypted(key);
196
+ if (fromFile) {
197
+ return fromFile;
198
+ }
199
+ }
200
+ return null;
201
+ }
192
202
  const resolved = await resolveCredentialAppForDeployment(appName);
193
203
  return resolved.credentials;
194
204
  }
@@ -6,6 +6,8 @@
6
6
 
7
7
  'use strict';
8
8
 
9
+ const fs = require('fs');
10
+
9
11
  const { buildDefaultLocalContainerName } = require('./environment-scoped-resources');
10
12
 
11
13
  /**
@@ -88,10 +90,31 @@ function getWorkerCompanionAppKeys(appKey, appConfig) {
88
90
  return resolveWorkerSidecars(appKey, appConfig).companions.map((entry) => entry.key);
89
91
  }
90
92
 
93
+ /**
94
+ * @param {string|undefined|null} envFilePath
95
+ * @returns {boolean}
96
+ */
97
+ function isLocalModeFromEnvFile(envFilePath) {
98
+ if (!envFilePath || typeof envFilePath !== 'string') {
99
+ return false;
100
+ }
101
+ try {
102
+ const content = fs.readFileSync(envFilePath, 'utf8');
103
+ const match = content.match(/^LOCAL_MODE=(.+)$/m);
104
+ if (!match) {
105
+ return false;
106
+ }
107
+ const value = match[1].trim().replace(/^["']|["']$/g, '').toLowerCase();
108
+ return value === 'true' || value === '1' || value === 'yes';
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
91
114
  /**
92
115
  * @param {string} appKey
93
116
  * @param {object} appConfig
94
- * @param {{ devId: string|number, idNum: number }} ids
117
+ * @param {{ devId: string|number, idNum: number, envFilePath?: string }} ids
95
118
  * @returns {{
96
119
  * enabled: boolean,
97
120
  * worker?: { key: string, containerName: string, command: string },
@@ -99,6 +122,9 @@ function getWorkerCompanionAppKeys(appKey, appConfig) {
99
122
  * }}
100
123
  */
101
124
  function buildWorkerSidecarComposeContext(appKey, appConfig, ids) {
125
+ if (isLocalModeFromEnvFile(ids.envFilePath)) {
126
+ return { enabled: false };
127
+ }
102
128
  const sidecars = resolveWorkerSidecars(appKey, appConfig);
103
129
  if (!sidecars.enabled) {
104
130
  return { enabled: false };
@@ -133,6 +159,7 @@ function imageRunToStartupCommand(imageRun) {
133
159
  }
134
160
 
135
161
  module.exports = {
162
+ isLocalModeFromEnvFile,
136
163
  readImageRunCommand,
137
164
  isWorkerStackEnabled,
138
165
  resolveWorkerSidecars,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.47.4",
3
+ "version": "2.48.0",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -1 +0,0 @@
1
- {"apiVersion":"dataplane.aifabrix.ai/v1","kind":"Protection","metadata":{"key":"hubspot-country-sales","displayName":"HubSpot Country Sales Access"},"spec":{"enabled":true,"datasourceKey":"hubspot-companies","mode":"replaceForSource","rules":[{"key":"sales-country-users","principal":{"type":"group","expression":"Sales {{fk.country.metadata.iso3}} Users"},"grants":[{"dimensionKey":"country","valueExpression":"{{fk.country.metadata.iso2}}"}]}]}}
@@ -1,17 +0,0 @@
1
- apiVersion: dataplane.aifabrix.ai/v1
2
- kind: Protection
3
- metadata:
4
- key: hubspot-country-sales
5
- displayName: HubSpot Country Sales Access
6
- spec:
7
- enabled: true
8
- datasourceKey: hubspot-companies
9
- mode: replaceForSource
10
- rules:
11
- - key: sales-country-users
12
- principal:
13
- type: group
14
- expression: "Sales {{fk.country.metadata.iso3}} Users"
15
- grants:
16
- - dimensionKey: country
17
- valueExpression: "{{fk.country.metadata.iso2}}"
@@ -1 +0,0 @@
1
- {"apiVersion":"dataplane.aifabrix.ai/v1","kind":"Protection","metadata":{"key":"hubspot-country-sales","displayName":"HubSpot Country Sales Access"},"spec":{"enabled":true,"datasourceKey":"hubspot-companies","mode":"replaceForSource","rules":[{"key":"sales-country-users","principal":{"type":"group","expression":"Sales {{fk.country.metadata.iso3}} Users"},"grants":[{"dimensionKey":"country","valueExpression":"{{fk.country.metadata.iso2}}"}]}]}}
@@ -1,17 +0,0 @@
1
- apiVersion: dataplane.aifabrix.ai/v1
2
- kind: Protection
3
- metadata:
4
- key: hubspot-country-sales
5
- displayName: HubSpot Country Sales Access
6
- spec:
7
- enabled: true
8
- datasourceKey: hubspot-companies
9
- mode: replaceForSource
10
- rules:
11
- - key: sales-country-users
12
- principal:
13
- type: group
14
- expression: "Sales {{fk.country.metadata.iso3}} Users"
15
- grants:
16
- - dimensionKey: country
17
- valueExpression: "{{fk.country.metadata.iso2}}"
@@ -1,17 +0,0 @@
1
- apiVersion: dataplane.aifabrix.ai/v1
2
- kind: Protection
3
- metadata:
4
- key: hubspot-country-sales
5
- displayName: HubSpot Country Sales Access
6
- spec:
7
- enabled: true
8
- datasourceKey: hubspot-companies
9
- mode: replaceForSource
10
- rules:
11
- - key: sales-country-users
12
- principal:
13
- type: group
14
- expression: "Sales {{fk.country.metadata.iso3}} Users"
15
- grants:
16
- - dimensionKey: country
17
- valueExpression: "{{fk.country.metadata.iso2}}"
@@ -1,17 +0,0 @@
1
- apiVersion: dataplane.aifabrix.ai/v1
2
- kind: Protection
3
- metadata:
4
- key: hubspot-country-sales
5
- displayName: HubSpot Country Sales Access
6
- spec:
7
- enabled: true
8
- datasourceKey: hubspot-companies
9
- mode: replaceForSource
10
- rules:
11
- - key: sales-country-users
12
- principal:
13
- type: group
14
- expression: "Sales {{fk.country.metadata.iso3}} Users"
15
- grants:
16
- - dimensionKey: country
17
- valueExpression: "{{fk.country.metadata.iso2}}"
@@ -1,17 +0,0 @@
1
- apiVersion: dataplane.aifabrix.ai/v1
2
- kind: Protection
3
- metadata:
4
- key: hubspot-country-sales
5
- displayName: HubSpot Country Sales Access
6
- spec:
7
- enabled: true
8
- datasourceKey: hubspot-companies
9
- mode: replaceForSource
10
- rules:
11
- - key: sales-country-users
12
- principal:
13
- type: group
14
- expression: "Sales {{fk.country.metadata.iso3}} Users"
15
- grants:
16
- - dimensionKey: country
17
- valueExpression: "{{fk.country.metadata.iso2}}"
@@ -1,17 +0,0 @@
1
- apiVersion: dataplane.aifabrix.ai/v1
2
- kind: Protection
3
- metadata:
4
- key: hubspot-country-sales
5
- displayName: HubSpot Country Sales Access
6
- spec:
7
- enabled: true
8
- datasourceKey: hubspot-companies
9
- mode: replaceForSource
10
- rules:
11
- - key: sales-country-users
12
- principal:
13
- type: group
14
- expression: "Sales {{fk.country.metadata.iso3}} Users"
15
- grants:
16
- - dimensionKey: country
17
- valueExpression: "{{fk.country.metadata.iso2}}"
@@ -1,17 +0,0 @@
1
- apiVersion: dataplane.aifabrix.ai/v1
2
- kind: Protection
3
- metadata:
4
- key: hubspot-country-sales
5
- displayName: HubSpot Country Sales Access
6
- spec:
7
- enabled: true
8
- datasourceKey: hubspot-companies
9
- mode: replaceForSource
10
- rules:
11
- - key: sales-country-users
12
- principal:
13
- type: group
14
- expression: "Sales {{fk.country.metadata.iso3}} Users"
15
- grants:
16
- - dimensionKey: country
17
- valueExpression: "{{fk.country.metadata.iso2}}"
@@ -1,17 +0,0 @@
1
- apiVersion: dataplane.aifabrix.ai/v1
2
- kind: Protection
3
- metadata:
4
- key: hubspot-country-sales
5
- displayName: HubSpot Country Sales Access
6
- spec:
7
- enabled: true
8
- datasourceKey: hubspot-companies
9
- mode: replaceForSource
10
- rules:
11
- - key: sales-country-users
12
- principal:
13
- type: group
14
- expression: "Sales {{fk.country.metadata.iso3}} Users"
15
- grants:
16
- - dimensionKey: country
17
- valueExpression: "{{fk.country.metadata.iso2}}"
@@ -1,5 +0,0 @@
1
- lint:fix exit=0
2
- lint exit=0
3
- test exit=0
4
- miso typecheck exit=0
5
- miso build exit=0