@appport/services

中文文档
0.15.0发布于 2026/11/04ISCTypeScriptES Module近 30 天下载 3587

API keys, durable webhooks, background jobs, and provider-neutral secrets for AppPort applications

原始使用文档
文档目录37 节
  1. Create and run an application
  2. Standalone management host
  3. Consumer API
  4. Sharing the host application's FeltDB instance
  5. Version, health, evidence, and configuration transfer
  6. What is AppPort Services?
  7. See It In Action
  8. Architecture
  9. Dependency direction
  10. Why is API Keys separate from AuthPort?
  11. Installation
  12. How do I enable API keys?
  13. How do I create one?
  14. How does an application authenticate?
  15. Option 1: Framework-neutral HTTP adapter
  16. Option 2: Express middleware
  17. Tenant safety
  18. Low-level Bearer token extraction
  19. How does authorization happen?
  20. How do I use webhooks?
  21. Setup
  22. Register endpoint
  23. Emit event
  24. Worker: deliver webhooks
  25. Retry policy
  26. Disable endpoint
  27. How do I use durable jobs?
  28. Setup
  29. Register handler
  30. Enqueue job
  31. Execute job (worker)
  32. Schedule recurring job
  33. Retry policy
  34. How do I send notifications?
  35. Where does durable state live?
  36. What happens to the secret?
  37. Repository layout

AppPort Services

AppPort Services provides durable operational application capabilities that sit beside AuthPort.

The service set includes API Keys, Jobs, Schedules, Secrets, Webhooks, Files, and Notifications. Notifications are stored and delivered by AppPort Services; they are not an attention-management layer. Attn may consume them to derive attention separately.

Create and run an application

For a new application:

npx create-appport my-app
cd my-app
npm run dev

The generated source contains business handlers only. It does not create an HTTP server, configure persistence, implement CORS/SSE, run job workers, deliver webhooks, or install signal handlers.

For an existing application:

Install the package in your application:

npm install @appport/runtime

Initialize AppPort with every capability, or select only what the application uses:

npx @appport/runtime init
npx @appport/runtime init --use api,webhooks,jobs

With no flags, init asks which capabilities to enable and defaults each one to yes. The --use form is available for scripts and CI.

This creates two files that should be committed:

  • appport.toml declares the AppPort capabilities your application uses.
  • feltdb.flow is your application's authoritative FeltDB contract. AppPort's internal template remains inside the npm package; application developers do not edit or import AppPort storage internals.

Bootstrap AppPort from the contract:

import { appport } from '@appport/runtime';

const app = await appport();

// Only capabilities declared in appport.toml are initialized.
await app.api.keys.createApiKey(/* ... */);

appport() reads ./appport.toml by default and owns service construction, persistence, audit infrastructure, and lifecycle. Call await app.close() during graceful shutdown. Accessing an undeclared capability throws a CapabilityNotDeclaredError with the declaration needed to enable it.

appport.toml is the authoritative application contract. It is parsed, validated, normalized, and frozen once at startup. It declares application identity, deployment and state authority, tenancy, HTTP/CORS, API keys, webhook delivery, job types, events, authorization, observability, lifecycle, and development defaults. The sibling feltdb.flow is deployed into FeltDB as the authoritative state contract.

Legacy files containing only use api, use webhooks, and use jobs remain supported. Expand one to the canonical contract with a recoverable backup using:

npx @appport/runtime config migrate

Application code supplies behavior:

import { appport } from '@appport/runtime';

const application = await appport({
  routes: {
    'POST /invoices': async ({ body, tenantId }) => createInvoice(body, tenantId),
  },
  jobs: {
    'invoice.process': async (job) => processInvoice(job.payload),
  },
});

await application.publish('invoice.created', { id: 'inv-123' });

The public application.state and application.events APIs provide provider-neutral state and subscriptions. Application code never reaches through a capability to access its private database.

The runtime owns the stable management contract:

/_appport/health
/_appport/overview
/_appport/capabilities
/_appport/events
/_appport/api/keys
/_appport/webhooks
/_appport/jobs

