Semântica de execução, restrições e segurança

Esta página resume como um Script se comporta em tempo de execução (ordem, transações, erros, bloqueio), a quais restrições estáticas ele está sujeito no momento de salvar e o seu modelo de segurança. Para a sintaxe, consulte o catálogo de Statements e as expressões de valor; para combinações práticas, consulte o cookbook.

Ordem de execução

  • Os statements são executados sequencialmente de cima para baixo. Ao alcançar um Return, a execução termina nesse ponto.
  • A execução acontece inline, no caminho que processa a requisição da chamada. A resposta da chamada é o próprio resultado da execução (para o formato da resposta, consulte Requisição e resposta na visão geral do Script), e o tempo concedido a uma execução é abordado abaixo em Orçamento de tempo.

Semântica de execução

Guard (pré-condições)

Não existe um statement guard dedicado. Você o expressa com If e then:[Return]. Quando a condição é violada, retorna um resultado e não executa os statements seguintes (um Script sem guard também é perfeitamente possível).

{ "type": "If", "condition": { "<": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] },
  "then": [ { "type": "Return", "value": { "ok": false, "reason": "insufficient credit" }, "statusCode": 402 } ] }

Sem transações e compensação best-effort

Um Script não é uma transação. Em caso de falha, o engine tenta compensar (compensation) o trabalho realizado até então e retorna a causa do erro, mas com as seguintes limitações (assumidas por decisão de projeto).

  • Desfazer uma exclusão cria um novo sys.id, então as referências que apontavam para ele quebram.
  • Efeitos externos (Http) são irreversíveis (uma chamada que já foi feita, e a cobrança dela, não podem ser desfeitas).
  • A compensação pode simplesmente não ser executada, deixando um estado não compensado.

Se você precisa de atomicidade real, escreva a compensação no Script por conta própria ou coloque as operações irreversíveis (como chamadas externas) por último. A ordenação mais perigosa é aquela que "encadeia até o fim, mas não consegue reverter, e ainda assim parece segura".

Bloqueio otimista

Você reduz a contenção de update/patch com o version em ResourceUpdate e ResourcePatch. Se fornecer version (uma expressão de valor, Int), a atualização acontece somente quando coincide com o sys.version atual do alvo; uma divergência aborta com um erro de conflito de versão (que você pode tratar localmente com Try/catch). Se omitir, vale last-write-wins, sem verificação. Normalmente você lê primeiro com ResourceRead ou ResourceFind e passa esse sys.version (consulte CAS de bloqueio otimista no cookbook).

Gravações vão para origin

As gravações sempre chegam ao origin (draft), e a exposição na delivery (CDA/ACDA) é controlada por publish (o publish em ResourceCreate/ResourceUpdate/ResourcePatch, ou ResourcePublish/ResourceUnpublish).

O que conta como falha

  • Uma falha real é um erro em tempo de execução de statement: um status final de Http de 400 ou superior (4xx·5xx; não é uma falha quando ignoreStatusCode: true) ou timeout, um corpo de resposta que excede 10MiB, ou uma operação de recurso malsucedida (alvo inexistente, conflito de versão, operação não suportada, etc.). Nessas falhas o engine aborta e compensa, e você pode tratá-las localmente com Try/catch/finally.
  • Um Return não é um erro, mas uma saída antecipada normal. Não é alvo de catch (não existe o conceito de throw pelo usuário).
  • Dentro de um catch, você referencia { message } via /error. Em qual statement a falha ocorreu não é incluído.

No servidor, apenas a contagem

A contagem é feita pelo ResourceCount no servidor. Como ele não traz os itens, não fica sujeito ao limite máximo de itens processados.

Para sum e group-by não há operações de servidor dedicadas. Essas agregações você precisa calcular por conta própria, percorrendo com ResourceForEach e usando SetVar e JsonLogic, portanto fica limitado pelo limite máximo de itens processados (inadequado para agregar milhões de registros). Se você precisa apenas da contagem, não percorra: use ResourceCount.

