@owox/backend 0.38.0 → 0.39.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 (179) hide show
  1. package/dist/src/common/helpers/read-bounded-body.helper.d.ts +1 -0
  2. package/dist/src/common/helpers/read-bounded-body.helper.js +28 -0
  3. package/dist/src/common/scheduler/system-tasks/system-trigger-type.d.ts +3 -1
  4. package/dist/src/common/scheduler/system-tasks/system-trigger-type.js +2 -0
  5. package/dist/src/config/data-source-options.config.d.ts +4 -3
  6. package/dist/src/config/data-source-options.config.js +10 -3
  7. package/dist/src/config/plugin-collections-body-parser.config.js +3 -0
  8. package/dist/src/data-marts/credentials/credential.types.d.ts +26 -2
  9. package/dist/src/data-marts/credentials/credential.types.js +18 -6
  10. package/dist/src/data-marts/credentials/dto/credential-api.dto.d.ts +5 -1
  11. package/dist/src/data-marts/credentials/dto/credential-api.dto.js +21 -0
  12. package/dist/src/data-marts/credentials/dto/credential.dto.d.ts +2 -1
  13. package/dist/src/data-marts/credentials/entities/credential.entity.d.ts +2 -1
  14. package/dist/src/data-marts/credentials/entities/credential.entity.js +5 -0
  15. package/dist/src/data-marts/credentials/facades/credential-consumer-binding.facade.impl.js +6 -1
  16. package/dist/src/data-marts/credentials/mappers/credential.mapper.js +2 -0
  17. package/dist/src/data-marts/credentials/services/builtin-credential-definitions.js +8 -0
  18. package/dist/src/data-marts/credentials/services/credential-external-definition-registry.service.d.ts +4 -0
  19. package/dist/src/data-marts/credentials/services/credential-external-definition-registry.service.js +36 -17
  20. package/dist/src/data-marts/credentials/services/credential-fetch.service.d.ts +8 -0
  21. package/dist/src/data-marts/credentials/services/credential-fetch.service.js +115 -46
  22. package/dist/src/data-marts/credentials/services/credential-validation-probe.service.d.ts +3 -1
  23. package/dist/src/data-marts/credentials/services/credential-validation-probe.service.js +10 -2
  24. package/dist/src/data-marts/credentials/services/credential.service.d.ts +4 -0
  25. package/dist/src/data-marts/credentials/services/credential.service.js +36 -0
  26. package/dist/src/data-marts/credentials/services/google-service-account-key.d.ts +6 -0
  27. package/dist/src/data-marts/credentials/services/google-service-account-key.js +46 -0
  28. package/dist/src/data-marts/credentials/services/google-service-account-token.service.d.ts +25 -0
  29. package/dist/src/data-marts/credentials/services/google-service-account-token.service.js +177 -0
  30. package/dist/src/data-marts/credentials/use-cases/create-credential.service.d.ts +1 -0
  31. package/dist/src/data-marts/credentials/use-cases/create-credential.service.js +10 -2
  32. package/dist/src/data-marts/credentials/use-cases/update-credential.service.d.ts +1 -0
  33. package/dist/src/data-marts/credentials/use-cases/update-credential.service.js +28 -4
  34. package/dist/src/data-marts/credentials/use-cases/validate-credential.service.js +2 -5
  35. package/dist/src/data-marts/data-marts.module.js +4 -0
  36. package/dist/src/data-marts/data-storage-types/snowflake/adapters/snowflake-api.adapter.js +1 -0
  37. package/dist/src/data-marts/dto/presentation/run-data-mart-request-api.dto.js +4 -2
  38. package/dist/src/data-marts/services/connector/backfill-segment.d.ts +22 -0
  39. package/dist/src/data-marts/services/connector/backfill-segment.js +91 -0
  40. package/dist/src/data-marts/services/connector/connector-execution.service.d.ts +1 -0
  41. package/dist/src/data-marts/services/connector/connector-execution.service.js +3 -0
  42. package/dist/src/data-marts/services/connector/connector-executor.service.d.ts +4 -1
  43. package/dist/src/data-marts/services/connector/connector-executor.service.js +107 -23
  44. package/dist/src/data-marts/services/connector/connector-run.service.d.ts +3 -0
  45. package/dist/src/data-marts/services/connector/connector-run.service.js +98 -4
  46. package/dist/src/data-marts/services/data-mart-run.service.d.ts +1 -0
  47. package/dist/src/data-marts/services/data-mart-run.service.js +9 -0
  48. package/dist/src/data-marts/system-triggers/processors/repair-backfill-chains-processor.d.ts +14 -0
  49. package/dist/src/data-marts/system-triggers/processors/repair-backfill-chains-processor.js +45 -0
  50. package/dist/src/data-marts/use-cases/cancel-data-mart-run.service.d.ts +4 -0
  51. package/dist/src/data-marts/use-cases/cancel-data-mart-run.service.js +46 -1
  52. package/dist/src/data-marts/utils/manual-backfill-range.d.ts +52 -0
  53. package/dist/src/data-marts/utils/manual-backfill-range.js +110 -8
  54. package/dist/src/ee/mcp/instructions/mcp-system-instructions.d.ts +1 -1
  55. package/dist/src/ee/mcp/instructions/mcp-system-instructions.js +1 -0
  56. package/dist/src/idp/decorators/auth-context.decorator.js +1 -0
  57. package/dist/src/idp/guards/idp.guard.d.ts +3 -0
  58. package/dist/src/idp/guards/idp.guard.js +38 -8
  59. package/dist/src/idp/oauth/controllers/oauth-authorization.controller.js +8 -6
  60. package/dist/src/idp/oauth/mcp-client-integration.d.ts +1 -0
  61. package/dist/src/idp/oauth/mcp-client-integration.js +15 -0
  62. package/dist/src/idp/ports/plugin-runtime-authorizer.port.d.ts +6 -0
  63. package/dist/src/idp/types/auth.types.d.ts +1 -0
  64. package/dist/src/migrations/1790900000000-add-plugin-version-backend.d.ts +6 -0
  65. package/dist/src/migrations/1790900000000-add-plugin-version-backend.js +21 -0
  66. package/dist/src/migrations/1790900000001-create-plugin-action-execution.d.ts +6 -0
  67. package/dist/src/migrations/1790900000001-create-plugin-action-execution.js +77 -0
  68. package/dist/src/migrations/1791000000000-add-credential-public-metadata.d.ts +6 -0
  69. package/dist/src/migrations/1791000000000-add-credential-public-metadata.js +23 -0
  70. package/dist/src/plugin-host/collections/dto/domain/plugin-collection.types.d.ts +1 -0
  71. package/dist/src/plugin-host/collections/errors/plugin-collection.errors.d.ts +6 -0
  72. package/dist/src/plugin-host/collections/errors/plugin-collection.errors.js +19 -1
  73. package/dist/src/plugin-host/collections/services/plugin-collection-declaration.service.js +3 -3
  74. package/dist/src/plugin-host/collections/use-cases/put-plugin-collection-document.service.js +7 -0
  75. package/dist/src/plugin-host/constants/plugin-action-limits.d.ts +34 -0
  76. package/dist/src/plugin-host/constants/plugin-action-limits.js +51 -0
  77. package/dist/src/plugin-host/controllers/plugin-action-runtime.controller.d.ts +20 -0
  78. package/dist/src/plugin-host/controllers/plugin-action-runtime.controller.js +123 -0
  79. package/dist/src/plugin-host/controllers/plugin-credential-runtime.controller.js +11 -19
  80. package/dist/src/plugin-host/controllers/plugin-history.controller.d.ts +10 -0
  81. package/dist/src/plugin-host/controllers/plugin-history.controller.js +57 -0
  82. package/dist/src/plugin-host/controllers/plugin-release-check.controller.d.ts +10 -0
  83. package/dist/src/plugin-host/controllers/plugin-release-check.controller.js +58 -0
  84. package/dist/src/plugin-host/dto/domain/check-plugin-release.command.d.ts +22 -0
  85. package/dist/src/plugin-host/dto/domain/check-plugin-release.command.js +17 -0
  86. package/dist/src/plugin-host/dto/domain/list-plugin-history.command.d.ts +10 -0
  87. package/dist/src/plugin-host/dto/domain/list-plugin-history.command.js +18 -0
  88. package/dist/src/plugin-host/dto/domain/plugin-action-execution.command.d.ts +23 -0
  89. package/dist/src/plugin-host/dto/domain/plugin-action-execution.command.js +44 -0
  90. package/dist/src/plugin-host/dto/domain/plugin-action-execution.dto.d.ts +36 -0
  91. package/dist/src/plugin-host/dto/domain/plugin-action-execution.dto.js +3 -0
  92. package/dist/src/plugin-host/dto/domain/plugin-history.dto.d.ts +29 -0
  93. package/dist/src/plugin-host/dto/domain/plugin-history.dto.js +3 -0
  94. package/dist/src/plugin-host/dto/domain/plugin-view.dto.d.ts +1 -0
  95. package/dist/src/plugin-host/dto/presentation/plugin-action-api.dto.d.ts +44 -0
  96. package/dist/src/plugin-host/dto/presentation/plugin-action-api.dto.js +208 -0
  97. package/dist/src/plugin-host/dto/presentation/plugin-gallery-api.dto.d.ts +2 -0
  98. package/dist/src/plugin-host/dto/presentation/plugin-gallery-api.dto.js +7 -0
  99. package/dist/src/plugin-host/dto/presentation/plugin-history-api.dto.d.ts +29 -0
  100. package/dist/src/plugin-host/dto/presentation/plugin-history-api.dto.js +148 -0
  101. package/dist/src/plugin-host/dto/presentation/publication-api.dto.d.ts +17 -0
  102. package/dist/src/plugin-host/dto/presentation/publication-api.dto.js +95 -1
  103. package/dist/src/plugin-host/entities/plugin-action-execution.entity.d.ts +24 -0
  104. package/dist/src/plugin-host/entities/plugin-action-execution.entity.js +136 -0
  105. package/dist/src/plugin-host/entities/plugin-version.entity.d.ts +2 -1
  106. package/dist/src/plugin-host/entities/plugin-version.entity.js +5 -0
  107. package/dist/src/plugin-host/enums/plugin-action-execution-status.enum.d.ts +9 -0
  108. package/dist/src/plugin-host/enums/plugin-action-execution-status.enum.js +22 -0
  109. package/dist/src/plugin-host/enums/release-rejection-code.enum.d.ts +3 -0
  110. package/dist/src/plugin-host/enums/release-rejection-code.enum.js +6 -0
  111. package/dist/src/plugin-host/errors/plugin-action.errors.d.ts +45 -0
  112. package/dist/src/plugin-host/errors/plugin-action.errors.js +70 -0
  113. package/dist/src/plugin-host/errors/plugin-host.errors.d.ts +6 -0
  114. package/dist/src/plugin-host/errors/plugin-host.errors.js +10 -1
  115. package/dist/src/plugin-host/filters/plugin-host-exception.filter.js +2 -2
  116. package/dist/src/plugin-host/mappers/plugin-action-execution.mapper.d.ts +3 -0
  117. package/dist/src/plugin-host/mappers/plugin-action-execution.mapper.js +22 -0
  118. package/dist/src/plugin-host/mappers/plugin-presentation.mapper.d.ts +16 -1
  119. package/dist/src/plugin-host/mappers/plugin-presentation.mapper.js +80 -1
  120. package/dist/src/plugin-host/plugin-host.module.js +32 -0
  121. package/dist/src/plugin-host/services/action-input-validator.service.d.ts +23 -0
  122. package/dist/src/plugin-host/services/action-input-validator.service.js +277 -0
  123. package/dist/src/plugin-host/services/external-credential-definition-sync.service.d.ts +3 -0
  124. package/dist/src/plugin-host/services/external-credential-definition-sync.service.js +47 -15
  125. package/dist/src/plugin-host/services/plugin-action-dispatcher.service.d.ts +13 -0
  126. package/dist/src/plugin-host/services/plugin-action-dispatcher.service.js +140 -0
  127. package/dist/src/plugin-host/services/plugin-action-execution.service.d.ts +69 -0
  128. package/dist/src/plugin-host/services/plugin-action-execution.service.js +239 -0
  129. package/dist/src/plugin-host/services/plugin-action-starter.service.d.ts +22 -0
  130. package/dist/src/plugin-host/services/plugin-action-starter.service.js +81 -0
  131. package/dist/src/plugin-host/services/plugin-audit.service.d.ts +13 -0
  132. package/dist/src/plugin-host/services/plugin-audit.service.js +78 -0
  133. package/dist/src/plugin-host/services/plugin-publication.service.d.ts +2 -0
  134. package/dist/src/plugin-host/services/plugin-publication.service.js +18 -10
  135. package/dist/src/plugin-host/services/plugin-runtime-authorizer.service.d.ts +5 -2
  136. package/dist/src/plugin-host/services/plugin-runtime-authorizer.service.js +31 -3
  137. package/dist/src/plugin-host/services/plugin-version.service.d.ts +2 -1
  138. package/dist/src/plugin-host/services/release-candidate-rules.service.d.ts +51 -0
  139. package/dist/src/plugin-host/services/release-candidate-rules.service.js +198 -0
  140. package/dist/src/plugin-host/services/remote-url-validator.service.d.ts +7 -0
  141. package/dist/src/plugin-host/services/remote-url-validator.service.js +4 -0
  142. package/dist/src/plugin-host/system-triggers/plugin-action-execution-sweep.processor.d.ts +14 -0
  143. package/dist/src/plugin-host/system-triggers/plugin-action-execution-sweep.processor.js +50 -0
  144. package/dist/src/plugin-host/use-cases/cancel-plugin-action-execution.service.d.ts +8 -0
  145. package/dist/src/plugin-host/use-cases/cancel-plugin-action-execution.service.js +39 -0
  146. package/dist/src/plugin-host/use-cases/check-plugin-release.service.d.ts +22 -0
  147. package/dist/src/plugin-host/use-cases/check-plugin-release.service.js +152 -0
  148. package/dist/src/plugin-host/use-cases/get-current-plugin-action-execution.service.d.ts +14 -0
  149. package/dist/src/plugin-host/use-cases/get-current-plugin-action-execution.service.js +85 -0
  150. package/dist/src/plugin-host/use-cases/get-plugin-action-execution.service.d.ts +9 -0
  151. package/dist/src/plugin-host/use-cases/get-plugin-action-execution.service.js +67 -0
  152. package/dist/src/plugin-host/use-cases/get-plugin-installation-entry.service.js +2 -7
  153. package/dist/src/plugin-host/use-cases/install-plugin.service.js +4 -0
  154. package/dist/src/plugin-host/use-cases/list-plugin-history.service.d.ts +25 -0
  155. package/dist/src/plugin-host/use-cases/list-plugin-history.service.js +147 -0
  156. package/dist/src/plugin-host/use-cases/plugin-publisher-access.d.ts +5 -0
  157. package/dist/src/plugin-host/use-cases/plugin-publisher-access.js +28 -0
  158. package/dist/src/plugin-host/use-cases/start-plugin-action-execution.service.d.ts +12 -0
  159. package/dist/src/plugin-host/use-cases/start-plugin-action-execution.service.js +64 -0
  160. package/dist/src/plugin-host/use-cases/sync-plugin-releases.service.d.ts +3 -5
  161. package/dist/src/plugin-host/use-cases/sync-plugin-releases.service.js +15 -59
  162. package/dist/src/plugin-host/use-cases/uninstall-plugin.service.js +3 -1
  163. package/dist/src/plugin-host/use-cases/update-plugin.service.d.ts +0 -1
  164. package/dist/src/plugin-host/use-cases/update-plugin.service.js +3 -25
  165. package/dist/src/plugin-host/utils/action-input-schema.util.d.ts +14 -0
  166. package/dist/src/plugin-host/utils/action-input-schema.util.js +133 -0
  167. package/dist/src/plugin-host/utils/credential-handles.util.d.ts +3 -0
  168. package/dist/src/plugin-host/utils/credential-handles.util.js +13 -0
  169. package/dist/src/plugin-host/utils/execution-token.util.d.ts +5 -0
  170. package/dist/src/plugin-host/utils/execution-token.util.js +13 -0
  171. package/dist/src/plugin-host/utils/json-shape.util.d.ts +1 -0
  172. package/dist/src/plugin-host/utils/json-shape.util.js +21 -0
  173. package/dist/src/plugin-host/utils/plugin-action-outcome.util.d.ts +10 -0
  174. package/dist/src/plugin-host/utils/plugin-action-outcome.util.js +67 -0
  175. package/dist/src/plugin-host/utils/plugin-manifest.util.d.ts +121 -0
  176. package/dist/src/plugin-host/utils/plugin-manifest.util.js +111 -1
  177. package/dist/src/plugin-host/utils/request-abort-signal.util.d.ts +5 -0
  178. package/dist/src/plugin-host/utils/request-abort-signal.util.js +19 -0
  179. package/package.json +10 -8