Health is public. /_appport/capabilities is the unauthenticated discovery surface and returns the mounted capability catalog for the running application. Other endpoints follow the contract's authorization and tenant rules. Events uses SSE for GET and publishes domain events with POST; API keys, webhooks, and jobs support runtime-owned create/list operations.

@appport/services supplies the CLI and capability implementation, but application source imports only @appport/runtime. Existing applications may continue using createServices() from @appport/services as a compatibility API.

Run your application with its usual command, such as npm run dev. Operational CLI commands are available through the installed binary:

npx @appport/runtime api-key list --tenant acme
npx @appport/runtime webhook list --tenant acme
npx @appport/runtime job list --tenant acme

Standalone management host

You do not need an application to operate AppPort Services. serve starts the management host, which owns its durable state, ServiceGateway, authentication, management router, and HTTP server:

npx @appport/services init     # appport.toml + feltdb.flow for this deployment
npx @appport/services serve    # GET /v1/ui, /services, and the other management pages

It binds 127.0.0.1:4100 by default; override with --host / --port. State is the same durable FeltDB deployment the CLI commands use, so it survives restart. Management requests authenticate with an AppPort API key (Authorization: Bearer <secret>) or an operator identity adapter; authorization stays with AuthBoundry. See docs/management.md.

AppPort manages its FeltDB runtime dependency; consumers do not import @feltdb/core or AppPort's internal stores.

