@frank/crm (0.1.1)
Installation
@frank:registry=npm install @frank/crm@0.1.1"@frank/crm": "0.1.1"About this package
@frank/crm
One Frappe CRM site per customer, driven from a Node server. Ported from Blockwise src/lib/crm unchanged in behaviour: per-site vault credentials, command_id idempotency, the provision state machine with the real ready-probe, and the delivery worker with wait-not-fail semantics. Server-only; nothing here may be imported by a client component.
Public API (src/index.ts)
createWorkspaceCrm- mapping + credential + client + commands for a workspacecreateCrmClient,createCrmCommands- transport and typed command surfaceconfigFromEnv,CrmConfig,CrmNames,defaultCrmNames- configurationloadCrmSiteCredential,upsertCrmSiteCredential,clearCrmSiteCredentialresolveCrmSite,requireCrmSite,isValidCrmSite,crmSiteForSlugsignSsoToken,crmPublicHostForSite,ssoLoginUrl- the CRM sign-in tokenprovisionCrmSiteForWorkspace,createVaultCrmSiteProbe,createCrmProvisionTenantProvisioner- the state machineensureLeadCrmDeliveryJob,queueLeadCrmDelivery,executeLeadCrmDeliveryJobById,crmCaptureCommandId,formatLeadCrmDeliveryCrmErrorand helpers, domain types (CrmLead, stages, tasks)
Config
configFromEnv(env, names) is the only env reader: base URL (the CRM backend,
never an nginx frontend), timeout, vault key, delivery kill switch. Table,
queue-kind, RPC and Frappe-namespace names are a CrmNames parameter;
defaultCrmNames holds the Blockwise values. Queue helpers take the product's
job-queue enqueue as a parameter; this package owns no queue. Tests run with
npm run test: node --test, no network or database.
Usage
const crm = await createWorkspaceCrm({ supabase, serviceSupabase, workspaceId, config });
await crm.commands.captureEnquiry({
commandId: `crm-capture:${lead.source}:${lead.submissionId}`, // replay-safe
sourceProvider: lead.source,
sourceSubmissionId: lead.submissionId,
});
CRM sign-in token
signSsoToken mints the short-lived HS256 JWT that signs a workspace member
into Frappe's own CRM screens on their site (design step 3). The signing key is
the site's vault credential apiSecret, the same api_secret
blockwise_crm/sso.py decrypts on the site, so there is no second secret.
Claims: iss blockwise.sale, aud the site name, sub the trimmed
lower-cased email, workspace_id, role (owner, admin, member, operator),
iat, exp (default 60s, maximum 120s, matching the verifier), jti (24
random url-safe characters, consumed once per site). Bad input throws
CrmSsoInputError; it refuses the inputs the site is known to refuse (the verifier stays the authority).
const { token, expiresAt } = signSsoToken({
credential, siteName, email, workspaceId, role,
});
// The browser posts the token to ssoLoginUrl(siteName), which is
// crmPublicHostForSite(siteName) under https: acme.crm.internal becomes
// crm-acme.blockwise.sale (first level; the public base domain is
// names.publicBaseDomain).
Dependencies
Dependencies
| ID | Version |
|---|---|
| @supabase/supabase-js | ^2.49.8 |
Development dependencies
| ID | Version |
|---|---|
| @types/node | ^22.0.0 |