{"_id":"@alphatechma/atch-template-keycloak-rn","_rev":"2-54667e5ab035085a1f1d9314253cf4ed","name":"@alphatechma/atch-template-keycloak-rn","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@alphatechma/atch-template-keycloak-rn","version":"1.0.0","keywords":["expo","react-native","template","keycloak","oidc","oauth2","pkce","typescript","alphatech"],"license":"MIT","_id":"@alphatechma/atch-template-keycloak-rn@1.0.0","maintainers":[{"name":"mesquitadev","email":"mesquitadev@gmail.com"}],"homepage":"https://github.com/alphatechma/atch-template-keycloak-rn#readme","bugs":{"url":"https://github.com/alphatechma/atch-template-keycloak-rn/issues"},"dist":{"shasum":"5016c1d5349a9485588795f5314833f7f7e45212","tarball":"https://registry.npmjs.org/@alphatechma/atch-template-keycloak-rn/-/atch-template-keycloak-rn-1.0.0.tgz","fileCount":82,"integrity":"sha512-Ms/0P3vqggsz9udNq2X96U6UQrmgxIZw+azenMTVet0JiVxzh9hKwJOk9WwB+4w4eXz+ODZopeIkCd//QHyyrQ==","signatures":[{"sig":"MEUCIQCYDktzwKwxsRV48tRBCQ3jmNsrBMvoA+CudPeXh0GN9gIgCWARfXvq65l1flhiVYoGTXaVwjKERl5C2/WHBrqp3vc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":158465},"main":"index.ts","engines":{"node":">=22.13.0"},"gitHead":"ad373481101259fa3ba2e818605af6adb47f2934","scripts":{"ios":"expo run:ios","web":"expo start --web","lint":"eslint .","test":"jest","start":"expo start","doctor":"npx expo-doctor","format":"prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"","verify":"yarn typecheck && yarn lint && yarn test --passWithNoTests","android":"expo run:android","prepare":"husky","release":"semantic-release","test:ci":"jest --ci --coverage","lint:fix":"eslint . --fix","prebuild":"expo prebuild --clean","typecheck":"tsc --noEmit","format:check":"prettier --check \"**/*.{js,jsx,ts,tsx,json,md}\""},"_npmUser":{"name":"mesquitadev","email":"mesquitadev@gmail.com"},"repository":{"url":"git+https://github.com/alphatechma/atch-template-keycloak-rn.git","type":"git"},"_npmVersion":"11.19.1","description":"Template oficial Alphatech para apps React Native com Expo autenticados via Keycloak (OIDC + PKCE).","directories":{},"lint-staged":{"*.{js,jsx,ts,tsx}":["eslint --fix","prettier --write"],"*.{json,md,yml,yaml}":["prettier --write"]},"resolutions":{"conventional-changelog-conventionalcommits":"9.3.1"},"_nodeVersion":"22.23.2","dependencies":{"yup":"^1.7.1","expo":"~57.0.21","react":"19.2.3","date-fns":"^4.4.0","expo-font":"~57.0.3","react-dom":"19.2.3","jwt-decode":"^4.0.0","expo-crypto":"~57.0.2","react-redux":"^9.3.0","expo-linking":"~57.0.9","expo-updates":"~57.0.21","react-native":"0.86.3","@expo/log-box":"~57.0.4","expo-constants":"~57.0.17","expo-system-ui":"~57.0.3","expo-status-bar":"~57.0.1","react-hook-form":"^7.87.0","@reduxjs/toolkit":"^2.12.0","expo-web-browser":"~57.0.2","react-native-svg":"15.15.4","react-native-web":"~0.21.0","@expo/dom-webview":"~57.0.1","expo-auth-session":"~57.0.11","expo-secure-store":"~57.0.3","styled-components":"^6.5.3","@expo/vector-icons":"^15.0.2","expo-splash-screen":"~57.0.8","@expo/metro-runtime":"~57.0.15","@hookform/resolvers":"^5.9.1","@gorhom/bottom-sheet":"^5.2.14","react-native-screens":"~4.26.0","react-native-worklets":"0.10.1","react-native-reanimated":"4.5.1","@react-navigation/native":"^7.3.18","@expo-google-fonts/roboto":"^0.4.3","react-native-gesture-handler":"~2.32.0","@react-navigation/bottom-tabs":"^7.18.18","@react-navigation/native-stack":"^7.18.10","react-native-safe-area-context":"~5.7.0","@react-native-masked-view/masked-view":"0.3.2","@react-native-async-storage/async-storage":"2.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"yarn@1.22.22","devDependencies":{"jest":"^29.7.0","husky":"^9.1.7","eslint":"^9.39.0","prettier":"^3.9.6","jest-expo":"~57.0.5","typescript":"~6.0.3","@babel/core":"^7.29.7","@types/jest":"^29.5.14","@types/node":"^22.14.0","lint-staged":"^17.5.1","@types/react":"~19.2.2","test-renderer":"^1.2.0","@commitlint/cli":"^21.2.2","semantic-release":"^25.0.9","eslint-config-expo":"~57.0.2","@semantic-release/git":"^10.0.1","eslint-config-prettier":"^10.1.8","@react-native/jest-preset":"0.86.3","@react-native/metro-config":"0.86.3","@semantic-release/changelog":"^6.0.3","@testing-library/react-native":"^14.0.1","@commitlint/config-conventional":"^21.2.2"},"_npmOperationalInternal":{"tmp":"tmp/atch-template-keycloak-rn_1.0.0_1789052248919_0.22595515128654298","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"_id":"@alphatechma/atch-template-keycloak-rn@1.1.0","bugs":{"url":"https://github.com/alphatechma/atch-template-keycloak-rn/issues"},"dist":{"shasum":"768ae09132b3dad15e88e96340d362c2168ddd76","tarball":"https://registry.npmjs.org/@alphatechma/atch-template-keycloak-rn/-/atch-template-keycloak-rn-1.1.0.tgz","fileCount":112,"integrity":"sha512-iiefiG42ZC50xirI2Rc+1+e7jchAF8f5deZyPNS/v/UuQhCDz/sajASwqHALTYF11QNzigqVwgS0jYU/roDyQA==","signatures":[{"sig":"MEUCIA5ehGeahetRNKmif55ZfQzLOP/1lFta9G00IFRlIayyAiEAuTtHJGIoYp4mvUY2W0RPzoEtw0jEH7vPjaztjj9TDso=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFcMo+ThTOoPHXRpqilIqSa8VDxO3vFYEfMuLEqGH8HkAiEA+/0rz4PgSM5SOey8APjEbz2yhGJ5CzRoTy6PSPjQvtg="}],"unpackedSize":205202},"main":"index.ts","name":"@alphatechma/atch-template-keycloak-rn","engines":{"node":">=22.13.0"},"gitHead":"79a6f1f662104b8f11e1389718b1adde839e9f7b","license":"MIT","scripts":{"ios":"expo run:ios","web":"expo start --web","lint":"eslint .","test":"jest","start":"expo start","doctor":"npx expo-doctor","format":"prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"","verify":"yarn typecheck && yarn lint && yarn test --passWithNoTests","android":"expo run:android","prepare":"husky","release":"semantic-release","test:ci":"jest --ci --coverage","lint:fix":"eslint . --fix","prebuild":"expo prebuild --clean","typecheck":"tsc --noEmit","format:check":"prettier --check \"**/*.{js,jsx,ts,tsx,json,md}\""},"version":"1.1.0","_npmUser":{"name":"mesquitadev","email":"mesquitadev@gmail.com"},"homepage":"https://github.com/alphatechma/atch-template-keycloak-rn#readme","keywords":["expo","react-native","template","keycloak","oidc","oauth2","pkce","typescript","alphatech"],"repository":{"url":"git+https://github.com/alphatechma/atch-template-keycloak-rn.git","type":"git"},"_npmVersion":"11.19.1","description":"Template oficial Alphatech para apps React Native com Expo autenticados via Keycloak (OIDC + PKCE).","directories":{},"lint-staged":{"*.{js,jsx,ts,tsx}":["eslint --fix","prettier --write"],"*.{json,md,yml,yaml}":["prettier --write"]},"maintainers":[{"name":"mesquitadev","email":"mesquitadev@gmail.com"}],"resolutions":{"conventional-changelog-conventionalcommits":"9.3.1"},"_nodeVersion":"22.23.2","dependencies":{"zod":"^4.6.1","expo":"~57.0.21","react":"19.2.3","date-fns":"^4.4.0","expo-font":"~57.0.3","react-dom":"19.2.3","jwt-decode":"^4.0.0","expo-crypto":"~57.0.2","react-redux":"^9.3.0","expo-linking":"~57.0.9","expo-updates":"~57.0.21","react-native":"0.86.3","@expo/log-box":"~57.0.4","expo-constants":"~57.0.17","expo-system-ui":"~57.0.3","expo-status-bar":"~57.0.1","react-hook-form":"^7.87.0","@reduxjs/toolkit":"^2.12.0","expo-web-browser":"~57.0.2","react-native-svg":"15.15.4","react-native-web":"~0.21.0","@expo/dom-webview":"~57.0.1","expo-auth-session":"~57.0.11","expo-secure-store":"~57.0.3","styled-components":"^6.5.3","@expo/vector-icons":"^15.0.2","expo-splash-screen":"~57.0.8","@expo/metro-runtime":"~57.0.15","@hookform/resolvers":"^5.9.1","@gorhom/bottom-sheet":"^5.2.14","react-native-screens":"~4.26.0","react-native-worklets":"0.10.1","react-native-reanimated":"4.5.1","@react-navigation/native":"^7.3.18","@expo-google-fonts/roboto":"^0.4.3","react-native-gesture-handler":"~2.32.0","@react-navigation/bottom-tabs":"^7.18.18","@react-navigation/native-stack":"^7.18.10","react-native-safe-area-context":"~5.7.0","@react-native-masked-view/masked-view":"0.3.2","@react-native-async-storage/async-storage":"2.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"yarn@1.22.22","devDependencies":{"jest":"^29.7.0","husky":"^9.1.7","eslint":"^9.39.0","prettier":"^3.9.6","jest-expo":"~57.0.5","typescript":"~6.0.3","@babel/core":"^7.29.7","@types/jest":"^29.5.14","@types/node":"^22.14.0","lint-staged":"^17.5.1","@types/react":"~19.2.2","test-renderer":"^1.2.0","@commitlint/cli":"^21.2.2","semantic-release":"^25.0.9","eslint-config-expo":"~57.0.2","@semantic-release/git":"^10.0.1","eslint-config-prettier":"^10.1.8","@react-native/jest-preset":"0.86.3","@react-native/metro-config":"0.86.3","@semantic-release/changelog":"^6.0.3","@testing-library/react-native":"^14.0.1","@commitlint/config-conventional":"^21.2.2"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/atch-template-keycloak-rn_1.1.0_1789053481863_0.7755041155717288"}}},"time":{"created":"2026-09-10T14:57:28.723Z","modified":"2026-09-10T15:18:02.140Z","1.0.0":"2026-09-10T14:57:29.087Z","1.1.0":"2026-09-10T15:18:01.954Z"},"bugs":{"url":"https://github.com/alphatechma/atch-template-keycloak-rn/issues"},"license":"MIT","homepage":"https://github.com/alphatechma/atch-template-keycloak-rn#readme","keywords":["expo","react-native","template","keycloak","oidc","oauth2","pkce","typescript","alphatech"],"repository":{"url":"git+https://github.com/alphatechma/atch-template-keycloak-rn.git","type":"git"},"description":"Template oficial Alphatech para apps React Native com Expo autenticados via Keycloak (OIDC + PKCE).","maintainers":[{"name":"mesquitadev","email":"mesquitadev@gmail.com"}],"readme":"# atch-template-keycloak-rn\n\nTemplate oficial da Alphatech para apps **React Native + Expo autenticados via Keycloak**.\n\nÉ o [`atch-template-expo`](https://github.com/alphatechma/atch-template-expo)\ncom a camada de autenticação OIDC por cima. Se o projeto **não** usa Keycloak,\nuse o template base — tudo que está documentado no README dele vale aqui\ntambém (imports, variáveis de ambiente, erros, testes, release).\n\n```bash\nnpx create-expo-app meu-app --template @alphatechma/atch-template-keycloak-rn\ncd meu-app\ncp .env.example .env      # URL do Keycloak, realm, client id e URL da API\nyarn start\n```\n\n## O que a camada de auth faz\n\n- **Authorization Code + PKCE** (`expo-auth-session`), com discovery automático\n  a partir do issuer do realm.\n- **Sessão no SecureStore** (keychain/keystore) — access, refresh e id token.\n- **Refresh automático**: `obterAccessToken()` renova quando o token está a\n  menos de 60s de expirar, e o `baseQuery` do RTK Query o consulta antes de cada\n  requisição.\n- **Re-auth em 401**: uma retentativa com token renovado; persistindo, a sessão\n  cai em vez de entrar em loop.\n- **Logout** com `end_session_endpoint`, sem travar o logout local se a rede falhar.\n- **Papéis** de `realm_access` e `resource_access[clientId]` unificados em\n  `usuario.papeis`.\n\n### PKCE não é opcional\n\nUm app nativo é um **cliente público** OAuth: não existe lugar no bundle para\nguardar um client secret. Sem PKCE, o authorization code pode ser interceptado\npor outro app registrado no mesmo scheme (RFC 8252, §8.1). A versão anterior\ndeste template passava `usePKCE: false` e guardava os tokens em AsyncStorage\nem texto puro — as duas coisas foram corrigidas.\n\nPor isso **não há `CLIENT_SECRET` no `.env.example`**. No Keycloak, o client\ndeve ser criado como _public_ com _Standard flow_ habilitado.\n\n## Configurando o client no Keycloak\n\n| Campo                           | Valor                                            |\n| ------------------------------- | ------------------------------------------------ |\n| Client type                     | OpenID Connect                                   |\n| Client authentication           | **Off** (public)                                 |\n| Standard flow                   | On                                               |\n| Direct access grants            | Off                                              |\n| Valid redirect URIs             | `atchtemplate://auth/redirect` e `exp://*` (dev) |\n| Valid post logout redirect URIs | `atchtemplate://auth/redirect`                   |\n| Web origins                     | vazio                                            |\n\nO `scheme` sai do `app.config.ts` — troque-o junto com `NOME_BASE` e\n`BUNDLE_BASE` ao iniciar um projeto, e ajuste os redirect URIs de acordo.\n\nPara receber refresh token, habilite o escopo `offline_access` no client (o\ntemplate já o pede em `scopes`).\n\n## Como o fluxo se encaixa\n\n```\nAppProvider\n  └─ AuthProvider          descobre o issuer, restaura a sessão do SecureStore\n       └─ Routes\n            ├─ carregando  → tela Carregando\n            ├─ deslogado   → PublicRoutes → SignIn\n            └─ logado      → AuthRoutes\n```\n\nO estado `carregando` é um estado real, não um detalhe: enquanto o discovery não\nresponde e a sessão guardada não foi lida, mostrar a tela de login faria quem já\nestá autenticado ver \"Entrar\" a cada abertura do app.\n\nO `Routes` também registra as funções de sessão em `services/authBridge.ts`,\nque é como o `baseQuery` do RTK Query — que vive fora do React e não pode usar\nhook — obtém um token válido sem duplicar a lógica de refresh.\n\n## Autorização por papel\n\n```tsx\nimport { Permission } from '@/components';\nimport { usePapeis, usePermissoesCrud } from '@/hooks';\n\n// Esconde um trecho de quem não tem o papel. Lista = basta UM deles.\n<Permission allow={['almoxarife', 'fiscal']}>\n  <Button title=\"Aprovar\" onPress={aprovar} />\n</Permission>;\n\n// Verificação imperativa\nconst { pode, carregando, papeis } = usePapeis();\nif (pode('fiscal')) {\n  /* ... */\n}\n\n// Quatro verbos de um recurso, a partir do prefixo\nconst kits = usePermissoesCrud('kits'); // kits:criar, kits:atualizar, ...\n{\n  kits.podeExcluir && <BotaoExcluir />;\n}\n```\n\nDuas regras que a política em `src/auth/autorizacao.ts` garante:\n\n- **Fail-closed enquanto carrega.** Uma lista de papéis vazia porque o token\n  ainda não foi lido é indistinguível de uma lista vazia porque a pessoa não tem\n  o papel. Quem chama deve tratar `carregando` como \"espere\", não como \"negue\" —\n  é o que o `Routes` faz com a tela `Carregando`.\n- **Quem pode escrever pode ver.** `usePermissoesCrud` deriva `podeVer` de\n  qualquer verbo de escrita; exigir `visualizar` em separado esconderia a tela\n  de quem tem permissão de mexer nela, o que é um perfil quebrado e não uma\n  restrição legítima.\n\n> Isto é interface, não segurança. Quem chamar a API direto continua sendo\n> barrado pelo backend, que é onde a permissão vale de verdade.\n\n## Estrutura específica desta camada\n\n```\nsrc/\n  auth/\n    autorizacao.ts  política de papéis (pura, fail-closed)\n    jwt.ts          leitura de claims do access token (não é validação)\n    keycloak.ts     redirect uri, refresh, logout, janela de renovação\n    tipos.ts        UsuarioKeycloak e EstadoAuth\n  components/Permission/\n  contexts/AuthContext.tsx\n  hooks/{useAuth,usePapeis}.ts\n  services/authBridge.ts\n  routes/{PublicRoutes,AuthRoutes}.tsx\n  screens/{SignIn,Carregando}/\n```\n\n## Componentes\n\nA biblioteca é a mesma do template base e está documentada\n[lá](https://github.com/alphatechma/atch-template-expo#componentes): `Button`,\n`Text`, `Card`, `Loading`, `EmptyState`, os campos (`Input` com máscaras,\n`Select`, `Checkbox`, `Switch`) e as versões ligadas ao react-hook-form\n(`FormInput`, `FormSelect`, `FormCheckbox`, `FormSwitch`). Validação com **zod**.\n\nDaqui sai um componente a mais, `Permission`, descrito acima.\n\nO restante (tokens de design, store, utilitários, erros, lint, release, CI) é\nidêntico ao template base — veja o README de lá.\n\n## Scripts\n\n| Script                      | O que faz                                     |\n| --------------------------- | --------------------------------------------- |\n| `yarn start`                | Servidor de desenvolvimento                   |\n| `yarn android` / `yarn ios` | Build nativo local (dev client)               |\n| `yarn verify`               | typecheck + lint + testes (é o que o CI roda) |\n| `yarn doctor`               | `expo-doctor`                                 |\n| `yarn release`              | semantic-release (só no CI)                   |\n\n## CI/CD\n\nQuatro workflows em `.github/workflows/`:\n\n| Workflow                | Quando roda              | O que faz                                                |\n| ----------------------- | ------------------------ | -------------------------------------------------------- |\n| `ci.yml` · **verify**   | PR e push em `main`      | typecheck, lint, format, testes, `expo-doctor`           |\n| `ci.yml` · **template** | PR e push em `main`      | cria um projeto de verdade a partir do template e valida |\n| `eas-build.yml`         | manual                   | build nativo por perfil, com submit opcional             |\n| `eas-update.yml`        | push em `main`/`develop` | atualização OTA no canal correspondente                  |\n| `release.yml`           | push em `main`           | semantic-release: versão, CHANGELOG, tag, publish no npm |\n\n### Por que existe o job `template`\n\nRodar a suíte **dentro** do template não prova que ele funciona **como**\ntemplate. Três bugs reais passaram pelo `verify` e só apareceram quando um\nprojeto foi criado de fato: o projeto nascia com o nome do template, o app abria\nem tela branca sem `.env`, e o `npm install` quebrava por conflito de peer deps.\nO job faz o ciclo completo — `npm pack`, `create-expo-app`, `npm install`,\ntypecheck, lint, testes, `expo-doctor` e `expo export` sem `.env`.\n\n### Secrets e variáveis\n\n| Nome             | Tipo     | Para quê                                                                     |\n| ---------------- | -------- | ---------------------------------------------------------------------------- |\n| `EXPO_TOKEN`     | secret   | EAS Build e EAS Update ([expo.dev](https://expo.dev/settings/access-tokens)) |\n| `NPM_TOKEN`      | secret   | publicar no npm (só no repositório do template)                              |\n| `EAS_HABILITADO` | variable | `true` liga o EAS Update automático                                          |\n\n```bash\ngh secret set EXPO_TOKEN\ngh variable set EAS_HABILITADO --body true\n```\n\nO `eas-update.yml` fica inerte enquanto `EAS_HABILITADO` não for `true` — assim\no workflow viaja no template sem tentar publicar nada.\n\n### Build vs update\n\n`runtimeVersion` usa a policy `appVersion`: uma atualização OTA só alcança\nbuilds da **mesma versão** do app. Mudança que exija código nativo novo — uma\ndependência com módulo nativo, uma permissão a mais — precisa de build, não de\nupdate. Por isso o build é manual e separado: ele custa minuto de EAS e tem que\nser uma decisão, não um efeito colateral de push.\n\n## Ao iniciar um projeto\n\n1. Ajuste `NOME_BASE`, `BUNDLE_BASE` e `scheme` no `app.config.ts`.\n2. Cadastre os redirect URIs correspondentes no client do Keycloak.\n3. Preencha o `.env`.\n4. Troque os assets em `assets/`.\n5. Apague `src/features/example/` e adapte os papéis usados em `usePermissoesCrud`.\n","readmeFilename":"README.md"}