node-sispnode-sisp
Beta

@akira-io/sisp beta documentation. APIs may change before the stable release.

Architecture

The port keeps the architecture of laravel-sisp 2.x: actions, builders, pipelines, drivers, and contracts. Laravel’s container is replaced by explicit constructor wiring in createSisp. The layout is hexagonal: core contracts and domain sit at the centre; application orchestrates them; infrastructure provides the adapters; presentation exposes the HTTP surface.

src/
  core/
    contracts/           SispDriver, CredentialsResolver, PaymentPipe, CallbackPipe, SispStorage (port)
  domain/
    enums/               statuses, transaction codes, message types, translations
    errors/              SispError and typed subclasses
    policies/            domain rules (retry eligibility, refund limits, ...)
    value-objects/       CallbackPayload, PaymentRequest, RefundRequest, credentials
  application/
    config.ts            SispConfig, defaults, credential mapping
    create-sisp.ts       composition root
    sisp.ts              public facade
    scoped-sisp.ts       forCredentials facade
    wiring.ts            credential-scoped service wiring (shared with ScopedSisp)
    events.ts            typed emitter
    sandbox.ts           fake gateway payload builder
    actions/             one unit of work each, ported 1:1 from the PHP actions
    builders/            PaymentBuilder, RefundBuilder
    pipelines/
      payment/           context plus default payment pipes
      callback/          context plus default callback pipes
  infrastructure/
    drivers/             SispManager, production, sandbox, TransactionStatusClient
    fingerprints/        token, payment, callback, refund algorithms
    http/                pure handlers, idempotency resolver, validation, results, auto-submit forms
    storage/
      column-codec.ts    how each adapter reads and writes an encrypted column
      rekey.ts           the per-row rekey decision shared by the three adapters
      reencrypt-batch.ts the page walk and the counters behind reencryptBatch
      knex/              KnexStorage, the default built from the database config
        models/          repository implementations (Transaction, TransactionAttempt, PaymentIntent, ...)
        migrations/      bundled schema, mirror of the Laravel migrations
        create-knex.ts   knex instance factory
        auto-migrate.ts  migration runner
        encryption.ts    AES-256-GCM payload cipher
        locking.ts       row-level lock helper for supported drivers
        log-context.ts   AsyncLocalStorage log source
        records.ts       raw DB record types
      prisma/            PrismaStorage, at the ./prisma subpath
      drizzle/           DrizzleStorage, at the ./drizzle subpath
  presentation/
    cli/                 sisp binary (migrate, reconcile-pending, prisma, prune-metadata, rotate-key)
    express/             thin Express adapter
    fastify/             thin Fastify adapter
    nest/                thin NestJS adapter
  support/               SispAmount, generators, countries, signed URLs, user agent

Key decisions

  • Pipelines are arrays of { handle(context, next) } objects executed by a tiny async runner. The default pipe sets can be customized per flow through pipelines.payment and pipelines.callback.
  • Models are the knex adapter’s repository implementations behind the SispStorage port. Transaction.update diffs changes, encrypts the payload, and appends the audit log in one place.
  • Payment intents live at the HTTP boundary. They reserve checkout keys, link keys to transactions, and allow safe replay of the same checkout.
  • Transaction attempts live under the parent transaction. They preserve every gateway submission and let callbacks update the exact attempt that SISP answered.
  • Drivers decide the payment endpoint and the status API client. manager.extend('custom', factory) registers new gateways.
  • Credential scoping rebuilds only the credential-dependent services (wiring.ts) around a static resolver, which is how forCredentials works without a container.
  • Parity with the PHP package is pinned by golden vectors generated from the real implementation, not by re-derived constants.

Storage adapters

The persistence layer sits behind an ORM-neutral port, SispStorage, defined in src/core/contracts/storage.ts: ten repositories plus a transaction() unit-of-work, an optional migrate?(), and destroy(). The port leaks no engine types.

Three adapters ship with the package:

  • KnexStorage (src/infrastructure/storage/knex/) - the default, built from the database config. Handles migrations automatically. The main entry loads it through a dynamic import taken only when no storage is injected, so a consumer that brings its own adapter never evaluates the adapter or the knex package. In the ESM build the adapter is a separate chunk the entry never imports statically; the CJS build ships it inline in dist/index.cjs, still behind the same lazy branch. Import knexOf from @akira-io/sisp/knex for the typed instance and raw queries.
  • PrismaStorage (src/infrastructure/storage/prisma/) - available at the @akira-io/sisp/prisma subpath. Injected via createSisp({ storage: createPrismaStorage(prisma, tables, appKey, { provider }) }). The core bundle never imports @prisma/client.
  • DrizzleStorage (src/infrastructure/storage/drizzle/) - available at the @akira-io/sisp/drizzle subpath. Injected via createSisp({ storage: createDrizzleStorage(db, tables, appKey, { dialect }) }). The core bundle never imports drizzle-orm. Its schema definitions, its DDL and its repositories all read one canonical table specification, so the three dialects cannot drift apart.

All three adapters are validated by the shared contract suite tests/storage/contract.ts, which guarantees behavioral parity. See Storage Adapters for the Prisma quick start and instructions for implementing custom adapters.

The application layer runs every database transaction through storage.transaction(tx => ...) with locked reads via the repository ...ForUpdate methods, so atomicity and locking are adapter-decided.

Knex-coupled surfaces live at the @akira-io/sisp/knex subpath: knexOf, createKnexInstance, runMigrations, MIGRATIONS_TABLE, PayloadCipher, runWithLogSource. The main entry’s database.connection config type is package-owned (string | Record<string, unknown>), not knex’s, and Sisp exposes no raw driver handle at all. The CLI migrate command still talks to knex directly, since it is a binary and not part of the package’s type surface. Engine selection and genericizing the database config are a later phase.

Differences from the Laravel package

  • Rendering is out of scope: the core returns render-ready data and HTML auto-submit forms only.
  • PDF invoice generation is deferred; the invoice rows and statuses are maintained.
  • Geolocation providers are not bundled; metadata captures device data only.
  • Scheduling is the host’s job: call reconcilePending() from your scheduler or cron the CLI.

Next: Index