Sem espera ou atraso

Um Script não tem statement Delay. Um Script é executado uma vez e pronto, e não espera nem faz polling internamente até um job externo terminar.

Restrições estáticas (validadas ao salvar)

Os itens a seguir são verificados no momento em que um Script é salvo (na criação/atualização). Se algum for violado, o salvamento é rejeitado (a falha ocorre no momento da autoria, não em tempo de execução). A seção Erros mostra qual violação é recusada com qual código.

RestriçãoValor
Máximo de chamadas externas por definição (Http·EmailSend)Por plano (veja Planos)
Máximo de itens processados por ResourceForEach (sem um limit declarado, percorre até este valor; falha se atingi-lo ainda havendo correspondências)10.000
Máximo de SetVar por definição (incluindo aninhados)10
Máximo de Cache por definição (incluindo aninhados, somados independentemente da operação)5. Acima disso, salvamento recusado
Cache dentro do bloco de um Loop·ResourceForEachSalvamento recusado
Cache.keySomente literal, no máximo 128 caracteres. Se for uma expressão de valor, salvamento recusado
Cache.ttlEntre 1 e 30 segundos; se omitido, 5. Fora disso, salvamento recusado
Máximo total de statements por definição (incluindo aninhados)Por plano (veja Planos)
Limite de Http.retry2
Comprimento de Regex.pattern128 caracteres
Statement que altera um ServiceUserSalvamento recusado. Somente os três statements de leitura aceitam este recurso
Se anonymousCallEnabled for true, o createdBy: ":self" de whereSalvamento recusado

Os limites fixos da tabela acima são valores definidos pela plataforma, portanto são os mesmos independentemente do plano. Já o número total de statements e o número de chamadas externas por definição são limites por plano. Esses dois não são erros de validação, mas limites de plano; se ultrapassados, o salvamento e a edição são recusados por estouro do limite do plano (a mesma definição é permitida em um plano superior) e são liberados com um upgrade. Os valores por plano estão em Planos.

A ingestão de arquivo de Media, ao contrário de chamadas externas como Http·EmailSend, não entra no limite de chamadas externas por definição.

O ResourceForEach é um statement composto que possui filhos, portanto ele próprio não conta no número de chamadas externas. Os statements de chamada externa dentro de onEach (Http·EmailSend) é que são contados (estaticamente contam como 1, mas são de fato executados a cada item durante o percurso). Em onEach você pode incluir chamadas externas ou ingestão de arquivo de Media, e o mesmo vale para o body de Loop. Quantas voltas a iteração dá de fato não entra nessa contagem: em vez disso, ela entra como multiplicação no Orçamento de tempo abaixo.

Limites de comprimento de valor (tempo de execução)

Os statements de assinatura e de tratamento de texto, e também o Cache, têm um limite máximo para o tamanho do valor que manipulam. Não é o comprimento da expressão, mas o comprimento do valor resolvido dessa expressão (os dezesseis caracteres de { /rawPayload } apontam para dezenas de KB), e é por isso que a verificação ocorre durante a execução, e não no momento do salvamento.

AlvoLimite máximoSe exceder
O value de Signature65.536 caracteresEsse statement falha (status 422)
O value de Hash128 caracteresEsse statement falha (status 422)
O value de Regex10.240 caracteres (10KiB)Esse statement falha (status 400)
O value de Cache10.240 bytes (10KiB)Esse statement falha (status 422)
  • Os quatro são iguais a qualquer outra falha em tempo de execução, portanto podem ser tratados localmente com Try/catch.
  • O limite máximo de Signature foi ajustado ao tamanho dos corpos que os provedores realmente enviam (um evento de pagamento tem alguns KB; um webhook de pedido chega a dezenas de KB). O de Hash é bem mais estreito, por ser a posição em que se concatenam alguns campos.
  • Os 128 caracteres de Regex.pattern são a verificação de salvamento que está nas Restrições estáticas acima. Esse comprimento não é um dispositivo para impedir explosões de custo ((a+)+$ já é perigoso com seis caracteres). O que impede a explosão é a regra que obriga o padrão a ser apenas literal, junto com o orçamento de tempo abaixo; o que o comprimento promete é somente um tamanho que uma pessoa consiga ler e revisar.

