@oondemand/oon-core-back 0.6.11 → 0.6.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oondemand/oon-core-back",
3
- "version": "0.6.11",
3
+ "version": "0.6.12",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oondemand/oon-platform.git",
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+
3
+ // Explicit prepared-member path. This module never imports the administrative
4
+ // provisioner, reads platform Secrets, or falls back to provisionRuntime.
5
+ const crypto = require("node:crypto");
6
+ const { spawnSync } = require("node:child_process");
7
+ const { isDeepStrictEqual } = require("node:util");
8
+ const { resolveDeliveryTechnicalIdentity } = require("./technicalIdentity");
9
+ const MEMBERS = new Set(["meus-apps", "oon-workspace", "oon-docs"]);
10
+ const AUTHORITY_API = "https://central-ativacao.oonapps.online/api";
11
+ const ENVIRONMENTS = new Set(["desenvolvimento", "homologacao", "producao"]);
12
+ const OBJECT_ID = /^[a-f0-9]{24}$/;
13
+ const UID = /^[23456789abcdefghjkmnopqrstuvwxyz]{16}$/;
14
+ const VERSION = /^[0-9]+$/;
15
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
16
+ const PROFILES = Object.freeze({
17
+ nano: ["50m", "250m", "96Mi", "256Mi"], small: ["100m", "500m", "128Mi", "512Mi"],
18
+ medium: ["250m", "1", "256Mi", "1Gi"], large: ["500m", "2", "512Mi", "2Gi"],
19
+ });
20
+ function fail(code) { const e = new Error(code); e.code = code; throw e; }
21
+ function demand(value, code) { if (!value) fail(code); }
22
+ function stable(value) {
23
+ if (Array.isArray(value)) return value.map(stable);
24
+ if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map(k => [k, stable(value[k])]));
25
+ return value;
26
+ }
27
+ const hash = value => crypto.createHash("sha256").update(JSON.stringify(stable(value))).digest("hex");
28
+ const copy = value => JSON.parse(JSON.stringify(value));
29
+
30
+ // authority is trusted executor configuration, separate from the member context.
31
+ // Callers must fetch/authorize the context through Central Prod; no browser input.
32
+ function expectedPreparation({ context: c, imageDigest, authority: a }) {
33
+ demand(c && a && MEMBERS.has(c.appCode) && c.repository === `oondemand/${c.appCode}` &&
34
+ c.lifecycleMode === "global_singleton" && ENVIRONMENTS.has(c.environment) &&
35
+ OBJECT_ID.test(c.appId || "") && OBJECT_ID.test(c.technicalInstanceId || "") &&
36
+ OBJECT_ID.test(c.deploymentId || "") && c.technicalIdentityVersion === 1 &&
37
+ /^[A-Za-z0-9_-]{1,120}$/.test(c.releaseId || "") && UUID.test(c.leaseId || "") &&
38
+ typeof c.deploymentToken === "string" && c.deploymentToken.length >= 32 && c.deploymentToken.length <= 8192 &&
39
+ /^g[a-z0-9-]{1,39}$/.test(a.installationId || "") && UID.test(a.instanceUid || "") &&
40
+ a.apiUrl === AUTHORITY_API && c.activationApiUrl === a.apiUrl && c.instanceUid !== a.instanceUid &&
41
+ /^sha256:[a-f0-9]{64}$/.test(imageDigest || "") && (!c.imageDigest || c.imageDigest === imageDigest) &&
42
+ c.imageRepository === `ghcr.io/oondemand/${c.appCode}`, "PREPARED_RUNTIME_CONTEXT_INVALID");
43
+ let id; try { id = resolveDeliveryTechnicalIdentity(c, { requireCanonical: true }); }
44
+ catch { fail("PREPARED_RUNTIME_IDENTITY_INVALID"); }
45
+ const frontendHostname = `${id.frontendDnsLabel}.oonapps.online`, backendHostname = `${id.backendDnsLabel}.oonapps.online`;
46
+ demand(c.namespace === id.namespace && c.frontendHostname === frontendHostname && c.backendHostname === backendHostname &&
47
+ c.frontendUrl === `https://${frontendHostname}` && c.backendUrl === `https://${backendHostname}/api` &&
48
+ c.publicUrl === c.frontendUrl && c.hostname === frontendHostname, "PREPARED_RUNTIME_URL_INVALID");
49
+ const profile = c.resourceProfile || "small";
50
+ demand(Object.hasOwn(PROFILES, profile), "PREPARED_RUNTIME_PROFILE_INVALID");
51
+ const binding = { schemaVersion: 1, installationId: a.installationId, authorityInstanceUid: a.instanceUid,
52
+ authorityApiUrl: a.apiUrl, appId: c.appId, appCode: c.appCode, technicalInstanceId: c.technicalInstanceId,
53
+ instanceUid: id.instanceUid, identityVersion: 1, environment: c.environment, namespace: id.namespace,
54
+ releaseId: c.releaseId, leaseId: c.leaseId, imageDigest, imageRepository: c.imageRepository,
55
+ deploymentId: c.deploymentId, deploymentTokenSha256: hash(c.deploymentToken),
56
+ frontendUrl: c.frontendUrl, backendUrl: c.backendUrl, resourceProfile: profile,
57
+ capabilitiesSha256: hash({ capabilities: c.capabilities || {}, functionalCapabilities: c.functionalCapabilities || {} }) };
58
+ return { binding, identity: id, receiptName: `oon-preparation-${hash(binding).slice(0, 32)}` };
59
+ }
60
+
61
+ function pin(doc, name, namespace, kind) {
62
+ demand(doc?.kind === kind && doc.metadata?.name === name && doc.metadata.namespace === namespace &&
63
+ UUID.test(doc.metadata.uid || "") && VERSION.test(doc.metadata.resourceVersion || "") &&
64
+ !doc.metadata.deletionTimestamp, "PREPARED_RUNTIME_RESOURCE_INVALID");
65
+ return { name, uid: doc.metadata.uid, resourceVersion: doc.metadata.resourceVersion };
66
+ }
67
+ function samePin(doc, expected, namespace, kind) {
68
+ demand(isDeepStrictEqual(pin(doc, expected?.name, namespace, kind), expected), "PREPARED_RUNTIME_RESOURCE_CHANGED");
69
+ }
70
+ function decodeRuntime(secret) {
71
+ const values = {};
72
+ for (const [key, value] of Object.entries(secret.data || {})) {
73
+ demand(typeof value === "string" && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value), "PREPARED_RUNTIME_SECRET_INVALID");
74
+ values[key] = Buffer.from(value, "base64").toString("utf8");
75
+ }
76
+ return values;
77
+ }
78
+ function assertRuntime(secret, c, id, a) {
79
+ const env = decodeRuntime(secret);
80
+ const expected = { NODE_ENV: "production", APP_ENVIRONMENT: c.environment, APP_CODE: c.appCode,
81
+ PUBLIC_APP_URL: c.frontendUrl, CENTRAL_ATIVACAO_API_URL: a.apiUrl,
82
+ CENTRAL_ATIVACAO_URL: a.apiUrl.replace(/\/api$/, ""), OON_INSTANCE_UID: id.instanceUid,
83
+ OON_TECHNICAL_IDENTITY_VERSION: "1", OON_DEPLOYMENT_ID: c.deploymentId,
84
+ OON_DEPLOYMENT_TOKEN: c.deploymentToken, OON_APP_CODE: c.appCode };
85
+ demand(Object.entries(expected).every(([k, v]) => env[k] === v) &&
86
+ typeof env.INSTANCE_CREDENTIAL_ENCRYPTION_KEY === "string" && env.INSTANCE_CREDENTIAL_ENCRYPTION_KEY.length >= 32 &&
87
+ !["MONGO_ADMIN_URI", "DIGITALOCEAN_TOKEN", "KUBECONFIG", "KUBECONFIG_B64", "PUBLISHER_KUBECONFIG_B64",
88
+ "GITHUB_APP_PRIVATE_KEY", "GITHUB_CONTROL_PLANE_APP_PRIVATE_KEY"].some(k => Object.hasOwn(env, k)), "PREPARED_RUNTIME_SECRET_INVALID");
89
+ let uri, username; try { uri = new URL(env.MONGO_URI); username = decodeURIComponent(uri.username); }
90
+ catch { fail("PREPARED_RUNTIME_MONGO_INVALID"); }
91
+ demand(["mongodb:", "mongodb+srv:"].includes(uri.protocol) && uri.hostname && uri.password &&
92
+ username === id.mongoUsername && uri.pathname === `/${id.database}` &&
93
+ uri.searchParams.get("authSource") === "admin", "PREPARED_RUNTIME_MONGO_INVALID");
94
+ if (c.functionalCapabilities?.["core.transactional-email"]?.enabled) {
95
+ let ring; try { ring = JSON.parse(env.OON_TRANSACTIONAL_EMAIL_KEK_KEYRING); } catch { fail("PREPARED_RUNTIME_CAPABILITY_MISSING"); }
96
+ demand(typeof ring.currentKeyId === "string" && typeof ring.keys?.[ring.currentKeyId] === "string" &&
97
+ Buffer.from(ring.keys[ring.currentKeyId], "base64").length === 32, "PREPARED_RUNTIME_CAPABILITY_MISSING");
98
+ }
99
+ if (c.capabilities?.pdfRendering) {
100
+ demand(env.OON_CAPABILITY_PDF_RENDERING_URL === c.capabilities.pdfRendering.endpoint &&
101
+ env.OON_CAPABILITY_PDF_RENDERING_VERSION === c.capabilities.pdfRendering.implementationVersion &&
102
+ env.OON_CAPABILITY_PDF_RENDERING_USERNAME && env.OON_CAPABILITY_PDF_RENDERING_PASSWORD, "PREPARED_RUNTIME_CAPABILITY_MISSING");
103
+ }
104
+ }
105
+
106
+ function kubernetesClient(spawn = spawnSync) {
107
+ const command = (args, input, code) => {
108
+ let r; try { r = spawn("kubectl", [...args, "--request-timeout=60s"], {
109
+ input: input === undefined ? undefined : JSON.stringify(input), encoding: "utf8", timeout: 70000,
110
+ maxBuffer: 4 * 1024 * 1024, stdio: ["pipe", "pipe", "pipe"],
111
+ }); } catch { fail(code); }
112
+ if (r.error || r.status !== 0) fail(code); // Never include provider output or args in diagnostics.
113
+ return String(r.stdout || "");
114
+ };
115
+ const json = (raw, code) => { try { return JSON.parse(raw); } catch { fail(code); } };
116
+ return {
117
+ read: (kind, name, namespace) => {
118
+ const raw = command(["-n", namespace, "get", kind, name, "--ignore-not-found", "-o", "json"], undefined, "PREPARED_RUNTIME_READ_FAILED");
119
+ return raw.trim() ? json(raw, "PREPARED_RUNTIME_READ_FAILED") : null;
120
+ },
121
+ write: (doc, existing) => {
122
+ let next = copy(doc);
123
+ if (existing) {
124
+ next.metadata = { ...existing.metadata, ...next.metadata, resourceVersion: existing.metadata.resourceVersion,
125
+ uid: existing.metadata.uid, labels: { ...existing.metadata.labels, ...next.metadata.labels },
126
+ annotations: { ...next.metadata.annotations } };
127
+ delete next.metadata.managedFields;
128
+ if (next.kind === "Service") {
129
+ for (const field of ["clusterIP", "clusterIPs", "ipFamilies", "ipFamilyPolicy", "internalTrafficPolicy"]) {
130
+ if (existing.spec[field] !== undefined) next.spec[field] = existing.spec[field];
131
+ }
132
+ }
133
+ }
134
+ // create refuses a concurrent occupant; replace uses resourceVersion CAS.
135
+ return json(command([existing ? "replace" : "create", "-f", "-", "-o", "json"], next,
136
+ "PREPARED_RUNTIME_WRITE_OUTCOME_UNKNOWN"), "PREPARED_RUNTIME_WRITE_OUTCOME_UNKNOWN");
137
+ },
138
+ rollout: (name, namespace) => command(["-n", namespace, "rollout", "status", `deployment/${name}`, "--timeout=60s"], undefined, "PREPARED_RUNTIME_ROLLOUT_PENDING"),
139
+ };
140
+ }
141
+
142
+ function renderPreparedRuntime({ context: c, imageDigest, authority }, receipt) {
143
+ const { identity: id, binding } = expectedPreparation({ context: c, imageDigest, authority });
144
+ const [cpuRequest, cpuLimit, memoryRequest, memoryLimit] = PROFILES[binding.resourceProfile];
145
+ const labels = { "app.kubernetes.io/name": c.appCode, "oon.runtime/instance-uid": id.instanceUid, "oon.runtime/identity-version": "1" };
146
+ const ownership = { "oon.delivery/installation": authority.installationId, "oon.delivery/authority-uid": authority.instanceUid,
147
+ "oon.delivery/technical-instance": c.technicalInstanceId, "oon.delivery/environment": c.environment };
148
+ const metadata = name => ({ name, namespace: id.namespace, labels, annotations: ownership });
149
+ const ingress = (name, hostname, tlsSecret, route) => ({ apiVersion: "networking.k8s.io/v1", kind: "Ingress",
150
+ metadata: { ...metadata(name), annotations: { ...ownership, "cert-manager.io/cluster-issuer": receipt.clusterIssuer } },
151
+ spec: { ingressClassName: receipt.ingressClass, tls: [{ hosts: [hostname], secretName: tlsSecret }],
152
+ rules: [{ host: hostname, http: { paths: [{ path: route, pathType: "Prefix", backend: { service: { name: c.appCode, port: { name: "http" } } } }] } }] } });
153
+ return [
154
+ { apiVersion: "apps/v1", kind: "Deployment", metadata: metadata(c.appCode), spec: {
155
+ replicas: c.environment === "producao" ? 2 : 1, revisionHistoryLimit: 2,
156
+ strategy: { type: "RollingUpdate", rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } },
157
+ selector: { matchLabels: { "app.kubernetes.io/name": c.appCode } },
158
+ template: { metadata: { labels, annotations: { ...ownership, "oon.delivery/release": c.releaseId,
159
+ "oon.delivery/lease": c.leaseId, "oon.delivery/image-digest": imageDigest } }, spec: {
160
+ serviceAccountName: id.serviceAccount, automountServiceAccountToken: false,
161
+ securityContext: { runAsNonRoot: true, runAsUser: 1000, runAsGroup: 1000, seccompProfile: { type: "RuntimeDefault" } },
162
+ imagePullSecrets: [{ name: receipt.pullSecret.name }], containers: [{ name: "central", image: `${c.imageRepository}@${imageDigest}`,
163
+ imagePullPolicy: "IfNotPresent", ports: [{ name: "http", containerPort: 8080 }], envFrom: [{ secretRef: { name: id.secret } }],
164
+ securityContext: { allowPrivilegeEscalation: false, capabilities: { drop: ["ALL"] } },
165
+ resources: { requests: { cpu: cpuRequest, memory: memoryRequest }, limits: { cpu: cpuLimit, memory: memoryLimit } },
166
+ livenessProbe: { httpGet: { path: "/health/live", port: "http" }, initialDelaySeconds: 20 },
167
+ readinessProbe: { httpGet: { path: "/api/health/ready", port: "http" }, initialDelaySeconds: 10 } }],
168
+ } },
169
+ } },
170
+ { apiVersion: "v1", kind: "Service", metadata: metadata(c.appCode), spec: { type: "ClusterIP", selector: { "app.kubernetes.io/name": c.appCode }, ports: [{ name: "http", port: 80, targetPort: "http", protocol: "TCP" }] } },
171
+ ingress(c.appCode, c.frontendHostname, id.tlsSecret, "/"),
172
+ ingress(`${c.appCode}-api`, c.backendHostname, `${c.appCode}-api-tls`, "/api"),
173
+ ];
174
+ }
175
+
176
+ async function deployPreparedRuntime(options, dependencies = {}) {
177
+ const frozen = copy(options); // No caller mutation while awaiting I/O.
178
+ const { context: c, authority: a } = frozen;
179
+ const { binding, identity: id, receiptName } = expectedPreparation(frozen);
180
+ const kube = dependencies.kube || kubernetesClient();
181
+ const receiptDoc = await kube.read("configmap", receiptName, id.namespace);
182
+ demand(receiptDoc, "PREPARED_RUNTIME_REQUIRED");
183
+ const receiptPin = pin(receiptDoc, receiptName, id.namespace, "ConfigMap");
184
+ let receipt; try { receipt = JSON.parse(receiptDoc.data?.receipt); } catch { fail("PREPARED_RUNTIME_RECEIPT_INVALID"); }
185
+ demand(receiptDoc.immutable === true && receipt?.phase === "prepared" && isDeepStrictEqual(receipt.binding, binding) &&
186
+ receipt.ingressClass === "nginx" && receipt.clusterIssuer === "letsencrypt-production-http01" &&
187
+ receipt.runtimeSecret?.name === id.secret && receipt.runtimeServiceAccount?.name === id.serviceAccount &&
188
+ receipt.pullSecret?.name === "ghcr-oon", "PREPARED_RUNTIME_RECEIPT_INVALID");
189
+ const checkPreparation = async () => {
190
+ samePin(await kube.read("configmap", receiptName, id.namespace), receiptPin, id.namespace, "ConfigMap");
191
+ const secret = await kube.read("secret", id.secret, id.namespace);
192
+ samePin(secret, receipt.runtimeSecret, id.namespace, "Secret");
193
+ assertRuntime(secret, c, id, a);
194
+ const sa = await kube.read("serviceaccount", id.serviceAccount, id.namespace);
195
+ samePin(sa, receipt.runtimeServiceAccount, id.namespace, "ServiceAccount");
196
+ demand(sa.automountServiceAccountToken === false, "PREPARED_RUNTIME_RESOURCE_INVALID");
197
+ const pull = await kube.read("secret", receipt.pullSecret.name, id.namespace);
198
+ samePin(pull, receipt.pullSecret, id.namespace, "Secret");
199
+ demand(pull.type === "kubernetes.io/dockerconfigjson", "PREPARED_RUNTIME_RESOURCE_INVALID");
200
+ };
201
+ await checkPreparation();
202
+ const docs = renderPreparedRuntime(frozen, receipt), existing = [];
203
+ for (const doc of docs) {
204
+ const current = await kube.read(doc.kind.toLowerCase(), doc.metadata.name, id.namespace);
205
+ if (current) {
206
+ pin(current, doc.metadata.name, id.namespace, doc.kind);
207
+ const ownership = doc.metadata.annotations;
208
+ demand(Object.entries(ownership).every(([k,v]) => current.metadata.annotations?.[k] === v), "PREPARED_RUNTIME_TARGET_OCCUPIED");
209
+ // An owned Ingress may still carry routing/auth/snippet directives that
210
+ // change its behavior. Require explicit reconciliation before any write.
211
+ demand(isDeepStrictEqual(current.metadata.annotations, ownership), "PREPARED_RUNTIME_ANNOTATIONS_UNAPPROVED");
212
+ }
213
+ existing.push(current);
214
+ }
215
+ for (let index = 0; index < docs.length; index++) {
216
+ await checkPreparation();
217
+ const doc = docs[index], before = existing[index], current = await kube.read(doc.kind.toLowerCase(), doc.metadata.name, id.namespace);
218
+ demand(before ? current?.metadata?.uid === before.metadata.uid && current.metadata.resourceVersion === before.metadata.resourceVersion : current === null,
219
+ "PREPARED_RUNTIME_TARGET_CHANGED");
220
+ await kube.write(doc, before);
221
+ }
222
+ await checkPreparation();
223
+ await kube.rollout(c.appCode, id.namespace);
224
+ await checkPreparation();
225
+ return { infraVersion: "prepared-member-v1", imageRef: `${c.imageRepository}@${frozen.imageDigest}`,
226
+ technicalIdentity: id, preparation: { name: receiptName, uid: receiptPin.uid },
227
+ proofLimits: ["no-distributed-lock-with-administrative-preparation", "rollout-is-not-functional-acceptance"] };
228
+ }
229
+
230
+ module.exports = { expectedPreparation, renderPreparedRuntime, deployPreparedRuntime, kubernetesClient };
@@ -0,0 +1,107 @@
1
+ # Runtime preparado para membros da plataforma — B5
2
+
3
+ API opt-in `deployPreparedRuntime({context, imageDigest, authority})`, exportada por
4
+ `delivery/runtime.js`, para `meus-apps`, `oon-workspace` e `oon-docs`. O executor atual
5
+ carrega delivery pelo caminho físico do pacote; nenhum novo subpath npm é exportado.
6
+ O workflow `version-pr` prepara a versão 0.6.12; integrar em main aciona
7
+ `release-npm`, que publica os três pacotes após validação. A publicação só deve ser
8
+ registrada como concluída após o run de release. Isso não liga os consumidores
9
+ existentes ao novo adapter.
10
+
11
+ ## Fronteira de confiança
12
+
13
+ `context` deve vir da Central Prod pelo protocolo autenticado de publicação e lease.
14
+ `authority` é configuração confiável separada, com `installationId`, `instanceUid`
15
+ da raiz e `apiUrl`. Nenhum desses valores pode vir de input livre do workflow.
16
+ O adapter valida consistência; não consulta Mongo, autentica Central nem autoriza
17
+ uma reserva. O consumidor deve validar lease vigente e comando antes de chamá-lo.
18
+
19
+ A preparação administrativa ainda será implementada na Infra. Deve ler novamente
20
+ singleton/configuração bloqueada, comandos e concorrência; conferir identidade,
21
+ release Prod e reservas nativas; preparar somente os alvos aprovados. Durante a
22
+ publicação, deve manter a exclusão mútua e a vigência do lease com a Central. Não
23
+ usar `ensureTechnicalInstance` para descoberta, nem adotar recursos por labels.
24
+
25
+ O preparador administra Namespace, quota, LimitRange, políticas de rede/admissão,
26
+ ServiceAccount runtime sem token, usuário Mongo limitado ao banco, runtime Secret,
27
+ pull secret e capacidades. Deve preservar recursos/chaves existentes e parar em
28
+ divergência. Nenhuma credencial administrativa deve ser projetada no membro.
29
+
30
+ ## Recibo por publicação
31
+
32
+ `expectedPreparation(options)` retorna `binding`, `identity` e `receiptName`.
33
+ O preparador deverá usar a mesma função/versão fixada. `receiptName` é
34
+ `oon-preparation-` mais os primeiros 32 hex de SHA256 do JSON canônico de `binding`.
35
+ Objetos são ordenados recursivamente; arrays mantêm ordem. O hash do token também
36
+ usa JSON canônico da string, não os bytes crus. Nunca persistir o token no recibo.
37
+
38
+ No namespace canônico, criar ConfigMap `immutable: true` com `data.receipt` contendo
39
+ JSON com estes campos:
40
+
41
+ | Campo | Valor exigido |
42
+ | --- | --- |
43
+ | `phase` | `prepared` |
44
+ | `binding` | Exatamente o retornado pelo SDK: autoridade, identidade, ambiente, release, lease, imagem/digest, deployment, URLs, perfil e hashes das capacidades/token |
45
+ | `ingressClass` | `nginx` |
46
+ | `clusterIssuer` | `letsencrypt-production-http01` |
47
+ | `runtimeSecret` | `{name, uid, resourceVersion}` do Secret canônico final |
48
+ | `runtimeServiceAccount` | `{name, uid, resourceVersion}` da SA canônica final |
49
+ | `pullSecret` | `{name: "ghcr-oon", uid, resourceVersion}` do Secret local final |
50
+
51
+ O recibo é o último recurso da preparação. Não é uma assinatura: sua autoridade
52
+ depende de o executor não poder criar/alterar/apagar ConfigMaps, Secrets ou SAs.
53
+ Não reutilizar recibo em outro lease/digest. UID e resourceVersion dos recursos são
54
+ reconferidos antes de cada escrita e após o rollout. Isso detecta deriva observada,
55
+ mas não substitui um lock distribuído com o preparador.
56
+
57
+ O runtime Secret deve conter identidade/URLs, credencial de deployment e chave de
58
+ criptografia já preparadas. Mongo exige URI de host único `mongodb`/`mongodb+srv`,
59
+ usuário e banco canônicos e `authSource=admin`; o adapter não prova os privilégios
60
+ desse usuário. E-mail habilitado exige keyring existente; PDF exige endpoint,
61
+ versão e credenciais projetadas. O adapter nunca gera chaves ou busca Secrets
62
+ administrativos. A recusa de nomes administrativos conhecidos é defesa adicional,
63
+ não uma prova da origem ou do escopo de todas as credenciais.
64
+
65
+ ## Permissões necessárias e limites
66
+
67
+ Leitura `get` somente dos nomes do recibo, runtime Secret, pull Secret e runtime SA
68
+ no alvo; leitura/escrita de Deployment, Service e dois Ingresses canônicos; rollout
69
+ consulta/observa o Deployment nomeado. Não requer namespaces, RBAC, nodes, jobs,
70
+ pods/exec, serviceaccounts/token ou acesso a `oon-system`/outros namespaces.
71
+
72
+ Não aplicar uma Role genérica com base apenas nesta lista. `create` de recursos
73
+ principais não pode ser limitado por `resourceNames` no RBAC; workloads também
74
+ podem montar Secrets e escolher SAs. Antes de conceder escrita, a Infra deve
75
+ implementar e provar admissão restritiva (nomes, imagem, hosts, SAs, referências a
76
+ Secrets, pod security) ou um mecanismo administrativo equivalente. RBAC deve ser
77
+ por alvo/ambiente/instalação, com autenticação dedicada e recusas cruzadas. Ver
78
+ [limites oficiais do RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#referring-to-resources).
79
+
80
+ O renderer produz somente Deployment, Service e Ingresses frontend/backend, por
81
+ digest, sem token de SA, sem privilégio, UID/GID 1000 e seccomp RuntimeDefault.
82
+ A compatibilidade das imagens dos três membros com esse usuário ainda precisa
83
+ ser provada. Não oferece manifest arbitrário, initContainers, volumes ou hooks.
84
+
85
+ Um alvo existente exige ownership correspondente e UID/resourceVersion observados;
86
+ annotations adicionais são recusadas antes de qualquer escrita, inclusive regras
87
+ de rewrite, redirect, autenticação ou snippets de Ingress. Exigem reconciliação
88
+ administrativa explícita; não são herdadas silenciosamente na substituição.
89
+ `replace` usa CAS, `create` recusa ocupante concorrente. Essa conferência no cliente
90
+ não é autorização nem prova de ownership canônico no banco. Não adota recursos
91
+ legados. Falha parcial/resultado de escrita incerto interrompe sem retry ou rollback.
92
+ O operador deve reconciliar resultado com o comando antes de tentar novamente.
93
+ Rollout tem limite de 60 segundos; timeout é pendência, não motivo para nova escrita.
94
+ Rollout bem-sucedido não prova HTTP, TLS, callback ou aceite funcional.
95
+
96
+ ## Gate de uso
97
+
98
+ Manter delivery bloqueado até implementar preparador e admissão/bindings, provar
99
+ positivos e recusas por alvo, publicar a versão do SDK e ligar ambos os consumidores
100
+ ao novo adapter. O preparador de kubeconfig atual exige `create namespaces` e o
101
+ script atual ainda chama provisionamento administrativo: ambos precisam de caminho
102
+ específico para membros. Não satisfazer esse guard concedendo namespace create.
103
+ Nenhuma chamada deve cair no provisionador antigo se o recibo estiver ausente.
104
+
105
+ Testes locais: `node --test packages/oonCore-back/test/prepared-runtime.test.js
106
+ packages/oonCore-back/test/delivery-technical-identity.test.js`. Os fakes verificam
107
+ comportamento do adapter; não comprovam permissões Kubernetes ou integração real.
@@ -363,6 +363,8 @@ module.exports = {
363
363
  runtimeManifest,
364
364
  provisionRuntime,
365
365
  deployRuntime,
366
+ // Explicit opt-in for a separately prepared target; never a provisioning fallback.
367
+ deployPreparedRuntime: require("./preparedRuntime").deployPreparedRuntime,
366
368
  waitHttp,
367
369
  activateMember,
368
370
  };