@odla-ai/chapter 0.0.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +305 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +356 -5
- package/dist/index.d.ts +356 -5
- package/dist/index.js +305 -4
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +167 -6
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +81 -0
- package/dist/worker/index.d.ts +81 -0
- package/dist/worker/index.js +165 -4
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,28 @@ import { CrmConfig, Crm } from '@odla-ai/crm';
|
|
|
4
4
|
* (join, Stripe membership, booking, member area, admin, CRM); `hub` is
|
|
5
5
|
* admin-only and CRM-focused (a directory/registry over the same CRM). */
|
|
6
6
|
type ChapterMode = "chapter" | "hub";
|
|
7
|
+
/** One odla-db transaction op — the wire shape both the real admin client and a
|
|
8
|
+
* test double (`FakeDb`) accept. Built as a plain object, never via a `tx` proxy. */
|
|
9
|
+
interface DbOp {
|
|
10
|
+
t: "update" | "merge" | "retract" | "delete";
|
|
11
|
+
ns: string;
|
|
12
|
+
id: string;
|
|
13
|
+
attrs?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
/** The structural view of the odla-db admin client the worker builds and the
|
|
16
|
+
* member routes consume — so route logic is testable against an in-memory fake. */
|
|
17
|
+
interface ChapterDb {
|
|
18
|
+
query(q: Record<string, unknown>): Promise<Record<string, Array<Record<string, unknown>>>>;
|
|
19
|
+
transact(ops: DbOp[], opts?: {
|
|
20
|
+
mutationId?: string;
|
|
21
|
+
}): Promise<{
|
|
22
|
+
txId: number;
|
|
23
|
+
duplicate: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
secrets: {
|
|
26
|
+
get(name: string): Promise<string>;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
7
29
|
/** The scalar kinds an odla-db attribute can hold. */
|
|
8
30
|
type AttrType = "string" | "number" | "boolean" | "json";
|
|
9
31
|
/** One odla-db attribute: its type and index/uniqueness/optionality flags. */
|
|
@@ -93,6 +115,75 @@ interface ChapterScheduling {
|
|
|
93
115
|
windowDays?: number;
|
|
94
116
|
summaryTemplate?: string;
|
|
95
117
|
}
|
|
118
|
+
/** How a signed-in user's role is determined.
|
|
119
|
+
*
|
|
120
|
+
* - `"claim"` reads a role ladder from a JWT claim (Silver & Salt's model:
|
|
121
|
+
* `provisional → member → admin`). Admin is the last (highest) rung; the
|
|
122
|
+
* read-only `superAdmins` tier sits above it.
|
|
123
|
+
* - `"table"` gates on the deny-all `admins` allowlist (Built Not Found's model):
|
|
124
|
+
* a binary "email is an admin or isn't", set only in odla Studio.
|
|
125
|
+
*
|
|
126
|
+
* Defaults: `"claim"` in `chapter` mode, `"table"` in `hub` mode. */
|
|
127
|
+
interface ChapterAuth {
|
|
128
|
+
source?: "claim" | "table";
|
|
129
|
+
/** JWT claim holding the role, for `source: "claim"`. Default `"role"`. */
|
|
130
|
+
claim?: string;
|
|
131
|
+
/** Role ladder low→high, for `source: "claim"`. The last entry is admin.
|
|
132
|
+
* Default `["provisional", "member", "admin"]`. */
|
|
133
|
+
ladder?: readonly string[];
|
|
134
|
+
/** Provision the read-only `superAdmins` tier table — the only tier that may
|
|
135
|
+
* create/modify admins, and (like every namespace) deny-all + written only in
|
|
136
|
+
* odla Studio. Default: `true` for `"claim"`, `false` for `"table"`. */
|
|
137
|
+
superAdmins?: boolean;
|
|
138
|
+
}
|
|
139
|
+
/** The fully-resolved auth policy (defaults applied) carried on the {@link Chapter}. */
|
|
140
|
+
interface ResolvedAuth {
|
|
141
|
+
source: "claim" | "table";
|
|
142
|
+
claim: string;
|
|
143
|
+
ladder: readonly string[];
|
|
144
|
+
/** The highest ladder rung (last entry) — the "admin" gate. */
|
|
145
|
+
adminRole: string;
|
|
146
|
+
superAdmins: boolean;
|
|
147
|
+
}
|
|
148
|
+
/** The application status pipeline. Which statuses exist, and the subsets a site
|
|
149
|
+
* allows a call to be booked from / an application approved from. Defaults to
|
|
150
|
+
* Silver & Salt's pipeline. Status never moves backwards (package-enforced). */
|
|
151
|
+
interface ChapterPipeline {
|
|
152
|
+
stages?: readonly string[];
|
|
153
|
+
bookableFrom?: readonly string[];
|
|
154
|
+
approvableFrom?: readonly string[];
|
|
155
|
+
/** The status a new application starts at. Default: the first stage. */
|
|
156
|
+
initial?: string;
|
|
157
|
+
}
|
|
158
|
+
/** The fully-resolved pipeline (defaults applied) carried on the {@link Chapter}. */
|
|
159
|
+
interface ResolvedPipeline {
|
|
160
|
+
stages: readonly string[];
|
|
161
|
+
bookableFrom: readonly string[];
|
|
162
|
+
approvableFrom: readonly string[];
|
|
163
|
+
initial: string;
|
|
164
|
+
}
|
|
165
|
+
/** The application (join form) validation surface — which string fields are
|
|
166
|
+
* required vs accepted, their max lengths, and the request body cap. Drives
|
|
167
|
+
* submit validation + the CRM slot projection; defaults to Silver & Salt's form.
|
|
168
|
+
* The `applications` schema attrs stay fixed (byte-equal to S&S); this is
|
|
169
|
+
* validation config, not schema generation. */
|
|
170
|
+
interface ChapterApplication {
|
|
171
|
+
required?: readonly string[];
|
|
172
|
+
optional?: readonly string[];
|
|
173
|
+
/** Per-field character cap. Fields not listed use `defaultMaxLen`. */
|
|
174
|
+
maxLen?: Record<string, number>;
|
|
175
|
+
defaultMaxLen?: number;
|
|
176
|
+
/** Max JSON request body in bytes. Default 32768. */
|
|
177
|
+
bodyCap?: number;
|
|
178
|
+
}
|
|
179
|
+
/** The fully-resolved application config carried on the {@link Chapter}. */
|
|
180
|
+
interface ResolvedApplication {
|
|
181
|
+
required: readonly string[];
|
|
182
|
+
optional: readonly string[];
|
|
183
|
+
maxLen: Record<string, number>;
|
|
184
|
+
defaultMaxLen: number;
|
|
185
|
+
bodyCap: number;
|
|
186
|
+
}
|
|
96
187
|
/** The `defineChapter()` config a site fills in. */
|
|
97
188
|
interface ChapterConfig {
|
|
98
189
|
/** Slug: app id, tenant, group id, worker name. `[a-z0-9-]`. */
|
|
@@ -111,6 +202,12 @@ interface ChapterConfig {
|
|
|
111
202
|
/** `notificationEmail` required in `chapter` mode. */
|
|
112
203
|
emails?: ChapterEmails;
|
|
113
204
|
scheduling?: ChapterScheduling;
|
|
205
|
+
/** Application status pipeline (stages + bookable/approvable subsets). Defaults to S&S's. */
|
|
206
|
+
pipeline?: ChapterPipeline;
|
|
207
|
+
/** Join-form validation (required/optional fields, max lengths, body cap). */
|
|
208
|
+
application?: ChapterApplication;
|
|
209
|
+
/** Role source + ladder + super-admin tier. Defaults by mode (see {@link ChapterAuth}). */
|
|
210
|
+
auth?: ChapterAuth;
|
|
114
211
|
/** odla services (db implied). Default `["db","calendar","o11y"]`. */
|
|
115
212
|
services?: readonly string[];
|
|
116
213
|
}
|
|
@@ -123,6 +220,12 @@ interface Chapter {
|
|
|
123
220
|
mode: ChapterMode;
|
|
124
221
|
/** Resolved CRM engine (from `defineCrm`). */
|
|
125
222
|
crm: Crm;
|
|
223
|
+
/** Resolved auth policy (source, claim, ladder, super-admin tier). */
|
|
224
|
+
auth: ResolvedAuth;
|
|
225
|
+
/** Resolved application status pipeline (stages + bookable/approvable subsets). */
|
|
226
|
+
pipeline: ResolvedPipeline;
|
|
227
|
+
/** Resolved join-form validation config. */
|
|
228
|
+
application: ResolvedApplication;
|
|
126
229
|
/** The chapter's own odla-db namespaces (mode-dependent; excludes `crm_*`). */
|
|
127
230
|
schema: DbSchema;
|
|
128
231
|
rules: DbRules;
|
|
@@ -179,10 +282,15 @@ interface ChapterIntegrationDescriptor {
|
|
|
179
282
|
*/
|
|
180
283
|
declare function createChapterIntegration(chapter: Chapter, options?: ChapterIntegrationOptions): ChapterIntegrationDescriptor;
|
|
181
284
|
|
|
182
|
-
/** The chapter's own schema + deny-all rules for a mode
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
|
|
285
|
+
/** The chapter's own schema + deny-all rules for a mode + auth policy.
|
|
286
|
+
*
|
|
287
|
+
* Operational tables (`applications`/`groups`/`meetings`/`emailLog`) are added in
|
|
288
|
+
* `chapter` mode only. The auth tables follow {@link ResolvedAuth}: `source:
|
|
289
|
+
* "table"` adds the `admins` allowlist (hub/BNF); `superAdmins` adds the
|
|
290
|
+
* read-only super-admin tier (default on for the `"claim"` ladder). A `"claim"`
|
|
291
|
+
* chapter therefore emits exactly Silver & Salt's namespace set — `applications`,
|
|
292
|
+
* `groups`, `meetings`, `emailLog`, `superAdmins` — with no `admins` table. */
|
|
293
|
+
declare function chapterDb(mode: ChapterMode, auth: ResolvedAuth): {
|
|
186
294
|
schema: DbSchema;
|
|
187
295
|
rules: DbRules;
|
|
188
296
|
};
|
|
@@ -195,4 +303,247 @@ declare function defaultCrm(mode: ChapterMode): CrmConfig;
|
|
|
195
303
|
* empty copy / defaults, so a minimal config still provisions cleanly. */
|
|
196
304
|
declare function buildGroupSeed(config: ChapterConfig): Record<string, unknown>;
|
|
197
305
|
|
|
198
|
-
|
|
306
|
+
/**
|
|
307
|
+
* Apply defaults + validate the auth config into a {@link ResolvedAuth}. Defaults
|
|
308
|
+
* by mode: `chapter` → the `provisional/member/admin` claim ladder with the
|
|
309
|
+
* `superAdmins` tier (Silver & Salt); `hub` → the `admins` allowlist table, no
|
|
310
|
+
* super tier (Built Not Found). Throws at import on a bad policy.
|
|
311
|
+
*/
|
|
312
|
+
declare function resolveAuth(mode: ChapterMode, auth: ChapterAuth | undefined): ResolvedAuth;
|
|
313
|
+
/** The role from a verified JWT payload, per the resolved policy. An unknown or
|
|
314
|
+
* missing claim falls back to the lowest ladder rung (fail safe, never admin). */
|
|
315
|
+
declare function roleFromClaim(payload: Record<string, unknown>, auth: ResolvedAuth): string;
|
|
316
|
+
/** Does a role meet the admin bar (the highest ladder rung)? */
|
|
317
|
+
declare function isAdminRole(role: string, auth: ResolvedAuth): boolean;
|
|
318
|
+
/** Inputs to the role-change guard — resolved by the caller (route) from the
|
|
319
|
+
* identity provider + the read-only `superAdmins` table. */
|
|
320
|
+
interface RoleChangeContext {
|
|
321
|
+
actorId: string;
|
|
322
|
+
actorIsSuper: boolean;
|
|
323
|
+
targetId: string;
|
|
324
|
+
targetCurrentRole: string;
|
|
325
|
+
targetIsSuper: boolean;
|
|
326
|
+
newRole: string;
|
|
327
|
+
auth: ResolvedAuth;
|
|
328
|
+
}
|
|
329
|
+
/** The result of {@link canChangeRole}: allow, or deny with the HTTP status +
|
|
330
|
+
* message the route should return. */
|
|
331
|
+
type GuardResult = {
|
|
332
|
+
ok: true;
|
|
333
|
+
} | {
|
|
334
|
+
ok: false;
|
|
335
|
+
status: number;
|
|
336
|
+
error: string;
|
|
337
|
+
};
|
|
338
|
+
/**
|
|
339
|
+
* The privilege-escalation guard — package-enforced so every site gets it and
|
|
340
|
+
* none re-derives it. Denies: an out-of-ladder role; changing your own role;
|
|
341
|
+
* touching a super-admin unless you are one; and (when a `superAdmins` tier
|
|
342
|
+
* exists) creating or altering an admin unless you are a super-admin. Note the
|
|
343
|
+
* super-admin tier itself is never writable here — it lives in the read-only
|
|
344
|
+
* `superAdmins` table, set only in odla Studio.
|
|
345
|
+
*/
|
|
346
|
+
declare function canChangeRole(ctx: RoleChangeContext): GuardResult;
|
|
347
|
+
/** Structural view of odla-db's tenant-vault read, so chapter takes no runtime
|
|
348
|
+
* dependency on @odla-ai/db. The worker's admin client satisfies this. */
|
|
349
|
+
interface SecretStore {
|
|
350
|
+
secrets: {
|
|
351
|
+
get(name: string): Promise<string>;
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Read a tenant-vault secret by name; `undefined` when it is absent or the vault
|
|
356
|
+
* errors, so callers degrade gracefully (e.g. `paymentsReady: false`) rather than
|
|
357
|
+
* throwing. Never logs the value.
|
|
358
|
+
*/
|
|
359
|
+
declare function getVaultSecret(db: SecretStore, name: string): Promise<string | undefined>;
|
|
360
|
+
|
|
361
|
+
/** One owner-editable template row on the group. `enabled` absent = enabled. */
|
|
362
|
+
interface EmailTemplateRow {
|
|
363
|
+
subject: string;
|
|
364
|
+
text: string;
|
|
365
|
+
enabled?: boolean;
|
|
366
|
+
}
|
|
367
|
+
/** The `groups`-row fields the email pipeline reads. */
|
|
368
|
+
interface EmailGroup {
|
|
369
|
+
id: string;
|
|
370
|
+
name: string;
|
|
371
|
+
replyTo: string;
|
|
372
|
+
/** Non-prod debug inbox: all mail redirects here outside prod (E2). */
|
|
373
|
+
debugEmail?: string;
|
|
374
|
+
refundPolicyText?: string;
|
|
375
|
+
commitmentText?: string;
|
|
376
|
+
normsText?: string;
|
|
377
|
+
emailTemplates: Record<string, EmailTemplateRow>;
|
|
378
|
+
}
|
|
379
|
+
/** `{{placeholder}}` substitution; unknown placeholders render empty. */
|
|
380
|
+
declare function render(template: string, vars: Record<string, string>): string;
|
|
381
|
+
/**
|
|
382
|
+
* Re-render a template's body for history/preview (E4): the CRM comms history
|
|
383
|
+
* reads back emails whose body predates `emailLog.body` by rendering the current
|
|
384
|
+
* template with the recipient's vars. Same substitution + group vars as the send
|
|
385
|
+
* path. `null` for an unknown template. Reflects the copy as it reads today, not
|
|
386
|
+
* necessarily the exact bytes originally sent (only `emailLog.body` is byte-exact).
|
|
387
|
+
*/
|
|
388
|
+
declare function renderTemplateBody(group: EmailGroup, template: string, vars: Record<string, string>): string | null;
|
|
389
|
+
/**
|
|
390
|
+
* E1 (exactly-once): given the prior `emailLog` rows for a `dedupeKey`, has the
|
|
391
|
+
* mail already been delivered? A prior row with **no error** means yes — the
|
|
392
|
+
* caller short-circuits the resend. Failure rows (which carry an `error` and are
|
|
393
|
+
* written without the dedupe mutationId) do not count, so a retry after a failure
|
|
394
|
+
* can still succeed.
|
|
395
|
+
*/
|
|
396
|
+
declare function isAlreadySent(priorRows: ReadonlyArray<{
|
|
397
|
+
error?: unknown;
|
|
398
|
+
}>): boolean;
|
|
399
|
+
/** The pure delivery decision produced by {@link planDelivery}. */
|
|
400
|
+
type DeliveryDecision = {
|
|
401
|
+
deliver: false;
|
|
402
|
+
reason: "template-missing" | "disabled";
|
|
403
|
+
} | {
|
|
404
|
+
deliver: true;
|
|
405
|
+
/** Which transport to use — `log-only` records the send but delivers nothing. */
|
|
406
|
+
transport: "cloudflare" | "log-only";
|
|
407
|
+
to: string;
|
|
408
|
+
subject: string;
|
|
409
|
+
text: string;
|
|
410
|
+
/** True when redirected to the non-prod debug inbox. */
|
|
411
|
+
redirected: boolean;
|
|
412
|
+
};
|
|
413
|
+
/**
|
|
414
|
+
* The pure delivery decision (E2 fail-safe + E3 enabled). Given the env, group,
|
|
415
|
+
* template, recipient, and whether a real Cloudflare transport is wired:
|
|
416
|
+
* - missing template → not delivered (`template-missing`);
|
|
417
|
+
* - disabled template and not forced → not delivered (`disabled`);
|
|
418
|
+
* - **non-prod with a debug inbox** → REDIRECT to it, `"[dev] "` subject prefix,
|
|
419
|
+
* a dev-redirect note in the body, so test applicants never receive real mail;
|
|
420
|
+
* - **non-prod with NO debug inbox** → force `log-only` (deliver nothing) — the
|
|
421
|
+
* fail-safe that protects every site's test data;
|
|
422
|
+
* - prod → deliver via the real transport (`cloudflare` if wired, else `log-only`).
|
|
423
|
+
*/
|
|
424
|
+
declare function planDelivery(input: {
|
|
425
|
+
envName: string;
|
|
426
|
+
group: EmailGroup;
|
|
427
|
+
template: string;
|
|
428
|
+
to: string;
|
|
429
|
+
vars: Record<string, string>;
|
|
430
|
+
/** Whether a Cloudflare Email Service transport (binding + verified from) is wired. */
|
|
431
|
+
cloudflareReady: boolean;
|
|
432
|
+
/** The admin test route may send a disabled template. */
|
|
433
|
+
force?: boolean;
|
|
434
|
+
}): DeliveryDecision;
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Apply defaults + validate the pipeline config. With no config, the full Silver
|
|
438
|
+
* & Salt pipeline. With `stages` given but the subsets omitted, the subsets
|
|
439
|
+
* default to empty (a site opts in to bookable/approvable states explicitly).
|
|
440
|
+
* Throws at import on a bad pipeline (empty/duplicate stages, an initial or a
|
|
441
|
+
* subset entry not on the ladder).
|
|
442
|
+
*/
|
|
443
|
+
declare function resolvePipeline(p: ChapterPipeline | undefined): ResolvedPipeline;
|
|
444
|
+
/** The ordinal of a status in the ladder, or -1 if unknown. */
|
|
445
|
+
declare function stageIndex(status: string, p: ResolvedPipeline): number;
|
|
446
|
+
/**
|
|
447
|
+
* The status-never-moves-backwards invariant: a transition is allowed only when
|
|
448
|
+
* both statuses are on the ladder and `to` is at or ahead of `from`. The worker
|
|
449
|
+
* calls this before every status write; a violation is a 409, never a silent
|
|
450
|
+
* downgrade.
|
|
451
|
+
*/
|
|
452
|
+
declare function canTransition(from: string, to: string, p: ResolvedPipeline): boolean;
|
|
453
|
+
/** May an intro call be booked from this status? */
|
|
454
|
+
declare function canBook(status: string, p: ResolvedPipeline): boolean;
|
|
455
|
+
/** May an application be approved (→ member) from this status? */
|
|
456
|
+
declare function canApprove(status: string, p: ResolvedPipeline): boolean;
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Verify a Stripe webhook signature (C3): HMAC-SHA256 over `` `${t}.${payload}` ``
|
|
460
|
+
* with the endpoint signing secret, a replay window (default 5 minutes), and a
|
|
461
|
+
* constant-time compare. Package-enforced — never left to a site. Returns `false`
|
|
462
|
+
* (never throws) on a malformed header, a non-numeric or stale timestamp, or a
|
|
463
|
+
* signature mismatch. `now`/`toleranceSec` are injectable for tests.
|
|
464
|
+
*/
|
|
465
|
+
declare function verifyStripeSignature(payload: string, header: string, secret: string, opts?: {
|
|
466
|
+
now?: number;
|
|
467
|
+
toleranceSec?: number;
|
|
468
|
+
}): Promise<boolean>;
|
|
469
|
+
|
|
470
|
+
/** Apply defaults + validate the application config. Throws at import on bad shape. */
|
|
471
|
+
declare function resolveApplication(a: ChapterApplication | undefined): ResolvedApplication;
|
|
472
|
+
/** A validated submission, or a 400-worthy validation error the route returns. */
|
|
473
|
+
type SubmitResult = {
|
|
474
|
+
ok: true;
|
|
475
|
+
id: string;
|
|
476
|
+
duplicate: boolean;
|
|
477
|
+
status: string;
|
|
478
|
+
} | {
|
|
479
|
+
ok: false;
|
|
480
|
+
error: string;
|
|
481
|
+
};
|
|
482
|
+
/**
|
|
483
|
+
* Submit a membership application (B2 + B3). Validates the configured required
|
|
484
|
+
* fields + max lengths, writes the `applications` row at the pipeline's initial
|
|
485
|
+
* status, and — when the client supplies a `submissionId` — stamps it as the
|
|
486
|
+
* transaction's mutationId (`join:${submissionId}`) so a double-tap can never
|
|
487
|
+
* create two applications (the second returns `duplicate: true`). Idempotency is
|
|
488
|
+
* package-enforced. `now`/`newId` are injected (deterministic in tests).
|
|
489
|
+
*/
|
|
490
|
+
declare function submitApplication(db: ChapterDb, chapter: Chapter, fields: Record<string, unknown>, opts: {
|
|
491
|
+
submissionId?: string;
|
|
492
|
+
groupId?: string;
|
|
493
|
+
now: number;
|
|
494
|
+
newId: () => string;
|
|
495
|
+
}): Promise<SubmitResult>;
|
|
496
|
+
/** The `groups`-row fields the join config exposes. */
|
|
497
|
+
interface JoinConfigGroup {
|
|
498
|
+
id: string;
|
|
499
|
+
name: string;
|
|
500
|
+
standardPriceCents?: number;
|
|
501
|
+
foundingDiscountCents?: number;
|
|
502
|
+
disclaimerText?: string;
|
|
503
|
+
refundPolicyText?: string;
|
|
504
|
+
trustCopy?: string;
|
|
505
|
+
commitmentText?: string;
|
|
506
|
+
normsText?: string;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* The public join config (B1) a site's join page reads: copy + prices from the
|
|
510
|
+
* group row plus `paymentsReady`. When payments aren't wired the join flow drops
|
|
511
|
+
* the payment step (C2) — the worker computes `paymentsReady` from the group's
|
|
512
|
+
* Stripe keys + vault secret. Pure.
|
|
513
|
+
*/
|
|
514
|
+
declare function joinConfig(group: JoinConfigGroup, paymentsReady: boolean): Record<string, unknown>;
|
|
515
|
+
|
|
516
|
+
/** The contact data the hub shares for a prospect. `hubRecordId` is the stable
|
|
517
|
+
* idempotency key (the hub's crm_record id). */
|
|
518
|
+
interface SharedPerson {
|
|
519
|
+
email: string;
|
|
520
|
+
name?: string;
|
|
521
|
+
firstName?: string;
|
|
522
|
+
lastName?: string;
|
|
523
|
+
phone?: string;
|
|
524
|
+
linkedin?: string;
|
|
525
|
+
hubRecordId: string;
|
|
526
|
+
}
|
|
527
|
+
/** Map a shared prospect to a crm `person` input (only the fields the default
|
|
528
|
+
* person type accepts). Name falls back to first+last, then the email. */
|
|
529
|
+
declare function sharedPersonInput(person: SharedPerson): Record<string, unknown>;
|
|
530
|
+
/** Deps for the projection — the resolved CRM engine, the structural db, and
|
|
531
|
+
* injected clock/id (deterministic in tests). */
|
|
532
|
+
interface ProjectionDeps {
|
|
533
|
+
crm: Crm;
|
|
534
|
+
db: ChapterDb;
|
|
535
|
+
now: () => number;
|
|
536
|
+
newId: () => string;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Upsert a hub-shared prospect into this chapter's `crm_record` (push
|
|
540
|
+
* projection). Resolves an existing person by lowercased `primaryEmail` and
|
|
541
|
+
* updates it, else creates one with a `share:${hubRecordId}` mutationId. Returns
|
|
542
|
+
* the chapter-side record id. Callers wrap this in `.catch` so a projection
|
|
543
|
+
* failure never fails the hub's share request.
|
|
544
|
+
*/
|
|
545
|
+
declare function projectSharedRecord(deps: ProjectionDeps, person: SharedPerson): Promise<{
|
|
546
|
+
recordId: string;
|
|
547
|
+
}>;
|
|
548
|
+
|
|
549
|
+
export { type Attr, type AttrType, type Chapter, type ChapterApplication, type ChapterAuth, type ChapterBrand, type ChapterConfig, type ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterScheduling, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type GuardResult, type JoinConfigGroup, type ProjectionDeps, type ResolvedApplication, type ResolvedAuth, type ResolvedPipeline, type RoleChangeContext, type Rule, type SecretStore, type SharedPerson, type SubmitResult, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, chapterDb, createChapterIntegration, defaultCrm, defineChapter, getVaultSecret, isAdminRole, isAlreadySent, joinConfig, planDelivery, projectSharedRecord, render, renderTemplateBody, resolveApplication, resolveAuth, resolvePipeline, roleFromClaim, sharedPersonInput, stageIndex, submitApplication, verifyStripeSignature };
|