Orçamento de tempo (tempo de execução)

O tempo concedido a uma execução é definido por uma única fórmula: min(30 segundos + soma dos tempos declarados pelos statements, 180 segundos).

  • O orçamento é calculado a partir daquele Script. Ao orçamento base soma-se apenas o tempo que a definição declarou. O único tempo declarado é o timeoutMs de Http e de EmailSend. O Http gasta o seu timeoutMs de novo a cada nova tentativa, portanto conta como timeoutMs × (1 + retry); o EmailSend não tenta de novo, portanto conta uma vez. Se você não escrever timeoutMs, conta-se o valor padrão (30 segundos em Http, 10 segundos em EmailSend).
  • O trabalho que não tem tempo declarado sai do orçamento base de 30 segundos. Entram aqui a leitura e a gravação de recursos, a ingestão de arquivo de Media e o que uma iteração faz por dentro. É por isso que o orçamento base não é um valor de formalidade, mas uma parcela real.
  • A forma de somar segue a estrutura dos statements. Os statements dispostos em sequência somam-se; o If toma o maior entre os dois ramos, e o Parallel, o maior entre os branches. O Loop multiplica o body pela quantidade de iterações (maxIterations; 10.000 se não houver declaração) e o ResourceForEach multiplica o onEach pela quantidade de itens processados (limit; 10.000 se não houver declaração).
  • Uma iteração sem chamada externa tem tempo declarado 0. Por isso o orçamento base de 30 segundos passa a ser o limite efetivo, e é também nesse ponto que um Script que contém uma iteração de fato é barrado.
  • O limite de 180 segundos não impede o salvamento: ele corta. Mesmo que o cálculo ultrapasse o limite, aquele Script é salvo e executado, e ao alcançar os 180 segundos ele é interrompido ali.

Limites de quantidade por plano

Um Script tem a quantidade por Organization limitada por plano.

PlanQuantidade de Script
Free10
Basic30
Pro100
EnterpriseIlimitado

À parte disso, o número de statements e o número de chamadas externas (Http·EmailSend) que uma definição de Script pode conter também são limitados por plano. Ao salvar ou editar uma definição, se ela ultrapassar o limite daquele plano, é recusada; para os valores concretos, consulte Planos.

Quando o limite é atingido, a criação de um novo Script é rejeitada.

Modelo de segurança

Cabeçalhos secret

Um item em Http.headers com secret:true é exclusivo do CMA (administrador): não é exposto ao usuário final (ServiceUser) e é descriptografado apenas imediatamente antes do envio. Coloque aqui segredos como uma chave de API de LLM (mesmo quando empacotado em um App Bundle, o valor secret é mascarado e nunca sai do Space de origem).

O secret de Signature não recebe esse tratamento dentro do Space. Ele não é criptografado: fica armazenado exatamente como escrito na definição, portanto seu valor é visível para os papéis que podem ler aquele Script. Um membro (ServiceUser) não consegue ler a definição de um Script (a consulta e a autoria são exclusivas da CMA, e a ACMA não tem a API de Script). Em um Script que guarda uma chave de verificação, é mais seguro manter estreito o conjunto de papéis capazes de lê-lo.

Sair do Space é diferente. Quando aquele Script é empacotado em um App Bundle, o secret de Signature é mascarado e não sai do Space de origem. Em Http.headers, o que fica oculto são os itens com a flag secret e o cabeçalho Authorization; no Signature, por outro lado, o secret fica oculto sem condição alguma, porque o próprio campo é a chave de assinatura. Um Signature aninhado dentro de If·Loop·Try também fica oculto.