The repository defines AppPort capabilities for tenant-scoped API keys, durable webhooks, durable job execution (including schedules), provider-neutral Secrets metadata/lifecycle and scoped-resolution contracts, durable notifications, and files. Legacy runtime state uses @feltdb/core (the host's copy, 0.16.1 or later 0.x). Secret material remains with an authorized provider and is never durable AppPort state.

            Application
                 │
     ┌───────────┴───────────┐
     │                       │
  AuthPort             AppPort Services
     │                       │
identity/authz        API Keys, Webhooks, Jobs, Secrets
     │                       │
     └───────────┬───────────┘
                 │
              FeltDB

Consumer API

Sharing the host application's FeltDB instance

A host that already owns its application's FeltDB instance can run every service on it, instead of letting AppPort Services open a second instance:

import { createFeltDB } from '@feltdb/core';
import { createServices } from '@appport/services';

const db = createFeltDB({ mode: 'local', namespace: 'invoices', path: './state' });
const services = createServices({ db, application: 'invoices', authorizer: authBoundry });
// Service stores, effect evidence, and services.transaction() all use `db`.

The host keeps ownership: AppPort Services never closes an instance it was given. The instance's own deployment mode (db.capabilities().deployment) is used for mode-dependent behavior. Use one copy of @feltdb/core for the host and this package: it is a peer dependency (>=0.16.1 <1.0.0), so the host's copy is the one Services uses, and npm installs no second one.

To check which instance a services object runs on (local, memory, or remote), use services.db (read-only) or services.usesInstance(db), which compares identity; services.ownsDb is false for a host-supplied instance:

assert.ok(services.usesInstance(db));

Version, health, evidence, and configuration transfer

import { VERSION } from '@appport/services';             // equals package.json "version"
import manifest from '@appport/services/package.json' with { type: 'json' };

const health = await services.health({ tenantId: 'acme' });
// health.status: 'ok' | 'degraded' | 'unconfigured'; health.services.webhooks.state; health.attention.failedWebhookDeliveries

const principal = services.identify({ principalId, principalType: 'user', tenantId, runId: requestId });
await services.invoke('files.write', input, { principal });
const { state, records } = await services.evidence.findByRunId(requestId); // state: 'none' | 'started' | 'completed' | 'failed'

const document = await services.configuration.export({ principal, environment: 'staging' });
const report = await other.configuration.import(document, { principal, dryRun: true });
  • services.health() — per-service state and counts needing attention, from bounded queries. See docs/health.md.
  • services.evidence.findByRunId() — the effect evidence recorded under a run id (the caller's correlation id), and what a started row means after a crash. See docs/AUTHORITY.md.
  • services.configuration.export() / import() — non-secret configuration between environments; secret values never leave. See docs/configuration.md.
  • webhooks.deliveries.read — its own capability for reading webhook deliveries (invoke('webhooks.deliveries.read', { status: 'failed' }), GET /_appport/webhooks/deliveries).

Existing authenticated Express applications can mount the supported management runtime against their existing service instance:

import { createManagementRouter, createServices } from '@appport/services';

const services = createServices({ path: '.appport', application: 'invoices', authorizer: authBoundry, credentials: authBoundryCustody });
app.use(createManagementRouter({
  services,
  authority: services.gateway,
  authenticate: (request) => hostAuthentication(request), // identity only
}));

The host owns authentication, AuthBoundry owns authorization, and AppPort Services enforces AuthBoundry's decisions before any effect. @appport/services executes effects. It does not decide who is allowed to cause them. See the authority model, webhook security, and job security. API-key management requires apikeys.read, apikeys.create, and apikeys.revoke (plus apikeys.update, apikeys.rotate, and apikeys.audit for editing, rotation, and audit), always uses the authenticated tenant, and returns a plaintext credential only in creation and rotation responses. See Composable management runtime for the full contract and standalone/embedded behavior. Other packages can add their own pages, routes and capabilities to the same host through management extensions.

Outbound credentials use a server-only scoped protocol. AppPort defines the reference, context, lifecycle, audit, errors, and callback contract; AuthBoundry authorizes and AppBoundry resolves provider-held material. AppPort ships no resolver, secret store, provider adapter, or policy engine, and exposes no browser-facing credential-value route.

When the host's custody is a SecretCustody (withSecret plus store and status), credential.store collects a value (authorized, audited, versioned, never echoed or persisted in FeltDB) and hands it to the host, and credential.status reports whether material exists. The host still owns where material lives; a resolver-only host gets CUSTODY_READ_ONLY.

await secrets.withSecret({
  reference: { secretId, tenantId: 'acme', provider: 'pipedrive' },
  context: { tenantId: 'acme', principalId: 'integration:pipedrive', purpose: 'person.lookup', authorizationRef },
}, async ({ value }) => callProvider(value));

See the outbound credential guide for ownership, authorization handoff, lifecycle, and failure semantics.

See the capability inventory and capability authority summary for the consolidated capability matrix and mounted-state rules.

The contract determines which runtime APIs are available:

import { appport } from '@appport/runtime';

const app = await appport({ authorizer: authBoundry, credentials: authBoundryCustody });

// Identity comes from authentication; AuthBoundry authorizes each capability.
const principal = await app.authenticate(request);

// Machine identity (an API key identifies a caller; it carries no scopes)
await app.invoke('apikeys.create', { name: 'server-key' }, { principal });

// Durable outbound notifications, bound to a signing credential in AuthBoundry custody
await app.invoke('webhooks.register', {
  url: 'https://acme.example.com/webhooks',
  events: ['invoice.created'],
  signingCredentialRef: 'credential-ref:whsec_acme',
}, { principal });

// Durable deferred execution; the job runs as this principal and is re-authorized on every run
await app.invoke('jobs.create', {
  type: 'invoice.process',
  payload: { invoiceId: 'inv-123' },
  maxAttempts: 3,
}, { principal });

The consumer does not need to know that FeltDB exists underneath. Only declared capabilities are constructed, and they share one durable runtime.

What is AppPort Services?

AppPort Services answers:

What operational capabilities does this application expose?

AuthPort still answers identity, authentication, and authorization questions.

See It In Action

A complete example application is available under examples/services-demo/. It demonstrates:

  • API key authentication with tenant-scoped principals
  • Durable invoice creation with automatic webhook and job intent
  • Webhook delivery with retry and replay
  • Background job processing with lease-based concurrency control
  • Tenant isolation and durability across process restart

See examples/services-demo/README.md for a five-minute quickstart.

Architecture

AppPort Services uses Flow (the @feltdb/core contract language) as the authoritative durable schema. The package's internal appport.flow is the template from which appport init generates the application's authoritative feltdb.flow:

API Keys vertical:

  • ApiKeys — API key credentials (secret stored as scrypt hash only)
  • ApiKeyPrefixes — Prefix lookup for efficient secret validation
  • ApiKeyAuditEvents — Access audit trail

Webhooks vertical:

  • WebhookEndpoints — Registered webhook receivers
  • WebhookDeliveries — Outbound delivery records with retry state
  • WebhookAuditEvents — Webhook lifecycle audit trail

Jobs vertical:

  • Jobs — Individual jobs with execution status and lease tracking
  • JobSchedules — Recurring job definitions
  • JobAuditEvents — Job execution audit trail

Secrets vertical:

  • Secrets — Tenant-scoped logical secret identity and lifecycle metadata
  • SecretVersions — Provider references and explicit rotation versions
  • SecretAuditEvents — Creation, resolution, rotation, revocation, retirement, and failure audit records

Secrets operations distinguish describeSecret (metadata only) from resolveSecret (authorized provider resolution). Secret values, plaintext, decrypted material, and provider credentials are not fields in appport.flow or audit events.

All collections are tenant-scoped via tenant_id field with tenant_idx for efficient queries.

Dependency direction

feltdb.flow (generated authoritative application contract)
       ↓
TypeScript implementation
       ↓
FeltDB (@feltdb/core, the host's copy)

The Flow contract is parsed and validated at test time. The TypeScript stores (FeltDbApiKeyStore, FeltDbWebhookEndpointStore, etc.) implement the contract semantics directly against FeltDB collections.

Why is API Keys separate from AuthPort?

API keys authenticate machine principals. They do not replace AuthPort authorization.

API key
   ↓
machine principal
   ↓
AuthPort authorization
   ↓
resource/action decision

Installation

npm install
npm run build

@feltdb/core is a peer dependency: install it beside Services (the latest 0.x release; npm 7 and later install it for you), and Services uses that one copy:

{
  "peerDependencies": {
    "@feltdb/core": ">=0.16.1 <1.0.0"
  }
}

How do I enable API keys?

Create the service with FeltDB’s real deployment model:

import { createApiKeyService } from '@appport/services';

const service = createApiKeyService({
  mode: 'local',
  namespace: 'appport-services',
  path: './.feltdb/appport-services'
});

Remote FeltDB deployments can use the same exported deployment fields that resolveFeltDBDeployment() understands.

How do I create one?

// `operator` is a verified principal (host authentication or an existing API key).
const created = await service.createApiKey({ name: 'production' }, operator);

console.log(created.secret); // only returned once

An API key identifies a caller and its application. It carries no scopes: AuthBoundry decides what the caller may do. Passing scopes is rejected with a migration error (docs/AUTHORITY.md).

A key can be given restrictions, a ceiling that only narrows what AuthBoundry allows, plus an expiry. Keys can be edited, rotated with an optional overlap, revoked one at a time or in bulk, and audited:

const key = await services.apiKeys.createApiKey({
  name: 'reporting-worker',
  restrictions: { capabilities: ['files.read'], resources: [{ type: 'file', attributes: { owner: 'reporting' } }] },
  expiresAt: new Date(Date.now() + 90 * 86_400_000),
}, operator);

const next = await services.apiKeys.rotateApiKey({ id: key.id, overlapSeconds: 300 }, operator); // next.secret shown once
await services.apiKeys.updateApiKey({ id: next!.id, name: 'reporting-worker-v2' }, operator);
const events = await services.apiKeys.listApiKeyAuditEvents({ credentialId: next!.id }, operator);
await services.apiKeys.revokeAllApiKeys({ createdBy: '[email protected]' }, operator);

Revocation and restriction changes apply on the next operation in every process. See docs/api-keys.md for the permission model, rotation semantics, verifier keys, errors, and migration notes.

CLI (the operator is identified through APPPORT_AUTHORITY or APPPORT_API_KEY):

APPPORT_AUTHORITY=./authority.mjs appport api-key create --tenant tenant-123 --name production

How does an application authenticate?

Option 1: Framework-neutral HTTP adapter

For applications using ordinary Node HTTP request/response semantics:

import { createApiKeyAuth } from '@appport/services';

const auth = createApiKeyAuth({ service });

// Optional authentication (returns null if missing/invalid)
const principal = await auth.authenticate(request);

// Required authentication (throws if missing/invalid)
const principal = await auth.require(request);

The adapter extracts the Authorization: Bearer <api-key> header and returns an AuthenticatedPrincipal:

interface AuthenticatedPrincipal {
  principalId: string;
  principalType: 'api_key';
  tenantId: string;
  applicationId: string;
  credentialId: string;
  verifiedBy: 'api_key';
}

Principals are branded: services accept only principals minted by an authentication path, never object literals or actor strings.

Option 2: Express middleware

For Express applications:

import { apiKeyAuth, requireApiKeyAuth } from '@appport/services';

const app = express();

// Optional authentication
app.use(apiKeyAuth(service));

app.get('/invoices', async (req, res) => {
  const principal = req.auth; // null if missing/invalid
  if (!principal) {
    return res.status(401).json({ error: 'Unauthenticated' });
  }
  // Handle request with principal
});

// Or, require authentication
app.use(requireApiKeyAuth(service));

app.get('/protected', async (req, res) => {
  const principal = req.auth; // guaranteed, or middleware rejects
  // Handle request with principal
});

The middleware attaches the principal to req.auth and provides request-scoped context via req.authContext.

Tenant safety

If your application accepts tenant context independently, validate it against the principal:

import { assertTenant } from '@appport/services';

assertTenant(principal, tenantIdFromRequest); // throws if mismatch

Low-level Bearer token extraction

For custom frameworks:

import { authenticateBearerToken } from '@appport/services';

const principal = await authenticateBearerToken(
  authorizationHeader,
  service,
);

How does authorization happen?

Authentication returns an identity. Authorization belongs to AuthBoundry: every service effect is authorized by the configured ServiceAuthorizer before it runs, credentials are resolved only after that decision, and evidence is written to FeltDB. See docs/AUTHORITY.md.

How do I use webhooks?

Webhooks provide durable signed delivery to external HTTP endpoints.

Setup

import { createServices } from '@appport/services';

const services = createServices({ path: './.appport', application: 'invoices', authorizer: authBoundry, credentials: authBoundryCustody });
const service = services.webhooks;

Register endpoint

const endpoint = await service.createWebhookEndpoint({
  url: 'https://customer.example.com/webhooks',
  events: ['invoice.created', 'invoice.paid'],
  // The signing secret lives in AuthBoundry custody; the endpoint stores only the reference.
  signingCredentialRef: 'credential-ref:whsec_customer',
}, principal);

// Private, loopback, link-local, and metadata destinations are rejected; redirects are never followed.
// See docs/WEBHOOK-SECURITY.md.

Emit event

await service.emitWebhookEvent({
  type: 'invoice.created',
  payload: { id: 'inv-456', amount: 100 }
}, principal);

// Delivery records created automatically for matching endpoints

Worker: deliver webhooks

const delivery = await service.getWebhookDelivery(tenantId, deliveryId);
const result = await service.deliverWebhook(tenantId, deliveryId);

if (result.success) {
  // Delivered
} else {
  // Failed or queued for retry
}

The delivery is signed with HMAC-SHA256. Each delivery includes:

{
  "id": "delivery-uuid",
  "eventId": "event-uuid",
  "type": "invoice.created",
  "data": { "id": "inv-456", "amount": 100 },
  "timestamp": "2026-09-10T22:30:00Z"
}

Header: X-AppPort-Signature: <hex-encoded-hmac-sha256>

Applications read deliveries under webhooks.deliveries.read (bounded, at most 200 per call):

const failed = await services.invoke('webhooks.deliveries.read', { status: 'failed', limit: 50 }, { principal });
const one = await services.invoke('webhooks.deliveries.read', { id: deliveryId }, { principal });

Consumer verification:

import crypto from 'node:crypto';

function verify(payload, signature, secret) {
  const expected = Buffer.from(crypto.createHmac('sha256', secret).update(payload).digest('hex'));
  const actual = Buffer.from(signature);
  return expected.length === actual.length && crypto.timingSafeEqual(expected, actual);
}

Retry policy

  • 2xx → delivered
  • 408 / 429 / 5xx / network failure → retry with bounded exponential backoff
  • 3xx → failed (terminal): redirects are never followed
  • other 4xx → failed (terminal)
  • AuthBoundry denial → failed; the provider is never called
  • AuthBoundry unavailable / timeout → retried later; the provider is never called
  • Max attempts → 5 (configurable)

Failed deliveries can be manually replayed:

await service.replayWebhookDelivery(tenantId, deliveryId, principal); // webhooks.replay

Disable endpoint

await service.disableWebhookEndpoint({ id: endpoint.id }, principal); // webhooks.remove

// No new deliveries created; existing pending deliveries blocked

Webhooks are at-least-once delivery. Consumers should treat eventId / deliveryId as idempotency identifiers.

How do I use durable jobs?

Jobs provide reliable background task execution with retry support, concurrency control via leases, and automatic recovery from worker failure.

Setup

import { createServices } from '@appport/services';

const services = createServices({ path: './.appport', application: 'invoices', authorizer: authBoundry });
const service = services.jobs;

Register handler

service.register('invoice.process', async (job, execution) => {
  const { invoiceId } = job.payload as { invoiceId: string };
  // The job runs as its durable principal; every effect is authorized again.
  await services.invoke('notifications.send', { recipient: 'finance', type: 'invoice.processed', title: invoiceId }, { principal: execution.principal });
});

Enqueue job

const job = await service.enqueue({
  type: 'invoice.process',
  payload: { invoiceId: 'inv-456' },
  maxAttempts: 3,
  delegationId: 'del_quote_agent', // optional AuthBoundry delegation, checked on every run
}, principal);

A job without a durable principal is never executed, and a revoked delegation stops the next run. See docs/JOB-SECURITY.md.

Execute job (worker)

const result = await service.executeJob(tenantId, jobId, 'worker-1');
if (result) {
  // Job completed successfully
}

Schedule recurring job

const schedule = await service.scheduleRecurring({
  type: 'invoice.reconcile',
  payload: { batchSize: 100 },
  interval: '1h',
}, principal);

Retry policy

  • Success → job marked completed
  • Failure → retry with exponential backoff (2^attemptCount)
  • Max attempts exceeded → job marked failed
  • Worker lease expired → job eligible for recovery by another worker
  • Manual retry → reset failed job to pending state

Jobs are durable and idempotent. Job state survives process restarts; workers claim jobs via version-based optimistic locking.

How do I send notifications?

Notifications are durable application events routed through one or more delivery channels. Applications decide what happened. AppPort Services decides how a durable notification is delivered.

Application       = meaning
AppPort Services  = delivery infrastructure
FeltDB            = durable state and evidence
Attn              = attention and judgment
const { notification, deliveries } = await app.notifications.notify({
  tenantId: 'tenant-a',
  recipient: 'user-1',
  type: 'monitor.triggered',
  title: 'Status changed',
  source: { type: 'monitor', id: 'monitor-7', eventId: 'observation-123' },
  channels: ['browser', 'in-app'],
}, principal);

await app.notifications.markRead('tenant-a', notification.id, recipientPrincipal);
await app.notifications.acknowledge('tenant-a', notification.id, recipientPrincipal);
  • Each channel has its own durable delivery record (pending, delivered, failed, retrying).
  • Repeating the same source event returns the same notification.
  • Retries run on the existing job infrastructure.
  • expiresAt stops obsolete deliveries but keeps the record as evidence.
  • Credentials (passwords, cookies, authorization headers, tokens, API keys, private keys) are rejected.
  • Closing a browser does not destroy notifications. The browser is one delivery channel, and a new session catches up from GET /notifications?unread=true.

Email, mobile push, SMS, and webhook channels are adapters registered with registerChannel(). See docs/notifications.md for the resource model, lifecycle, HTTP API, authorization, and the sensitive-data boundary.

Where does durable state live?

AppPort Services stores all state directly in FeltDB collections through the host's @feltdb/core (0.16.1 or later 0.x).

feltdb.flow (generated Flow contract)
      │
      ▼
AppPort Services
      │
      ├─→ FeltDbApiKeyStore (ApiKeys, ApiKeyPrefixes, ApiKeyAuditEvents)
      ├─→ FeltDbWebhookEndpointStore (WebhookEndpoints, WebhookAuditEvents)
      ├─→ FeltDbWebhookDeliveryStore (WebhookDeliveries)
      ├─→ FeltDbJobStore (Jobs, JobSchedules, JobAuditEvents)
      └─→ Secrets protocol (Secrets, SecretVersions, SecretAuditEvents)
      │
      ▼
 @feltdb/core (the host's copy)
      │
      ▼
 real FeltDB

What happens to the secret?

For AppPort Secrets, the boundary is:

application .flow → capability contract → AuthBoundry authorization → secret provider

Applications declare the secrets capability, but never put secret values in .flow. AppPort publishes identity, lifecycle, tenant, audit, and provider-reference metadata only; AppBoundry supplies provider execution after AuthBoundry authorization.

AppPort is declarative: it defines what the Secrets capability means. AppBoundry decides how it runs, and AuthBoundry decides who may use it. The Secrets contract contains no provider, storage, caching, injection, or authorization implementation.

  • generated with cryptographic randomness
  • returned exactly once at creation time
  • hashed with scrypt before persistence
  • never stored in the durable API-key record
  • excluded from audit records

Repository layout

  • appport.flow — Internal package template used to generate consumer feltdb.flow contracts
  • /src/api-keys — API-key models and semantic service
  • /src/authority — Policy Enforcement Point: capability manifest, verified principals, execution contexts, AuthBoundry protocol, destination policy, effect evidence
  • /src/webhooks — Webhook models, service, and signing helpers
    • models.ts — Endpoint, delivery, event contracts
    • service.ts — Webhook lifecycle and delivery orchestration
    • secrets.ts — Secret generation, encryption, HMAC signing
  • /src/jobs — Job models, service, and execution
    • models.ts — Job and schedule contracts
    • service.ts — Job lifecycle, retry, and scheduling
    • store.ts — FeltDB-backed storage with optimistic locking
    • worker.ts — Concurrent job execution with polling
  • /src/storage — FeltDB-backed store and audit sink
    • api-keys.ts — API key storage
    • webhooks.ts — Webhook endpoint/delivery storage
  • /src/runtime — HTTP/Express authentication adapters and Bearer-token extraction
    • api-keys.ts — Bearer token extraction
    • http-adapter.ts — Framework-neutral HTTP adapter
    • express-middleware.ts — Express middleware
  • /src/contract — AuthPort-facing principal contract
  • /tests — Node/TypeScript tests
    • contract.test.ts — Flow DSL validation and collection verification
    • api-keys.test.ts — Core service tests
    • http-adapter.test.ts — HTTP adapter tests (security, isolation, tenant safety)
    • integration-app.test.ts — Real HTTP application fixture
    • express-integration.test.ts — Express middleware integration tests
    • webhooks.test.ts — Webhook lifecycle and durability tests
    • webhooks-delivery.test.ts — HTTP delivery, signing, retry, and concurrency tests
    • jobs.test.ts — Job lifecycle, scheduling, and durability tests
    • jobs-execution.test.ts — Concurrent execution, leasing, and recovery tests
  • /docs — architecture notes

版本与发布记录

@appport/services 共发布 30 个版本,首次发布于 2026/09/11;过去 12 个月发布了 30 个版本。

版本发布日期距今
0.15.0latest2026/11/043 天前
0.14.02026/11/034 天前
0.13.02026/11/035 天前
0.12.02026/11/035 天前
0.10.02026/11/025 天前
0.9.12026/10/309 天前

关于 @appport/services 的常见问题

@appport/services 是什么?

API keys, durable webhooks, background jobs, and provider-neutral secrets for AppPort applications

如何安装 @appport/services?

在项目目录运行 npm install @appport/services,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@feltdb/core(>=0.16.1 <1.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。它还提供命令行工具 appport-services,可以用 npx appport-services 直接运行。

@appport/services 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@appport/services 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@appport/services 可以商用吗?

@appport/services 使用 ISC 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@appport/services 的下载量有多少?

根据 npm 官方下载统计,@appport/services 在 2026/10/07 – 2026/11/05 这 30 天内被下载 3,587 次,日均约 120 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@appport/services 还在维护吗?

最新版本 0.15.0 发布于 2026/11/04(3 天前);至今共发布 30 个版本,其中过去 12 个月发布了 30 个;首次发布于 2026/09/11。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。