LinkCattle

Account login

Phone login accepts mainland China mobile numbers, optionally prefixed with +86 or 0086, and stores the eleven-digit number in the existing unique rg_users.username field. A successful first verification creates an account. Email verification also supports automatic registration. Common ASCII email addresses are trimmed and lowercased; quoted and international mailbox formats are not supported. Email accounts use a separate email: identity namespace and never silently merge with phone or password accounts. Existing internal account IDs and ownership remain unchanged.

The platform sends six-digit codes valid for five minutes, enforces a sixty-second resend interval and bounded send/verification attempts, and consumes successful codes once. The client separately proves possession of its device and signing keys. Existing accounts retain their pinned public key; a platform-signed account-authority-v1 certificate maps each client's own signing key to that account identity. No account root private key is uploaded, recovered through SMS, or signed by the server. Revoked endpoints require a fresh endpoint after an explicit SMS login. Account deletion still requires the signed reviewed plan and receipt; phone and email accounts reauthenticate with a fresh code for their verified identity.

SMS configuration

Set LINKCATTLE_SMS_CONFIG_FILE to a protected JSON file containing access_key_id, access_key_secret, sign_name, template_code, and optionally template_param_name (defaults to code). Use the approved SparkData project SMS template and a specifically authorized credential; configuration does not create, change, or borrow credentials. On this Mac the actual file belongs under ~/Documents/self/, with a 700 parent directory and a 600 file. Production mounts may use a private secret directory with equivalent permissions. An absent configuration returns sms_not_configured; no message is reported as sent.

In the shared Kubernetes cluster, deploy/platform-cluster.json and deploy/platform.yaml reference the existing sparkdata/sparkdata-notification Secret directly. The platform reads LINKCATTLE_SMS_ACCESS_KEY_ID, LINKCATTLE_SMS_ACCESS_KEY_SECRET, LINKCATTLE_SMS_SIGN_NAME and LINKCATTLE_SMS_TEMPLATE_CODE, mapped respectively to access-key-id, access-key-secret, sms-sign-name and sms-template-code. Email uses the separately named DirectMail environment fields described below; the notification webhook is not imported. LINKCATTLE_SMS_TEMPLATE_PARAM_NAME optionally overrides code. All required fields must be present and valid. An explicitly set LINKCATTLE_SMS_CONFIG_FILE takes precedence and never falls back to environment credentials if its file is invalid. Secret changes require recreating platform Pods to refresh their environment. Updating these manifests alone does not deploy the service or verify SMS delivery.

Requests use Alibaba Cloud's SendSms API and V3 request signing, with TLS verification and fixed service endpoint. Codes and provider credentials are never returned to the App or included in logs.

API and native operations

All platform endpoints below are relative to /gateway/api:

EndpointRequestResult
POST /accounts/sms/send{phone}{sent,retry_after,expires_in}
POST /accounts/sms/identity{phone,code,signing_key,proof}Pinned identity and a short-lived, single-use challenge bound to the client signing key
POST /accounts/sms/login{phone,device,device_proof}Existing session shape and account authority

The SDK supplies the proofs. Native operations are sms_send {phone} and sms_login {phone,code}. Neither returns the code or a private key. sms_send also provides fresh verification for the current phone account's deletion. The existing deletion request accepts sms_code or its legacy password field for that verification code.

sh tests/sms-summary.sh starts isolated MySQL/Redis and a loopback-only synthetic SMS provider. It does not send real SMS, contact an external model, or migrate production accounts. A real provider call, receiving a phone message, and a production rollout require separate validation.

Set LINKCATTLE_SMS_CONNECTION_CHECK=1 when running that script to also verify the real CLI, complete website URLs, mobile browser bootstrap, scoped file transfers and SMS account switching. Build linkcattle-runtime, rg-client, rg-local-runtime and rg-endpoint-fixture first; NODE_BIN selects an installed Node.js outside PATH.

Email configuration

The approved Kubernetes configuration reuses only access-key-id, access-key-secret and dm-account-name from the existing sparkdata/sparkdata-notification Secret. They map to LINKCATTLE_EMAIL_ACCESS_KEY_ID, LINKCATTLE_EMAIL_ACCESS_KEY_SECRET and LINKCATTLE_EMAIL_FROM, with LINKCATTLE_EMAIL_PROVIDER=aliyun. The implementation uses Alibaba Cloud SingleSendMail over TLS with ACS3 signing. OTP text is sent in the request body. It does not create a sender, rotate credentials or grant provider permissions.

Alternatively, LINKCATTLE_EMAIL_CONFIG_FILE points to a protected JSON file containing access_key_id, access_key_secret and from_address, with the same path and permission rules as SMS configuration. Missing or invalid configuration returns email_not_configured; an uncertain provider response is not automatically retried. A configured sender does not prove delivery; provider acceptance and receipt in a real mailbox require separate validation.