Identidade de execução e autorização

  • Identidade de execução: durante a execução, toda operação de recurso é realizada sob a identidade do usuário que chamou /execute. O createdBy/updatedBy de qualquer recurso criado ou atualizado é o chamador, e um escopo createdBy: ":self" também é resolvido em relação ao chamador. A exceção é a chamada anônima. Uma execução que entra por /execute/anonymous não tem chamador, portanto ambos são resolvidos em relação ao autor (veja Chamada anônima).
  • Há dois limites de autorização e, em tempo de execução, o engine não reverifica as permissões de recurso a cada statement.
    1. No momento da autoria (salvar): ao salvar um Script, verifica-se se o autor realmente possui as permissões de recurso e de ação que seus statements usam. Se faltar uma que seja, o salvamento é rejeitado. Ou seja, um Script que contém uma operação não autorizada nunca chega a ser salvo. Todo statement que seleciona um recurso passa por essa verificação, seja ele um leaf ou um ResourceForEach que possui um bloco. Uma definição já salva também é verificada de novo ao ser editada, portanto, depois que uma permissão é revogada, não é possível corrigir e salvar aquela definição.
      • O diretório de membros (ServiceUser) é verificado pelo eixo de configurações, não pelo mapa de permissões. Para usar resource: "ServiceUser" nos três statements de leitura, o settings do SpaceRole do autor precisa ter SETTING_SERVICE_LOGIN (ou SETTING_ALL) (veja settings do SpaceRole). Isso porque o diretório de membros é, em todos os outros caminhos também, um recurso governado pelas configurações do Space.
      • Um statement que altera um membro não é salvo com nenhum papel. Como no Script não existe caminho algum para criar, editar ou excluir um membro, a recusa não é por falta de permissão (403), mas por statement escrito de forma inválida (400). Ou seja, não é uma lacuna que se feche acrescentando permissões.
    2. No momento da chamada (/execute): verifica-se apenas a permissão Execute do chamador sobre o Script. Sem ela, o resultado é 403. Uma vez aprovado, as permissões de recurso por statement não são verificadas novamente em tempo de execução; a execução prossegue. Funciona como a permissão de execução de função na programação. Se você tem permissão para executar a função, a permissão de cada operação individual dentro dela não é perguntada de novo. No caminho de chamada anônima, essa verificação não existe. É porque não há chamador a verificar e, por isso, abrir aquele caminho equivale a publicar um Script sem autenticação.
  • Bloqueio de chamada direta (directCallEnabled): se o directCallEnabled de um Script for false, a própria chamada direta a /execute é rejeitada. Este gate é aplicado depois que a verificação da permissão Execute é aprovada, então ele bloqueia mesmo quem tem a permissão Execute. Um chamador sem essa permissão recebe 403 antes de chegar a este gate. Como esse gate existe apenas naquele endpoint, a ação de vínculo (script) de um Webhook e um Scheduler continuam executando o Script normalmente. O padrão é true (chamada direta permitida).
  • Chamada anônima (anonymousCallEnabled): o padrão é false. Se você deixá-lo como true, apenas aquele Script passa a ser executável também por um caminho dedicado e sem autenticação (/execute/anonymous), e nesse caso a identidade de execução é o autor, não o chamador. Dos dois limites acima, a verificação no momento da chamada (permissão Execute) não existe nesse caminho, portanto a autenticação efetiva é feita pelo próprio Script (verificando a assinatura da requisição recebida). As condições para ativá-lo e as regras de salvamento são abordadas em Chamada anônima.
  • Escopo de propriedade: createdBy: ":self" em um filtro where significa "apenas o que o chamador atual criou" (por exemplo, consultar apenas a própria carteira). Esse filtro não pode ser usado em um Script que permite chamada anônima. Como não há chamador e ele é resolvido para o autor, o sentido original de escopo de propriedade não se sustenta.
  • Permissões delegadas (atenção ao autor): combinando os dois limites acima, executar um Script equivale a agir com as permissões do autor delegadas a ele. O chamador precisa apenas de Execute, e os statements dentro do Script são executados exatamente dentro do escopo para o qual o autor foi autorizado no momento de salvar. Como consequência, uma operação de recurso que o chamador não conseguiria realizar sozinho ainda pode acontecer por meio do Script. Como as permissões concedidas ao autor são o alcance efetivo desse Script, defina com cuidado quais ações você coloca em um Script.

