@zstrikehq/sdk
v0.2.0
Published
ZStrike authorization SDK: PDP authorize + PAP entity sync
Readme
@zstrikehq/sdk
ZStrike authorization SDK — PDP authorize + PAP entity sync. Node 20+ and edge runtimes (Cloudflare Workers, Vercel Edge, Deno). Server-side only: the app token and client token are secrets; never bundle this into browser code.
Install
npm install @zstrikehq/sdkConcepts
ZStrike splits authorization into two services, and this SDK talks to both:
- PDP — Policy Decision Point. Answers "is this allowed?" at request time.
authorize()andpermissions()call it, authenticated with your client token. - PAP — Policy Administration Point. Holds the entities (users, groups,
resources and their attributes/relationships) and policies the PDP evaluates.
client.entities.*and thezstrike-syncCLI write to it, authenticated with your app token. It defaults to the hosted PAP (https://api.zstrike.io), so you don't configure it.
Configuration
Grab your project id and tokens from your ZStrike dashboard, then set these in
the environment (or pass them to new ZStrikeClient({ ... })):
| Variable | What it is | Used by |
|---|---|---|
| ZSTRIKE_APP_TOKEN | App token — writes entities and policies to the PAP | entity sync, CLI |
| ZSTRIKE_CLIENT_TOKEN | Client token — makes authorization decisions | authorize, permissions |
| ZSTRIKE_PDP_URL | PDP base URL for your project | authorize, permissions |
| ZSTRIKE_PROJECT_ID | Your project id | entity sync |
ZSTRIKE_PDP_URL has no default — the client fails fast if it's needed and
missing. The PAP URL is baked into the SDK (hosted https://api.zstrike.io,
pinned to API v2026-01); override it only for self-hosted or testing with
papUrl / ZSTRIKE_PAP_URL (must be https://, or set allowInsecureHttp:
true). Both tokens are secrets: keep this SDK server-side.
Quickstart
import { ZStrikeClient } from '@zstrikehq/sdk';
// Reads ZSTRIKE_APP_TOKEN, ZSTRIKE_CLIENT_TOKEN, ZSTRIKE_PAP_URL,
// ZSTRIKE_PDP_URL, ZSTRIKE_PROJECT_ID from the environment.
const client = new ZStrikeClient();
// Or pass config explicitly with the factory (equivalent to `new`):
// const client = ZStrikeClient.init({ projectId: 'proj_123', pdpUrl: 'https://pdp.example' });
// Sync an entity to the PAP
await client.entities.create({
uid: { type: 'App::User', id: 'alice' },
attrs: { role: 'admin' },
});
// Ask the PDP for a decision (Deny is a result, not an exception)
const result = await client.authorize('App::User::"alice"', 'App::Action::"read"', 'App::Doc::"doc1"');
if (result.allowed) {
// proceed
}Bulk permissions readout
One call answers "what can this principal do to this resource" — for gating
UI, not for enforcement (it emits no audit events; keep authorize() on the
mutation path):
const { decisions } = await client.permissions(
'DocumentApp::User::"alice"',
'DocumentApp::Document::"doc1"',
{ actions: ['read', 'update', 'share'] }, // short names, not UIDs
);
if (decisions.share) showShareButton();Entity sync
Build entities with the fluent builder, push them in bulk (batches are chunked to the server's 250-op cap automatically), or reconcile full state (the server diffs and deletes what's absent):
import { Entity } from '@zstrikehq/sdk';
const user = Entity.create('App::User', 'alice')
.attr('email', '[email protected]')
.parent('App::Org', 'org1')
.build();
await client.entities.batch([{ operation: 'overwrite', ...user }]);
await client.entities.reconcile([user], { scope: { entityTypes: ['App::User'] } });Entity.update(type, id) builds JSON-Patch style partial updates (not
retry-safe — prefer overwrite in sync pipelines); Entity.delete(type, id)
builds a delete op.
zstrike-sync CLI
Write one module and let the CLI handle batching, retries, validation, and the cursor:
import { Entity, defineSync } from '@zstrikehq/sdk';
export default defineSync({
async *fetch(cursor) {
const rows = await db.query('SELECT * FROM employees WHERE updated_at > $1', [cursor ?? 0]);
yield { rows, cursor: maxUpdatedAt(rows) };
},
transform(row) {
return [Entity.create('App::User', row.id).attr('email', row.email).build()];
},
});zstrike-sync run sync.mjs # delta: fetch since cursor, push as overwrite ops
zstrike-sync run sync.mjs --reconcile # full pass via /reconcile (server deletes the rest)
zstrike-sync run sync.mjs --dry-run # NDJSON to stdout, no network
zstrike-sync run sync.mjs --input rows.ndjson # file source, fetch() not neededConfig: ZSTRIKE_APP_TOKEN env var or --token (the PAP base defaults to the
hosted URL; override with --base / ZSTRIKE_API_BASE only for self-hosted or
testing). The cursor lives in .zstrike-sync-cursor.json (--cursor-file).
.mjs/.js modules load natively; run TypeScript modules through tsx:
npx tsx node_modules/@zstrikehq/sdk/dist/cli.js run sync.ts.
Errors
All failures are typed: ConfigurationError, AuthenticationError (401),
PermissionError (403), NotFoundError (404), ValidationError (400/422),
RateLimitError (429), ServerError (5xx), TransportError (network/timeout).
License: MIT.
