Releases: DavideCarvalho/adonis-agora-authkit
Release list
@adonis-agora/authkit-server@0.71.0
Minor Changes
-
#226
35195aeThanks @DavideCarvalho! - feat(authkit-server)!: a ponte de compatibilidade de e-mail sai; entra a migraçãoauthkit:users:normalize-emailsPASSO OBRIGATÓRIO DE UPGRADE. Quem tem contas criadas antes da 0.69 precisa rodar
node ace authkit:users:normalize-emails --applyao subir esta versão. Sem isso, toda
conta gravada com o endereço mutilado (ou com maiúsculas) fica inalcançável — e, por o
login ser à prova de enumeração, sem nenhuma mensagem de erro para o dono dela.O que saiu. A ponte
login.legacyEmailFallback(0.70), que no login tentava o endereço
exatamente como digitado e a normalização legada do validator.js quando a forma normalizada
não achava conta. Saíram com ela: a opção de config,legacyNormalizeEmailIdentifier,
resolveEmailIdentifier, os tiposEmailIdentifierLookupeResolvedEmailIdentifier, a
segunda chave de sessão do login (authkit_login_email_lookup) e ooptions.legacyFallback
deimportUsers. Umlogin: { legacyEmailFallback: ... }no config vira uma chave
desconhecida (ignorada) — apague-a. Fica uma normalização,normalizeEmailIdentifier
(trim+toLowerCase), e a busca direta por ela.O que entrou.
authkit:users:normalize-emails: varre as contas, calcula a forma
normalizada e, por padrão, só relata — quantas mudariam, quais, e as colisões (duas
contas que colapsam no mesmo endereço). Com--applygrava, recusando-se a tocar em
qualquer conta envolvida numa colisão e listando-as para decisão humana: a migração nunca
funde contas nem escolhe vencedor. Sai com código != 0 quando sobram colisões. A escrita
usa a capacidade novaAccountEmailRewriteCapability(rewriteAccountEmail, presente no
store Lucid; probe porsupportsAccountEmailRewrite) — que não manda e-mail, não pede
confirmação e não mexe ememail_verified_at, porque a caixa postal é a mesma, só a
grafia gravada muda. Store sem a capacidade: o relatório funciona e o--applyrecusa.Por quê. A ponte eram ~120 linhas de compatibilidade, uma opção de config, uma segunda
chave de sessão e uma chamada em cada ponto que resolve e-mail (login, cadastro, social,
admin, import) para um problema que se resolve UMA vez, no banco. E ela cobrava caro no pior
lugar: buscas extras a cada e-mail desconhecido no login, que é o caminho de ataque.Anti-enumeração intacta — na verdade, mais apertada: o passo de identificador agora não
consulta o account store nenhuma vez (a ponte consultava), então não há nem diferença de
tempo entre "achei" e "não achei". A tela continua mostrando o que a pessoa digitou e o
passo 1 continua respondendo o mesmo redirect incondicional.
@adonis-agora/authkit-server@0.70.0
Minor Changes
-
#223
6adae6dThanks @DavideCarvalho! - feat(authkit-server): escritas de organização e de segundo fator em JSON (/account/api/*)Um host que desenha as PRÓPRIAS telas de conta não tinha como criar org, convidar, trocar
papel, remover membro, revogar/aceitar convite nem gerenciar TOTP/passkeys sem mandar o
usuário ao console/account/*: essas operações só existiam como POST de formulário, com
redirect. As leituras já eram JSON; faltava o outro lado.- Orgs:
POST /account/api/orgs,/orgs/deactivate,/orgs/invitations/:token/accept,
/orgs/:id/{activate,leave,invitations},DELETE /orgs/:id/invitations/:invId,
PATCH|DELETE /orgs/:id/members/:accountId. Mesmos guards do formulário (escopo por
conta, owner/admin na org do path, catálogo de papéis,ownersó por owner) e a MESMA
política efetiva — o resolver saiu parahost/org_policy.ts, compartilhado com o console. - Segundo fator:
POST /account/api/mfa/totp/{enroll,confirm,disable},
/mfa/recovery-codese a cerimônia de passkey em JSON
(/mfa/passkeys/{options,verify}) — a clássica responde 302 e não cabe numa SPA. Mesmos
gates de sudo do console; a recusa vira403 sudo_requiredem vez de um redirect.
requireSudopassa a delegar a decisão ao novoisSudoSatisfied, que é o que o caminho
JSON consome — uma política só, duas formas de recusar. Oconfirmleva o throttle do
bucket de sudo (o form não tem; o JSON fica mais apertado, nunca mais frouxo). MfaCapabilityganhacountRecoveryCodeseregenerateRecoveryCodesOPCIONAIS
(capability-probe viasupportsRecoveryCodeCount/supportsRecoveryCodeRegeneration),
implementados nolucidAccountStore.GET /account/api/mfapassa a devolver
recovery.remainingerecovery.regenerable. Novo evento de auditoria
mfa.recovery_codes_regenerated.
Compatível: o console HTML não muda: rota nova, resposta nova. As rotas JSON são montadas
mesmo com as telasorgs/mfadesligadas — é justamente o host com telas próprias que
precisa delas. - Orgs:
-
#224
db70f9dThanks @DavideCarvalho! - fix(authkit-server): uma normalização só para a identidade por e-mail (cadastro e login)O cadastro validava o e-mail com
.normalizeEmail()do VineJS — os defaults do
validator.js, que no gmail removem os pontos e o sub-endereço+tag— enquanto o passo
de identificador do login não normalizava nada e buscava por igualdade exata. Quem se
cadastrou comdavi.carvalho96@gmail.comteve a conta gravada como
davicarvalho96@gmail.come, ao tentar entrar com o endereço certo, nunca recebia e-mail;
por a tela ser à prova de enumeração, sem nenhuma mensagem de erro. E, por o login ser
sensível a maiúsculas, nemDavi@x.comachavadavi@x.com.Agora há UMA normalização, conservadora e exportada —
normalizeEmailIdentifier
(trim+toLowerCase, nada além disso) —, aplicada no cadastro (com e sem senha), no
"esqueci a senha", na troca de e-mail, na criação de usuário por admin, no convite de
organização, no import de usuários, no cadastro social e no passo de identificador do
login, que antes não usava nenhuma.Mudança de comportamento observável: o cadastro passa a gravar o endereço que a pessoa
digitou.davi.carvalho96@gmail.comedavi.carvalho96+lastro@gmail.comdeixam de colapsar
emdavicarvalho96@gmail.com— são identidades distintas, como em qualquer outro provedor.
Contas já gravadas não são alteradas.Compatibilidade:
login.legacyEmailFallback(defaulttrue) é uma ponte temporária —
quando o endereço normalizado não acha conta, o login (OIDC e console de conta) e o
"esqueci a senha" tentam a forma exatamente como digitada e a normalização legada, e só
aceitam se apontarem para exatamente uma conta; empate é tratado como "não achei". O
cadastro, o "Continuar com o Google", a criação de usuário por admin e o
authkit:users:importusam a mesma resolução para não criar uma SEGUNDA conta para quem já
tem uma gravada mutilada (importUsersrecebe o flag poroptions.legacyFallback). Nada
disso muda a tela, a mensagem ou o comportamento à prova de enumeração. Migre os endereços
gravados e desligue a ponte.
@adonis-agora/authkit-server@0.69.0
Minor Changes
-
#221
7a56652Thanks @DavideCarvalho! - feat(authkit-server): área de conta aceita a sessão do IdP (SSO) —accountSession.acceptIdpSessionO console de conta (
/account/*, admin) só aceitava a própria sessão
(ACCOUNT_SESSION_KEY, criada peloPOST /account/login); o login da interaction OIDC
cria só a sessão do oidc-provider. Resultado: quem acabou de entrar num app OIDC (ou no
próprio host, quando ele é IdP e RP) levava um segundo pedido de login ao abrir
/account/*.Com
accountSession: { acceptIdpSession: true }, os guards do console (e o
AccountAuthMiddleware/aceite de convite de org) abrem o console para a conta de uma
sessão do IdP válida (cookie assinado, não expirada, conta existente e habilitada). O
console aberto assim fica amarrado a essa sessão: termina quando ela termina, e o "Sair"
do console encerra também a sessão do IdP. Novo helper públicoensureConsoleSession(ctx)
para guards do host. Defaultfalse— nada muda para quem não liga. -
#221
7a56652Thanks @DavideCarvalho! - feat(authkit-server): política de redirect URI no registro dinâmico (RFC 7591/7592)O
/regaceitava qualquer redirect sintaticamente válido (qualquerhttps://): com
registro aberto, qualquer um registrava um client com callback no próprio domínio e
usava a tela de consent do IdP como isca. Nova opção
dynamicRegistration.redirectUriPolicy(loopback,exact,appSchemes,anyHttps),
aplicada antes do provider noPOST /rege noPUT /reg/:id, que também restringe o
client ao fluxo de código (authorization_code+refresh_token,response_type=code)
e registra clientes só-loopback/app instalado comoapplication_type: native. Gancho
dynamicRegistration.validateRegistrationpara regras do host (RegistrationPolicyError
→400).Mudança de default: registro aberto (sem
initialAccessToken) passa a aceitar só
redirects loopback (http://localhost|127.0.0.1|[::1], qualquer porta). Callbacks web de
fornecedores e esquemas de app precisam ser listados emexact/appSchemes
(anyHttps: trueouredirectUriPolicy: falserestauram o comportamento anterior).
Registro cominitialAccessTokennão muda. -
#221
7a56652Thanks @DavideCarvalho! - feat(authkit-server): resource indicators (RFC 8707) com access tokens opacosA feature
resourceIndicatorsdo oidc-provider só era montada quando algum AT era JWT;
com tokens opacos, umresourceno authorize (clientes MCP sempre mandam) era recusado
cominvalid_target. AgoraaccessTokens.resourcestambém liga a feature com tokens
opacos: oresourcedeclarado (tolerando barra final) é concedido no consent e amarrado
ao AT (aud), que continua opaco, encontrável porAccessToken.finde introspecionável.
Pedidos semresourcenão mudam (AT opaco semaud, userinfo funciona).resourcefora da lista declarada (chaves deresources+ oaudienceno modo JWT)
passa a ser recusado cominvalid_targettambém no modo JWT — antes, um resource
desconhecido saía como token opaco comaudarbitrário.
Patch Changes
-
#221
7a56652Thanks @DavideCarvalho! - fix(authkit-server): política de organizations (allowSelfCreate,roles,invitationTtlHours) volta a valerDesde a remoção da config de política legada,
resolveOrganizationsfixava
allowSelfCreate: false(e roles/TTL no default), e oAccountOrgsControllerlia só
esse valor estático — nunca a settingorganizations_policyque a doc manda usar. Pior:
declararorganizationsnodefineConfigtrava a setting, então um host com
organizations: { enabled: true }não tinha jeito nenhum de ligar o self-create
(POST /account/orgs→ 403).OrganizationsConfigInputaceita de novoallowSelfCreate,rolese
invitationTtlHours— com a setting travada, eles são a política efetiva./account/orgs(tela, criação e convites) e o TTL dos convites do admin/Admin API
resolvem a política efetiva: setting da org → setting global → config → default.
@adonis-agora/authkit-react@0.22.0
Minor Changes
-
#223
6adae6dThanks @DavideCarvalho! - feat(authkit-react): client e hooks para as escritas de conta (orgs + segundo fator)Lado React do espelho JSON de
/account/api/*, para telas do próprio host.client.account.orgsganhacreate,activate,deactivate,leave,invite,
revokeInvitation,updateMemberRole,removeMembereacceptInvitation.client.account.mfavira uma FUNÇÃO COM PROPRIEDADES:mfa()continua sendo a leitura do
status (assinatura preservada) e as escritas penduram nela —mfa.enroll(),
mfa.confirm(code),mfa.disable(),mfa.regenerateRecoveryCodes()e
mfa.passkeys.{options,verify}.- Hooks novos:
useAccount{Create,Activate,Deactivate,Leave}OrgMutationOptions,
useAccount{InviteOrgMember,RevokeOrgInvitation,AcceptOrgInvitation,UpdateOrgMemberRole,RemoveOrgMember}MutationOptions,
use{Enroll,Confirm,Disable}TotpMutationOptions,
useRegenerateRecoveryCodesMutationOptionseuseRegisterPasskeyMutationOptions. O
prefixouseAccount…nos de org evita colidir com os homônimos da superfície ADMIN, que
agem sobre qualquer org. registerPasskeyJson(client, deps?): a cerimônia de registro de passkey inteira por
fetch (options →startRegistration→ verify), o par headless dousePasskeyRegistration
(que é por form de página inteira e não muda). Sem React, para quem não usa TanStack.authkitKeys.account.mutations.*: as chaves das novas mutations, com prefixosorgs()e
mfa()parauseIsMutating.AccountMfaStatus.recoveryganharemainingeregenerable.
@adonis-agora/authkit-server@0.68.4
Patch Changes
-
#219
9279596Thanks @DavideCarvalho! - fix(authkit-server): cookie de org assinado chega URL-encoded no contexto KoaComplemento do fix anterior (0.68.3). A verificação da assinatura checava
startsWith('s:')no valor cru, mas o jar Koa do oidc-provider devolve o valor
como está no header — sem URL-decode — e o browser reenvia o cookie exatamente
como o host o escreveu. Na prática chegas%3A<b64>.<hmac>, então a checagem nunca
casava e a org continuava sendo descartada: oloadExistingGrantseguia sem
reconciliar oactiveOrg.O leitor agora normaliza antes de decidir o formato (tenta
decodeURIComponent
e só então checa o prefixo assinado). Confirmado contra o cookie real do app, com a
APP_KEYreal:raw (como no header): s%3AeyJtZXNzYWdlIjoib3JnLWZtby1kZW1v… SEM appKey : null COM appKey : { orgId: 'org-fmo-demo', orgSlug: 'demo', orgRole: 'owner' }O teste ganhou o caso da forma URL-encoded, que é a que o browser de fato envia.
@adonis-agora/authkit-server@0.68.3
Patch Changes
-
#217
927c70bThanks @DavideCarvalho! - fix(authkit-server): cookie de org assinado não era verificado no contexto KoaO host grava o cookie
authkit_active_orgviactx.response.cookie, e o AdonisJS
o assina (s:<base64url>.<hmac>,MessageVerifier). As duas leituras no
contexto do oidc-provider liam o valor cru e só tentavam URL-decode, então o
envelope assinado não parseava e a org era descartada:readActiveOrgFromKoaCtx— usada porloadExistingGrantno authorize.readActiveOrgFromHostCtx— no caminho Koa (o caminho Adonis já desassina sozinho).
Efeito:
loadExistingGrantnunca reconciliava oactiveOrgdo Grant. Como o
consent só roda uma vez por grant, quem ativava a organização depois do
primeiro login não recebiaorg_id/org_slug/org_roleno token — e
/o/:orgSlug/...(protegida pelorequireOrg) redirecionava de volta para a
escolha de organização, indefinidamente.O leitor agora verifica a assinatura com a
appKeyantes de aceitar o valor:
HMAC-SHA256 sobre o payload,purpose= nome do cookie, comparação em tempo
constante. Sem a chave o valor assinado é recusado — este cookie decide a claim de
organização, e aceitar um valor não verificado deixaria o usuário trocar de tenant
forjando o cookie. As formas crua e URL-encoded continuam aceitas para hosts que
não assinam.
@adonis-agora/authkit-server@0.68.2
Patch Changes
-
#211
035f277Thanks @DavideCarvalho! - fix(authkit-server): formulários de organizações sem token CSRFaccount/orgs.edgeera a única view da lib que renderizava formulários POST
sem<input name="_csrf">(ativar, desativar, sair, aceitar convite e criar org),
e oaccountOrgsController.indexnão passavacsrfTokenno contexto de render —
ao contrário de todos os outros controllers, que passam
csrfToken: ctx.request.csrfToken.Com CSRF ligado (o padrão de um app Adonis com
@adonisjs/shield), todo POST de
organização falhava com "Invalid or expired CSRF token": ativar ou trocar de
organização nunca funcionou. O usuário ficava preso no seletor de organização e,
como o token de sessão nunca ganhava a claim de org, nenhuma rota de tenant
(/o/:orgSlug/..., protegida pelorequireOrg) abria — mesmo o fluxo de código
já emitindo as claims de org corretamente.O teste de
_csrfemedge_viewstinha a lista de views escrita à mão, e
account/orgs.edgenão estava nela, então a falha passou despercebida. Ele agora
varre o diretório de views e exige_csrfem qualquer view que tenha form
POST, para que uma view nova não escape do mesmo jeito.
@adonis-agora/authkit-server@0.68.1
Patch Changes
-
#209
73f6723Thanks @DavideCarvalho! - fix(authkit-server): claims de organização no fluxo authorization codeAs claims
org_id/org_slug/org_rolenunca eram emitidas no fluxoresponse_type=code: a org ativa tem uma única fonte — o cookie do browser — e oid_tokendesse fluxo é emitido no/oidc/token, uma requisição server-a-servidor do app, sem os cookies do usuário. OfindAccountlia apenas o cookie,activeOrgeranulle as três claims (opcionais) sumiam em silêncio — orequireOrgdoauthkit-cliententão recusava toda rota de tenant.A correção persiste a org no Grant durante o consent (aí a request é do browser, com o cookie presente) e a lê no mint via
ctx.oidc.entities.Grant.activeOrg, com o cookie como fallback para fluxos em que oid_tokensai no próprio authorize. Como o modeloGrantdo oidc-provider filtra o payload porIN_PAYLOAD, a lib estendeIN_PAYLOADcomactiveOrge injeta a subclasse na instância do provider (o oidc-provider v9 não expõe a opçãomodels). Grants reaproveitados (consent já lembrado) são reconciliados com o cookie no authorize, para que uma troca de org não fique presa na org antiga até o grant expirar. O leitor do cookie via jar Koa também passou a URL-decodificar o valor, que ocookiesdevolve como está no header.
@adonis-agora/authkit-server@0.68.0
Minor Changes
-
#206
84c62dcThanks @DavideCarvalho! - Organizations passa a existir por padrão: aOrganizationsCapabilitynão some mais em silêncio quando o host esqueceorganizationModels.O
lucidAccountStore(Model, options)só anexava a capability quandooptions.organizationModelseratrueou o trio explícito:...(OrgModels ? buildOrganizations({...}) : {})
Consequência: um host que já tinha dito
organizations: { enabled: true }— e cujas rotas/account/orgs*já eram montadas por default (register_auth_host, blocoif (mountOrgs), controller capability-probed) — recebia a tela respondendo 403 em vez de funcionar, sem nenhum erro de boot. Oauthkit:doctoravisava, mas só para quem o rodasse.Agora
organizationModelsé opcional:- Ausente (novo default) → usa os models default da lib (
defaultOrganizationModels, as três tabelas lib-owned criadas peloensureAuthkitSchema). A capability existe; a mesma tela que já estava montada passa a funcionar. true→ idêntico ao default. É o atalho explícito que apps existentes já usam e continua valendo.{ OrgModel, MemberModel, InvitationModel }→ escape hatch inalterado, para quem guarda as tabelas de auth numa conexão/schema próprios.false(novo opt-out explícito) → capability ausente. Preserva o comportamento antigo para quem hoje depende desupportsOrganizations === false.
Como migrar: se você dependia da ausência da capability sem declarar nada, passe
organizationModels: false. Se quer conexão/schema próprios, continue passando o trio. Caso contrário, nada a fazer — a feature que você já habilitava agora funciona.O
authkit:doctor(checkOrganizations) acompanha: owarnde "enabled=true sem capability" passa a ser inalcançável no caminho default e menciona ofalse; a mensagem deokdiz de onde vêm os models (default da lib ou trio explícito). OlucidStorestambém aceitaorganizations: falsepara o mesmo opt-out.Cobertura em
tests/organizations/organizations_store.spec.ts:lucidAccountStore(Model)sem options liga a capability e roda CRUD de ponta a ponta;organizationModels: falsedesliga mesmo com as tabelas presentes;truee o trio explícito seguem iguais; e olucidStorescobre default + opt-out. O teste "sem tabelas org → supportsOrganizations false" foi removido porque a premissa dele era exatamente o contrato que deixou de valer. - Ausente (novo default) → usa os models default da lib (
@adonis-agora/authkit-server@0.67.0
Minor Changes
-
#200
f250d6eThanks @DavideCarvalho! - Garante a colunalogin_methodsna tabela da conta em vez de assumirusers, e faz o boot avisar quando não deu para garantir.O
ensureAuthkitSchemaadicionava a coluna numa tabela de nome fixousers. Como o nome da tabela da conta é decisão do host, isso errava nos dois sentidos: com a conta emauth_users(o nome que o próprio scaffold da lib usa) e uma tabelausersqualquer no banco — o starter do AdonisJS cria uma — oALTERacertava a tabela errada e passava; sem nenhumausers, nada acontecia. Nos dois casos aLoginMethodsPreferenceCapabilityficava sem a coluna, sem erro e sem aviso.Agora o
lucidAccountStoreexpõeaccountTable(deModel.tablee, se o model ainda não bootou, da naming strategy — a mesma função que o Lucid usa), o provider repassa emEnsureSchemaOptions.accountTable, e oensureusa esse nome. Store próprio que não exponha o metadado continua nousersde antes, então nada muda para quem já estava certo.O
EnsureSchemaReportpassa a trazerloginMethods: { table, ensured }: quando a tabela da conta não existe, o provider loga um warning — antes o sintoma só aparecia longe da causa, como login/callback OIDC quebrado por "column ...login_methods does not exist".Cobertura em
tests/schema/ensure_schema.spec.ts: o caso da tabela homônima (a coluna vai paraauth_userse não parausers), o back-compat semaccountTable, oensured: falsee a derivação do nome pelo store. Reverter só oensure.tsderruba três desses casos.E o
catchdo bloco não mente mais: o re-probe passou a ser da coluna, não da tabela. Antes, umALTERque falhasse (permissão, lock, DDL) viravaensured: true— o report dizia sucesso com a coluna ausente e o boot não avisava nada, que é exatamente o silêncio que este bloco existe para acabar. Agora só o caso de corrida (a coluna já existe porque outra instância a criou) é engolido; qualquer outra falha propaga para o provider, que já sabe degradar logando warning. Dois testes cobrem os dois lados. -
#200
f250d6eThanks @DavideCarvalho! -organizationModels: truepassa a usar models default das tabelas de organizations — sem o host precisar escrever model nenhum.As três tabelas (
auth_organizations,auth_organization_members,auth_organization_invitations) são lib-owned: quem as cria e evolui é oensureAuthkitSchema. Ainda assim, para ligar aOrganizationsCapabilitycada host tinha que transcrever à mão os models que espelham essas colunas. Isso rendia duas coisas ruins: boilerplate sem nenhuma decisão do host, e drift silencioso — uma coluna nova chega peloautoManagee o model escrito à mão no host não sabe dela, sem erro nenhum. Foi o que aconteceu no próprio fixture de teste deste pacote: ele criavaauth_organization_memberssemupdated_at, que existe na tabela real.Agora
lucidAccountStore(Model, { organizationModels: true })usa os models default da lib (exportados comodefaultOrganizationModels, junto deAuthOrganization,AuthOrganizationMembereAuthOrganizationInvitation). O caminho explícito{ OrgModel, MemberModel, InvitationModel }continua valendo como escape hatch — é para quem guarda as tabelas de auth numa conexão/schema próprios, que os defaults não declaram (static connection).A mensagem do
authkit:doctorpara o caso "organizations.enabled: true sem capability" passa a apontar oorganizationModels: trueprimeiro.Cobertura em
tests/organizations/organizations_store.spec.ts:organizationModels: trueliga a capability e rodacreateOrg/findOrgBySlug/getOrgMembership/listOrgsForAccountde ponta a ponta contra as tabelas reais; o escape hatch explícito continua funcionando; e ficou registrado o comportamento de desenho — sem as tabelas, a capability está ligada e o erro é alto no uso (barulhento de propósito: o silêncio era o problema).O mesmo vale no caminho do
lucidStores, que tem tipo próprio:organizations: truetambém é aceito lá — sem isso o atalho não chegava no wiring "declarado uma vez", que é o recomendado em app maior.A documentação foi atualizada junto (
organizations.mdx,account-store.mdx): otrueaparece como caminho recomendado, o objeto explícito fica como escape hatch, e a linha doenableddeixou de dizer que ele "detecta tabelas" — ele é sinal de intenção para oauthkit:doctor, e quem monta as rotas é a capability.
Patch Changes
-
#200
f250d6eThanks @DavideCarvalho! - Conserta onode ace configure, que quebrava em todos os presets, e oconfig/authkit.tsejetado pelo preset React, que vinha com a API de identidade antiga.O
configuregerava os arquivos comcodemods.makeUsingStub, e o gerador de stubs compila o.stubcomo template delimitado por crase. Qualquer crase dentro do stub fecha o template mais cedo e o texto seguinte passa a ser parseado como JavaScript — daí osSyntaxErrorque apareciam com o nome de um identificador qualquer do comentário (Unexpected identifier 'views'no stub do React,Unexpected identifier 'id'no do model,Invalid or unexpected tokenno da migration). Sóstubs/config/authkit.stubcompilava, e é justamente o único sem crase: era ele que fazia o--ui=edgeavançar até o stub da migration antes de estourar.Isso derrubava o caminho documentado de instalação (
pnpm add→node ace configure --ui=...) para todo consumidor, que passava a ter que reconstruir os arquivos ejetados à mão a partir destubs/.As crases saíram dos comentários dos três stubs afetados. Junto, o
stubs/config/authkit_react.stubdeixou de declararfindAccount/verifyCredentials: essas chaves não existem noAuthServerConfigInput(que aceitaaccountStore) e a lib as deriva do store — o preset React ejetava um config semaccountStore, ou seja, sem verificação de credenciais e sem o resto do store (MFA, capabilities). Agora usa o mesmoaccountStore: lucidAccountStore(AuthUser)do stub sem preset.A lacuna que deixou isso passar foi de teste: nenhum spec chamava o
makeUsingStub. O e2e da migration lê o stub direto e remove o frontmatter{{{ ... }}}, então o stub nunca era compilado como template. Entratests/stubs_compile.spec.ts, que compila todos os.stubdo build pelo mesmo mecanismo e falha listando os que quebram — com um caso separado para a crase, que é a causa raiz.