Checklist de resumo

Antes de salvar, verifique o seguinte.

  • Se você ativou a chamada anônima (anonymousCallEnabled), não há createdBy: ":self" em where e você colocou no início o statement que verifica a requisição recebida (Signature etc.).
  • O número de chamadas externas (Http·EmailSend) e o total de statements estão dentro do limite do plano, SetVar é 10 ou menos e Cache é 5 ou menos.
  • Se você usou Cache, escreveu a key como literal e não a colocou dentro de um Loop ou de um ResourceForEach.
  • Se você percorrer um conjunto grande com ResourceForEach, declarou um limit ou verificou que o tamanho é conclusível.
  • Se você incluiu uma iteração (Loop·ResourceForEach), confirmou que ela entra como multiplicação no Orçamento de tempo (sem chamadas externas, o limite é o orçamento base de 30 segundos).
  • Os valores secret foram colocados apenas via secret:true em Http.headers (como Signature.secret não é armazenado criptografado, você conferiu quais papéis podem ler aquele Script).
  • A mensagem usada na verificação de assinatura foi tomada de { /rawPayload }, e não de /payload.
  • Se houver um statement que lê um membro (ServiceUser), o autor tem SETTING_SERVICE_LOGIN e você não incluiu nenhum statement que altere esse recurso.
  • As operações irreversíveis (chamadas externas) foram colocadas o mais tarde possível.
  • Se você se preocupa com a contenção de update/patch, use o version em ResourceUpdate ou ResourcePatch.
  • Para retornar um resultado, você especificou Return.value.

Erros

São os códigos que aparecem quando o salvamento é recusado porque a forma da definição viola uma restrição estática. Os códigos que violam as regras das expressões de valor estão em Erros em Expressões de valor, e os que aparecem na chamada e na exclusão, em Erros em Recurso e endpoints do Script. Para os códigos comuns a todos os recursos, consulte Erros comuns.

CódigoCondição
WGL400066A definição contém mais de 5 statements Cache.
WGL400068A definição coloca um statement Cache dentro do bloco de um Loop ou de um ResourceForEach.
WGL400067A key de um statement Cache traz uma referência { /pointer } em vez de um literal.
WGL400065O ttl de um statement Cache está fora da faixa permitida.
WGL400063Um statement Cache traz um campo que não corresponde ao seu action (ttl em um Get, defaultValue em um Set).
WGL400060Um statement de escrita (ResourceCreate·ResourceUpdate·ResourcePatch·ResourceDelete e os statements de publicação e arquivamento) traz "ServiceUser" em resource.
WGL400061Em um Script que permite chamada anônima (anonymousCallEnabled), um statement de leitura traz createdBy: ":self" em where.
WGL400023A definição contém mais de 10 statements SetVar.
WGL400026O retry de um statement Http ultrapassa o limite máximo de 2.
WGL400036O ResourceForEach ultrapassa o limite máximo de itens que ele pode processar.
WGL429005O número total de statements que a definição contém ultrapassa o limite do plano.
WGL429006O número de chamadas externas (Http·EmailSend) que a definição contém ultrapassa o limite do plano.
WGL403015O autor não possui as permissões de recurso e de ação que os statements da definição usam. Mesmo que ele tenha a permissão, o salvamento é recusado se essa autorização vier com um filtro contentType, createdBy ou tag. A autorização precisa ser incondicional. A única exceção é o Create de Content, para o qual também vale uma autorização com escopo de contentType, que é comparada com o contentType escrito no statement (o Create de Media não tem essa exceção). Um statement de leitura que traz "ServiceUser" em resource quando o autor não tem SETTING_SERVICE_LOGIN também recebe este código.