@auth-craft/backend-sdk
v0.8.4
Published
Type-safe internal service SDK for Auth-Craft service-to-service calls
Downloads
379
Readme
@auth-craft/backend-sdk
⚠️ Experimental / Internal Use
This package is published for convenience only.
- No stability guarantee
- Breaking changes may happen at any time
- No support
Use at your own risk.
Type-safe internal service SDK for Auth-Craft service-to-service calls. Uses the Web Crypto API for edge compatibility (Cloudflare Workers, Deno, Node 20+).
Request and response types are inferred from @auth-craft/api-contract/internal — the same valibot schemas the backend's internal router validates with. There are no hand-written request shapes to drift: change the contract and every call site here re-types at build. The SDK covers all 21 internal endpoints, 1:1 with the contract and the router; a type-level guard (src/types.test-d.ts) fails the build if an endpoint ever gains a method-less gap or a method drifts from its schema.
Dependencies: ts-micro-result (the Result<T> type), @auth-craft/api-contract (the contract, read via typeof for inference), and valibot (type-level inference only — not bundled at runtime).
Installation
npm install @auth-craft/backend-sdkQuick Start
import { InternalClient } from '@auth-craft/backend-sdk';
const client = new InternalClient({
baseUrl: 'https://auth.example.com',
jwt: {
privateKey: process.env.INTERNAL_JWT_PRIVATE_KEY!, // hex 64-char or base64 JWK
serviceId: 'my-service',
audience: process.env.INTERNAL_JWT_AUDIENCE,
issuer: process.env.INTERNAL_JWT_ISSUER,
},
});
const result = await client.bootstrapTenantOwner({
tenantId: 'tenant-abc',
userId: 'user-123',
});
if (result.ok) {
console.log(result.data);
}Configuration
InternalClientConfig
| Field | Type | Default | Description |
|---|---|---|---|
| baseUrl | string | — | Base URL of the auth service |
| basePath | string | '' | Path prefix before serviceRoutePath |
| serviceRoutePath | string | '/internal' | Internal route path segment |
| jwt | ServiceJwtConfig | — | Service JWT signing configuration |
| fetchImpl | typeof fetch | globalThis.fetch | Custom fetch implementation |
| timeout | number | 30000 | Request timeout in milliseconds |
| headers | Record<string, string> | {} | Custom headers for all requests |
| secretHeader | { value: string; name?: string } | — | Optional shared secret header for direct backend access |
| logger | { warn(...) } | — | Optional transport diagnostic logger |
ServiceJwtConfig
| Field | Type | Default | Description |
|---|---|---|---|
| privateKey | string | — | Private key — hex 64-char (EdDSA) or base64-encoded JWK |
| alg | 'EdDSA' \| 'ES256' \| 'RS256' \| 'PS256' | 'EdDSA' | Signing algorithm |
| serviceId | string | — | Service identity — becomes JWT sub claim |
| audience | string | 'auth-craft:internal' | JWT aud claim — must match server config |
| issuer | string | 'auth-craft' | JWT iss claim |
| expiresInMinutes | number | 10 | Token TTL in minutes |
| roles | string[] | — | Service roles embedded in token |
Key formats for privateKey:
# EdDSA — hex string (64 chars = 32-byte seed)
INTERNAL_JWT_PRIVATE_KEY=06A70CB7A313A1CEA2371F8178EAF6CF...
# All algorithms — base64-encoded JWK
# Generate (Node.js):
# crypto.subtle.generateKey({name:'Ed25519'}, true, ['sign','verify'])
# .then(k => crypto.subtle.exportKey('jwk', k.privateKey))
# .then(jwk => btoa(JSON.stringify(jwk)))Tokens are auto-cached and reused until 60 seconds before expiry.
Methods
All 21 internal endpoints, grouped by domain. Every method takes a single typed
request object (inferred from the contract) and returns Promise<Result<T>>.
Paths shown are relative to serviceRoutePath (default /internal).
Tenant (3)
bootstrapTenantOwner(request) — POST /internal/tenant/bootstrap-owner
Assign owner role to a user for a new tenant. Typically called by tenant-provisioning service after creating a tenant.
getTenantProfile(request) — POST /internal/tenant/profile
Get per-tenant user profile with membership and permissions.
updateTenantProfile(request) — POST /internal/tenant/profile/update
Update per-tenant user profile fields.
Tenant members (4)
listTenantMembers(request) — POST /internal/tenant/members/list
List members of a tenant with profiles and permissions.
addMemberToTenant(request) — POST /internal/tenant/members/add
Add an existing user to a tenant directly (no invite flow).
updateTenantMemberStatus(request) — POST /internal/tenant/members/update-status
Update a tenant member's status.
updateTenantMemberPermission(request) — POST /internal/tenant/members/update-permission
Update a tenant member's roles and permissions.
removeTenantMember(request) — POST /internal/tenant/members/remove
Remove a member from a tenant.
Tenant invites (6)
inviteToTenant(request) — POST /internal/tenant/invites/create
Create a tenant invite (bypasses INVITE_MEMBERS permission — service is trusted).
acceptInvite(request) — POST /internal/tenant/invites/accept
Accept a tenant invite on behalf of a user.
declineInvite(request) — POST /internal/tenant/invites/decline
Decline a tenant invite.
revokeInvite(request) — POST /internal/tenant/invites/revoke
Revoke a pending invite.
listTenantInvites(request) — POST /internal/tenant/invites/list
List invites for a tenant.
checkPendingInvites(request) — POST /internal/tenant/invites/check-pending
Check pending invites by email or phone.
User (1)
listUserTenants(request) — POST /internal/user/tenants/list
List all tenants a user belongs to.
Accounts (7)
listAccounts(request) — POST /internal/accounts/list
List user accounts (requires VIEW_ACCOUNTS permission in system tenant).
getAccount(request) — POST /internal/accounts/get
Get account details (requires VIEW_ACCOUNTS permission in system tenant).
updateAccountProfile(request) — POST /internal/accounts/update-profile
Update account profile (requires UPDATE_ACCOUNT_PROFILE permission).
updateAccountStatus(request) — POST /internal/accounts/update-status
Update account status (requires UPDATE_ACCOUNT_STATUS permission).
resetAccountCredentials(request) — POST /internal/accounts/reset-credentials
Reset account credentials (requires RESET_ACCOUNT_CREDENTIALS permission).
removeAccount(request) — POST /internal/accounts/remove
Hard-delete a user account (requires REMOVE_ACCOUNTS permission).
Error Handling
All methods return Result<T> from ts-micro-result.
const result = await client.getTenantProfile({ tenantId, userId });
if (!result.ok) {
const error = result.errors[0];
switch (error?.code) {
case 'SDK_NETWORK_ERROR': // Network request failed
case 'SDK_TIMEOUT': // Request timed out
case 'SDK_INVALID_RESPONSE': // Non-JSON response
case 'SDK_JWT_SIGN_FAILED': // Failed to sign service JWT
default: // Auth service application error
}
}For transport failures such as a 403 HTML page from a proxy, the SDK enriches result.meta.params with:
httpStatuscontentTypeurlpathresponsePreview(trimmed body snippet)
You can also opt into runtime diagnostics:
const client = new InternalClient({
baseUrl: 'https://auth.example.com',
jwt: {
privateKey: process.env.INTERNAL_JWT_PRIVATE_KEY!,
serviceId: 'my-service',
},
logger: {
warn(message, context) {
console.warn(message, context);
},
},
});Direct Lambda / Docker Access
If you call the backend directly instead of going through the Cloudflare Worker, the backend may require a secret header such as X-Gateway-Secret.
const client = new InternalClient({
baseUrl: 'https://lambda-or-docker.example.com',
jwt: {
privateKey: process.env.INTERNAL_JWT_PRIVATE_KEY!,
serviceId: 'my-service',
},
secretHeader: {
value: process.env.GATEWAY_SECRET!,
// name defaults to 'X-Gateway-Secret'
},
});You can override the header name if your backend expects a different one:
secretHeader: {
name: 'X-Internal-Secret',
value: process.env.INTERNAL_SECRET!,
}Auth Permission Strings
Use AUTH_PERM_STRINGS constants when setting authPerms:
import { AUTH_PERM_STRINGS } from '@auth-craft/backend-sdk';
await client.updateTenantMemberPermission({
tenantId: 'org-123',
userId: 'user-abc',
actorUserId: 'admin-xyz',
authPerms: [AUTH_PERM_STRINGS.INVITE_MEMBERS, AUTH_PERM_STRINGS.VIEW_MEMBERS],
});Changelog
0.8.x
- Request/response types are now inferred from
@auth-craft/api-contract/internal— no hand-written shapes; a contract change re-types every call site at build. - Added a type-level coverage/shape guard (
src/types.test-d.ts): the build fails if any of the 21 internal endpoints lacks a method, a method has no matching endpoint, or a method's argument drifts from its contract body. - Documented the previously-undocumented
addMemberToTenantmethod.
0.2.0
- Breaking:
ServiceJwtConfig.privateKeyJwkrenamed toprivateKey— accepts hex 64-char (EdDSA) or base64 JWK - Breaking: Removed
strategyfield — usealginstead ('EdDSA' | 'ES256' | 'RS256' | 'PS256') - Fix: EdDSA hex key import now uses PKCS#8 envelope — compatible with Node.js v20+
0.1.0
- Initial release
License
MIT
