agents/openai.yaml
interface: display_name: "Make App Auth" short_description: "Integrate Make App unified login" default_prompt: "Use $make-app-auth to wire unified login, authenticated Make requests, and logout behavior."
qfeius/make-platform-skills · GitHub
Use when generating, modifying, reviewing, or debugging Make App unified login and authenticated /api/make requests with @qfeius/make-app-auth. Covers unified login, OAuth/ngrok mode, 401/403 handling, logout, current-user menu logout wiring, cookies, sessions, redirect callbacks, and Make App auth troubleshooting. Preserve authenticated context for the default /api/make/app/principal/permission flow. Does not cover UI layout, account menu placement, page structure, build output, Service API contracts, permission logic, DSL modeling, or canvas-table internals; use makeui for the current-user header menu surface and make-app-permission for single-app permission enforcement.
프로젝트 폴더에서 아래 명령어를 실행하고, 설치할 에이전트를 선택하세요.
npx skills add qfeius/make-platform-skills --skill make-app-auth설치 명령을 직접 실행해야 적용됩니다. 지원 에이전트와 필요한 권한·라이선스는 제작자의 안내를 확인하세요.
agents/openai.yamlinterface: display_name: "Make App Auth" short_description: "Integrate Make App unified login" default_prompt: "Use $make-app-auth to wire unified login, authenticated Make requests, and logout behavior."
references/logout-and-401.md# Logout And 401
Use this reference when implementing or reviewing user-facing behavior for auth failures and logout.
401 means browser session is missing or expired.
Behavior:
- Show only a neutral loading state while the browser is being redirected.
- Use `auth.login({ redirect: true })` to enter the Org login page.
- If the current App session is diagnosed as stale or broken, clear the App session with `auth.logout({ redirect: false })`, then call `auth.login({ redirect: true })`. Do not make logout-before-login the default 401 path.
- Do not render an App-owned login page, login transition page, or signed-out completion page.
- Prefer `createMakeAppAuth({ apiAuthRedirect: true })` for generated unified-login Apps with `@qfeius/make-app-auth >= 0.1.3`, so SDK handles API 401/403 redirect checks with built-in loop protection.
- Do not leave business views in a schema/list/create/update/delete error state for 401. Route 401 through the shared expired-session handler.
- If `auth.init({ redirect: true })` returns `reason="state_expired"` or `reason="challenge_expired"`, show `登录已过期,请重新登录` and wait for the user to click.
- After the user clicks relogin, call `auth.login({ redirect: true })`. Do not implement multiple automatic retries.
Logout:
```js
await auth.logout();
```
Generated App shells must expose logout as a visible account action. The default Make UI placement is the top-header current-user dropdown: avatar plus display name opens a menu below the header, and the menu contains `退出`. If the host project already has an equivalent account menu, use that established surface, but the action must still call `auth.logout()`.
Do not construct Org logout URLs in generated App code. make-gateway and Org own global logout behavior.
The SDK calls make-gateway logout and follows the gateway-provided App `redirectUri`. Generated App code must not rebuild this flow, consume deprecated `orgSsoLogoutUrl` directly, or patch wrong logout URLs in UI code. After the App loads again, `auth.init({ redirect: true })` decides whether the user should enter the Org login page.
## Error Handling Pattern
Handle 401/403 in one Make API adapter or data-source layer. Every frontend request to the Make backend, including schema/meta, records, lookup, user, department, and file APIs, must go through that shared handler. Read `request-adapter.md` for the implementation pattern.
## Anti-patterns
- Generating token mode, local debug token prompts, or no-login bypasses.
- Handling 401 only in App bootstrap while business requests use unhandled `auth.api` calls.
- Calling `auth.api` directly from scattered UI components without the shared 401/403 handler.
- Using raw `window.fetch('/api/make/...')` for any Make backend request.
- Automatically retrying unified login multiple times after state/challenge expiration.
- Hand-writing per-request 401/403 login wrappers when the SDK option `apiAuthRedirect: true` is available.
- Rebuilding Org authorize/logout URLs in App code.
- Hiding logout in page-specific actions instead of exposing it through the account/current-user surface.
- Hard-coding Org, unified-login, or account-center environment domains in App code.
- Clearing `zs_session` or `make_app_session` from App code.
- Treating every 403 as a login-expired state after SDK login check confirms the user is already authenticated.
references/request-adapter.md# Request Adapter
Use this reference when generating or reviewing Make backend request code.
## Rule
All frontend requests to Make backend must go through one shared adapter that wraps `auth.api`. In direct-gateway mode this includes schema/meta loading, record list/get/create/update/delete, cell updates, attachment/file APIs, lookup resolution, user candidates, department candidates, and other `/api/make/**` calls. In Service-fronted mode the same UI adapter calls Service-owned `/app/**` paths through `gatewayBaseUrl=/api/make`.
Do not call raw `window.fetch('/api/make/...')`. Do not scatter unhandled `auth.api` calls across UI components, drawers, tables, field editors, or route loaders.
## Request Shape
Business code should pass relative paths to `auth.api`. If an absolute URL is unavoidable, it must be under the same origin and path scope as `gatewayBaseUrl`; otherwise the SDK rejects it.
The SDK defaults Make backend requests to `credentials: 'include'`. Generated adapters may still keep a shared request init so cookie behavior is auditable in one place; do not repeat credential handling in UI components.
```ts
const makeRequestInit = {
credentials: 'include' as const
};
```
Direct gateway mode example:
```ts
export async function listRecords(payload: unknown) {
return auth.api.post('/data/v1/record', payload, makeRequestInit);
}
```
Service-fronted mode example. Use this only after `service-fronted-mode.md` confirms the `UI -> Service -> make-gateway` contract:
```ts
// With createMakeAppAuth({ gatewayBaseUrl: '/api/make', ... }),
// this reaches browser path /api/make/app/schema.
export async function loadSchema() {
return auth.api.get('/app/schema', makeRequestInit);
}
export async function listRecords(entityKey: string, payload: unknown) {
return auth.api.post(`/app/records/${entityKey}`, payload, makeRequestInit);
}
```
Apply the same adapter path to schema/meta, list, get, create, update, delete, file, lookup, user, and department APIs. Do not fix one endpoint while leaving another endpoint on raw fetch or a different helper.
Do not use `/app/**` in direct gateway mode. Do not use `/data/**` or `/meta/**` from UI in Service-fronted mode.
For passive browser resource loading, `auth.api` cannot wrap `<img src>`, `<object data>`, or a plain file link. In Service-fronted apps, normalize Make file values to the Service-owned download proxy URL `/api/make/app/files/download/**` before rendering them. Do not render raw `/data/v1/download/**`, `/make/data/v1/download/**`, or `/api/make/data/v1/download/**` values.
Custom headers are allowed through the SDK request options:
```js
const result = await auth.api.post('/data/v1/record', body, {
credentials: 'include',
headers: {
'X-Make-Target': 'MakeService.ListResources',
'X-Trace-Id': traceId
}
});
```
If a list request has no real filters, omit `filter`. Do not send `filter: []`.
## Error Handling
When `apiAuthRedirect: true` is available, the SDK owns the normal unified-login 401/403 redirect. The shared adapter still owns three things:
- preventing scattered `auth.api` calls
- fallback UI for errors that cannot redirect
```js
async function handleMakeRequestError(error) {
if (error instanceof MakeAppUnauthorizedError) {
showNeutralLoading();
if (!makeAuthConfig.apiAuthRedirect) {
await auth.login({ redirect: true });
}
return;
}
if (error instanceof MakeAppForbiddenError) {
renderForbidden();
return;
}
throw error;
}
export async function listRecords(payload) {
try {
return await auth.api.post('/data/v1/record', payload, {
credentials: 'include',
headers: { 'X-Make-Target': 'MakeService.ListResources' }
});
} catch (error) {
return handleMakeRequestError(error);
}
}
```
Do not call `auth.logout({ redirect: false })` as the default 401 path when `apiAuthRedirect` is enabled. Use logout-before-login only for a diagnosed stale or corrupted App session, not as the normal request adapter behavior.
## Tests
When touching request code, add or update tests for:
- 403 forbidden response
- unified-login API 401/403 with `apiAuthRedirect: true`
- schema/list/create/update/delete 401 entering the shared expired-session handler
- schema/meta/list/get/create/update/delete/file/lookup/user/department calls use the shared adapter and the same cookie-capable request init
- Service-fronted auth and business proxy calls use the same host-context helper, deriving `X-Forwarded-Host` from inbound `Host` and not passing through client-supplied `X-Forwarded-Host`
- Service-fronted proxy calls add `X-Forwarded-Proto` before calling make-gateway
- Service-fronted proxy calls use k8s-internal make-gateway paths without the external `/api` prefix, for example `http://make-gateway/make/auth/**`, `/make/meta/**`, and `/make/data/**`
- no raw `window.fetch('/api/make/...')`
- no scattered unhandled `auth.api` calls in UI components
references/sdk-integration.md# SDK Integration
Use `@qfeius/make-app-auth` for Make App authentication and Make backend requests.
## Responsibility Boundary
The SDK owns auth bootstrap, unified-login browser state, cookies, redirects, logout, and scoped request helpers under `/api/make/**`.
App code owns page state, user-facing messages, and business feature logic.
The SDK and this skill diagnose auth only. They should answer whether a request is authenticated, unauthenticated, forbidden, expired, missing a cookie, blocked by callback routing, or failing because the Service auth proxy contract is absent.
The SDK should not diagnose runtime schema shape, object-field normalization, canvas-table rendering, white screens, published asset routing, or business data correctness. When authenticated `/api/make/**` requests succeed but UI rendering fails, hand off to `makeui` and the host app smoke tests.
SDK improvement backlog, not current generated-App contract:
- `unauthenticated`
- `forbidden`
- `session_expired`
- `state_expired`
- `challenge_expired`
- `cookie_missing`
- `cookie_not_sent`
- `callback_exchange_failed`
- `auth_proxy_missing`
- `logout_failed`
These labels describe the desired future diagnostic vocabulary only. Do not generate App code that branches on these labels unless the installed SDK type definitions expose them. Current generated App code should branch on the published SDK contract, such as `MakeAppUnauthorizedError`, `MakeAppForbiddenError`, `status`, and `reason`.
Do not add schema, table, route-rendering, or publish-platform labels to SDK diagnostics.
## Dependency
Use the public npm package by default when the SDK behavior is stable:
```json
{
"dependencies": {
"@qfeius/make-app-auth": "^0.1.3"
}
}
```
Install command:
```bash
pnpm add @qfeius/make-app-auth@^0.1.3 --registry=https://registry.npmjs.org/
```
Published unified-login Apps require `@qfeius/make-app-auth >= 0.1.3`. Do not generate or publish Apps with older npm dependencies.
## Startup Shape
Generated Apps use unified login. Direct App entry should call `auth.init({ redirect: true })` and go to the Org login page instead of showing an App-owned login page.
```js
import {
createMakeAppAuth
} from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({
gatewayBaseUrl: '/api/make',
unifiedLogin: true,
apiAuthRedirect: true
});
const boot = await auth.init({ redirect: true });
if (boot.status === 'authenticated') {
renderApp({
auth,
context: boot.context,
onLogout: () => auth.logout()
});
} else if (boot.reason === 'state_expired' || boot.reason === 'challenge_expired') {
renderLoginExpired({
message: boot.message || '登录已过期,请重新登录',
onRelogin: () => auth.login({ redirect: true })
});
} else if (boot.status === 'forbidden') {
renderForbidden();
} else {
renderLoading();
}
```
## Business Requests
Use `auth.api` for Make backend calls. In direct gateway mode, the SDK handles `/api/make`, cookies, JSON request bodies, and unified auth errors.
`gatewayBaseUrl` is the SDK option for the Make backend API base. In Make tooling the effective local-preview backend origin is resolved by `makecli configure resolve --target local-preview --output=json`; consume the returned `make_api_origin` and add `/api/make` in Service-only local preview. Legacy `configure get environment` / `configure get meta-server-url` probing is only a fallback for older makecli installations. Reuse that host Make backend config when generating App configuration; do not invent a separate backend URL setting.
For a deployed same-origin direct-gateway unified-login App, prefer the SDK default `/api/make`. For a Service-fronted published App, also configure `gatewayBaseUrl: "/api/make"` and keep business calls as `auth.api("/app/**")`, which normally reaches `/api/make/app/**`. Browser code must not read `~/.make/config` directly.
`gatewayBaseUrl` is not the unified login, Org, or account-center URL.
Business code should pass relative paths to `auth.api`, for example `/data/v1/record`. Do not generate absolute business URLs. If an absolute URL is unavoidable, it must be under the same origin and path scope as `gatewayBaseUrl`; otherwise the SDK rejects it.
Available helpers:
```js
auth.api.get(path, init);
auth.api.post(path, body, init);
auth.api.put(path, body, init);
auth.api.patch(path, body, init);
auth.api.delete(path, init);
auth.api.request(path, init);
```
For shared adapter, 401/403, request headers, and no-scattered-`auth.api` rules, read `request-adapter.md`.
For Service-fronted Apps where UI calls App Service and Service calls make-gateway, read `service-fronted-mode.md`; keep browser auth/oauth paths under `/api/make/**` and keep Service upstream paths under internal `/make/**`.
For published/vibe Apps, auth integration is not complete until the agent or platform has verified the domain entry path, current-context challenge/context path, callback/session completion, and at least one authenticated business request through the generated adapter. Run `scripts/audit-auth-contract.mjs <project-root> --published` when a generated project tree is available. Do not make the user discover these failures by opening DevTools after publish.
## Tests To Add When Touching Auth
- 403 forbidden response.
- Unified-login API 401/403 with `apiAuthRedirect: true` redirects through SDK login once.
- Unified-login unauthenticated state does not loop redirects.
- Business-request 401 from schema/list/create/update/delete enters the shared expired-session handler.
- Make backend calls are routed through the shared adapter; no raw `window.fetch('/api/make/...')` and no scattered unhandled `auth.api` calls in UI components.
- Unified-login state/challenge expiration renders a relogin prompt instead of automatically redirecting again.
- Authenticated unified-login state exposes a visible logout action wired to `auth.logout()`, preferably in the top-header current-user menu defined by `makeui`.
- Logout does not consume or rewrite `orgSsoLogoutUrl` in App code; the SDK calls make-gateway logout and follows gateway `redirectUri`, which should be an App return URL rather than an account-center or Org logout URL.
- Service-fronted unified-login Apps proxy `/api/make/auth/current-context`, `/api/make/auth/session/complete`, and logout through Service without swallowing redirect or cookie headers.
## Never Generate
- Reading, storing, or forwarding Org access tokens.
- Reading, writing, or deleting `zs_session` or `make_app_session`.
- Browser code that tries to read `~/.make/credentials`.
- Raw `Authorization` header logic outside the SDK.
- Token-mode SDK options such as `unifiedLogin: false`, `accessToken`, `token`, or `tokenProvider`.
- Token-mode environment switches such as `VITE_MAKE_AUTH_MODE=token`.
- Using makecli credentials in UI. If local preview needs makecli token, keep it in Service-only code guarded by `MAKE_APP_LOCAL_PREVIEW=true`.
- Hard-coded Org, unified-login, or account-center domains in App code.
- Passing arbitrary absolute URLs to `auth.api`.
- Constructing Org OAuth URLs, `redirect_uri`, `state`, or `code_challenge`.
- Constructing Org logout URLs or adding App-side fallback logic for `token不能为空`.
- Handling Org OAuth `code` in the App.
- Raw `window.fetch('/api/make/...')` for Make backend calls.
- Monkey-patching `window.fetch`.
- Treating browser context data as server-trusted authorization.
references/service-fronted-mode.md# Service-Fronted Mode
Use this reference when the generated App keeps a Service layer between UI and make-gateway.
## Boundary
Preserve this contract:
```text
UI -> auth.api('/app/**') -> App Service -> make-gateway -> Make Platform
```
UI business code calls Service-owned paths such as:
```js
await auth.api.get('/app/schema');
await auth.api.post('/app/records/customer', payload);
```
UI must not bypass Service by calling `/data/**`, `/meta/**`, meta/data service domains, or k8s-internal service names directly.
## Local Preview
Local preview keeps the same UI and Service source as published mode. UI still uses `createMakeAppAuth({ gatewayBaseUrl: "/api/make", unifiedLogin: true, apiAuthRedirect: true })` and business code still calls `auth.api("/app/**")`.
When `MAKE_APP_LOCAL_PREVIEW=true`, the local Service should run `makecli configure resolve --target local-preview --output=json` on the server side, read `make_api_origin`, and use the makecli token only for Service-to-Make requests. The browser must not receive the token, must not configure SDK token mode, and must not read `~/.make/credentials`.
Enable local preview only from the local dev command, for example:
```bash
MAKE_APP_LOCAL_PREVIEW=true pnpm run dev
```
A generated project may wrap that command as `pnpm run dev:preview`, but it must not persist `MAKE_APP_LOCAL_PREVIEW=true` in `.env`, `.env.local`, `.env.example`, generated README setup steps, or any deployment environment. Env files are easy to copy between local and publish paths; treating the flag as a one-shot process variable keeps preview behavior out of committed and deployed runtime.
Local preview must use the same backend environment that makecli is using. Resolve it with:
```bash
makecli configure resolve --target local-preview --output=json
```
`makecli --env <dev|test|production> configure resolve --target local-preview --output=json` may override the configured environment for a single command; a local preview script may expose an equivalent explicit environment option, but it must not silently hard-code dev. Treat the returned `make_api_origin` as a bare public gateway origin and add `/api/make` inside the adapter. If the installed makecli does not yet support `configure resolve`, legacy fallback may read `configure get environment` plus profile `meta-server-url`, but fallback code must keep the same precedence and normalize path-scoped values such as `https://dev-make.qtech.cn/api/make` back to origin. Public gateway traffic passes through nginx, so Service-to-Make calls in local preview use the browser-facing `/api/make` scope.
Runtime mode matrix:
| Mode | Trigger | Gateway origin | Gateway scope | Credential |
| --- | --- | --- | --- | --- |
| Local preview | temporary process env `MAKE_APP_LOCAL_PREVIEW=true` | `makecli configure resolve --target local-preview --output=json` field `make_api_origin` | `/api/make` | Service-side makecli token and configured tenant/operator headers |
| Published | flag absent or false in deployed runtime | deployment-injected k8s internal origin, for example `http://make-gateway.make-dev` | `/make` | Browser App session Cookie plus forwarded host/proto |
Generated code should model this explicitly, for example `runtimeMode -> gatewayOrigin + gatewayScope + credentialStrategy`. Do not let individual routes hard-code `/api/make` or `/make`, and do not use local-preview public gateway settings in published runtime.
In local preview, Service should handle:
- `GET /api/make/auth/current-context`: return a preview context with real values available from makecli config/token claims, such as `userId`, `tenantId`, `name`, and `avatar`, plus `authMode: "token"` and `localPreview: true`.
- `GET /api/make/auth/runtime-view`: return a preview runtime view with `authMode: "token"` and `localPreview: true`.
- `/api/make/app/**`: call `make_api_origin + /api/make` with server-side `Authorization: Bearer <makecli token>`, makecli tenant/operator headers when configured, and the `/api/make` path scope.
This is a development-only convenience. Published runtime must keep the deployed chain below and must fail closed if local preview mode is enabled in production.
The preview auth routes must be strictly gated. Do not register or mount preview `current-context` / `runtime-view` handlers unless `MAKE_APP_LOCAL_PREVIEW=true`, or guard them inline before returning any preview response. When the flag is absent or false, `/api/make/auth/current-context` and `/api/make/auth/runtime-view` must fall through to the make-gateway auth proxy.
Match preview auth routes by path only, never by a raw URL string that may include a query string. The SDK can call `/api/make/auth/current-context?return_url=...`; that must still return the local preview context when `MAKE_APP_LOCAL_PREVIEW=true`. In Fetch-style handlers, compare `new URL(request.url).pathname`. In Express handlers mounted at `/api/make/auth`, compare `req.path` to `/current-context` or `/runtime-view`. Do not use exact equality against `req.url` or `req.originalUrl` for these preview routes.
Safe shape:
```ts
if (isLocalPreviewEnabled() && url.pathname === '/api/make/auth/current-context') {
return localPreviewCurrentContext();
}
// Express equivalent when mounted with app.use('/api/make/auth', ...)
if (isLocalPreviewEnabled() && req.path === '/current-context') {
return localPreviewCurrentContext();
}
if (url.pathname.startsWith('/api/make/auth/')) {
return proxyMakeAuth(request, stripBrowserMakePrefix(url));
}
```
Unsafe shape:
```ts
// Wrong: this shadows the published auth proxy.
app.get('/api/make/auth/current-context', localPreviewCurrentContext);
app.use('/api/make/auth', proxyMakeAuth);
// Wrong: SDK requests can append ?return_url=..., so this misses local preview.
if (isLocalPreviewEnabled() && req.originalUrl === '/api/make/auth/current-context') {
return localPreviewCurrentContext();
}
```
## Deployed Chain
Browser calls stay same-origin under `gatewayBaseUrl=/api/make`.
Business APIs:
```text
browser -> /api/make/app/** -> App Service -> http://make-gateway/make/meta|data/**
```
Auth APIs:
```text
browser -> /api/make/auth/** -> App Service -> http://make-gateway/make/auth/**
browser -> /api/make/oauth/** -> App Service -> http://make-gateway/make/oauth/**
```
Service code running inside the cluster must call k8s-internal make-gateway routes without the external `/api` prefix. UI code must not call internal routes directly.
Published runtime must not reuse makecli public gateway settings. `MAKE_API_BASE_URL` / `MAKE_SERVER_URL` in deployed Service containers are k8s internal gateway origins, and Service adds `/make` before calling auth, meta, or data APIs.
Do not publish a Service-fronted unified-login App without namespace-level auth and OAuth proxies. A missing `/api/make/auth/current-context` route means the browser cannot start or verify unified login, even if business routes such as `/api/make/app/schema` work. An endpoint-only allowlist is also incomplete because future auth routes and recovery callbacks must use the same proxy path.
Do not drop the `/make` segment from browser-facing Service-fronted auth routes. The current platform entry uses `/api/make/auth/**` and `/api/make/oauth/**` for unified login, while Service-to-gateway upstream calls still use internal `/make/**` paths without the external `/api` prefix.
Do not fix auth proxy gaps by adding a broad `/api/make/** -> /make/**` passthrough. Auth and OAuth are the default transparent namespaces; Service-owned business APIs stay under explicit `/api/make/app/**` adapters, and unmatched `/api/make/**` paths should fail closed.
Published `/api/make/auth/current-context` must be the make-gateway response. It may return an authenticated context or `401 + authorizationUrl`, but it must not return a local preview context such as `localPreview: true`, `grantVersion: "local-preview"`, `authMode: "token"`, or `userId: "local-preview-user"`.
The Service auth proxy must forward browser auth context:
- request `Cookie`
- request host/proxy headers needed by make-gateway to resolve the published App domain, especially `X-Forwarded-Host` and `X-Forwarded-Proto`
- gateway `Set-Cookie`, `Location`, and status code back to the browser
Do not convert the gateway response into a JSON envelope for auth routes.
## Attachment Download Proxy
In Service-fronted apps, file previews and downloads must stay on Service-owned browser paths:
```text
browser img/link -> /api/make/app/files/download/** -> App Service -> make-gateway /make/data/v1/download/**
```
Do not use raw Make download paths such as `/data/v1/download/**`, `/make/data/v1/download/**`, or `/api/make/data/v1/download/**` as UI `src`, `href`, or file metadata URLs when a Service proxy exists.
When the Make download endpoint requires `Authorization`, the browser still must not receive the token. The Service download route must:
- derive forwarded host/proto with the same helper used by auth/business gateway calls
- require the browser App session Cookie
- verify the session first through make-gateway, for example `${makeAuthBaseUrl}/auth/current-context`
- use the deployment-injected download token only after the session check passes
- remove or overwrite any inbound browser `Authorization` before attaching the Service token
- return the upstream binary bytes or stream with safe `Content-Type` and `Content-Disposition`
- keep tokens, cookies, Authorization, and signed query strings out of logs and `/api/config`
## Host Context Helper
Generated Service-fronted Apps must centralize host/proto forwarding in one helper and use it for both auth routes and business routes. Do not trust or pass through client-supplied `X-Forwarded-Host`; derive it from inbound `Host`.
```ts
function applyForwardedHostContext(headers: Headers, source: Headers): void {
const host = source.get('host');
if (host) {
headers.set('x-forwarded-host', firstHeaderValue(host));
}
if (!headers.get('x-forwarded-proto')) {
headers.set('x-forwarded-proto', isLocalHost(headers.get('x-forwarded-host')) ? 'http' : 'https');
}
}
function firstHeaderValue(value: string): string {
const commaIndex = value.indexOf(',');
return commaIndex >= 0 ? value.substring(0, commaIndex).trim() : value.trim();
}
function isLocalHost(host: string | null): boolean {
const hostname = stripPort(host);
return hostname === 'localhost' || hostname === '127.0.0.1';
}
function stripPort(host: string | null): string | null {
if (!host) {
return host;
}
const portIndex = host.indexOf(':');
return portIndex >= 0 ? host.substring(0, portIndex) : host;
}
```
Apply the helper before every upstream make-gateway request:
```ts
const authHeaders = pickProxyHeaders(inboundHeaders, ['cookie']);
applyForwardedHostContext(authHeaders, inboundHeaders);
const businessHeaders = new Headers(init.headers);
applyForwardedHostContext(businessHeaders, inboundHeaders);
```
Internal make-gateway URLs must not use the external `/api` prefix:
```ts
const makeAuthBaseUrl = 'http://make-gateway/make';
const makeBusinessBaseUrl = 'http://make-gateway/make';
businessHeaders.set('Content-Type', 'application/json');
businessHeaders.set('X-Make-Target', 'MakeService.ListResources');
const recordPayload = { appKey: config.appKey, entityKey, fields, pagination };
await fetch(`${makeAuthBaseUrl}/auth/current-context`, { headers: authHeaders });
await fetch(`${makeBusinessBaseUrl}/data/v1/record`, {
method: 'POST',
headers: businessHeaders,
body: JSON.stringify(recordPayload)
});
```
Local-preview URLs are different because they leave the user's machine through the public gateway resolved by makecli:
```ts
const publicGatewayOrigin = readMakecliResolveJson().make_api_origin;
const localPreviewBaseUrl = `${publicGatewayOrigin}/api/make`;
const schemaHeaders = new Headers(makecliTokenHeaders);
schemaHeaders.set('Content-Type', 'application/json');
schemaHeaders.set('X-Make-Target', 'MakeService.GetResource');
const dataHeaders = new Headers(makecliTokenHeaders);
dataHeaders.set('Content-Type', 'application/json');
dataHeaders.set('X-Make-Target', 'MakeService.ListResources');
await fetch(`${localPreviewBaseUrl}/meta/v1/schema`, {
method: 'POST',
headers: schemaHeaders,
body: JSON.stringify({ appKey: config.appKey })
});
await fetch(`${localPreviewBaseUrl}/data/v1/record`, {
method: 'POST',
headers: dataHeaders,
body: JSON.stringify(recordPayload)
});
```
## Session Complete
When proxying `/api/make/auth/session/complete`, Service must return the gateway response to the browser:
- preserve `302`
- preserve `Set-Cookie`
- preserve `Location`
In Node Service code, use `redirect: "manual"` or the equivalent. Do not let server-side fetch follow the redirect internally.
## Validation
These checks belong to the agent, generated tests, CI, or publish pipeline. Do not require the end user to open DevTools or inspect k8s logs after publish to discover the issue.
- Run `scripts/audit-auth-contract.mjs <project-root> --mode service-fronted --published` when a generated project tree is available.
- Browser business requests go to Service-owned `/api/make/app/**` paths.
- Browser auth and OAuth requests go to namespace proxies under `/api/make/auth/**` and `/api/make/oauth/**`.
- `/api/make/auth/current-context` is reachable from the published domain and returns a challenge or authenticated context, not a Service 404.
- In published mode, `/api/make/auth/current-context` and `/api/make/auth/runtime-view` are not served by local preview handlers and do not return `localPreview`, `local-preview-user`, `grantVersion: "local-preview"`, or `authMode: "token"`.
- `/api/make/auth/session/complete` and at least one future/unknown auth-namespaced path use the same proxy mapping instead of a hand-written endpoint list.
- UI uses `createMakeAppAuth({ gatewayBaseUrl: "/api/make", unifiedLogin: true, apiAuthRedirect: true })`, then calls `auth.api("/app/**")` for Service-owned business routes.
- Service calls internal business routes such as `http://make-gateway/make/meta/**` and `http://make-gateway/make/data/**`.
- Service calls internal auth routes such as `http://make-gateway/make/auth/**`.
- Service does not call k8s-internal `/api/make/auth/**`, `/api/make/oauth/**`, `/api/make/meta/**`, or `/api/make/data/**`; `/api/make` is only for browser/ingress access.
- Local preview calls makecli resolve `make_api_origin` with `/api/make/**`, not k8s-internal `/make/**`.
- Published mode and local preview mode are both covered by contract tests; tests must fail if `MAKE_APP_LOCAL_PREVIEW=false` still routes upstream calls through `/api/make`.
- Service does not expose a production catch-all for all `/api/make/**`; unknown paths outside documented auth/oauth/app routes fail closed.
- Service forwards browser cookies on every auth and business request that depends on App session.
- Service derives `X-Forwarded-Host` from inbound `Host`, does not pass through client-supplied `X-Forwarded-Host`, and adds `X-Forwarded-Proto`; auth and business requests share this helper.
- `session/complete` reaches the browser as `302 + Set-Cookie + Location`.
- At least one authenticated schema/meta request and one record-list request pass through the same Service/auth adapter path before reporting publish success.
references/service-fronted-node-example.md# Service-fronted Node example
Use this compact example after reading `service-fronted-mode.md`. It illustrates
the route ownership and test seams; it is not a project template and must not
replace the host's established Service layering.
## Reference shape
```text
apps/ui/src/auth.ts # unified-login SDK bootstrap
apps/ui/src/makeApi.ts # auth.api calls to /app/**
apps/service/src/routes.ts # narrow browser-route dispatcher
apps/service/src/makeGatewayProxy.ts # auth/business proxy adapter
apps/service/src/makecliPreview.ts # local-preview adapter only
```
Keep all browser business calls behind `auth.api` and the Service-owned
`/api/make/app/**` namespace:
```ts
export const auth = createMakeAppAuth({
gatewayBaseUrl: '/api/make',
unifiedLogin: true,
apiAuthRedirect: true,
});
export const loadSchema = () => auth.api.get('/app/schema', {
credentials: 'include',
});
export const listRecords = (entityKey: string, payload: unknown) =>
auth.api.post(`/app/records/${entityKey}`, payload, {
credentials: 'include',
headers: { 'X-Make-Target': 'MakeService.ListResources' },
});
```
## Route ownership
- `/api/make/auth/**` and `/api/make/oauth/**` forward to the published
make-gateway auth namespace. Preserve the upstream status, `Set-Cookie`, and
`Location`; use manual redirect handling.
- `/api/make/app/**` contains explicitly registered Service business routes.
Do not create a broad `/api/make/**` passthrough; unmatched paths fail closed.
- `MAKE_APP_LOCAL_PREVIEW=true` may provide only the documented local preview
handlers. Its makecli token and resolved gateway origin stay inside Service.
When the flag is absent, `current-context` and `runtime-view` continue through
the published auth proxy.
For every upstream request, forward the browser Cookie and derive
`X-Forwarded-Host` from inbound `Host`; add `X-Forwarded-Proto`. Do not trust a
client-provided forwarded-host header.
## Required tests
- Published `session/complete` returns the upstream `302`, `Set-Cookie`, and
`Location` unchanged.
- Local-preview paths use `makecli configure resolve` and server-side credentials
only; published paths use the internal `/make/**` scope.
- Query strings do not bypass the local-preview path guard.
- Unknown `/api/make/**` routes return a closed failure response.
Read `service-fronted-mode.md` for the full local-preview matrix, proxy rules,
attachment handling, and validation checklist.
references/troubleshooting.md# Troubleshooting
Use this when diagnosing Make App auth failures. This runbook is for the agent, platform, or operator; it is not a checklist that a vibe user must perform after publishing.
Goal: quickly decide whether the issue is App code, SDK usage, make-gateway, Org, browser cookie state, Service auth proxy, or local proxy configuration.
Scope boundary: stop this auth runbook once authenticated Make backend requests are reaching the app. Schema normalization, table rendering, blank pages, object routes, and business UI states belong to `makeui` and app smoke tests.
## First Split
Generated and reviewed Apps use unified login only.
- If generated code contains token mode, `unifiedLogin: false`, `accessToken`, `tokenProvider`, local credentials, or no-login bypasses, treat that as an App code bug.
- Unified login requires a real browser, external domain/ngrok or published domain, Org whitelist, cookies, and callback routing.
## Evidence To Collect First
- Request URL and status for the failing authenticated call: `/api/make/**` in direct-gateway mode, or `/api/make/auth/**` / `/api/make/oauth/**` / `/api/make/app/**` in Service-fronted mode.
- Whether the request was made through the shared Make API adapter that wraps `auth.api`.
- Browser request headers: especially Cookie presence, without exposing full token values in reports.
- User-facing message shown by the App.
- make-gateway response body, request ID, or trace ID when available.
For published/vibe Apps, collect this evidence with an automated browser, request tracing, CI smoke, platform logs, or generated tests where possible. The expected product behavior is that the user opens the published domain and either reaches the app or sees a clear auth/error state; the user should not need DevTools, k8s log access, or cookie knowledge.
## Unified Mode Checklist
For redirect/callback failures:
- Confirm the App is reachable through a registered external HTTPS domain or ngrok.
- Confirm Org whitelist contains the exact callback `redirect_uri`.
- Direct gateway: confirm `/api/make/**` routes to make-gateway from that domain.
- Service-fronted: confirm `/api/make/auth/**`, `/api/make/oauth/**`, and the documented business path such as `/api/make/app/**` route to App Service from that domain.
- Confirm every schema/meta/list/create/update/delete/file/user/department request goes through the shared Make API adapter, not scattered unhandled `auth.api` calls or raw fetch.
- Confirm browser accepts and sends cookies for the App domain.
- Confirm the page does not auto-loop login on every 401.
- If the App URL contains `make_auth_error=session_expired`, the expected UI is a "登录已过期,请重新登录" prompt, not another immediate redirect.
For Service-fronted Apps:
- Confirm browser business requests go to `/api/make/app/**` or the host-documented Service business path, not directly to `/api/make/meta/**` or `/api/make/data/**`.
- Confirm `/api/make/auth/**` and `/api/make/oauth/**` are transparent proxy traffic from Service to `http://make-gateway/make/auth/**` and `http://make-gateway/make/oauth/**`.
- Confirm `/api/make/auth/current-context` exists on the published domain. A Service 404 for this route is an auth proxy contract bug, not a user login problem.
- Confirm Service-fronted UI configures `gatewayBaseUrl: "/api/make"` so `auth.api("/app/**")` reaches `/api/make/app/**`.
- Confirm Service calls k8s-internal business routes as `http://make-gateway/make/meta/**` and `http://make-gateway/make/data/**`; `/api/make/meta/**` usually indicates the wrong internal gateway path.
- Confirm Service proxy keeps `session/complete` redirects manual. If Node `fetch` follows the gateway 302 internally, the browser URL can stay on `/api/make/auth/session/complete?...` and cause a login loop.
For cookie problems:
- Inspect browser DevTools Application/Cookies for App domain.
- Check whether duplicate session cookies exist.
- Check request Cookie header sent to `/api/make/**` in direct-gateway mode, or `/api/make/auth/**` / `/api/make/oauth/**` / `/api/make/app/**` in Service-fronted mode.
- Confirm make-gateway logs for session verification and challenge generation.
- Confirm the request is same-origin or has the expected credentials behavior.
For logout problems:
- Confirm App calls `auth.logout()`, not a hand-built Org URL.
- Confirm make-gateway returns the SDK-expected logout result.
- Confirm make-gateway returns the App return URL as `redirectUri`; account-center or Org logout URLs should not be exposed to generated App code.
- Verify post-logout browser state with a real browser, not curl only.
## Quick Decision
- Generated token mode or `Authorization`: App code bug; regenerate unified-login auth.
- 401 with no Cookie: browser cookie/session problem or exchange did not set cookie.
- 401 with Cookie present: make-gateway or Org token verification problem.
- Repeated redirects after login: callback/exchange/cookie persistence problem, not UI layout.
- Gateway 404 for `/api/make/meta/**` from Service: internal gateway business path should likely be `/make/meta/**`.
- Browser stays on `/api/make/auth/session/complete?login_ticket=...`: Service likely swallowed the gateway 302 instead of returning it to the browser.
- Authenticated schema/list APIs return 200 but the page is blank: leave auth runbook; this is likely runtime-schema normalization, render error handling, or UI smoke coverage.
## Fast Root-Cause Labels
- `token_mode_generated`: App generated token mode or local-token fallback even though only unified login is supported.
- `redirect_uri_not_whitelisted`: Org rejects callback.
- `api_proxy_missing`: frontend loads, but the expected authenticated prefix does not reach the correct backend.
- `auth_proxy_missing`: Service-fronted App did not proxy `/api/make/auth/**` and `/api/make/oauth/**`, so unified login cannot start or complete.
- `api_adapter_missing`: Make backend requests bypass the shared adapter, so business-request 401 is not routed into the login recovery flow.
- `cookie_not_set`: exchange/login succeeded but browser has no App session cookie.
- `cookie_not_sent`: browser stores cookie but request does not include it.
- `logout_contract_mismatch`: App expects redirect/link but gateway returns a different shape.
- `state_expired`: user stayed on Org login/callback flow too long; SDK should show relogin prompt.
- `challenge_expired`: gateway challenge expired before callback completed; SDK should show relogin prompt.
- `service_followed_complete_redirect`: App Service followed `session/complete` 302 internally; browser did not receive Set-Cookie/Location as intended.
- `service_internal_gateway_path_mismatch`: Service called `/api/make/meta|data/**` on k8s-internal gateway instead of `/make/meta|data/**`.
references/unified-login-mode.md# Unified Login Mode
Use unified mode for generated and published Make Apps.
## Preconditions
Unified mode requires:
- A deployed App domain or an ngrok/external HTTPS domain that can receive browser callbacks.
- The callback `redirect_uri` registered in Org whitelist.
- Direct-gateway Apps: `/api/make/**` from the external domain routed to make-gateway.
- Service-fronted Apps: `/api/make/auth/**` and `/api/make/oauth/**` from the external domain routed to App Service, with the Service proxying auth/oauth to make-gateway.
- Browser testing with real cookies enabled.
If these are missing in local development, report the missing prerequisite as a blocker. Do not fall back to token mode or a no-login bypass.
## SDK Setup
```js
const auth = createMakeAppAuth({
gatewayBaseUrl: '/api/make',
unifiedLogin: true,
apiAuthRedirect: true
});
const boot = await auth.init({ redirect: true });
if (boot.status === 'authenticated') {
renderApp({ auth, context: boot.context });
} else if (boot.status === 'redirecting') {
renderLoading();
} else if (boot.reason === 'state_expired' || boot.reason === 'challenge_expired') {
renderLoginExpired({
message: boot.message || '登录已过期,请重新登录',
onRelogin: () => auth.login({ redirect: true })
});
} else {
await auth.login({ redirect: true });
}
```
## Rules
- Published unified-login Apps should use the Org login page directly. Do not render an App-owned login page, login transition page, or signed-out completion page.
- Keep only a neutral loading state while SDK/browser redirection is in progress.
- If login callback returns to the App with an expired state/challenge marker, show a simple relogin prompt. Do not immediately redirect again.
- The relogin button should call `auth.login({ redirect: true })`; the SDK removes the expired-login URL parameters before creating the next challenge.
- Unified-login Apps must not pass `accessToken`, `token`, or `tokenProvider`; after login, browser requests rely on the App session Cookie written by make-gateway.
- Set `apiAuthRedirect: true` for generated unified-login Apps with `@qfeius/make-app-auth >= 0.1.3`.
- Unified login challenge URLs are returned by make-gateway. Logout returns an App `redirectUri`; App UI code must not configure or hard-code account-center or Org logout URLs.
- Do not construct Org authorize URLs in App code.
- Do not handle `code` or `state` in App code unless the SDK contract explicitly requires it.
- Do not use the App domain directly as an Org logout `redirect_uri`.
- Do not normalize or rewrite `orgSsoLogoutUrl` in App code. `@qfeius/make-app-auth` calls make-gateway logout and follows the gateway-provided App `redirectUri`.
- Authenticated unified-login pages must include a visible logout button or icon in the App shell header so testers can re-enter the phone/code login flow without editing cookies.
- Test callback and cookie behavior in a real browser, not only curl.
- Keep environment-specific domains in configuration or gateway responses, not hard-coded App UI logic.
## Expected Request Path
```text
browser -> App domain/ngrok -> local or deployed frontend -> /api/make proxy -> make-gateway -> Org
```
ngrok only exposes the frontend. It does not automatically forward `/api/make/**` unless the frontend/dev server proxy is configured.
Service-fronted deployed Apps use this split:
```text
browser -> App domain -> /api/make/auth/**, /api/make/oauth/**, and /api/make/app/** -> App Service -> make-gateway
```
Read `service-fronted-mode.md`. Do not duplicate Service proxy details in generated UI code, and do not drop `/make` from browser-facing auth/oauth paths.
## Validation Checklist
- Run `scripts/audit-auth-contract.mjs <project-root> --published` when a generated project tree is available.
- Open the App through the registered external domain or ngrok URL.
- Direct gateway: confirm `/api/make/**` reaches make-gateway.
- Service-fronted: confirm `/api/make/auth/**`, `/api/make/oauth/**`, and the documented business Service path such as `/api/make/app/**` reach App Service, then Service reaches make-gateway through internal `/make/**`.
- Complete Org login and callback in the same browser.
- Confirm the next protected request succeeds without hand-written `Authorization`.
- Confirm business-request 401 from schema/meta/list/create/update/delete/file/user/department APIs follows `request-adapter.md`.
- Confirm logout uses the SDK and follows the make-gateway-provided App `redirectUri`. A browser stuck on a gateway `/api/org/public/sso/logout` or account-center URL means the gateway logout response or route configuration should be fixed, not patched in App UI code.
scripts/audit-auth-contract.mjs#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
const SDK_PACKAGE_NAME = '@qfeius/make-app-auth';
const MINIMUM_SDK_VERSION = [0, 1, 3];
const USAGE = `Usage:
node skills/make-app-auth/scripts/audit-auth-contract.mjs <project-root> [--mode direct|service-fronted|auto] [--published]
Checks Make App unified-login contract drift. This is auth-scoped; it does not verify schema rendering or UI layout.`;
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(USAGE);
process.exit(0);
}
let projectRoot = null;
let mode = 'auto';
let published = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === '--published') {
published = true;
continue;
}
if (arg === '--mode') {
mode = args[index + 1] || '';
index += 1;
continue;
}
if (arg.startsWith('--mode=')) {
mode = arg.slice('--mode='.length);
continue;
}
if (!projectRoot) {
projectRoot = arg;
continue;
}
failUsage(`Unexpected argument: ${arg}`);
}
if (!projectRoot) {
projectRoot = process.cwd();
}
if (!['auto', 'direct', 'service-fronted'].includes(mode)) {
failUsage(`Invalid --mode: ${mode}`);
}
const root = path.resolve(projectRoot);
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
failUsage(`Project root does not exist or is not a directory: ${root}`);
}
const uiFiles = collectSourceFiles(firstExisting([
'apps/ui/src',
'apps/ui',
'ui/src',
'src'
]));
const serviceFiles = collectSourceFiles(firstExisting([
'apps/service/src',
'apps/service',
'service/src',
'server/src'
]));
const serviceRuntimeFiles = serviceFiles.filter(isRuntimeSourceFile);
const allProjectFiles = collectSourceFiles(root);
const uiText = readJoined(uiFiles);
const serviceText = readJoined(serviceRuntimeFiles);
const projectText = readJoined(allProjectFiles);
const sdkDependencyDeclarations = findSdkDependencyDeclarations(allProjectFiles);
const sdkVersionOverrides = findSdkVersionOverrides(allProjectFiles);
const inferredMode = mode === 'auto' ? inferMode() : mode;
const failures = [];
const warnings = [];
if (uiFiles.length === 0) {
failures.push('no_ui_source: cannot find UI source under apps/ui/src, apps/ui, ui/src, or src');
}
if (!/@qfeius\/make-app-auth/.test(projectText) && !/createMakeAppAuth\s*\(/.test(projectText)) {
failures.push('sdk_missing: project does not appear to use @qfeius/make-app-auth');
}
for (const hit of findRawMakeFetches(uiFiles)) {
failures.push(`raw_make_fetch: ${relative(hit.file)} uses raw fetch for ${hit.url}; use auth.api through the shared adapter`);
}
if (hasTokenMode(uiText) || hasServiceTokenModeWithoutLocalPreview(serviceText)) {
failures.push('token_mode_present: make-app-auth skill only supports unified login in UI and published runtime; remove browser token options and unguarded Service token-mode switches');
}
if (published) {
if (sdkDependencyDeclarations.length === 0) {
failures.push('sdk_version_missing: published Apps must declare @qfeius/make-app-auth >= 0.1.3 in package.json');
}
for (const declaration of sdkDependencyDeclarations) {
const status = classifySdkVersionRange(declaration.version, MINIMUM_SDK_VERSION);
if (status === 'too-old') {
failures.push(`sdk_version_too_old: ${relative(declaration.file)} declares @qfeius/make-app-auth ${declaration.version}; published Apps require >= 0.1.3`);
} else if (status === 'unverifiable') {
failures.push(`sdk_version_unverifiable: ${relative(declaration.file)} declares unsupported @qfeius/make-app-auth source ${declaration.version}; published Apps require a verifiable registry range >= 0.1.3`);
}
}
for (const override of sdkVersionOverrides) {
const status = classifySdkVersionRange(override.version, MINIMUM_SDK_VERSION);
if (status === 'too-old') {
failures.push(`sdk_version_override_too_old: ${relative(override.file)} ${override.source} forces @qfeius/make-app-auth ${override.version}; published Apps require >= 0.1.3`);
} else if (status === 'unverifiable') {
failures.push(`sdk_version_override_unverifiable: ${relative(override.file)} ${override.source} uses unsupported @qfeius/make-app-auth source ${override.version}; published Apps require a verifiable registry range >= 0.1.3`);
}
}
if (!/apiAuthRedirect\s*:\s*true/.test(projectText)) {
warnings.push('published_api_auth_redirect_missing: generated unified-login Apps should set apiAuthRedirect:true with SDK >= 0.1.3');
}
if (hasUnsupportedSdkReadyStatus(uiText)) {
failures.push('unsupported_sdk_ready_status: @qfeius/make-app-auth init returns authenticated/redirecting/unauthenticated/forbidden/failed, not ready');
}
if (!hasRecoverableAuthExpiredHandling(uiText)) {
failures.push('recoverable_auth_expired_missing: generated unified-login Apps must handle state_expired/challenge_expired by showing a relogin prompt');
}
}
if (inferredMode === 'service-fronted') {
if (hasUiDirectGatewayCalls(uiText)) {
failures.push('service_fronted_ui_bypass: UI calls /data/** or /meta/** through auth.api; UI should call Service-owned /app/** paths');
}
for (const hit of findRawMakeDownloadResourceUrls(uiFiles)) {
failures.push(`service_fronted_raw_download_resource: ${relative(hit.file)} uses ${hit.attribute} with raw Make download URL ${hit.url}; use a Service-owned download proxy URL`);
}
if (hasRawMakeDownloadLiteral(uiText) && !hasServiceDownloadProxyLiteral(uiText)) {
warnings.push('service_fronted_download_proxy_not_obvious: UI mentions raw Make download paths but no Service download proxy path was found');
}
if (hasServiceFrontedApiOnlyPrefix(projectText)) {
failures.push('service_fronted_missing_make_prefix: Service-fronted published Apps use /api/make/auth/** and normally /api/make/app/**, not /api/auth/** or /api/app/**');
}
if (!hasServiceFrontedGatewayBaseMakePrefix(uiText)) {
failures.push('service_fronted_gateway_base_wrong: Service-fronted UI must configure gatewayBaseUrl as /api/make so auth.api("/app/**") reaches /api/make/app/**');
}
const hasAuthNamespaceProxy = hasServiceFrontedNamespaceProxy(serviceText, 'auth');
const hasOauthNamespaceProxy = hasServiceFrontedNamespaceProxy(serviceText, 'oauth');
if (!hasAuthNamespaceProxy || !hasOauthNamespaceProxy) {
failures.push('auth_proxy_missing: Service-fronted App must proxy /api/make/auth/** and /api/make/oauth/** as namespace-level routes to make-gateway');
}
if (hasBroadMakeGatewayPassthrough(serviceText)) {
failures.push('service_fronted_catch_all_passthrough: Service-fronted App must not proxy broad /api/make/** traffic to make-gateway; keep auth/oauth namespace proxies and explicit /api/make/app/** business routes');
}
if (hasBroadServiceAppBusinessPassthrough(serviceText)) {
failures.push('service_fronted_app_catch_all_passthrough: Service-fronted App must not proxy broad /api/make/app/** traffic to raw Make data/meta paths; keep Service-owned business routes explicit');
}
if (published && hasPublishedPreviewAuthShadow(serviceText)) {
failures.push('local_preview_auth_shadow: published /api/make/auth/current-context or runtime-view must not be served by local preview handlers; gate preview routes with MAKE_APP_LOCAL_PREVIEW=true and let published auth paths proxy to make-gateway');
}
if (hasQuerySensitivePreviewAuthRouteMatch(serviceText)) {
failures.push('local_preview_auth_query_sensitive_match: local preview current-context/runtime-view must match pathname or mounted req.path, not exact req.url/originalUrl; SDK may append return_url query parameters');
}
if (!/\/api\/make\/app\b/.test(projectText) && !/auth\.api\.(?:get|post|put|patch|delete|request)\(\s*[`'"]\/app\//.test(uiText)) {
warnings.push('service_fronted_app_route_missing: could not find Service-owned /api/make/app/** or UI /app/** calls');
}
if (hasAuthNamespaceProxy && !hasManualRedirectPreservation(serviceText)) {
failures.push('session_complete_redirect_not_manual: session/complete proxy must preserve gateway 302/Set-Cookie/Location');
}
if (hasAuthNamespaceProxy && !hasSetCookiePassthrough(serviceText)) {
failures.push('session_complete_set_cookie_not_preserved: auth proxy must preserve gateway Set-Cookie for /api/make/auth/session/complete');
}
if (hasAuthNamespaceProxy && !hasLocationPassthrough(serviceText)) {
failures.push('session_complete_location_not_preserved: auth proxy must preserve gateway Location for /api/make/auth/session/complete');
}
if (hasInternalGatewayApiPrefix(serviceText)) {
failures.push('service_fronted_business_gateway_scope_wrong: published Service running inside k8s must call make-gateway without /api prefix, for example http://make-gateway/make/auth|meta|data/**; reserve public /api/make upstream scope for MAKE_APP_LOCAL_PREVIEW=true only');
}
if (hasForwardedHostPassthrough(serviceText)) {
failures.push('forwarded_host_passthrough_present: Service-fronted proxy must not trust or pass through client supplied X-Forwarded-Host; derive it from inbound Host');
}
if (!hasForwardedHostFallback(serviceText)) {
failures.push('forwarded_host_context_missing: Service-fronted proxy must derive X-Forwarded-Host from inbound Host when the header is absent');
}
if (!hasForwardedProtoFallback(serviceText)) {
failures.push('forwarded_proto_context_missing: Service-fronted proxy must add X-Forwarded-Proto when forwarding to make-gateway');
}
if (!/(req\.headers\.cookie|headers\.cookie|(?:request|req)\.headers\.get\(\s*[`'"]cookie[`'"]|(?:request|req)\.header\(\s*[`'"]cookie[`'"]|(?:source|inboundHeaders|headers)\.get\(\s*[`'"]cookie[`'"]|cookie\s*:)/i.test(serviceText)) {
warnings.push('cookie_forwarding_not_obvious: could not find obvious Cookie forwarding in Service code');
}
} else {
if (/auth\.api\.(?:get|post|put|patch|delete|request)\(\s*[`'"]\/app\//.test(uiText)) {
failures.push('direct_mode_app_route: direct gateway mode should not call Service-owned /app/** routes');
}
}
printResult();
process.exit(failures.length > 0 ? 1 : 0);
function failUsage(message) {
console.error(message);
console.error(USAGE);
process.exit(2);
}
function firstExisting(candidates) {
for (const candidate of candidates) {
const absolute = path.join(root, candidate);
if (fs.existsSync(absolute) && fs.statSync(absolute).isDirectory()) {
return absolute;
}
}
return null;
}
function collectSourceFiles(start) {
if (!start) {
return [];
}
const files = [];
const stack = [start];
while (stack.length > 0) {
const current = stack.pop();
let stat;
try {
stat = fs.statSync(current);
} catch {
continue;
}
if (stat.isDirectory()) {
if (shouldSkipDir(path.basename(current))) {
continue;
}
for (const child of fs.readdirSync(current)) {
stack.push(path.join(current, child));
}
continue;
}
if (stat.isFile() && isSourceFile(current)) {
files.push(current);
}
}
return files.sort();
}
function shouldSkipDir(name) {
return new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.next', '.turbo']).has(name);
}
function isSourceFile(file) {
return /\.(cjs|mjs|js|jsx|ts|tsx|json|html|vue|svelte)$/i.test(file);
}
function isRuntimeSourceFile(file) {
const basename = path.basename(file);
return !/\.(?:test|spec)\.[cm]?[jt]sx?$/i.test(basename)
&& !/(?:^|[/\\])(?:__tests__|test|tests)(?:[/\\]|$)/i.test(file);
}
function readJoined(files) {
return files.map((file) => {
try {
return fs.readFileSync(file, 'utf8');
} catch {
return '';
}
}).join('\n');
}
function findSdkDependencyDeclarations(files) {
const declarations = [];
for (const file of files.filter((candidate) => path.basename(candidate) === 'package.json')) {
let manifest;
try {
manifest = JSON.parse(fs.readFileSync(file, 'utf8'));
} catch {
continue;
}
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
const version = manifest?.[section]?.[SDK_PACKAGE_NAME];
if (typeof version === 'string' && version.trim()) {
declarations.push({ file, version: version.trim() });
}
}
}
return declarations;
}
function findSdkVersionOverrides(files) {
const overrides = [];
for (const file of files.filter((candidate) => path.basename(candidate) === 'package.json')) {
let manifest;
try {
manifest = JSON.parse(fs.readFileSync(file, 'utf8'));
} catch {
continue;
}
collectSdkOverrideEntries(manifest?.overrides, file, 'npm overrides', overrides);
collectSdkOverrideEntries(manifest?.resolutions, file, 'Yarn resolutions', overrides);
collectSdkOverrideEntries(manifest?.pnpm?.overrides, file, 'pnpm overrides', overrides);
}
for (const file of findPnpmWorkspaceFiles()) {
overrides.push(...readPnpmWorkspaceSdkOverrides(file));
}
return overrides;
}
function collectSdkOverrideEntries(value, file, source, overrides) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return;
}
for (const [selector, selectedValue] of Object.entries(value)) {
if (targetsSdkPackage(selector)) {
const version = typeof selectedValue === 'string'
? selectedValue.trim()
: typeof selectedValue?.['.'] === 'string'
? selectedValue['.'].trim()
: '';
overrides.push({ file, source, version });
}
if (selectedValue && typeof selectedValue === 'object') {
collectSdkOverrideEntries(selectedValue, file, source, overrides);
}
}
}
function findPnpmWorkspaceFiles() {
return ['pnpm-workspace.yaml', 'apps/pnpm-workspace.yaml']
.map((candidate) => path.join(root, candidate))
.filter((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
}
function readPnpmWorkspaceSdkOverrides(file) {
const overrides = [];
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
let overridesIndent = null;
for (const line of lines) {
const section = line.match(/^(\s*)overrides\s*:\s*(.*)$/);
if (section && section[1].length === 0) {
overridesIndent = section[1].length;
for (const version of parseInlineSdkOverrides(section[2])) {
overrides.push({ file, source: 'pnpm workspace overrides', version });
}
continue;
}
if (overridesIndent === null || !line.trim() || line.trimStart().startsWith('#')) {
continue;
}
const indent = line.match(/^\s*/)[0].length;
if (indent <= overridesIndent) {
overridesIndent = null;
continue;
}
const entry = line.match(/^\s*(?:"([^"]+)"|'([^']+)'|([^:#][^:]*?))\s*:\s*(.*?)\s*$/);
if (!entry) {
continue;
}
const selector = (entry[1] || entry[2] || entry[3] || '').trim();
if (targetsSdkPackage(selector)) {
overrides.push({
file,
source: 'pnpm workspace overrides',
version: normalizeYamlScalar(entry[4])
});
}
}
return overrides;
}
function parseInlineSdkOverrides(value) {
const overrides = [];
const entries = value.matchAll(/(?:^|[{,]\s*)(?:"([^"]+)"|'([^']+)'|([^,:{}\s]+))\s*:\s*(?:"([^"]*)"|'([^']*)'|([^,}\s]+))/g);
for (const entry of entries) {
const selector = entry[1] || entry[2] || entry[3] || '';
if (targetsSdkPackage(selector)) {
overrides.push((entry[4] || entry[5] || entry[6] || '').trim());
}
}
return overrides;
}
function normalizeYamlScalar(value) {
const withoutComment = value.replace(/\s+#.*$/, '').trim();
if (
(withoutComment.startsWith('"') && withoutComment.endsWith('"')) ||
(withoutComment.startsWith("'") && withoutComment.endsWith("'"))
) {
return withoutComment.slice(1, -1).trim();
}
return withoutComment;
}
function targetsSdkPackage(selector) {
const index = selector.indexOf(SDK_PACKAGE_NAME);
if (index < 0) {
return false;
}
const before = index === 0 || selector[index - 1] === '>' || selector[index - 1] === '/';
const afterIndex = index + SDK_PACKAGE_NAME.length;
const after = afterIndex === selector.length || selector[afterIndex] === '@';
return before && after;
}
function classifySdkVersionRange(versionRange, minimum) {
if (!versionRange) {
return 'unverifiable';
}
const clauses = versionRange.split('||').map((clause) => clause.trim());
if (clauses.length === 0 || clauses.some((clause) => !clause)) {
return 'unverifiable';
}
for (const clause of clauses) {
const lowerBound = resolveRangeLowerBound(clause);
if (!lowerBound) {
return 'unverifiable';
}
if (compareVersions(lowerBound, minimum) < 0) {
return 'too-old';
}
}
return 'supported';
}
function resolveRangeLowerBound(clause) {
const hyphenRange = clause.match(/^v?(\d+)\.(\d+)\.(\d+)\s+-\s+v?\d+\.\d+\.\d+$/i);
if (hyphenRange) {
return hyphenRange.slice(1, 4).map(Number);
}
let lowerBound = [0, 0, 0];
for (const token of clause.split(/\s+/)) {
if (/^(?:x|\*)$/i.test(token)) {
continue;
}
const match = token.match(/^(>=|>|<=|<|\^|~|=)?v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?$/i);
if (!match) {
return null;
}
const operator = match[1] || '';
const hasWildcard = [match[3], match[4]].some((part) => /^(?:x|\*)$/i.test(part || ''));
if (hasWildcard && operator) {
return null;
}
if (operator === '>' && (match[3] === undefined || match[4] === undefined || hasWildcard)) {
return null;
}
if (operator === '<' || operator === '<=') {
continue;
}
const candidate = [
Number(match[2]),
/^\d+$/.test(match[3] || '') ? Number(match[3]) : 0,
/^\d+$/.test(match[4] || '') ? Number(match[4]) : 0
];
if (operator === '>') {
candidate[2] += 1;
}
if (compareVersions(candidate, lowerBound) > 0) {
lowerBound = candidate;
}
}
return lowerBound;
}
function compareVersions(left, right) {
for (let index = 0; index < 3; index += 1) {
if (left[index] !== right[index]) {
return left[index] - right[index];
}
}
return 0;
}
function inferMode() {
if (
serviceFiles.length > 0 &&
(/auth\.api\.(?:get|post|put|patch|delete|request)\(\s*[`'"]\/app\//.test(uiText) || /\/api\/app\b/.test(projectText) || /\/api\/make\/app\b/.test(projectText))
) {
return 'service-fronted';
}
return 'direct';
}
function findRawMakeFetches(files) {
const hits = [];
const rawFetch = /(?:window\.)?fetch\s*\(\s*([`'"])([^`'"]*\/api\/make[^`'"]*)\1/g;
for (const file of files) {
const text = fs.readFileSync(file, 'utf8');
let match;
while ((match = rawFetch.exec(text))) {
hits.push({ file, url: match[2] });
}
}
return hits;
}
function hasTokenMode(text) {
return (
/authMode\s*[:=]\s*[`'"]token[`'"]/.test(text) ||
/VITE_MAKE_AUTH_MODE[\s\S]{0,120}(?:\?\?|\|\|)\s*[`'"]token[`'"]/.test(text) ||
/MAKE_AUTH_MODE[\s\S]{0,120}(?:\?\?|\|\|)\s*[`'"]token[`'"]/.test(text) ||
/unifiedLogin\s*:\s*false/.test(text) ||
/\b(?:accessToken|tokenProvider)\s*:/.test(text) ||
/createMakeAppAuth\s*\(\s*\{(?:(?!\}\s*\)).){0,1000}\btoken\s*:\s*[^,}\n]+/s.test(text) ||
/~\/\.make\/credentials/.test(text)
);
}
function hasServiceTokenModeWithoutLocalPreview(text) {
const hasServiceTokenMode = /MAKE_AUTH_MODE[\s\S]{0,120}(?:\?\?|\|\|)\s*[`'"]token[`'"]/.test(text) ||
/unifiedLogin\s*:\s*false/.test(text) ||
/\b(?:accessToken|tokenProvider)\s*:/.test(text) ||
/createMakeAppAuth\s*\(\s*\{(?:(?!\}\s*\)).){0,1000}\btoken\s*:\s*[^,}\n]+/s.test(text) ||
/~\/\.make\/credentials/.test(text);
return hasServiceTokenMode && !/MAKE_APP_LOCAL_PREVIEW/.test(text);
}
function hasUiDirectGatewayCalls(text) {
return /auth\.api\.(?:get|post|put|patch|delete|request)\(\s*[`'"]\/(?:data|meta)\b/.test(text);
}
function findRawMakeDownloadResourceUrls(files) {
const hits = [];
const resourceLiteral = /\b(src|href|data)\s*=\s*\{?\s*([`'"])([^`'"]*(?:\/api\/make\/data\/v1\/download|\/api\/data\/v1\/download|\/make\/data\/v1\/download|(?<![\w-])\/data\/v1\/download)[^`'"]*)\2\s*\}?/g;
for (const file of files) {
const text = fs.readFileSync(file, 'utf8');
let match;
while ((match = resourceLiteral.exec(text))) {
hits.push({
file,
attribute: match[1],
url: match[3],
});
}
}
return hits;
}
function hasRawMakeDownloadLiteral(text) {
return /(?:\/api\/make\/data\/v1\/download|\/api\/data\/v1\/download|\/make\/data\/v1\/download|(?<![\w-])\/data\/v1\/download)/.test(text);
}
function hasServiceDownloadProxyLiteral(text) {
return /(?:\/api\/make\/app\/files\/download|\/api\/files\/download|\/app\/files\/download)/.test(text);
}
function hasRecoverableAuthExpiredHandling(text) {
return /state_expired/.test(text)
&& /challenge_expired/.test(text)
&& /auth\.login\(\s*\{\s*redirect\s*:\s*true\s*\}\s*\)/.test(text);
}
function hasUnsupportedSdkReadyStatus(text) {
return /\b(?:result|boot|initResult|authResult)\.status\s*={2,3}\s*[`'"]ready[`'"]/.test(text);
}
function hasInternalGatewayApiPrefix(text) {
const patterns = [
/make-gateway[^`'"\s)]{0,160}\/api\/make\b/gi,
/fetch\(\s*[`'"]\/api\/make\/(?:auth|meta|data)\b/gi,
/\$\{\s*[^}]*makeGateway[^}]*\}\s*\/api\/make\b/gi,
/\b(?:makeGatewayBaseUrl|makeGatewayOrigin|gatewayOrigin)\b\s*\+\s*[`'"]\/api\/make\b/gi,
];
return patterns.some((pattern) => hasUngatedMatch(text, pattern, isPreviewRouteGated));
}
function hasUngatedMatch(text, pattern, isGated) {
pattern.lastIndex = 0;
let match;
while ((match = pattern.exec(text))) {
const contextStart = Math.max(0, match.index - 360);
const contextEnd = Math.min(text.length, pattern.lastIndex + 360);
if (!isGated(text.slice(contextStart, contextEnd))) {
return true;
}
}
return false;
}
function hasManualRedirectPreservation(text) {
return /redirect\s*:\s*[`'"]manual[`'"]/.test(text) || /maxRedirects\s*:\s*0/.test(text);
}
function hasSetCookiePassthrough(text) {
return /getSetCookie\s*\(/.test(text)
|| /(?:append|set|header|setHeader)\(\s*[`'"]set-cookie[`'"]/i.test(text)
|| /headers\.raw\(\)\s*\[\s*[`'"]set-cookie[`'"]\s*\]/i.test(text);
}
function hasLocationPassthrough(text) {
return /(?:append|set|header|setHeader)\(\s*[`'"]location[`'"]/i.test(text)
|| /headers\.get\(\s*[`'"]location[`'"]\s*\)/i.test(text)
|| /\[[^\]]*[`'"]location[`'"][^\]]*\][\s\S]{0,240}(?:setHeader|header|set)\(\s*\w+/i.test(text);
}
function hasServiceFrontedGatewayBaseMakePrefix(text) {
return /gatewayBaseUrl\s*:\s*[`'"]\/api\/make[`'"]/.test(text);
}
function hasServiceFrontedApiOnlyPrefix(text) {
return /\/api\/(?:auth|oauth|app)\b/.test(text);
}
function hasServiceFrontedNamespaceProxy(text, namespace) {
const browserPath = `/api/make/${namespace}`;
const internalPath = `/make/${namespace}`;
const escapedBrowserPath = escapeRegExp(browserPath);
const escapedInternalPath = escapeRegExp(internalPath);
const browserPathConstants = constantNamesForStringLiteral(text, browserPath);
const internalPathConstants = constantNamesForStringLiteral(text, internalPath);
const browserRouteToken = routeTokenPattern(browserPathConstants);
const hasNamespaceRoute = new RegExp(
String.raw`(?:app|router|server)\.(?:use|all|any)\s*\(\s*(?:[\`'"]${escapedBrowserPath}(?:\/(?:\*|\*\*))?\/?[\`'"]${browserRouteToken ? `|${browserRouteToken}` : ''})`,
'i'
).test(text) || new RegExp(
String.raw`\.startsWith\(\s*[\`'"]${escapedBrowserPath}\/?[\`'"]\s*\)`,
'i'
).test(text) || hasRegexRouteForPath(text, browserPath);
if (!hasNamespaceRoute) {
return false;
}
const hasDirectInternalPath = new RegExp(escapedInternalPath).test(text) || internalPathConstants.length > 0;
const stripsExternalApiPrefix = /replace\(\s*(?:\/\^\\?\/api|[`'"]\/api[`'"])/i.test(text);
const hasGatewayMakeBase = /make-gateway[\s\S]{0,160}\/make/i.test(text) || /MAKE_[A-Z_]*BASE_URL[\s\S]{0,160}\/make/.test(text);
return hasDirectInternalPath || (stripsExternalApiPrefix && hasGatewayMakeBase);
}
function constantNamesForStringLiteral(text, literal) {
const names = [];
const escapedLiteral = escapeRegExp(literal);
const declaration = new RegExp(
String.raw`\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*[\`'"]${escapedLiteral}[\`'"]`,
'g'
);
let match;
while ((match = declaration.exec(text))) {
names.push(match[1]);
}
return names;
}
function routeTokenPattern(names) {
if (names.length === 0) {
return '';
}
return names.map((name) => escapeRegExp(name)).join('|');
}
function hasRegexRouteForPath(text, pathLiteral) {
const escapedAsRegex = pathLiteral.replace(/\//g, String.raw`\\\/`);
const routeRegex = new RegExp(
String.raw`(?:app|router|server)\.(?:use|all|any)\s*\(\s*\/\^${escapedAsRegex}`,
'i'
);
return routeRegex.test(text);
}
function hasBroadMakeGatewayPassthrough(text) {
return /(?:app|router|server)\.(?:use|all|any|get|post|put|patch|delete)\s*\(\s*[`'"]\/api\/make(?:\/(?:\*|\*\*))?[`'"][\s\S]{0,500}(?:fetch|proxy|httpProxy|createProxyMiddleware)[\s\S]{0,240}(?:make-gateway|\/make)/i.test(text)
|| /if\s*\([^)]*\.startsWith\(\s*[`'"]\/api\/make\/?[`'"]\s*\)[^)]*\)\s*\{[\s\S]{0,500}(?:fetch|proxy|proxyMake\w+)[\s\S]{0,240}(?:make-gateway|\/make)[\s\S]{0,240}replace\(\s*(?:\/\^\\?\/api|[`'"]\/api(?:\/make)?[`'"])/i.test(text);
}
function hasBroadServiceAppBusinessPassthrough(text) {
const hasBroadAppRoute = /(?:app|router|server)\.(?:use|all|any|get|post|put|patch|delete)\s*\(\s*[`'"]\/api\/make\/app(?:\/(?:\*|\*\*))?[`'"]/i.test(text)
|| /\.startsWith\(\s*[`'"]\/api\/make\/app\/?[`'"]\s*\)/i.test(text);
const rewritesToRawMakePath = /replace\(\s*[\s\S]{0,160}\/api\/make\/app[\s\S]{0,160}\/(?:data|meta)/i.test(text)
|| /proxyMakeBusiness\([\s\S]{0,160}replace\(\s*[\s\S]{0,160}\/api\/make\/app/i.test(text);
return hasBroadAppRoute && rewritesToRawMakePath;
}
function hasPublishedPreviewAuthShadow(text) {
if (!/(localPreview\s*:\s*true|local-preview-user|local-preview|authMode\s*:\s*[`'"]token[`'"])/.test(text)) {
return false;
}
return hasUnguardedPreviewPathHandler(text, '/api/make/auth/current-context')
|| hasUnguardedPreviewPathHandler(text, '/api/make/auth/runtime-view');
}
function hasUnguardedPreviewPathHandler(text, pathLiteral) {
const escapedPath = escapeRegExp(pathLiteral);
const ifRoute = new RegExp(
String.raw`if\s*\((?<condition>[^)]*${escapedPath}[^)]*)\)\s*\{(?<body>[\s\S]{0,360}?(?:localPreview|local-preview|previewCurrentContext|previewRuntimeView)[\s\S]{0,360}?)\}`,
'gi'
);
let match;
while ((match = ifRoute.exec(text))) {
const block = `${match.groups?.condition ?? ''}\n${match.groups?.body ?? ''}`;
if (!isPreviewRouteGated(block)) {
return true;
}
}
const routeQuote = '[`\'"]';
const mountedRoute = new RegExp(
String.raw`(?:app|router|server)\.(?:get|use|all|any)\s*\(\s*${routeQuote}${escapedPath}${routeQuote}[\s\S]{0,360}(?:localPreview|local-preview|previewCurrentContext|previewRuntimeView)`,
'gi'
);
while ((match = mountedRoute.exec(text))) {
const before = text.slice(Math.max(0, match.index - 260), match.index);
const block = `${before}\n${match[0]}`;
if (!isPreviewRouteGated(block)) {
return true;
}
}
return false;
}
function hasQuerySensitivePreviewAuthRouteMatch(text) {
const previewPathLiterals = [
'/api/make/auth/current-context',
'/api/make/auth/runtime-view',
];
return previewPathLiterals.some((pathLiteral) => hasRawUrlEqualityForPreviewPath(text, pathLiteral));
}
function hasRawUrlEqualityForPreviewPath(text, pathLiteral) {
const escapedPath = escapeRegExp(pathLiteral);
const rawUrlEquality = new RegExp(
String.raw`(?:\b(?:req|request)\.(?:originalUrl|url)\s*={2,3}\s*[\`'"]${escapedPath}[\`'"]|[\`'"]${escapedPath}[\`'"]\s*={2,3}\s*\b(?:req|request)\.(?:originalUrl|url))`,
'gi'
);
let match;
while ((match = rawUrlEquality.exec(text))) {
const contextStart = Math.max(0, match.index - 520);
const contextEnd = Math.min(text.length, rawUrlEquality.lastIndex + 520);
const context = text.slice(contextStart, contextEnd);
if (/(MAKE_APP_LOCAL_PREVIEW|isLocalPreviewEnabled|localPreview|local-preview|localPreviewCurrentContext|localPreviewRuntimeView|authMode\s*:\s*[`'"]token[`'"])/.test(context)) {
return true;
}
}
return false;
}
function isPreviewRouteGated(text) {
return /MAKE_APP_LOCAL_PREVIEW\s*={2,3}\s*[`'"]true[`'"]/.test(text)
|| /process\.env\.MAKE_APP_LOCAL_PREVIEW\s*={2,3}\s*[`'"]true[`'"]/.test(text)
|| /isLocalPreviewEnabled\s*\(\s*\)/.test(text);
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function hasForwardedHostPassthrough(text) {
return /(?:pickProxyHeaders|copyProxyHeaders|proxyHeaders)\s*\([^)]*[`'"]x-forwarded-host[`'"]/is.test(text)
|| /[`'"]x-forwarded-host[`'"]\s*:\s*(?:req|request|source|inboundHeaders|headers)\.headers?\.get\(\s*[`'"]x-forwarded-host[`'"]\s*\)/i.test(text)
|| /[`'"]x-forwarded-host[`'"]\s*:\s*(?:req|request|source|inboundHeaders|headers)\.get\(\s*[`'"]x-forwarded-host[`'"]\s*\)/i.test(text);
}
function hasForwardedHostFallback(text) {
const normalized = text.toLowerCase();
if (!normalized.includes('x-forwarded-host')) {
return false;
}
return (
/(?:source|inboundheaders|inbound|request\.headers|req\.headers|options\.headers|headers)\.get\(\s*[`'"]host[`'"]\s*\)/i.test(text) ||
/(?:request|req)\.headers\.host/i.test(text) ||
/headers\.set\(\s*[`'"]x-forwarded-host[`'"][\s\S]{0,240}\bhost\b/i.test(text) ||
/[`'"]x-forwarded-host[`'"]\s*:\s*[^,\n}]*\bhost\b/i.test(text)
);
}
function hasForwardedProtoFallback(text) {
const normalized = text.toLowerCase();
if (!normalized.includes('x-forwarded-proto')) {
return false;
}
return (
/headers\.set\(\s*[`'"]x-forwarded-proto[`'"]/i.test(text) ||
/[`'"]x-forwarded-proto[`'"]\s*:/i.test(text) ||
/x-forwarded-proto[\s\S]{0,240}(?:https|http|\$scheme|proto)/i.test(text)
);
}
function relative(file) {
return path.relative(root, file) || '.';
}
function printResult() {
console.log(`make-app-auth contract audit`);
console.log(`root: ${root}`);
console.log(`mode: ${inferredMode}${mode === 'auto' ? ' (auto)' : ''}`);
console.log(`published: ${published ? 'yes' : 'no'}`);
if (failures.length === 0 && warnings.length === 0) {
console.log('status: PASS');
return;
}
if (failures.length > 0) {
console.log('failures:');
for (const failure of failures) {
console.log(`- ${failure}`);
}
}
if (warnings.length > 0) {
console.log('warnings:');
for (const warning of warnings) {
console.log(`- ${warning}`);
}
}
console.log(`status: ${failures.length > 0 ? 'FAIL' : 'PASS_WITH_WARNINGS'}`);
}
scripts/test-audit-auth-contract.mjs#!/usr/bin/env node
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const auditScript = path.join(scriptDir, 'audit-auth-contract.mjs');
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'make-app-auth-audit-'));
try {
const goodRoot = createFixture('good-service-fronted', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
if (!headers.get('x-forwarded-host')) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
}
if (!headers.get('x-forwarded-proto')) headers.set('x-forwarded-proto', 'https');
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
const url = new URL(req.url);
if (url.pathname.startsWith('/api/make/auth/')) {
const upstream = await fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
const responseHeaders = new Headers(upstream.headers);
const cookies = upstream.headers.getSetCookie?.() ?? [];
for (const cookie of cookies) responseHeaders.append('set-cookie', cookie);
const location = upstream.headers.get('location');
if (location) responseHeaders.set('location', location);
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
}
if (url.pathname.startsWith('/api/make/oauth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
return fetch('http://make-gateway/make/data/v1/record', { method: 'POST', headers });
}
`
});
assert.match(runAudit(goodRoot), /status: PASS/);
for (const version of ['^0.1.3', '0.1.3', '>0.1.2', '>=0.1.3 <0.2.0', '0.1.3 - 0.2.0', '^1.0.0']) {
writeSdkDependency(goodRoot, version);
assert.match(runAudit(goodRoot), /status: PASS/, `expected ${version} to pass`);
}
for (const version of ['^0.1.2', '<0.1.3', '>=0.1.3 || 0.1.2']) {
writeSdkDependency(goodRoot, version);
const output = runAudit(goodRoot, { expectFailure: true });
assert.match(output, /sdk_version_too_old/, `expected ${version} to be rejected as too old`);
}
for (const version of ['workspace:*', 'file:../sdk', 'latest']) {
writeSdkDependency(goodRoot, version);
const output = runAudit(goodRoot, { expectFailure: true });
assert.match(output, /sdk_version_unverifiable/, `expected ${version} to be rejected as unverifiable`);
}
writeSdkDependency(goodRoot, '^0.1.3');
write(path.join(goodRoot, 'pnpm-workspace.yaml'), `
packages:
- apps/*
overrides:
'@qfeius/make-app-auth': 0.1.2
`);
const pnpmOverrideOutput = runAudit(goodRoot, { expectFailure: true });
assert.match(pnpmOverrideOutput, /sdk_version_override_too_old/);
fs.rmSync(path.join(goodRoot, 'pnpm-workspace.yaml'));
write(
path.join(goodRoot, 'pnpm-workspace.yaml'),
`overrides: { unrelated-package: 1.0.0, '@qfeius/make-app-auth': 0.1.2 }`
);
const pnpmInlineOverrideOutput = runAudit(goodRoot, { expectFailure: true });
assert.match(pnpmInlineOverrideOutput, /sdk_version_override_too_old/);
fs.rmSync(path.join(goodRoot, 'pnpm-workspace.yaml'));
write(path.join(goodRoot, 'package.json'), JSON.stringify({
private: true,
overrides: {
'@qfeius/make-app-auth': '0.1.2'
}
}));
const npmOverrideOutput = runAudit(goodRoot, { expectFailure: true });
assert.match(npmOverrideOutput, /sdk_version_override_too_old/);
write(path.join(goodRoot, 'package.json'), JSON.stringify({
private: true,
resolutions: {
'@qfeius/make-app-auth': 'file:../make-app-auth'
}
}));
const yarnResolutionOutput = runAudit(goodRoot, { expectFailure: true });
assert.match(yarnResolutionOutput, /sdk_version_override_unverifiable/);
write(path.join(goodRoot, 'package.json'), JSON.stringify({
private: true,
pnpm: {
overrides: {
'@qfeius/make-app-auth': '^0.1.3'
}
}
}));
assert.match(runAudit(goodRoot), /status: PASS/);
fs.rmSync(path.join(goodRoot, 'package.json'));
fs.rmSync(path.join(goodRoot, 'apps/ui/package.json'));
const missingSdkVersionOutput = runAudit(goodRoot, { expectFailure: true });
assert.match(missingSdkVersionOutput, /sdk_version_missing/);
const antdThemeTokenRoot = createFixture('antd-theme-token-not-auth-token', {
ui: `
import { ConfigProvider } from 'antd';
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export function App() {
return <ConfigProvider theme={{ token: { colorPrimary: '#2563eb' } }} />;
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
const url = new URL(req.url);
if (url.pathname.startsWith('/api/make/auth/')) {
const upstream = await fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
const responseHeaders = new Headers(upstream.headers);
const cookies = upstream.headers.getSetCookie?.() ?? [];
for (const cookie of cookies) responseHeaders.append('set-cookie', cookie);
const location = upstream.headers.get('location');
if (location) responseHeaders.set('location', location);
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
}
if (url.pathname.startsWith('/api/make/oauth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
return fetch('http://make-gateway/make/data/v1/record', { method: 'POST', headers });
}
`
});
assert.match(runAudit(antdThemeTokenRoot), /status: PASS/);
const constantNamespaceProxyRoot = createFixture('constant-namespace-proxy', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
const AUTH_BROWSER_PREFIX = '/api/make/auth';
const OAUTH_BROWSER_PREFIX = '/api/make/oauth';
const AUTH_GATEWAY_SCOPE = '/make/auth';
const OAUTH_GATEWAY_SCOPE = '/make/oauth';
function applyForwardedHostContext(headers, req) {
const host = req.header('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
function toConfiguredMakePath(path) {
return path.startsWith('/make/') ? path.slice('/make'.length) : path;
}
async function proxyMakeNamespace(req, res, browserPrefix, upstreamPrefix) {
const headers = new Headers();
const cookie = req.header('cookie');
if (cookie) headers.set('cookie', cookie);
applyForwardedHostContext(headers, req);
const upstreamPath = toConfiguredMakePath(upstreamPrefix + req.path.slice(browserPrefix.length));
const upstream = await fetch('http://make-gateway/make' + upstreamPath, { headers, redirect: 'manual' });
const setCookie = upstream.headers.get('set-cookie');
if (setCookie) res.setHeader('set-cookie', setCookie);
const location = upstream.headers.get('location');
if (location) res.setHeader('location', location);
}
app.use(AUTH_BROWSER_PREFIX, (req, res) => proxyMakeNamespace(req, res, AUTH_BROWSER_PREFIX, AUTH_GATEWAY_SCOPE));
app.use(OAUTH_BROWSER_PREFIX, (req, res) => proxyMakeNamespace(req, res, OAUTH_BROWSER_PREFIX, OAUTH_GATEWAY_SCOPE));
`
});
assert.match(runAudit(constantNamespaceProxyRoot), /status: PASS/);
const localPreviewServiceRoot = createFixture('local-preview-service-token-adapter', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
function localPreviewHeaders(headers) {
if (process.env.MAKE_APP_LOCAL_PREVIEW === 'true') {
const accessToken = 'server-only-local-token';
headers.set('authorization', 'Bearer ' + accessToken);
}
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
const url = new URL(req.url);
if (url.pathname.startsWith('/api/make/auth/')) {
const upstream = await fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
const responseHeaders = new Headers(upstream.headers);
const cookies = upstream.headers.getSetCookie?.() ?? [];
for (const cookie of cookies) responseHeaders.append('set-cookie', cookie);
const location = upstream.headers.get('location');
if (location) responseHeaders.set('location', location);
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
}
if (url.pathname.startsWith('/api/make/oauth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
localPreviewHeaders(headers);
return fetch('http://make-gateway/make/data/v1/record', { method: 'POST', headers });
}
`
});
assert.match(runAudit(localPreviewServiceRoot), /status: PASS/);
const gatedPreviewAuthRoot = createFixture('gated-preview-auth-route', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
function localPreviewCurrentContext() {
return Response.json({ data: { userId: 'local-preview-user', localPreview: true, authMode: 'token' } });
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
const url = new URL(req.url);
if (process.env.MAKE_APP_LOCAL_PREVIEW === 'true' && url.pathname === '/api/make/auth/current-context') {
return localPreviewCurrentContext();
}
if (url.pathname.startsWith('/api/make/auth/')) {
const upstream = await fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
const responseHeaders = new Headers(upstream.headers);
const cookies = upstream.headers.getSetCookie?.() ?? [];
for (const cookie of cookies) responseHeaders.append('set-cookie', cookie);
const location = upstream.headers.get('location');
if (location) responseHeaders.set('location', location);
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
}
if (url.pathname.startsWith('/api/make/oauth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
return fetch('http://make-gateway/make/data/v1/record', { method: 'POST', headers });
}
`
});
assert.match(runAudit(gatedPreviewAuthRoot), /status: PASS/);
const ungatedPreviewAuthRoot = createFixture('ungated-preview-auth-route', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
function localPreviewCurrentContext() {
return Response.json({ data: { userId: 'local-preview-user', localPreview: true, authMode: 'token', grantVersion: 'local-preview' } });
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
const url = new URL(req.url);
if (url.pathname === '/api/make/auth/current-context') {
return localPreviewCurrentContext();
}
if (url.pathname.startsWith('/api/make/auth/')) {
const upstream = await fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
const responseHeaders = new Headers(upstream.headers);
const cookies = upstream.headers.getSetCookie?.() ?? [];
for (const cookie of cookies) responseHeaders.append('set-cookie', cookie);
const location = upstream.headers.get('location');
if (location) responseHeaders.set('location', location);
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
}
if (url.pathname.startsWith('/api/make/oauth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
return fetch('http://make-gateway/make/data/v1/record', { method: 'POST', headers });
}
`
});
const ungatedPreviewAuthOutput = runAudit(ungatedPreviewAuthRoot, { expectFailure: true });
assert.match(ungatedPreviewAuthOutput, /local_preview_auth_shadow/);
const querySensitivePreviewAuthRoot = createFixture('query-sensitive-preview-auth-route', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function isLocalPreviewEnabled() {
return process.env.MAKE_APP_LOCAL_PREVIEW === 'true';
}
function applyForwardedHostContext(headers, source) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
function localPreviewCurrentContext() {
return Response.json({ data: { userId: 'local-preview-user', localPreview: true, authMode: 'token' } });
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
const url = new URL(req.url);
if (isLocalPreviewEnabled() && req.originalUrl === '/api/make/auth/current-context') {
return localPreviewCurrentContext();
}
if (url.pathname.startsWith('/api/make/auth/')) {
const upstream = await fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
const responseHeaders = new Headers(upstream.headers);
const cookies = upstream.headers.getSetCookie?.() ?? [];
for (const cookie of cookies) responseHeaders.append('set-cookie', cookie);
const location = upstream.headers.get('location');
if (location) responseHeaders.set('location', location);
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
}
if (url.pathname.startsWith('/api/make/oauth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
return fetch('http://make-gateway/make/data/v1/record', { method: 'POST', headers });
}
`
});
const querySensitivePreviewAuthOutput = runAudit(querySensitivePreviewAuthRoot, { expectFailure: true });
assert.match(querySensitivePreviewAuthOutput, /local_preview_auth_query_sensitive_match/);
const uiTokenModeRoot = createFixture('ui-token-mode', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', accessToken: 'browser-token' });
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
const url = new URL(req.url);
if (url.pathname.startsWith('/api/make/auth/')) {
const upstream = await fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
const responseHeaders = new Headers(upstream.headers);
const cookies = upstream.headers.getSetCookie?.() ?? [];
for (const cookie of cookies) responseHeaders.append('set-cookie', cookie);
const location = upstream.headers.get('location');
if (location) responseHeaders.set('location', location);
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
}
if (url.pathname.startsWith('/api/make/oauth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
return fetch('http://make-gateway/make/data/v1/record', { method: 'POST', headers });
}
`
});
const uiTokenModeOutput = runAudit(uiTokenModeRoot, { expectFailure: true });
assert.match(uiTokenModeOutput, /token_mode_present/);
const missingSetCookieRoot = createFixture('missing-set-cookie-passthrough', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
const url = new URL(req.url);
if (url.pathname.startsWith('/api/make/auth/')) {
const upstream = await fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
const responseHeaders = new Headers();
const location = upstream.headers.get('location');
if (location) responseHeaders.set('location', location);
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
}
if (url.pathname.startsWith('/api/make/oauth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
return fetch('http://make-gateway/make/data/v1/record', { method: 'POST', headers });
}
`
});
const missingSetCookieOutput = runAudit(missingSetCookieRoot, { expectFailure: true });
assert.match(missingSetCookieOutput, /session_complete_set_cookie_not_preserved/);
const testOnlySetCookieRoot = createFixture('test-only-set-cookie-passthrough', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
const url = new URL(req.url);
if (url.pathname.startsWith('/api/make/auth/')) {
const upstream = await fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
const responseHeaders = new Headers();
const location = upstream.headers.get('location');
if (location) responseHeaders.set('location', location);
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
}
if (url.pathname.startsWith('/api/make/oauth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
return fetch('http://make-gateway/make/data/v1/record', { method: 'POST', headers });
}
`,
serviceTest: `
export function testOnlyStringFixture() {
return 'set-cookie should not satisfy production proxy audit';
}
`
});
const testOnlySetCookieOutput = runAudit(testOnlySetCookieRoot, { expectFailure: true });
assert.match(testOnlySetCookieOutput, /session_complete_set_cookie_not_preserved/);
const missingLocationRoot = createFixture('missing-location-passthrough', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
const url = new URL(req.url);
if (url.pathname.startsWith('/api/make/auth/')) {
const upstream = await fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
const responseHeaders = new Headers();
const cookies = upstream.headers.getSetCookie?.() ?? [];
for (const cookie of cookies) responseHeaders.append('set-cookie', cookie);
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
}
if (url.pathname.startsWith('/api/make/oauth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
return fetch('http://make-gateway/make/data/v1/record', { method: 'POST', headers });
}
`
});
const missingLocationOutput = runAudit(missingLocationRoot, { expectFailure: true });
assert.match(missingLocationOutput, /session_complete_location_not_preserved/);
const missingHostRoot = createFixture('missing-host-context', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
return fetch('http://make-gateway/make/auth/session/complete', { headers, redirect: 'manual' });
}
`
});
const missingHostOutput = runAudit(missingHostRoot, { expectFailure: true });
assert.match(missingHostOutput, /forwarded_host_context_missing/);
assert.match(missingHostOutput, /forwarded_proto_context_missing/);
const missingExpiredRoot = createFixture('missing-expired-handling', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
await auth.init({ redirect: true });
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
if (!headers.get('x-forwarded-host')) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
}
if (!headers.get('x-forwarded-proto')) headers.set('x-forwarded-proto', 'https');
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
return fetch('http://make-gateway/make/auth/session/complete', { headers, redirect: 'manual' });
}
`
});
const missingExpiredOutput = runAudit(missingExpiredRoot, { expectFailure: true });
assert.match(missingExpiredOutput, /recoverable_auth_expired_missing/);
const unsupportedReadyStatusRoot = createFixture('unsupported-ready-status', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const result = await auth.init({ redirect: true });
if (result.status === 'ready') {
renderApp();
}
if (result.reason === 'state_expired' || result.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
if (!headers.get('x-forwarded-host')) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
}
if (!headers.get('x-forwarded-proto')) headers.set('x-forwarded-proto', 'https');
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
const url = new URL(req.url);
if (url.pathname.startsWith('/api/make/auth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
if (url.pathname.startsWith('/api/make/oauth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
if (url.pathname === '/api/make/app/schema') {
return fetch('http://make-gateway/make/meta/schema', { headers });
}
return new Response('not found', { status: 404 });
}
`
});
const unsupportedReadyStatusOutput = runAudit(unsupportedReadyStatusRoot, { expectFailure: true });
assert.match(unsupportedReadyStatusOutput, /unsupported_sdk_ready_status/);
const businessApiScopeRoot = createFixture('business-api-wrong-scope', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
const MAKE_API_BASE_URL = 'http://make-gateway/api/make';
function applyForwardedHostContext(headers, source) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
if (req.url.includes('/api/make/auth/session/complete')) {
return fetch('http://make-gateway/make/auth/session/complete', { headers, redirect: 'manual' });
}
return fetch(MAKE_API_BASE_URL + '/data/v1/record', { method: 'POST', headers });
}
`
});
const businessApiScopeOutput = runAudit(businessApiScopeRoot, { expectFailure: true });
assert.match(businessApiScopeOutput, /service_fronted_business_gateway_scope_wrong/);
const dynamicGatewayApiScopeRoot = createFixture('dynamic-gateway-api-wrong-scope', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
const config = { makeGatewayBaseUrl: 'http://make-gateway.make-dev' };
const AUTH_GATEWAY_SCOPE = '/make/auth';
const OAUTH_GATEWAY_SCOPE = '/make/oauth';
function applyForwardedHostContext(headers, source) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
function buildMakeUrl(path) {
const normalizedPath = path.startsWith('/') ? path : '/' + path;
return \`\${config.makeGatewayBaseUrl}/api/make\${normalizedPath}\`;
}
function buildNamespaceUrl(originalPath, browserPrefix, upstreamPrefix) {
return config.makeGatewayBaseUrl + upstreamPrefix + originalPath.slice(browserPrefix.length);
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
const url = new URL(req.url);
if (url.pathname.startsWith('/api/make/auth/')) {
const upstream = await fetch(buildNamespaceUrl(url.pathname, '/api/make/auth', AUTH_GATEWAY_SCOPE), { headers, redirect: 'manual' });
const responseHeaders = new Headers(upstream.headers);
const cookies = upstream.headers.getSetCookie?.() ?? [];
for (const cookie of cookies) responseHeaders.append('set-cookie', cookie);
const location = upstream.headers.get('location');
if (location) responseHeaders.set('location', location);
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
}
if (url.pathname.startsWith('/api/make/oauth/')) {
return fetch(buildNamespaceUrl(url.pathname, '/api/make/oauth', OAUTH_GATEWAY_SCOPE), { headers, redirect: 'manual' });
}
return fetch(buildMakeUrl('/data/v1/record'), { method: 'POST', headers });
}
`
});
const dynamicGatewayApiScopeOutput = runAudit(dynamicGatewayApiScopeRoot, { expectFailure: true });
assert.match(dynamicGatewayApiScopeOutput, /service_fronted_business_gateway_scope_wrong/);
const spoofedForwardedHostRoot = createFixture('spoofed-forwarded-host', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
if (!headers.get('x-forwarded-host')) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
}
if (!headers.get('x-forwarded-proto')) headers.set('x-forwarded-proto', 'https');
}
export async function proxy(req) {
const headers = new Headers({
cookie: req.headers.get('cookie') || '',
'x-forwarded-host': req.headers.get('x-forwarded-host') || ''
});
applyForwardedHostContext(headers, req.headers);
return fetch('http://make-gateway/make/auth/session/complete', { headers, redirect: 'manual' });
}
`
});
const spoofedForwardedHostOutput = runAudit(spoofedForwardedHostRoot, { expectFailure: true });
assert.match(spoofedForwardedHostOutput, /forwarded_host_passthrough_present/);
const rawDownloadUrlRoot = createFixture('raw-download-url', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export function ReceiptPreview() {
return <img src="/api/make/data/v1/download/SampleApp/receipt.jpg" />;
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
if (!headers.get('x-forwarded-host')) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
}
if (!headers.get('x-forwarded-proto')) headers.set('x-forwarded-proto', 'https');
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
if (req.url.includes('/api/make/auth/session/complete')) {
return fetch('http://make-gateway/make/auth/session/complete', { headers, redirect: 'manual' });
}
return fetch('http://make-gateway/make/data/v1/record', { method: 'POST', headers });
}
`
});
const rawDownloadUrlOutput = runAudit(rawDownloadUrlRoot, { expectFailure: true });
assert.match(rawDownloadUrlOutput, /service_fronted_raw_download_resource/);
const endpointOnlyProxyRoot = createFixture('endpoint-only-proxy', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
if (req.url.includes('/api/make/auth/session/complete')) {
return fetch('http://make-gateway/make/auth/session/complete', { headers, redirect: 'manual' });
}
if (req.url.includes('/api/make/auth/current-context')) {
return fetch('http://make-gateway/make/auth/current-context', { headers });
}
if (req.url.includes('/api/make/oauth/challenge')) {
return fetch('http://make-gateway/make/oauth/challenge', { headers });
}
return fetch('http://make-gateway/make/data/v1/record', { method: 'POST', headers });
}
`
});
const endpointOnlyProxyOutput = runAudit(endpointOnlyProxyRoot, { expectFailure: true });
assert.match(endpointOnlyProxyOutput, /auth_proxy_missing/);
const catchAllPassthroughRoot = createFixture('catch-all-passthrough', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
const url = new URL(req.url);
if (url.pathname.startsWith('/api/make/auth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
if (url.pathname.startsWith('/api/make/oauth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
if (url.pathname.startsWith('/api/make/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api/make', ''), { headers });
}
return new Response('not found', { status: 404 });
}
`
});
const catchAllPassthroughOutput = runAudit(catchAllPassthroughRoot, { expectFailure: true });
assert.match(catchAllPassthroughOutput, /service_fronted_catch_all_passthrough/);
const appCatchAllPassthroughRoot = createFixture('app-catch-all-passthrough', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api/make', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
const url = new URL(req.url);
if (url.pathname.startsWith('/api/make/auth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
if (url.pathname.startsWith('/api/make/oauth/')) {
return fetch('http://make-gateway/make' + url.pathname.replace('/api', ''), { headers, redirect: 'manual' });
}
if (url.pathname.startsWith('/api/make/app/')) {
return proxyMakeBusiness(req, url.pathname.replace('/api/make/app', '/data'));
}
return new Response('not found', { status: 404 });
}
`
});
const appCatchAllPassthroughOutput = runAudit(appCatchAllPassthroughRoot, { expectFailure: true });
assert.match(appCatchAllPassthroughOutput, /service_fronted_app_catch_all_passthrough/);
const apiOnlyServicePrefixRoot = createFixture('api-only-service-prefix', {
ui: `
import { createMakeAppAuth } from '@qfeius/make-app-auth';
const auth = createMakeAppAuth({ gatewayBaseUrl: '/api', unifiedLogin: true, apiAuthRedirect: true });
const init = await auth.init({ redirect: true });
if (init.reason === 'state_expired' || init.reason === 'challenge_expired') {
await auth.login({ redirect: true });
}
export async function loadSchema() {
return auth.api.get('/app/schema', { credentials: 'include' });
}
`,
service: `
function applyForwardedHostContext(headers, source) {
const host = source.get('host');
if (host) headers.set('x-forwarded-host', host);
headers.set('x-forwarded-proto', 'https');
}
export async function proxy(req) {
const headers = new Headers({ cookie: req.headers.get('cookie') || '' });
applyForwardedHostContext(headers, req.headers);
if (req.url.includes('/api/auth/session/complete')) {
return fetch('http://make-gateway/make/auth/session/complete', { headers, redirect: 'manual' });
}
return fetch('http://make-gateway/make/data/v1/record', { method: 'POST', headers });
}
`
});
const apiOnlyServicePrefixOutput = runAudit(apiOnlyServicePrefixRoot, { expectFailure: true });
assert.match(apiOnlyServicePrefixOutput, /service_fronted_gateway_base_wrong/);
assert.match(apiOnlyServicePrefixOutput, /service_fronted_missing_make_prefix/);
assert.match(apiOnlyServicePrefixOutput, /auth_proxy_missing/);
console.log('audit-auth-contract tests: PASS');
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
function createFixture(name, files) {
const root = path.join(tempRoot, name);
write(path.join(root, 'apps/ui/package.json'), JSON.stringify({
dependencies: {
'@qfeius/make-app-auth': '^0.1.3'
}
}));
write(path.join(root, 'apps/ui/src/app.ts'), files.ui);
write(path.join(root, 'apps/service/src/app.ts'), files.service);
if (files.serviceTest) {
write(path.join(root, 'apps/service/src/app.test.ts'), files.serviceTest);
}
return root;
}
function write(file, content) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, content);
}
function writeSdkDependency(root, version) {
write(path.join(root, 'apps/ui/package.json'), JSON.stringify({
dependencies: {
'@qfeius/make-app-auth': version
}
}));
}
function runAudit(root, options = {}) {
try {
return execFileSync(process.execPath, [
auditScript,
root,
'--mode',
'service-fronted',
'--published'
], { encoding: 'utf8' });
} catch (error) {
if (options.expectFailure) {
return `${error.stdout ?? ''}${error.stderr ?? ''}`;
}
throw error;
}
}
SKILL.md---
name: make-app-auth
description: Use when generating, modifying, reviewing, or debugging Make App unified login and authenticated /api/make requests with @qfeius/make-app-auth. Covers unified login, OAuth/ngrok mode, 401/403 handling, logout, current-user menu logout wiring, cookies, sessions, redirect callbacks, and Make App auth troubleshooting. Preserve authenticated context for the default /api/make/app/principal/permission flow. Does not cover UI layout, account menu placement, page structure, build output, Service API contracts, permission logic, DSL modeling, or canvas-table internals; use makeui for the current-user header menu surface and make-app-permission for single-app permission enforcement.
metadata:
version: 0.1.5
---
# make-app-auth
Use this skill for **Make App authentication and authenticated Make API access**.
## Scope
This skill covers:
- `@qfeius/make-app-auth` SDK integration
- unified login as the default generated/published Make App auth mode
- unified login, OAuth, SSO, ngrok, cookie, logout, and callback testing
- direct Make gateway `/api/make/**` authenticated requests
- Service-fronted published App auth under the deployed App Service prefix, normally `/api/make/auth/**`
- 401, 403, and logout behavior
- cookie, session, redirect, and callback troubleshooting
This skill does not cover:
- UI layout, component design, or Make App page structure; use `makeui`.
- Service build output, packaging, image entrypoints, or publish runtime readiness; use `make-app-runtime`.
- runtime schema normalization, object-field mapping, table rendering, blank-page diagnosis, or business data correctness; use `makeui` and the host app tests.
- DSL modeling or Make resource definitions; use `makedsl`.
- makecli command execution; use `makecli`.
- make-gateway or Org server implementation changes.
- single-app permission enforcement; use `make-app-permission`.
## Default Behavior
Default generated and published Make Apps use **unified login** with `unifiedLogin: true`, `apiAuthRedirect: true`, and `auth.init({ redirect: true })`.
This skill only supports unified login for generated and reviewed Make Apps. Missing unified-login prerequisites are blockers, not reasons to switch modes. Do not generate browser token mode, mock mode, or any no-login bypass from this skill.
Local preview exception: a Service-fronted App may provide a Service-only local preview adapter guarded by `MAKE_APP_LOCAL_PREVIEW=true`. Enable that flag only as a temporary process environment variable from the local dev command, for example `MAKE_APP_LOCAL_PREVIEW=true pnpm run dev` or a project-owned `dev:preview` script. Do not persist the flag in `.env`, `.env.local`, `.env.example`, generated README setup steps, or deployment environment. The adapter should resolve the effective public Make origin with `makecli configure resolve --target local-preview --output=json`, consume `make_api_origin`, add the browser-facing `/api/make` scope, and attach the token only on Service-to-Make requests. It must not expose the token to UI, must not change the published unified-login contract, and must fail closed in production.
## Hard Rules
- Always use `@qfeius/make-app-auth`; do not fork a separate auth implementation.
- Direct-gateway business requests to Make backend must go through `auth.api` under `/api/make/**`.
- All frontend requests to Make backend must go through `auth.api`, including schema/meta, list, get, create, update, delete, attachment/file, lookup, user, and department candidate requests.
- Generated Apps must centralize Make backend access in a shared API adapter or data-source layer that wraps `auth.api`.
- Service-fronted Apps must preserve the `UI -> Service -> make-gateway` contract; do not let UI bypass Service for meta/data calls.
- Service-fronted Apps must preserve this contract for the default permission call. UI uses `auth.api("/app/principal/permission")`, and the single-app permission behavior belongs to `make-app-permission`.
- Service-fronted published Apps use `gatewayBaseUrl: "/api/make"` in UI. UI calls `auth.api("/app/**")`, which becomes browser requests to `/api/make/app/**`. Auth bootstrap and OAuth callbacks must stay under `/api/make/auth/**` and `/api/make/oauth/**`; do not generate `/api/auth/**`, `/api/oauth/**`, or `gatewayBaseUrl: "/api"` for this mode.
- Do not generate raw `window.fetch('/api/make/...')` for Make backend calls.
- Do not hand-write `Authorization`.
- Browser resource requests such as `<img src>`, `<object data>`, and plain `<a href>` cannot attach custom `Authorization` headers. If a Make file download requires a bearer token, UI must use a same-origin Service download proxy URL, and the Service must validate the current App session before using any deployment-injected download token.
- `gatewayBaseUrl` is the SDK option for the Make backend API base. Reuse the host Make backend config first; for local preview, prefer `makecli configure resolve --target local-preview --output=json` and its `make_api_origin` field instead of creating a second environment concept for the same URL.
- `gatewayBaseUrl` is not the unified login or account-center URL. Prefer `/api/make` for both same-origin direct-gateway Apps and Service-fronted published Apps; the difference is whether UI business calls use direct Make backend paths such as `/data/**` or Service-owned paths such as `/app/**`.
- Do not configure or hard-code unified login, Org, or account-center URLs in generated App code; make-gateway returns those URLs.
- Do not read, write, persist, or delete `zs_session` or `make_app_session` in App code.
- Do not construct Org OAuth URLs, `redirect_uri`, `state`, `code_challenge`, token exchange, or Org logout URLs in generated App code.
- Browser code cannot read `~/.make/credentials`.
- Do not generate browser-side `unifiedLogin: false`, `accessToken`, `token`, `tokenProvider`, local credential loading, `VITE_MAKE_AUTH_MODE=token`, or equivalent token-mode switches.
- Service-only local preview may use makecli credentials only behind a temporary process-level `MAKE_APP_LOCAL_PREVIEW=true`; do not persist this flag in env files or generated docs. Local preview must use `makecli configure resolve --target local-preview --output=json`, call `make_api_origin + /api/make`, and published runtime must call the k8s-internal gateway with `/make`. current-context/runtime-view must be explicit preview responses, route matching must ignore query strings such as `return_url`, and business requests must attach the token only inside Service.
- Local preview auth routes must not shadow published auth proxy routes. In published runtime, `/api/make/auth/current-context` and `/api/make/auth/runtime-view` must reach make-gateway through the auth namespace proxy and must not return `localPreview`, `local-preview-user`, `authMode: "token"`, or other preview context.
- Do not silently downgrade generated Apps from unified login because local OAuth prerequisites are missing; report the blocker.
- Before reporting publish/login readiness, verify the auth path with the agent or platform checks. Do not leave domain access, DevTools, k8s logs, or cookie inspection as user-only validation steps.
- For Service-fronted Apps, `/api/make/auth/**` and `/api/make/oauth/**` are required namespace-level Service proxy contracts under the published App Service prefix, not optional convenience routes or endpoint-by-endpoint allowlists.
- Every Service-fronted Make App must ensure the default `/api/make/app/principal/permission` route receives the established browser session context.
- Do not implement auth readiness by adding a broad `/api/make/**` passthrough. Only auth/oauth are default transparent namespaces; Service-owned business requests stay under explicit `/api/make/app/**` routes, and unknown `/api/make/**` paths fail closed.
- For Service-fronted Apps, Service must preserve the App host context for every make-gateway call: derive `X-Forwarded-Host` from inbound `Host`, do not trust client-supplied `X-Forwarded-Host`, add `X-Forwarded-Proto`, and share the same helper for auth and business proxy requests.
- Generated authenticated App shells must expose a visible logout action in the current-user menu or the host's established account area, and that action must call `auth.logout()`. The visual menu surface belongs to `makeui`; this skill owns the auth handler and logout behavior. Do not implement logout by clearing cookies, rewriting Org URLs, or hiding logout in page-specific controls.
- Generated Apps must handle recoverable unified-login expiry: when SDK init returns `reason: "state_expired"` or `reason: "challenge_expired"`, show a relogin prompt and call `auth.login({ redirect: true })` from user action.
## Pre-flight Workflow
1. Use unified login. If unified-login prerequisites are missing, report the blocker instead of switching modes.
2. Read `references/sdk-integration.md` before generating or changing auth code.
3. Read the relevant mode reference unless troubleshooting requires more.
- Default unified login: `references/unified-login-mode.md`
- 401, 403, logout: `references/logout-and-401.md`
- Incident/debugging: `references/troubleshooting.md`
4. Read `references/service-fronted-mode.md` when the App keeps a Service layer between UI and make-gateway.
5. Read `references/request-adapter.md` whenever generating or reviewing Make backend requests.
6. Keep auth bootstrap thin. Business features must consume the project Make API adapter and auth state, not auth internals.
7. Before claiming publish/login readiness, verify the auth path: current-context route, unified redirect, session callback, cookie-preserving business requests, and Service-fronted auth proxy when applicable.
8. Run `scripts/audit-auth-contract.mjs <project-root> --published` for generated Apps when a project tree is available; use `--mode service-fronted` when the App keeps a Service layer.
9. When changing generated code, add or update tests for the touched auth path: unauthenticated session, expired session, 403, logout, unified-login redirect, callback proxy, or business-request 401 handling.
## Reference Selection
- SDK contract and request wrapper: `references/sdk-integration.md`
- Shared request adapter and 401/403 handling: `references/request-adapter.md`
- Default unified login mode: `references/unified-login-mode.md`
- Service-fronted unified-login mode: `references/service-fronted-mode.md`
- 401, 403, and logout behavior: `references/logout-and-401.md`
- Auth incident diagnosis: `references/troubleshooting.md`
- Minimal Service-fronted route-shape example: `references/service-fronted-node-example.md`; read it only after `references/service-fronted-mode.md`.
## Deterministic Checks
Use `scripts/audit-auth-contract.mjs` on generated App projects to catch contract drift before publish:
```bash
node skills/make-app-auth/scripts/audit-auth-contract.mjs <project-root> --published
node skills/make-app-auth/scripts/audit-auth-contract.mjs <project-root> --mode service-fronted --published
```
The audit is auth-scoped. It checks unified-login readiness, raw `/api/make` fetch usage, Service-fronted `/api/make/auth/**` and `/api/make/oauth/**` namespace proxy presence, local-preview auth shadowing, broad `/api/make/**` passthrough risk, and obvious direct-vs-Service route mismatches. It does not verify schema rendering or UI blank-page behavior.
Audit expectations:
- UI design-system theme fields named `token` are not auth token mode. Only flag token-mode options inside auth configuration, auth environment switches, browser credential access, or explicit `authMode: "token"` paths.
- Service namespace proxies may be expressed as direct string routes, constants, `.startsWith(...)`, or equivalent regex route mounts, as long as `/api/make/auth/**` and `/api/make/oauth/**` map to internal `/make/auth/**` and `/make/oauth/**`.
- Cookie forwarding may use `req.headers.cookie`, `req.header("cookie")`, Fetch `headers.get("cookie")`, or an equivalent inbound-header adapter.
- Keep tests in `scripts/test-audit-auth-contract.mjs` updated when changing audit heuristics, especially for false-positive and false-negative cases discovered in generated Apps.
## Collaboration With makeui
When `makeui` is generating or editing Make App frontend code, this skill owns all authentication decisions. `makeui` may design UI states around auth results, but it must not invent OAuth, cookie, token, logout, or `/api/make/**` request logic.
`make-app-auth` reports whether the user is authenticated, unauthenticated, forbidden, expired, or blocked by an auth proxy/callback problem. It should not diagnose schema shape mismatches, missing fields, render crashes, white screens, or record-table behavior after authenticated Make requests are already reaching the backend.