@oondemand/oon-core-back 0.6.12 → 0.6.13

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.12",
3
+ "version": "0.6.13",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oondemand/oon-platform.git",
@@ -4,6 +4,7 @@
4
4
  // provisioner, reads platform Secrets, or falls back to provisionRuntime.
5
5
  const crypto = require("node:crypto");
6
6
  const { spawnSync } = require("node:child_process");
7
+ const { performance } = require("node:perf_hooks");
7
8
  const { isDeepStrictEqual } = require("node:util");
8
9
  const { resolveDeliveryTechnicalIdentity } = require("./technicalIdentity");
9
10
  const MEMBERS = new Set(["meus-apps", "oon-workspace", "oon-docs"]);
@@ -27,6 +28,30 @@ function stable(value) {
27
28
  const hash = value => crypto.createHash("sha256").update(JSON.stringify(stable(value))).digest("hex");
28
29
  const copy = value => JSON.parse(JSON.stringify(value));
29
30
 
31
+ function leaseDeadline(value) {
32
+ const deadline = typeof value === "string" ? Date.parse(value) : NaN;
33
+ demand(Number.isFinite(deadline) && new Date(deadline).toISOString() === value,
34
+ "PREPARED_RUNTIME_LEASE_INVALID");
35
+ return deadline;
36
+ }
37
+
38
+ // Local freshness only. An in-flight API request may already have committed;
39
+ // expiry never authorizes takeover, rollback or release of an administrative lock.
40
+ function leaseGuard(expiresAt, { now = Date.now, monotonicNow = () => performance.now() } = {}) {
41
+ const deadline = leaseDeadline(expiresAt), started = now(), monotonicStarted = monotonicNow();
42
+ demand(Number.isFinite(started) && Number.isFinite(monotonicStarted), "PREPARED_RUNTIME_CLOCK_INVALID");
43
+ let lastMonotonic = monotonicStarted;
44
+ return () => {
45
+ const current = now(), monotonic = monotonicNow();
46
+ demand(Number.isFinite(current) && Number.isFinite(monotonic) && monotonic >= lastMonotonic,
47
+ "PREPARED_RUNTIME_CLOCK_INVALID");
48
+ lastMonotonic = monotonic;
49
+ // The original remaining budget cannot grow if the wall clock moves back.
50
+ demand(current < deadline && monotonic - monotonicStarted < deadline - started,
51
+ "PREPARED_RUNTIME_LEASE_EXPIRED");
52
+ };
53
+ }
54
+
30
55
  // authority is trusted executor configuration, separate from the member context.
31
56
  // Callers must fetch/authorize the context through Central Prod; no browser input.
32
57
  function expectedPreparation({ context: c, imageDigest, authority: a }) {
@@ -40,6 +65,7 @@ function expectedPreparation({ context: c, imageDigest, authority: a }) {
40
65
  a.apiUrl === AUTHORITY_API && c.activationApiUrl === a.apiUrl && c.instanceUid !== a.instanceUid &&
41
66
  /^sha256:[a-f0-9]{64}$/.test(imageDigest || "") && (!c.imageDigest || c.imageDigest === imageDigest) &&
42
67
  c.imageRepository === `ghcr.io/oondemand/${c.appCode}`, "PREPARED_RUNTIME_CONTEXT_INVALID");
68
+ leaseDeadline(c.leaseExpiresAt);
43
69
  let id; try { id = resolveDeliveryTechnicalIdentity(c, { requireCanonical: true }); }
44
70
  catch { fail("PREPARED_RUNTIME_IDENTITY_INVALID"); }
45
71
  const frontendHostname = `${id.frontendDnsLabel}.oonapps.online`, backendHostname = `${id.backendDnsLabel}.oonapps.online`;
@@ -48,10 +74,10 @@ function expectedPreparation({ context: c, imageDigest, authority: a }) {
48
74
  c.publicUrl === c.frontendUrl && c.hostname === frontendHostname, "PREPARED_RUNTIME_URL_INVALID");
49
75
  const profile = c.resourceProfile || "small";
50
76
  demand(Object.hasOwn(PROFILES, profile), "PREPARED_RUNTIME_PROFILE_INVALID");
51
- const binding = { schemaVersion: 1, installationId: a.installationId, authorityInstanceUid: a.instanceUid,
77
+ const binding = { schemaVersion: 2, installationId: a.installationId, authorityInstanceUid: a.instanceUid,
52
78
  authorityApiUrl: a.apiUrl, appId: c.appId, appCode: c.appCode, technicalInstanceId: c.technicalInstanceId,
53
79
  instanceUid: id.instanceUid, identityVersion: 1, environment: c.environment, namespace: id.namespace,
54
- releaseId: c.releaseId, leaseId: c.leaseId, imageDigest, imageRepository: c.imageRepository,
80
+ releaseId: c.releaseId, leaseId: c.leaseId, leaseExpiresAt: c.leaseExpiresAt, imageDigest, imageRepository: c.imageRepository,
55
81
  deploymentId: c.deploymentId, deploymentTokenSha256: hash(c.deploymentToken),
56
82
  frontendUrl: c.frontendUrl, backendUrl: c.backendUrl, resourceProfile: profile,
57
83
  capabilitiesSha256: hash({ capabilities: c.capabilities || {}, functionalCapabilities: c.functionalCapabilities || {} }) };
@@ -178,7 +204,14 @@ async function deployPreparedRuntime(options, dependencies = {}) {
178
204
  const { context: c, authority: a } = frozen;
179
205
  const { binding, identity: id, receiptName } = expectedPreparation(frozen);
180
206
  const kube = dependencies.kube || kubernetesClient();
181
- const receiptDoc = await kube.read("configmap", receiptName, id.namespace);
207
+ const checkLease = leaseGuard(c.leaseExpiresAt, dependencies);
208
+ const io = async (operation, ...args) => {
209
+ checkLease();
210
+ const result = await kube[operation](...args);
211
+ checkLease();
212
+ return result;
213
+ };
214
+ const receiptDoc = await io("read", "configmap", receiptName, id.namespace);
182
215
  demand(receiptDoc, "PREPARED_RUNTIME_REQUIRED");
183
216
  const receiptPin = pin(receiptDoc, receiptName, id.namespace, "ConfigMap");
184
217
  let receipt; try { receipt = JSON.parse(receiptDoc.data?.receipt); } catch { fail("PREPARED_RUNTIME_RECEIPT_INVALID"); }
@@ -187,21 +220,21 @@ async function deployPreparedRuntime(options, dependencies = {}) {
187
220
  receipt.runtimeSecret?.name === id.secret && receipt.runtimeServiceAccount?.name === id.serviceAccount &&
188
221
  receipt.pullSecret?.name === "ghcr-oon", "PREPARED_RUNTIME_RECEIPT_INVALID");
189
222
  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);
223
+ samePin(await io("read", "configmap", receiptName, id.namespace), receiptPin, id.namespace, "ConfigMap");
224
+ const secret = await io("read", "secret", id.secret, id.namespace);
192
225
  samePin(secret, receipt.runtimeSecret, id.namespace, "Secret");
193
226
  assertRuntime(secret, c, id, a);
194
- const sa = await kube.read("serviceaccount", id.serviceAccount, id.namespace);
227
+ const sa = await io("read", "serviceaccount", id.serviceAccount, id.namespace);
195
228
  samePin(sa, receipt.runtimeServiceAccount, id.namespace, "ServiceAccount");
196
229
  demand(sa.automountServiceAccountToken === false, "PREPARED_RUNTIME_RESOURCE_INVALID");
197
- const pull = await kube.read("secret", receipt.pullSecret.name, id.namespace);
230
+ const pull = await io("read", "secret", receipt.pullSecret.name, id.namespace);
198
231
  samePin(pull, receipt.pullSecret, id.namespace, "Secret");
199
232
  demand(pull.type === "kubernetes.io/dockerconfigjson", "PREPARED_RUNTIME_RESOURCE_INVALID");
200
233
  };
201
234
  await checkPreparation();
202
235
  const docs = renderPreparedRuntime(frozen, receipt), existing = [];
203
236
  for (const doc of docs) {
204
- const current = await kube.read(doc.kind.toLowerCase(), doc.metadata.name, id.namespace);
237
+ const current = await io("read", doc.kind.toLowerCase(), doc.metadata.name, id.namespace);
205
238
  if (current) {
206
239
  pin(current, doc.metadata.name, id.namespace, doc.kind);
207
240
  const ownership = doc.metadata.annotations;
@@ -214,15 +247,16 @@ async function deployPreparedRuntime(options, dependencies = {}) {
214
247
  }
215
248
  for (let index = 0; index < docs.length; index++) {
216
249
  await checkPreparation();
217
- const doc = docs[index], before = existing[index], current = await kube.read(doc.kind.toLowerCase(), doc.metadata.name, id.namespace);
250
+ const doc = docs[index], before = existing[index], current = await io("read", doc.kind.toLowerCase(), doc.metadata.name, id.namespace);
218
251
  demand(before ? current?.metadata?.uid === before.metadata.uid && current.metadata.resourceVersion === before.metadata.resourceVersion : current === null,
219
252
  "PREPARED_RUNTIME_TARGET_CHANGED");
220
- await kube.write(doc, before);
253
+ await io("write", doc, before);
221
254
  }
222
255
  await checkPreparation();
223
- await kube.rollout(c.appCode, id.namespace);
256
+ await io("rollout", c.appCode, id.namespace);
224
257
  await checkPreparation();
225
- return { infraVersion: "prepared-member-v1", imageRef: `${c.imageRepository}@${frozen.imageDigest}`,
258
+ checkLease();
259
+ return { infraVersion: "prepared-member-v2", imageRef: `${c.imageRepository}@${frozen.imageDigest}`,
226
260
  technicalIdentity: id, preparation: { name: receiptName, uid: receiptPin.uid },
227
261
  proofLimits: ["no-distributed-lock-with-administrative-preparation", "rollout-is-not-functional-acceptance"] };
228
262
  }
@@ -3,7 +3,7 @@
3
3
  API opt-in `deployPreparedRuntime({context, imageDigest, authority})`, exportada por
4
4
  `delivery/runtime.js`, para `meus-apps`, `oon-workspace` e `oon-docs`. O executor atual
5
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
6
+ O workflow `version-pr` prepara a próxima versão patch; integrar em main aciona
7
7
  `release-npm`, que publica os três pacotes após validação. A publicação só deve ser
8
8
  registrada como concluída após o run de release. Isso não liga os consumidores
9
9
  existentes ao novo adapter.
@@ -15,6 +15,10 @@ existentes ao novo adapter.
15
15
  da raiz e `apiUrl`. Nenhum desses valores pode vir de input livre do workflow.
16
16
  O adapter valida consistência; não consulta Mongo, autentica Central nem autoriza
17
17
  uma reserva. O consumidor deve validar lease vigente e comando antes de chamá-lo.
18
+ O contexto autenticado precisa incluir `leaseExpiresAt` no formato ISO UTC canônico
19
+ retornado pela Central (`Date.toISOString()`). Ausência, formato inválido ou prazo
20
+ vencido interrompem antes de acessar Kubernetes. Não completar esse campo no
21
+ consumidor com um prazo local nem renovar o lease para reutilizar um recibo.
18
22
 
19
23
  A preparação administrativa ainda será implementada na Infra. Deve ler novamente
20
24
  singleton/configuração bloqueada, comandos e concorrência; conferir identidade,
@@ -30,6 +34,13 @@ divergência. Nenhuma credencial administrativa deve ser projetada no membro.
30
34
  ## Recibo por publicação
31
35
 
32
36
  `expectedPreparation(options)` retorna `binding`, `identity` e `receiptName`.
37
+ O binding agora usa `schemaVersion: 2` e inclui `leaseExpiresAt` além de `leaseId`.
38
+ O cálculo é puro e valida o formato, não a vigência no instante de uso; o preparador
39
+ também precisa revalidar a autoridade e o prazo antes de seus próprios efeitos.
40
+ Recibos v1 não são aceitos pelo novo adapter. Não há migração nem adoção automática;
41
+ a futura preparação deve usar a mesma versão do SDK do consumidor. O adapter opt-in
42
+ 0.6.12 ainda não está ligado aos consumidores operacionais, portanto este delta não
43
+ altera o provisionador legado nem publica imagens dos membros.
33
44
  O preparador deverá usar a mesma função/versão fixada. `receiptName` é
34
45
  `oon-preparation-` mais os primeiros 32 hex de SHA256 do JSON canônico de `binding`.
35
46
  Objetos são ordenados recursivamente; arrays mantêm ordem. O hash do token também
@@ -41,7 +52,7 @@ JSON com estes campos:
41
52
  | Campo | Valor exigido |
42
53
  | --- | --- |
43
54
  | `phase` | `prepared` |
44
- | `binding` | Exatamente o retornado pelo SDK: autoridade, identidade, ambiente, release, lease, imagem/digest, deployment, URLs, perfil e hashes das capacidades/token |
55
+ | `binding` | Exatamente o retornado pelo SDK v2: autoridade, identidade, ambiente, release, lease e vencimento, imagem/digest, deployment, URLs, perfil e hashes das capacidades/token |
45
56
  | `ingressClass` | `nginx` |
46
57
  | `clusterIssuer` | `letsencrypt-production-http01` |
47
58
  | `runtimeSecret` | `{name, uid, resourceVersion}` do Secret canônico final |
@@ -54,6 +65,20 @@ Não reutilizar recibo em outro lease/digest. UID e resourceVersion dos recursos
54
65
  reconferidos antes de cada escrita e após o rollout. Isso detecta deriva observada,
55
66
  mas não substitui um lock distribuído com o preparador.
56
67
 
68
+ `deployPreparedRuntime` confere o prazo antes e depois de cada leitura, escrita e
69
+ rollout, além de antes do retorno de sucesso (`infraVersion: prepared-member-v2`).
70
+ O orçamento inicial de tempo também é acompanhado por relógio monotônico: um recuo
71
+ do relógio civil durante a chamada não prolonga o prazo. Observação inválida do
72
+ relógio interrompe. O relógio civil inicial do executor precisa estar sincronizado;
73
+ essa checagem local não consulta revogação, comando ou lease na Central.
74
+
75
+ Expiração durante uma requisição não cancela um efeito que já chegou ao servidor.
76
+ O erro `PREPARED_RUNTIME_LEASE_EXPIRED` pode corresponder a escrita parcial; preservar
77
+ e reconciliar os recursos com o comando, sem retry automático, rollback, remoção de
78
+ lock ou takeover. Um novo processo não deve reusar o mesmo lease como autorização
79
+ de concorrência. Exclusão administrativa, fencing/admissão e frescor de callbacks
80
+ permanecem requisitos separados de B5.
81
+
57
82
  O runtime Secret deve conter identidade/URLs, credencial de deployment e chave de
58
83
  criptografia já preparadas. Mongo exige URI de host único `mongodb`/`mongodb+srv`,
59
84
  usuário e banco canônicos e `authSource=admin`; o adapter não prova os privilégios