@@ -5,11 +5,18 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
7
7
  };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var CredentialFetchService_1;
8
12
  Object.defineProperty(exports, "__esModule", { value: true });
9
13
  exports.CredentialFetchService = void 0;
10
14
  const common_1 = require("@nestjs/common");
11
15
  const safe_url_helper_1 = require("../../../common/helpers/safe-url.helper");
12
16
  const guarded_dispatcher_1 = require("../../../common/helpers/guarded-dispatcher");
17
+ const read_bounded_body_helper_1 = require("../../../common/helpers/read-bounded-body.helper");
18
+ const credential_service_1 = require("./credential.service");
19
+ const google_service_account_token_service_1 = require("./google-service-account-token.service");
13
20
  const MAX_REQUEST_BODY_BYTES = 1024 * 1024;
14
21
  const MAX_RESPONSE_BODY_BYTES = 5 * 1024 * 1024;
15
22
  const MAX_REDIRECTS = 5;
@@ -37,35 +44,65 @@ const FORWARDED_RESPONSE_HEADERS = new Set([
37
44
  'retry-after',
38
45
  'x-request-id',
39
46
  ]);
40
- let CredentialFetchService = class CredentialFetchService {
47
+ let CredentialFetchService = CredentialFetchService_1 = class CredentialFetchService {
48
+ googleTokens;
49
+ credentials;
50
+ logger = new common_1.Logger(CredentialFetchService_1.name);
51
+ constructor(googleTokens, credentials) {
52
+ this.googleTokens = googleTokens;
53
+ this.credentials = credentials;
54
+ }
41
55
  async run(binding, input, limits = {}) {
42
56
  const response = await this.runRaw(binding, input, limits);
57
+ const body = await (0, read_bounded_body_helper_1.readBoundedBody)(response, limits.maxResponseBodyBytes ?? MAX_RESPONSE_BODY_BYTES);
58
+ if (body === null) {
59
+ throw new common_1.BadRequestException('Credential response body is too large');
60
+ }
43
61
  return {
44
62
  status: response.status,
45
63
  headers: pickResponseHeaders(response.headers),
46
- bodyBase64: (await readBoundedBody(response, limits.maxResponseBodyBytes ?? MAX_RESPONSE_BODY_BYTES)).toString('base64'),
64
+ bodyBase64: body.toString('base64'),
47
65
  };
48
66
  }
49
67
  async runRaw(binding, input, limits = {}) {
50
- let currentUrl = await this.assertAllowedUrl(input.url, binding.definition.origins);
51
- let currentMethod = input.method.toUpperCase();
52
- if (currentMethod.length === 0 ||
53
- currentMethod.length > 32 ||
54
- !HTTP_TOKEN_PATTERN.test(currentMethod) ||
55
- FORBIDDEN_FETCH_METHODS.has(currentMethod)) {
68
+ const url = await this.assertAllowedUrl(input.url, binding.definition);
69
+ const method = input.method.toUpperCase();
70
+ if (method.length === 0 ||
71
+ method.length > 32 ||
72
+ !HTTP_TOKEN_PATTERN.test(method) ||
73
+ FORBIDDEN_FETCH_METHODS.has(method)) {
56
74
  throw new common_1.BadRequestException('Credential request method is not Fetch-compatible');
57
75
  }
58
- let body = decodeBody(input.bodyBase64);
76
+ const body = decodeBody(input.bodyBase64);
59
77
  if (body.byteLength > MAX_REQUEST_BODY_BYTES) {
60
78
  throw new common_1.BadRequestException('Credential request body is too large');
61
79
  }
62
- if ((currentMethod === 'GET' || currentMethod === 'HEAD') && body.byteLength > 0) {
63
- throw new common_1.BadRequestException(`${currentMethod} requests cannot carry a body`);
80
+ if ((method === 'GET' || method === 'HEAD') && body.byteLength > 0) {
81
+ throw new common_1.BadRequestException(`${method} requests cannot carry a body`);
64
82
  }
65
83
  const timeoutSignal = AbortSignal.timeout(limits.timeoutMs ?? TIMEOUT_MS);
66
84
  const signal = limits.signal ? AbortSignal.any([timeoutSignal, limits.signal]) : timeoutSignal;
85
+ let googleToken;
86
+ const send = async () => {
87
+ const { header, token } = await this.authorization(binding, signal);
88
+ googleToken = token;
89
+ return this.send(binding, input.headers, url, method, body, header, signal, limits);
90
+ };
91
+ const response = await send();
92
+ if (binding.definition.auth.type !== 'google-service-account' || response.status !== 401) {
93
+ return response;
94
+ }
95
+ void response.body?.cancel().catch(() => undefined);
96
+ if (googleToken)
97
+ this.googleTokens.drop(googleTokenRequest(binding), googleToken);
98
+ return send();
99
+ }
100
+ async send(binding, supplied, startUrl, startMethod, startBody, authorization, signal, limits) {
101
+ let currentUrl = startUrl;
102
+ let currentMethod = startMethod;
103
+ let body = startBody;
67
104
  for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {
68
- const headers = this.buildRequestHeaders(binding, input.headers);
105
+ const headers = this.buildRequestHeaders(supplied, authorization);
69
106
  let response;
70
107
  try {
71
108
  response = await fetch(currentUrl, (0, guarded_dispatcher_1.withGuardedDispatcher)({
@@ -88,7 +125,7 @@ let CredentialFetchService = class CredentialFetchService {
88
125
  if (hop === MAX_REDIRECTS) {
89
126
  throw new common_1.ForbiddenException('Credential request exceeded the redirect limit');
90
127
  }
91
- currentUrl = await this.assertAllowedUrl(new URL(location, currentUrl).toString(), binding.definition.origins);
128
+ currentUrl = await this.assertAllowedUrl(new URL(location, currentUrl).toString(), binding.definition);
92
129
  if (response.status === 303 ||
93
130
  ((response.status === 301 || response.status === 302) && currentMethod === 'POST')) {
94
131
  currentMethod = currentMethod === 'HEAD' ? 'HEAD' : 'GET';
@@ -100,7 +137,46 @@ let CredentialFetchService = class CredentialFetchService {
100
137
  }
101
138
  throw new common_1.ForbiddenException('Credential request exceeded the redirect limit');
102
139
  }
103
- async assertAllowedUrl(rawUrl, origins) {
140
+ async authorization(binding, signal) {
141
+ const { auth } = binding.definition;
142
+ if (auth.type === 'header') {
143
+ return { header: [auth.headerName, `${auth.prefix ?? ''}${binding.secret.value}`] };
144
+ }
145
+ if (binding.requirement.scopes.length === 0) {
146
+ throw new common_1.BadRequestException({
147
+ code: 'CREDENTIAL_SCOPE_INVALID',
148
+ message: 'Google issued no access token for the scopes this plugin declares',
149
+ });
150
+ }
151
+ try {
152
+ const token = await this.googleTokens.getAccessToken({
153
+ ...googleTokenRequest(binding),
154
+ signal,
155
+ });
156
+ return { header: ['authorization', `Bearer ${token}`], token };
157
+ }
158
+ catch (error) {
159
+ if (!(error instanceof google_service_account_token_service_1.GoogleTokenError))
160
+ throw error;
161
+ if (error.failure === 'rejected') {
162
+ await this.credentials
163
+ .markRejected(binding.credentialId, binding.secret, error.message)
164
+ .catch(() => this.logger.warn(`Could not record the rejected key of Credential ${binding.credentialId}`));
165
+ throw new common_1.ForbiddenException({
166
+ code: 'CREDENTIAL_REJECTED',
167
+ message: `Google rejected the service account key: ${error.message}`,
168
+ });
169
+ }
170
+ if (error.failure === 'scope') {
171
+ throw new common_1.BadRequestException({
172
+ code: 'CREDENTIAL_SCOPE_INVALID',
173
+ message: 'Google issued no access token for the scopes this plugin declares',
174
+ });
175
+ }
176
+ throw new common_1.BadGatewayException('Credential request could not be completed');
177
+ }
178
+ }
179
+ async assertAllowedUrl(rawUrl, definition) {
104
180
  let url;
105
181
  try {
106
182
  url = await (0, safe_url_helper_1.assertPublicHttpUrl)(rawUrl, { allowedProtocols: ['https:'] });
@@ -108,15 +184,14 @@ let CredentialFetchService = class CredentialFetchService {
108
184
  catch {
109
185
  throw new common_1.ForbiddenException('Credential request URL is not allowed');
110
186
  }
111
- const allowedOrigins = new Set(origins.map(origin => new URL(origin).origin));
112
- if (!allowedOrigins.has(url.origin)) {
187
+ if (!definition.origins.some(origin => originAllows(origin, url, definition.auth.type))) {
113
188
  throw new common_1.ForbiddenException('Credential request origin is not declared by the definition');
114
189
  }
115
190
  return url.toString();
116
191
  }
117
- buildRequestHeaders(binding, supplied) {
192
+ buildRequestHeaders(supplied, [authName, authValue]) {
118
193
  const headers = new Headers();
119
- const secretHeader = binding.definition.auth.headerName.toLowerCase();
194
+ const secretHeader = authName.toLowerCase();
120
195
  const entries = Object.entries(supplied ?? {});
121
196
  if (entries.length > 100) {
122
197
  throw new common_1.BadRequestException('Credential request contains too many headers');
@@ -135,18 +210,36 @@ let CredentialFetchService = class CredentialFetchService {
135
210
  }
136
211
  headers.set(name, value);
137
212
  }
138
- headers.set(binding.definition.auth.headerName, `${binding.definition.auth.prefix ?? ''}${binding.secret.value}`);
213
+ headers.set(authName, authValue);
139
214
  return headers;
140
215
  }
141
216
  };
142
217
  exports.CredentialFetchService = CredentialFetchService;
143
- exports.CredentialFetchService = CredentialFetchService = __decorate([
144
- (0, common_1.Injectable)()
218
+ exports.CredentialFetchService = CredentialFetchService = CredentialFetchService_1 = __decorate([
219
+ (0, common_1.Injectable)(),
220
+ __metadata("design:paramtypes", [google_service_account_token_service_1.GoogleServiceAccountTokenService,
221
+ credential_service_1.CredentialService])
145
222
  ], CredentialFetchService);
223
+ function googleTokenRequest(binding) {
224
+ return {
225
+ credentialId: binding.credentialId,
226
+ secret: binding.secret,
227
+ scopes: binding.requirement.scopes,
228
+ };
229
+ }
230
+ function originAllows(origin, url, authType) {
231
+ const declared = new URL(origin);
232
+ if (authType === 'google-service-account' && declared.hostname.startsWith('*.')) {
233
+ return (url.protocol === 'https:' &&
234
+ url.port === '' &&
235
+ url.hostname.endsWith(declared.hostname.slice(1)));
236
+ }
237
+ return declared.origin === url.origin;
238
+ }
146
239
  function boundResponseBody(response, maxBytes) {
147
240
  const declaredLength = Number(response.headers.get('content-length'));
148
241
  if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
149
- void response.body?.cancel();
242
+ void response.body?.cancel().catch(() => undefined);
150
243
  throw new common_1.BadRequestException('Credential response body is too large');
151
244
  }
152
245
  if (!response.body)
@@ -192,30 +285,6 @@ function decodeBody(encoded) {
192
285
  }
193
286
  return body;
194
287
  }
195
- async function readBoundedBody(response, maxBytes) {
196
- const declaredLength = Number(response.headers.get('content-length'));
197
- if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
198
- await response.body?.cancel();
199
- throw new common_1.BadRequestException('Credential response body is too large');
200
- }
201
- if (!response.body)
202
- return Buffer.alloc(0);
203
- const reader = response.body.getReader();
204
- const chunks = [];
205
- let total = 0;
206
- while (true) {
207
- const { done, value } = await reader.read();
208
- if (done)
209
- break;
210
- total += value.byteLength;
211
- if (total > maxBytes) {
212
- await reader.cancel();
213
- throw new common_1.BadRequestException('Credential response body is too large');
214
- }
215
- chunks.push(value);
216
- }
217
- return Buffer.concat(chunks.map(chunk => Buffer.from(chunk)), total);
218
- }
219
288
  function pickResponseHeaders(headers) {
220
289
  const result = {};
221
290
  headers.forEach((value, name) => {
@@ -1,8 +1,10 @@
1
1
  import type { CredentialSecret, CredentialValidationResult } from '../credential.types';
2
2
  import type { ResolvedCredentialDefinition } from '../dto/credential-api.dto';
3
3
  import { CredentialFetchService } from './credential-fetch.service';
4
+ import { GoogleServiceAccountTokenService } from './google-service-account-token.service';
4
5
  export declare class CredentialValidationProbeService {
5
6
  private readonly credentialFetch;
6
- constructor(credentialFetch: CredentialFetchService);
7
+ private readonly googleTokens;
8
+ constructor(credentialFetch: CredentialFetchService, googleTokens: GoogleServiceAccountTokenService);
7
9
  run(definition: ResolvedCredentialDefinition, secret: CredentialSecret): Promise<CredentialValidationResult>;
8
10
  }
@@ -12,15 +12,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.CredentialValidationProbeService = void 0;
13
13
  const common_1 = require("@nestjs/common");
14
14
  const credential_fetch_service_1 = require("./credential-fetch.service");
15
+ const google_service_account_token_service_1 = require("./google-service-account-token.service");
15
16
  const PROBE_TIMEOUT_MS = 10_000;
16
17
  const PROBE_MAX_RESPONSE_BODY_BYTES = 64 * 1024;
17
18
  const DEFAULT_REJECTED_STATUSES = new Set([400, 401, 403]);
18
19
  let CredentialValidationProbeService = class CredentialValidationProbeService {
19
20
  credentialFetch;
20
- constructor(credentialFetch) {
21
+ googleTokens;
22
+ constructor(credentialFetch, googleTokens) {
21
23
  this.credentialFetch = credentialFetch;
24
+ this.googleTokens = googleTokens;
22
25
  }
23
26
  async run(definition, secret) {
27
+ if (definition.contract.auth.type === 'google-service-account') {
28
+ return this.googleTokens.verify(secret);
29
+ }
24
30
  const validatedAt = new Date();
25
31
  const probe = definition.contract.validation;
26
32
  if (!probe) {
@@ -37,6 +43,7 @@ let CredentialValidationProbeService = class CredentialValidationProbeService {
37
43
  definitionId: definition.contract.id,
38
44
  optional: false,
39
45
  models: [],
46
+ scopes: [],
40
47
  },
41
48
  secret,
42
49
  definition: definition.contract,
@@ -81,7 +88,8 @@ let CredentialValidationProbeService = class CredentialValidationProbeService {
81
88
  exports.CredentialValidationProbeService = CredentialValidationProbeService;
82
89
  exports.CredentialValidationProbeService = CredentialValidationProbeService = __decorate([
83
90
  (0, common_1.Injectable)(),
84
- __metadata("design:paramtypes", [credential_fetch_service_1.CredentialFetchService])
91
+ __metadata("design:paramtypes", [credential_fetch_service_1.CredentialFetchService,
92
+ google_service_account_token_service_1.GoogleServiceAccountTokenService])
85
93
  ], CredentialValidationProbeService);
86
94
  function isSuccessful(status) {
87
95
  return status >= 200 && status < 300;
@@ -1,5 +1,6 @@
1
1
  import { Repository } from 'typeorm';
2
2
  import { CredentialConsumerBinding } from '../entities/credential-consumer-binding.entity';
3
+ import type { CredentialSecret, CredentialValidationResult } from '../credential.types';
3
4
  import { Credential } from '../entities/credential.entity';
4
5
  export declare class CredentialService {
5
6
  private readonly credentials;
@@ -14,6 +15,9 @@ export declare class CredentialService {
14
15
  getLastUsedAt(credentialId: string): Promise<Date | null>;
15
16
  countActiveBindings(credentialId: string): Promise<number>;
16
17
  softDelete(id: string, projectId: string): Promise<void>;
18
+ writeValidation(id: string, checkedSecret: CredentialSecret, result: CredentialValidationResult): Promise<void>;
19
+ markRejected(id: string, rejectedSecret: CredentialSecret, message: string): Promise<void>;
20
+ private updateValidationWhileSecret;
17
21
  markLastUsed(input: {
18
22
  readonly credentialId: string;
19
23
  readonly consumerType: 'plugin-installation';
@@ -81,6 +81,42 @@ let CredentialService = class CredentialService {
81
81
  async softDelete(id, projectId) {
82
82
  await this.credentials.softDelete({ id, projectId });
83
83
  }
84
+ async writeValidation(id, checkedSecret, result) {
85
+ await this.updateValidationWhileSecret(id, checkedSecret, {
86
+ validationState: result.state,
87
+ validationMessage: result.message.slice(0, 2000),
88
+ validatedAt: result.validatedAt,
89
+ });
90
+ }
91
+ async markRejected(id, rejectedSecret, message) {
92
+ const cut = message.slice(0, 2000);
93
+ const stored = await this.credentials.findOne({
94
+ where: { id },
95
+ select: { id: true, validationState: true, validationMessage: true },
96
+ });
97
+ if (!stored || (stored.validationState === 'rejected' && stored.validationMessage === cut)) {
98
+ return;
99
+ }
100
+ await this.updateValidationWhileSecret(id, rejectedSecret, {
101
+ validationState: 'rejected',
102
+ validationMessage: cut,
103
+ validatedAt: new Date(),
104
+ });
105
+ }
106
+ async updateValidationWhileSecret(id, secret, values) {
107
+ const type = this.credentials.manager.connection.options.type;
108
+ const storedValue = type === 'mysql' || type === 'mariadb'
109
+ ? "JSON_UNQUOTE(JSON_EXTRACT(secret, '$.value'))"
110
+ : "JSON_EXTRACT(secret, '$.value')";
111
+ await this.credentials
112
+ .createQueryBuilder()
113
+ .update(credential_entity_1.Credential)
114
+ .set(values)
115
+ .where('id = :id', { id })
116
+ .andWhere('deletedAt IS NULL')
117
+ .andWhere(`${storedValue} = :secretValue`, { secretValue: secret.value })
118
+ .execute();
119
+ }
84
120
  async markLastUsed(input, now = new Date()) {
85
121
  const threshold = new Date(now.getTime() - 5 * 60 * 1000);
86
122
  try {
@@ -0,0 +1,6 @@
1
+ import { type GoogleServiceAccountKey } from '../../../common/schemas/google-service-account-key.schema';
2
+ import type { CredentialPublicMetadata } from '../credential.types';
3
+ export declare function parseGoogleServiceAccountKey(value: string): {
4
+ readonly key: GoogleServiceAccountKey;
5
+ readonly metadata: CredentialPublicMetadata;
6
+ };
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseGoogleServiceAccountKey = parseGoogleServiceAccountKey;
4
+ const common_1 = require("@nestjs/common");
5
+ const node_crypto_1 = require("node:crypto");
6
+ const google_service_account_key_schema_1 = require("../../../common/schemas/google-service-account-key.schema");
7
+ function parseGoogleServiceAccountKey(value) {
8
+ let raw;
9
+ try {
10
+ raw = JSON.parse(value);
11
+ }
12
+ catch {
13
+ throw new common_1.BadRequestException('The key is not valid JSON');
14
+ }
15
+ const parsed = google_service_account_key_schema_1.GoogleServiceAccountKeySchema.safeParse(raw);
16
+ if (!parsed.success) {
17
+ const [issue] = parsed.error.issues;
18
+ const path = issue.path.join('.');
19
+ throw new common_1.BadRequestException(path ? `${path}: ${issue.message}` : issue.message);
20
+ }
21
+ const key = parsed.data;
22
+ if (!key.client_email.toLowerCase().endsWith('.gserviceaccount.com')) {
23
+ throw new common_1.BadRequestException('client_email must be a service account address');
24
+ }
25
+ let rsa = false;
26
+ try {
27
+ rsa = (0, node_crypto_1.createPrivateKey)(key.private_key).asymmetricKeyType === 'rsa';
28
+ }
29
+ catch {
30
+ }
31
+ if (!rsa) {
32
+ throw new common_1.BadRequestException('private_key must be an RSA key');
33
+ }
34
+ if (key.universe_domain !== 'googleapis.com') {
35
+ throw new common_1.BadRequestException('Only the googleapis.com universe is supported');
36
+ }
37
+ return {
38
+ key,
39
+ metadata: {
40
+ clientEmail: key.client_email,
41
+ projectId: key.project_id,
42
+ privateKeyId: key.private_key_id,
43
+ },
44
+ };
45
+ }
46
+ //# sourceMappingURL=google-service-account-key.js.map
@@ -0,0 +1,25 @@
1
+ import { type JWTOptions } from 'google-auth-library';
2
+ import type { CredentialSecret, CredentialValidationResult } from '../credential.types';
3
+ export declare const GOOGLE_TOKEN_TRANSPORTER: unique symbol;
4
+ export type GoogleTokenTransporter = NonNullable<JWTOptions['transporter']>;
5
+ export type GoogleTokenFailure = 'rejected' | 'scope' | 'unavailable';
6
+ export declare class GoogleTokenError extends Error {
7
+ readonly failure: GoogleTokenFailure;
8
+ constructor(failure: GoogleTokenFailure, message: string);
9
+ }
10
+ export interface GoogleTokenRequest {
11
+ readonly credentialId: string;
12
+ readonly secret: CredentialSecret;
13
+ readonly scopes: readonly string[];
14
+ }
15
+ export declare class GoogleServiceAccountTokenService {
16
+ private readonly transporter?;
17
+ private readonly clients;
18
+ constructor(transporter?: GoogleTokenTransporter | undefined);
19
+ verify(secret: CredentialSecret): Promise<CredentialValidationResult>;
20
+ getAccessToken(input: GoogleTokenRequest & {
21
+ readonly signal: AbortSignal;
22
+ }): Promise<string>;
23
+ drop(input: GoogleTokenRequest, staleToken: string): void;
24
+ private createClient;
25
+ }
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.GoogleServiceAccountTokenService = exports.GoogleTokenError = exports.GOOGLE_TOKEN_TRANSPORTER = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const google_auth_library_1 = require("google-auth-library");
18
+ const node_crypto_1 = require("node:crypto");
19
+ const google_service_account_key_1 = require("./google-service-account-key");
20
+ exports.GOOGLE_TOKEN_TRANSPORTER = Symbol('GOOGLE_TOKEN_TRANSPORTER');
21
+ class GoogleTokenError extends Error {
22
+ failure;
23
+ constructor(failure, message) {
24
+ super(message);
25
+ this.failure = failure;
26
+ this.name = 'GoogleTokenError';
27
+ }
28
+ }
29
+ exports.GoogleTokenError = GoogleTokenError;
30
+ const IDLE_MS = 60 * 60 * 1000;
31
+ const VERIFY_TIMEOUT_MS = 10_000;
32
+ const TOKEN_REQUEST_TIMEOUT_MS = 10_000;
33
+ const NEW_KEY_HINT = ' A key created in the last minute may not work yet.';
34
+ const VERIFY_SCOPES = ['https://www.googleapis.com/auth/userinfo.email'];
35
+ const NO_ACCESS_TOKEN_MESSAGE = 'Could not refresh access token.';
36
+ let GoogleServiceAccountTokenService = class GoogleServiceAccountTokenService {
37
+ transporter;
38
+ clients = new Map();
39
+ constructor(transporter) {
40
+ this.transporter = transporter;
41
+ }
42
+ async verify(secret) {
43
+ const validatedAt = new Date();
44
+ let client;
45
+ try {
46
+ client = this.createClient(secret, VERIFY_SCOPES);
47
+ }
48
+ catch (error) {
49
+ return {
50
+ state: 'rejected',
51
+ message: error instanceof Error ? error.message : 'The key is not valid',
52
+ validatedAt,
53
+ };
54
+ }
55
+ try {
56
+ await mint(client, AbortSignal.timeout(VERIFY_TIMEOUT_MS));
57
+ return { state: 'verified', message: 'Google issued a token for this key', validatedAt };
58
+ }
59
+ catch (error) {
60
+ if (error instanceof GoogleTokenError && error.failure === 'rejected') {
61
+ return { state: 'rejected', message: `${error.message}${NEW_KEY_HINT}`, validatedAt };
62
+ }
63
+ return {
64
+ state: 'unknown',
65
+ message: 'Google could not be reached to check the key',
66
+ validatedAt,
67
+ };
68
+ }
69
+ }
70
+ async getAccessToken(input) {
71
+ const now = Date.now();
72
+ for (const [key, entry] of this.clients) {
73
+ if (now - entry.lastUsedAt > IDLE_MS)
74
+ this.clients.delete(key);
75
+ }
76
+ const key = cacheKey(input);
77
+ let entry = this.clients.get(key);
78
+ if (!entry) {
79
+ entry = { client: this.createClient(input.secret, input.scopes), lastUsedAt: now };
80
+ this.clients.set(key, entry);
81
+ }
82
+ entry.lastUsedAt = now;
83
+ try {
84
+ return await mint(entry.client, input.signal);
85
+ }
86
+ catch (error) {
87
+ if (!input.signal.aborted && this.clients.get(key) === entry)
88
+ this.clients.delete(key);
89
+ throw error;
90
+ }
91
+ }
92
+ drop(input, staleToken) {
93
+ const key = cacheKey(input);
94
+ if (this.clients.get(key)?.client.credentials.access_token === staleToken) {
95
+ this.clients.delete(key);
96
+ }
97
+ }
98
+ createClient(secret, scopes) {
99
+ let key;
100
+ try {
101
+ ({ key } = (0, google_service_account_key_1.parseGoogleServiceAccountKey)(secret.value));
102
+ }
103
+ catch (error) {
104
+ throw new GoogleTokenError('rejected', error instanceof Error ? error.message : 'The key is not valid');
105
+ }
106
+ return new google_auth_library_1.JWT({
107
+ email: key.client_email,
108
+ key: key.private_key,
109
+ scopes: [...scopes],
110
+ transporter: this.transporter,
111
+ useAuthRequestParameters: false,
112
+ transporterOptions: { timeout: TOKEN_REQUEST_TIMEOUT_MS },
113
+ });
114
+ }
115
+ };
116
+ exports.GoogleServiceAccountTokenService = GoogleServiceAccountTokenService;
117
+ exports.GoogleServiceAccountTokenService = GoogleServiceAccountTokenService = __decorate([
118
+ (0, common_1.Injectable)(),
119
+ __param(0, (0, common_1.Optional)()),
120
+ __param(0, (0, common_1.Inject)(exports.GOOGLE_TOKEN_TRANSPORTER)),
121
+ __metadata("design:paramtypes", [Object])
122
+ ], GoogleServiceAccountTokenService);
123
+ function cacheKey(input) {
124
+ const secretHash = (0, node_crypto_1.createHash)('sha256').update(input.secret.value).digest('hex');
125
+ return `${input.credentialId}:${secretHash}:${input.scopes.join(' ')}`;
126
+ }
127
+ async function mint(client, signal) {
128
+ let token;
129
+ try {
130
+ ({ token } = await raceSignal(client.getAccessToken(), signal));
131
+ }
132
+ catch (error) {
133
+ throw classify(error);
134
+ }
135
+ if (!token)
136
+ throw new GoogleTokenError('scope', 'Google issued no access token');
137
+ return token;
138
+ }
139
+ function raceSignal(promise, signal) {
140
+ return new Promise((resolve, reject) => {
141
+ const onAbort = () => reject(signal.reason ?? new Error('aborted'));
142
+ if (signal.aborted) {
143
+ onAbort();
144
+ }
145
+ else {
146
+ signal.addEventListener('abort', onAbort, { once: true });
147
+ }
148
+ promise.then(value => {
149
+ signal.removeEventListener('abort', onAbort);
150
+ resolve(value);
151
+ }, (error) => {
152
+ signal.removeEventListener('abort', onAbort);
153
+ reject(error);
154
+ });
155
+ });
156
+ }
157
+ function classify(error) {
158
+ if (error instanceof GoogleTokenError)
159
+ return error;
160
+ const response = error?.response;
161
+ if (!response) {
162
+ return error instanceof Error && error.message === NO_ACCESS_TOKEN_MESSAGE
163
+ ? new GoogleTokenError('scope', 'Google issued no access token')
164
+ : new GoogleTokenError('unavailable', 'Google could not be reached');
165
+ }
166
+ const status = response.status ?? 0;
167
+ const code = response.data?.error;
168
+ if (status >= 400 && status < 500 && status !== 429 && typeof code === 'string') {
169
+ if (code === 'invalid_scope') {
170
+ return new GoogleTokenError('scope', 'Google issued no access token');
171
+ }
172
+ const description = response.data?.error_description;
173
+ return new GoogleTokenError('rejected', typeof description === 'string' && description ? `${code}: ${description}` : code);
174
+ }
175
+ return new GoogleTokenError('unavailable', 'Google could not be reached');
176
+ }
177
+ //# sourceMappingURL=google-service-account-token.service.js.map
@@ -22,6 +22,7 @@ export declare class CreateCredentialService {
22
22
  private readonly credentialContexts;
23
23
  constructor(credentials: CredentialService, definitions: CredentialDefinitionService, view: CredentialViewService, validationProbe: CredentialValidationProbeService, contexts: ContextService, idpProjections: IdpProjectionsFacade, owners: Repository<CredentialOwner>, credentialContexts: Repository<CredentialContext>);
24
24
  run(context: AuthorizationContext, input: CreateCredentialApiDto): Promise<CredentialDto>;
25
+ private persist;
25
26
  }
26
27
  export declare function validateAiMappings(mappings: CredentialAiModelMappings | null | undefined, ai: CredentialDefinitionContract['ai'] | undefined): void;
27
28
  export declare function normalizeAiConfiguration(mappings: CredentialAiModelMappings | null | undefined, modes: CredentialAiModelMappingModes | null | undefined, ai: CredentialDefinitionContract['ai'] | undefined, current?: {