@odla-ai/chapter 0.42.0 → 0.42.2
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/README.md +32 -4
- package/dist/{chunk-MMDWXTJU.js → chunk-HWUTCXBV.js} +44 -1
- package/dist/chunk-HWUTCXBV.js.map +1 -0
- package/dist/{copy-context-DuJkH4U9.d.ts → copy-context-BBPsYe24.d.ts} +27 -1
- package/dist/index.cjs +144 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -3
- package/dist/index.d.ts +75 -3
- package/dist/index.js +144 -16
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +14 -3
- package/dist/ui/admin/index.js +3 -1
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +3 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +745 -163
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +73 -2
- package/dist/worker/index.d.ts +73 -2
- package/dist/worker/index.js +730 -148
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
- package/runbooks/greenfield.md +3 -0
- package/dist/chunk-MMDWXTJU.js.map +0 -1
|
@@ -186,6 +186,16 @@ interface ChapterNetworkTarget {
|
|
|
186
186
|
* this follower. Omitted by default: a target never gains write permission
|
|
187
187
|
* merely because its records are browsable. */
|
|
188
188
|
sharedNotes?: readonly string[];
|
|
189
|
+
/** Per-record-type upper bound on profile fields this leader permits its
|
|
190
|
+
* admins to edit remotely. The follower must independently delegate the
|
|
191
|
+
* same field; readable fields never become writable implicitly. */
|
|
192
|
+
editableFields?: Record<string, readonly string[]>;
|
|
193
|
+
/** Record types on which this leader permits remote lifecycle transitions.
|
|
194
|
+
* The follower must independently opt in and remains pipeline authority. */
|
|
195
|
+
stageTransitions?: readonly string[];
|
|
196
|
+
/** Allow package-owned desired admission decisions for existing leader CRM
|
|
197
|
+
* people to be delivered to this target. Omitted means default deny. */
|
|
198
|
+
admissionGrants?: boolean;
|
|
189
199
|
}
|
|
190
200
|
/** One signed peer allowed to browse a privacy-bounded projection of this
|
|
191
201
|
* site's CRM. The follower owns this allowlist; a leader target declaration
|
|
@@ -195,11 +205,21 @@ interface ChapterNetworkReader {
|
|
|
195
205
|
id: string;
|
|
196
206
|
/** Per-record-type field allowlist. Omitted types and fields never leave
|
|
197
207
|
* this site. Record id, name, stage, and timestamps are bounded metadata. */
|
|
198
|
-
fields
|
|
208
|
+
fields?: Record<string, readonly string[]>;
|
|
199
209
|
/** Record types on which this reader may append an explicitly shared note.
|
|
200
210
|
* Omitted by default: browse permission never silently grants write access.
|
|
201
211
|
* Shared notes land in the follower CRM activity feed. */
|
|
202
212
|
sharedNotes?: readonly string[];
|
|
213
|
+
/** Exact local CRM fields this reader may edit remotely, by record type.
|
|
214
|
+
* Every field must also be readable so the peer can perform an optimistic
|
|
215
|
+
* edit against the value and revision it actually reviewed. */
|
|
216
|
+
editableFields?: Record<string, readonly string[]>;
|
|
217
|
+
/** Local CRM record types on which this reader may request a lifecycle
|
|
218
|
+
* transition. Chapter still enforces the local pipeline and gates. */
|
|
219
|
+
stageTransitions?: readonly string[];
|
|
220
|
+
/** Accept signed desired admission decisions from this reader. Independent
|
|
221
|
+
* of record browsing; an admission-only reader may omit `fields`. */
|
|
222
|
+
admissionGrants?: boolean;
|
|
203
223
|
}
|
|
204
224
|
/** Leader/follower network configuration. A follower needs no code config to
|
|
205
225
|
* receive pushed records. It must explicitly declare `readers` before any peer
|
|
@@ -242,12 +262,18 @@ interface ResolvedNetworkTarget {
|
|
|
242
262
|
secretName: string;
|
|
243
263
|
fields?: Record<string, readonly string[]>;
|
|
244
264
|
sharedNotes: readonly string[];
|
|
265
|
+
editableFields: Record<string, readonly string[]>;
|
|
266
|
+
stageTransitions: readonly string[];
|
|
267
|
+
admissionGrants?: boolean;
|
|
245
268
|
}
|
|
246
269
|
/** A validated follower-owned record projection for one signed reader. */
|
|
247
270
|
interface ResolvedNetworkReader {
|
|
248
271
|
id: string;
|
|
249
272
|
fields: Record<string, readonly string[]>;
|
|
250
273
|
sharedNotes: readonly string[];
|
|
274
|
+
editableFields: Record<string, readonly string[]>;
|
|
275
|
+
stageTransitions: readonly string[];
|
|
276
|
+
admissionGrants?: boolean;
|
|
251
277
|
}
|
|
252
278
|
/** Fully-resolved leader network configuration. */
|
|
253
279
|
interface ResolvedNetwork {
|
package/dist/index.cjs
CHANGED
|
@@ -486,6 +486,50 @@ var admissionGrantRevisionsEntity = {
|
|
|
486
486
|
receivedAt: attr2("number", { indexed: true })
|
|
487
487
|
}
|
|
488
488
|
};
|
|
489
|
+
var leaderAdmissionAssignmentsEntity = {
|
|
490
|
+
attrs: {
|
|
491
|
+
id: id(),
|
|
492
|
+
targetId: attr2("string", { indexed: true }),
|
|
493
|
+
personId: attr2("string", { indexed: true }),
|
|
494
|
+
grantId: attr2("string", { indexed: true }),
|
|
495
|
+
decision: attr2("string", { indexed: true }),
|
|
496
|
+
note: attr2("string", { optional: true }),
|
|
497
|
+
expiresAt: attr2("number", { optional: true, indexed: true }),
|
|
498
|
+
active: attr2("boolean", { indexed: true }),
|
|
499
|
+
lastMutationId: attr2("string", { indexed: true }),
|
|
500
|
+
lastRequestDigest: attr2("string"),
|
|
501
|
+
createdAt: attr2("number", { indexed: true }),
|
|
502
|
+
createdBy: attr2("string", { indexed: true }),
|
|
503
|
+
updatedAt: attr2("number", { indexed: true }),
|
|
504
|
+
updatedBy: attr2("string", { indexed: true })
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
var leaderAdmissionDeliveriesEntity = {
|
|
508
|
+
attrs: {
|
|
509
|
+
id: id(),
|
|
510
|
+
targetId: attr2("string", { indexed: true }),
|
|
511
|
+
environment: attr2("string", { indexed: true }),
|
|
512
|
+
revision: attr2("number", { indexed: true }),
|
|
513
|
+
digest: attr2("string", { indexed: true }),
|
|
514
|
+
envelope: attr2("json"),
|
|
515
|
+
status: attr2("string", { indexed: true }),
|
|
516
|
+
attempts: attr2("number", { indexed: true }),
|
|
517
|
+
lastAttemptAt: attr2("number", { optional: true, indexed: true }),
|
|
518
|
+
lastError: attr2("string", { optional: true }),
|
|
519
|
+
acknowledgedAt: attr2("number", { optional: true, indexed: true }),
|
|
520
|
+
updatedAt: attr2("number", { indexed: true })
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
var leaderAdmissionMutationsEntity = {
|
|
524
|
+
attrs: {
|
|
525
|
+
id: id(),
|
|
526
|
+
targetId: attr2("string", { indexed: true }),
|
|
527
|
+
personId: attr2("string", { indexed: true }),
|
|
528
|
+
requestDigest: attr2("string", { indexed: true }),
|
|
529
|
+
revision: attr2("number", { indexed: true }),
|
|
530
|
+
createdAt: attr2("number", { indexed: true })
|
|
531
|
+
}
|
|
532
|
+
};
|
|
489
533
|
|
|
490
534
|
// src/schema-gifts.ts
|
|
491
535
|
var attr3 = (type, flags = {}) => ({
|
|
@@ -757,7 +801,7 @@ var emailLog = {
|
|
|
757
801
|
sentAt: attr4("number", { indexed: true })
|
|
758
802
|
}
|
|
759
803
|
};
|
|
760
|
-
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false) {
|
|
804
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false, includeLeaderAdmissions = false) {
|
|
761
805
|
const entities = {};
|
|
762
806
|
if (mode === "chapter") {
|
|
763
807
|
entities.applications = applicationsFor(application);
|
|
@@ -785,6 +829,11 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
785
829
|
entities.builderLibraryControls = builderLibraryControls;
|
|
786
830
|
entities.builderLibraryLocal = builderLibraryLocal;
|
|
787
831
|
}
|
|
832
|
+
if (includeLeaderAdmissions) {
|
|
833
|
+
entities.leaderAdmissionAssignments = leaderAdmissionAssignmentsEntity;
|
|
834
|
+
entities.leaderAdmissionDeliveries = leaderAdmissionDeliveriesEntity;
|
|
835
|
+
entities.leaderAdmissionMutations = leaderAdmissionMutationsEntity;
|
|
836
|
+
}
|
|
788
837
|
const schema = { entities, links: {} };
|
|
789
838
|
const rules = {};
|
|
790
839
|
for (const ns of Object.keys(entities)) {
|
|
@@ -1553,6 +1602,12 @@ function formationFields(crm, formation) {
|
|
|
1553
1602
|
return resolveFormationFlowFields(crm, formation);
|
|
1554
1603
|
}
|
|
1555
1604
|
|
|
1605
|
+
// src/network-contract.ts
|
|
1606
|
+
var DEFAULT_SHARE_FIELDS = {
|
|
1607
|
+
person: ["name", "email", "firstName", "lastName", "phone", "linkedin"],
|
|
1608
|
+
company: ["name", "domain", "industry", "location", "linkedin", "notes"]
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1556
1611
|
// src/config-network.ts
|
|
1557
1612
|
var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1558
1613
|
var FIELD = /^[a-z][a-zA-Z0-9_]*$/;
|
|
@@ -1564,6 +1619,34 @@ function validateSharedNotes(opts) {
|
|
|
1564
1619
|
throw new Error(`${opts.path}: must contain unique CRM types allowed by fields`);
|
|
1565
1620
|
}
|
|
1566
1621
|
}
|
|
1622
|
+
function validateEditableFields(opts) {
|
|
1623
|
+
for (const [type, names] of Object.entries(opts.values)) {
|
|
1624
|
+
const readable = opts.readable?.[type];
|
|
1625
|
+
const def = opts.crm.config.types[type];
|
|
1626
|
+
const duplicate = names.some((name, index) => names.indexOf(name) !== index);
|
|
1627
|
+
const unknown = names.some((name) => !FIELD.test(name) || !def?.fields[name]);
|
|
1628
|
+
const hidden = opts.readable ? !readable || names.some((name) => !readable.includes(name)) : false;
|
|
1629
|
+
if (!FIELD.test(type) || names.length === 0 || duplicate || !def || unknown || hidden) {
|
|
1630
|
+
throw new Error(
|
|
1631
|
+
`${opts.path}.${type}: must contain unique, readable fields declared on CRM type "${type}"`
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
function validateStageTransitions(opts) {
|
|
1637
|
+
const duplicate = opts.values.some((type, index) => opts.values.indexOf(type) !== index);
|
|
1638
|
+
const invalid = opts.values.some((type) => !FIELD.test(type) || !opts.crm.config.types[type]?.pipeline || (opts.readable ? !opts.readable[type] : false));
|
|
1639
|
+
if (duplicate || invalid) {
|
|
1640
|
+
throw new Error(`${opts.path}: must contain unique, readable CRM types with pipelines`);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
function validateAdmissionCapability(path, enabled, crm) {
|
|
1644
|
+
if (!enabled) return;
|
|
1645
|
+
const person = crm.config.types.person;
|
|
1646
|
+
if (!person?.emailField || person.fields[person.emailField]?.type !== "email") {
|
|
1647
|
+
throw new Error(`${path}: requires a CRM person type with an emailField`);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1567
1650
|
function resolveNetwork(config, crm) {
|
|
1568
1651
|
const seen = /* @__PURE__ */ new Set();
|
|
1569
1652
|
const targets = [];
|
|
@@ -1622,6 +1705,26 @@ function resolveNetwork(config, crm) {
|
|
|
1622
1705
|
crm,
|
|
1623
1706
|
fields
|
|
1624
1707
|
});
|
|
1708
|
+
const readableFields = fields ?? DEFAULT_SHARE_FIELDS;
|
|
1709
|
+
const editableFields = target.editableFields ?? {};
|
|
1710
|
+
validateEditableFields({
|
|
1711
|
+
path: `defineChapter.network.targets.${target.id}.editableFields`,
|
|
1712
|
+
values: editableFields,
|
|
1713
|
+
crm,
|
|
1714
|
+
readable: readableFields
|
|
1715
|
+
});
|
|
1716
|
+
const stageTransitions = target.stageTransitions ?? [];
|
|
1717
|
+
validateStageTransitions({
|
|
1718
|
+
path: `defineChapter.network.targets.${target.id}.stageTransitions`,
|
|
1719
|
+
values: stageTransitions,
|
|
1720
|
+
crm,
|
|
1721
|
+
readable: readableFields
|
|
1722
|
+
});
|
|
1723
|
+
validateAdmissionCapability(
|
|
1724
|
+
`defineChapter.network.targets.${target.id}.admissionGrants`,
|
|
1725
|
+
target.admissionGrants,
|
|
1726
|
+
crm
|
|
1727
|
+
);
|
|
1625
1728
|
targets.push({
|
|
1626
1729
|
id: target.id,
|
|
1627
1730
|
name: target.name?.trim() || target.id,
|
|
@@ -1629,7 +1732,10 @@ function resolveNetwork(config, crm) {
|
|
|
1629
1732
|
...binding ? { binding } : {},
|
|
1630
1733
|
secretName,
|
|
1631
1734
|
...fields ? { fields } : {},
|
|
1632
|
-
sharedNotes
|
|
1735
|
+
sharedNotes,
|
|
1736
|
+
editableFields,
|
|
1737
|
+
stageTransitions,
|
|
1738
|
+
...target.admissionGrants === true ? { admissionGrants: true } : {}
|
|
1633
1739
|
});
|
|
1634
1740
|
}
|
|
1635
1741
|
const readerIds = /* @__PURE__ */ new Set();
|
|
@@ -1642,9 +1748,12 @@ function resolveNetwork(config, crm) {
|
|
|
1642
1748
|
throw new Error(`defineChapter.network.readers: duplicate reader "${reader.id}"`);
|
|
1643
1749
|
}
|
|
1644
1750
|
readerIds.add(reader.id);
|
|
1645
|
-
const
|
|
1646
|
-
|
|
1647
|
-
|
|
1751
|
+
const readerFields = reader.fields ?? {};
|
|
1752
|
+
const entries = Object.entries(readerFields);
|
|
1753
|
+
if (entries.length === 0 && !reader.admissionGrants) {
|
|
1754
|
+
throw new Error(
|
|
1755
|
+
`defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required unless this is an admission-only reader`
|
|
1756
|
+
);
|
|
1648
1757
|
}
|
|
1649
1758
|
for (const [type, names] of entries) {
|
|
1650
1759
|
if (!FIELD.test(type) || names.length === 0 || names.some((name) => !FIELD.test(name))) {
|
|
@@ -1674,9 +1783,35 @@ function resolveNetwork(config, crm) {
|
|
|
1674
1783
|
path: `defineChapter.network.readers.${reader.id}.sharedNotes`,
|
|
1675
1784
|
values: sharedNotes,
|
|
1676
1785
|
crm,
|
|
1677
|
-
fields:
|
|
1786
|
+
fields: readerFields
|
|
1787
|
+
});
|
|
1788
|
+
const editableFields = reader.editableFields ?? {};
|
|
1789
|
+
validateEditableFields({
|
|
1790
|
+
path: `defineChapter.network.readers.${reader.id}.editableFields`,
|
|
1791
|
+
values: editableFields,
|
|
1792
|
+
crm,
|
|
1793
|
+
readable: readerFields
|
|
1794
|
+
});
|
|
1795
|
+
const stageTransitions = reader.stageTransitions ?? [];
|
|
1796
|
+
validateStageTransitions({
|
|
1797
|
+
path: `defineChapter.network.readers.${reader.id}.stageTransitions`,
|
|
1798
|
+
values: stageTransitions,
|
|
1799
|
+
crm,
|
|
1800
|
+
readable: readerFields
|
|
1801
|
+
});
|
|
1802
|
+
validateAdmissionCapability(
|
|
1803
|
+
`defineChapter.network.readers.${reader.id}.admissionGrants`,
|
|
1804
|
+
reader.admissionGrants,
|
|
1805
|
+
crm
|
|
1806
|
+
);
|
|
1807
|
+
readers.push({
|
|
1808
|
+
id: reader.id,
|
|
1809
|
+
fields: readerFields,
|
|
1810
|
+
sharedNotes,
|
|
1811
|
+
editableFields,
|
|
1812
|
+
stageTransitions,
|
|
1813
|
+
...reader.admissionGrants === true ? { admissionGrants: true } : {}
|
|
1678
1814
|
});
|
|
1679
|
-
readers.push({ id: reader.id, fields: reader.fields, sharedNotes });
|
|
1680
1815
|
}
|
|
1681
1816
|
return { targets, readers };
|
|
1682
1817
|
}
|
|
@@ -1807,7 +1942,8 @@ function defineChapter(config) {
|
|
|
1807
1942
|
application,
|
|
1808
1943
|
Boolean(signupControl),
|
|
1809
1944
|
Boolean(memberContent),
|
|
1810
|
-
Boolean(builderLibrary)
|
|
1945
|
+
Boolean(builderLibrary),
|
|
1946
|
+
network.targets.some((target) => target.admissionGrants)
|
|
1811
1947
|
);
|
|
1812
1948
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1813
1949
|
const account = config.account ?? "none";
|
|
@@ -3254,14 +3390,6 @@ function findGiftOffer(catalog, offerId, now) {
|
|
|
3254
3390
|
|
|
3255
3391
|
// src/network.ts
|
|
3256
3392
|
var import_crm5 = require("@odla-ai/crm");
|
|
3257
|
-
|
|
3258
|
-
// src/network-contract.ts
|
|
3259
|
-
var DEFAULT_SHARE_FIELDS = {
|
|
3260
|
-
person: ["name", "email", "firstName", "lastName", "phone", "linkedin"],
|
|
3261
|
-
company: ["name", "domain", "industry", "location", "linkedin", "notes"]
|
|
3262
|
-
};
|
|
3263
|
-
|
|
3264
|
-
// src/network.ts
|
|
3265
3393
|
function sharedPersonInput(person) {
|
|
3266
3394
|
const email = person.email.toLowerCase();
|
|
3267
3395
|
const fullName = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
|