Email uses POST /accounts/email/send {email}, /accounts/email/identity {email,code,signing_key,proof} and /accounts/email/login {email,device,device_proof}, with the same result shapes as SMS. Native operations are email_send and email_login. Account deletion accepts the verified email and email_code (or the existing password field carrying that code); a different email cannot reauthenticate the account.

Native session renewal

CLI profiles use ~/.linkcattle/config/profiles/. That dedicated credential directory can redirect to a protected physical directory while ordinary configuration and runtime data stay in the application home. On this workstation its target belongs under Documents/self/; the target directory must already be private, files remain 600 and profile-file symlinks are rejected. Move an existing directory only during a coordinated Runtime switch, after verifying contents and preserving active connections; an older Runtime may not support the directory mapping.

Native access sessions last up to 24 hours, bounded by their renewal grant's absolute deadline. Password, SMS, email and browser login can also establish a device-bound renewal grant (device-session-v1): 90 days of inactivity, with a 365-day absolute limit. The opaque renewal credential is kept only in protected client credentials; the server stores its digest. Renewal also requires a fresh, purpose-bound signature from the original device. It never stores a password, changes the account/device identity, enrolls a device or expands service permissions.

POST /accounts/session/enroll upgrades a still-valid native session using its bearer credential and device proof. An expired legacy session needs one explicit login. POST /accounts/session/refresh issues a new access session. Logout and /accounts/session/revoke, authorized by a valid renewal grant, revoke all login sessions and renewal grants for that account on the current device; other devices and service ownership are preserved. An expired grant can revoke only itself. Requests bind account, device, credential digest, nonce and a maximum 120-second proof deadline under separate signature domains. Proof nonces are consumed once. Enrollment can recover a lost response without resetting the grant's original absolute deadline. Access tokens may overlap until their normal expiry; logout, device revocation, account deletion/disable and renewal-grant expiry prevent further use and renewal.

The SDK coalesces renewal for a shared session and persists the resulting credentials through the existing host storage. Runtime checks profiles in the background and after resume; CLI uses the same protected profile. Transient network/server failures retain credentials and back off. A rejected or expired renewal grant reports that sign-in is required. Near the absolute deadline or at expiry, the platform queues one account-scoped authentication_required notification per grant; a newer active login on the same computer suppresses an obsolete warning. Queueing does not prove phone receipt. Recovery never automatically resends a submitted business operation. Older servers retain their existing login behavior. Browser cookies remain independent.

Legacy enrollment may be followed by an access refresh before the first profile save. Runtime preserves both resulting credentials when the stored login still matches the original snapshot; concurrent logout, a new login or a changed identity prevents that save. Regression covers the actual SDK and loopback server, followed by a new Runtime test process refreshing the persisted grant. Installing a package does not establish that an already-running older Runtime has switched to it.

sh tests/session-renewal.sh uses isolated MySQL/Redis, synthetic identities and loopback services. It covers renewal, signed-device boundaries, nonce replay, legacy enrollment, restart/response-loss recovery, concurrency, logout/revocation, disabled/deleted accounts and preservation of computer/service identities. It does not use production credentials or establish physical sleep/wake or phone notification delivery.

System browser login

CLI browser login follows RFC 8252 and PKCE S256. Runtime owns the loopback listener, verifier, state and device credentials. The CLI opens only the public authorization URL and polls Runtime status. The callback must be the exact http://127.0.0.1:<random-port>/callback path; arbitrary hosts, paths, query parameters and low ports are rejected. Codes expire after two minutes, are consumed once and remain bound to the original request, redirect, signing key and device endpoint. The five-minute flow cannot change its endpoint during exchange.

POST /accounts/browser/start returns request_id, authorization_url and expires_in. After browser authorization, /accounts/browser/token exchanges the short code, verifier and device proof for the existing identity challenge; /accounts/browser/login completes the ordinary device-signing proof and returns the existing device-session shape. No account or business private key is delegated to the browser page.

The browser has an independent host-only Secure, HttpOnly, SameSite=Lax cookie. It is not a service API bearer credential. Sessions roll after activity with a default 90-day idle timeout and a 365-day absolute cap. LINKCATTLE_BROWSER_SESSION_IDLE_SECONDS and LINKCATTLE_BROWSER_SESSION_ABSOLUTE_SECONDS configure these bounds (minimum 300 seconds, absolute maximum 365 days). Valid sessions return directly to the original callback without another OTP. Logout requires same-origin CSRF proof. Browser login, logout and expiry never start an Agent or replay a business operation. Tokens and PKCE verifiers are not stored in localStorage, URLs or logs; only the short callback code and state appear in the loopback URL.

sh tests/account-browser-login.sh uses isolated MySQL/Redis, synthetic OTPs and the official signing primitives. It does not send real email/SMS or register a production Runtime. Set LINKCATTLE_TEST_BIN_DIR to the directory containing linkcattle-platform and rg-auth-fixture before running it.