@adibilis/sdk (0.1.0-dev.18)
Installation
@adibilis:registry=npm install @adibilis/sdk@0.1.0-dev.18"@adibilis/sdk": "0.1.0-dev.18"About this package
@adibilis/sdk
Shop-site SDK for Adibilis CORE. Everything an Adibilis shop website needs to talk to the ERP: a server-side API client, a zod form layer, the order model, the checkout step machine, and the holidays client.
It exists so a shop site is mostly configuration and design. Before it, each site carried its own copy of the ERP integration, and those copies drifted.
The two entry points
The export map is the architectural boundary of this package, not a convenience:
| Import | Contains | Safe in a browser bundle |
|---|---|---|
@adibilis/sdk |
forms, order model, checkout context, holidays | yes |
@adibilis/sdk/server |
the API client — holds the WEBSITE key | no — server only |
A satellite that imports the wrong one gets a build error, not a leaked key. The server entry
carries import 'server-only', so pulling it into a client component fails the build:
Error: You're importing a module that depends on "server-only".
Client Component Browser:
./dist/server.js
./src/app/your-component.tsx
Verified against a real Next build in template/, not asserted. Two guards, covering two different
mistakes:
server-onlystops a consumer importing the server entry into a client component.test/boundary.test.tsstops the SDK itself re-exporting the client through the browser entry, by walking the builtdist/import graph.
Starter template
template/ is a runnable Next.js satellite showing both boundaries in place: a route handler on
@adibilis/sdk/server, a client component on the root entry, no stripe and no nodemailer. Copy
it to start a new shop. See template/README.md.
Install
pnpm add @adibilis/sdk
Reads from the registry are public — no token is required to install. Point the scope at it:
@adibilis:registry=https://forge.cloud.adibilis.ch/api/packages/core/npm/
(.npmrc.example carries exactly this.)
Release channels
The SDK carries its own semver — it is not pinned to the core release tag.
| Ref | Version | dist-tag |
|---|---|---|
tag v1.2.3 |
1.2.3 |
latest |
push to main |
0.0.0-dev.<run> |
dev |
A plain pnpm add @adibilis/sdk resolves to the newest released version and never to a dev build.
Opt in explicitly with pnpm add @adibilis/sdk@dev.
Relationship to @adibilis/api-types
@adibilis/api-types is generated from the CORE backend and versioned 1:1 with the core release tag.
The dependency range here is the statement of which core releases this SDK speaks to.
During 3c-i it is pinned to a dev build (0.0.0-dev.193), because core has not cut a release since
the package began publishing. This must be repinned to a released version before any satellite goes
to production — latest is the only channel carrying the 1:1-with-a-core-tag guarantee.
Two credentials, never confuse them
- Registry token — build-time, only needed to publish. Not needed to install.
- WEBSITE API key — runtime, lives in a satellite's environment, authenticates the API client against CORE.
Different lifetimes, different blast radii. Neither belongs in a commit.
What the WEBSITE key can reach
Measured against a running CORE instance with a provisioned key, not inferred from the source. Full
write-up: 2026-08-24-website-blast-radius-inventory.md.
Granted outright — the two endpoints gated on hasRole('WEBSITE'):
| Endpoint | Note |
|---|---|
GET /api/shop/holidays |
404 while the shop module is disabled |
POST /api/sales/order/ingest/stripe |
the order ingest path |
Reachable incidentally, because a WEBSITE principal satisfies isAuthenticated():
| Endpoint | What comes back |
|---|---|
GET /api/contact/person |
0 rows |
GET /api/company |
0 rows |
GET /api/company/select |
0 rows |
GET /api/contact-group/select |
0 rows |
GET /api/contact/search |
0 rows (scoped to the caller) |
GET /api/me/access |
its own permissions — empty |
GET /api/company/organization |
the install's org identity |
GET /api/team/search |
team names |
Everything catalog-gated (@perm.has) returns 403: the service user holds no contact-group
roles, so its effective action set is empty. Creates and deletes are denied.
The customer database is therefore closed — the relation data-grant predicate filters every row, and the service user holds no grants.
Two things to know before deploying a satellite
- A leaked key can rotate itself.
POST /api/security/api-keysucceeds for a WEBSITE principal and replaces the existing key. Whoever holds the key can mint a replacement only they know and break the live shop in the same call. Deliberate for a service account, questionable for a session authenticated by the very key being replaced. Open decision. - The empty blast radius is a seeding convention, not an enforced rule.
ApiKeyFilterintersects authorities withAPI_KEY_ROLES, which looks protective, but@permnever consults authorities — it recomputes from the user's contact-group roles in the database. Assigning any group role towebsite@adibilis.localwould silently widen every key that user holds.
Known issue: LocalDate in @adibilis/api-types
The generator types Java LocalDate as TypeScript Date:
export default interface HolidayRangeResponse { from: Date; to: Date }
JSON has no Date type, so what arrives is a string. .getTime() typechecks and then throws:
TypeError: parsed.from.getTime is not a function
52 generated types are affected — createDate, modifyDate, timestamp, invoiceDate. The SDK
therefore redeclares date-bearing shapes with string fields (see HolidayRange) rather than
importing the generated ones. The real fix belongs in the ajtg generator's type mapping.
Dependencies
Dependencies
| ID | Version |
|---|---|
| @adibilis/api-types | 0.0.0-dev.193 |
| server-only | ^0.0.1 |
Development dependencies
| ID | Version |
|---|---|
| @testing-library/react | ^16.3.2 |
| @types/react | ^19 |
| jsdom | ^30.0.1 |
| react-dom | ^19.2.8 |
| typescript | ^5 |
| vitest | ^4.1.10 |
Peer dependencies
| ID | Version |
|---|---|
| react | >=19 |
| zod | ^4.1.12 |