@auth-craft/backend-sdk
v0.8.1
Published
Type-safe internal service SDK for Auth-Craft service-to-service calls
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 Web Crypto API for edge compatibility — zero external dependencies (except ts-micro-result).
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
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.
listTenantMembers(request) — POST /internal/tenant/members/list
List members of a tenant with profiles and permissions.
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.
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.
listUserTenants(request) — POST /internal/user/tenants/list
List all tenants a user belongs to.
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.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
