@kelpie/schemas 0.1.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/LICENSE +661 -0
- package/README.md +33 -0
- package/dist/account.d.ts +79 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +66 -0
- package/dist/account.js.map +1 -0
- package/dist/activity.d.ts +23 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/activity.js +27 -0
- package/dist/activity.js.map +1 -0
- package/dist/agentTask.d.ts +97 -0
- package/dist/agentTask.d.ts.map +1 -0
- package/dist/agentTask.js +127 -0
- package/dist/agentTask.js.map +1 -0
- package/dist/candidate.d.ts +39 -0
- package/dist/candidate.d.ts.map +1 -0
- package/dist/candidate.js +40 -0
- package/dist/candidate.js.map +1 -0
- package/dist/company.d.ts +38 -0
- package/dist/company.d.ts.map +1 -0
- package/dist/company.js +57 -0
- package/dist/company.js.map +1 -0
- package/dist/dashboard.d.ts +112 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +165 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/deal.d.ts +40 -0
- package/dist/deal.d.ts.map +1 -0
- package/dist/deal.js +59 -0
- package/dist/deal.js.map +1 -0
- package/dist/decision.d.ts +46 -0
- package/dist/decision.d.ts.map +1 -0
- package/dist/decision.js +48 -0
- package/dist/decision.js.map +1 -0
- package/dist/form.d.ts +97 -0
- package/dist/form.d.ts.map +1 -0
- package/dist/form.js +98 -0
- package/dist/form.js.map +1 -0
- package/dist/formSubmission.d.ts +47 -0
- package/dist/formSubmission.d.ts.map +1 -0
- package/dist/formSubmission.js +47 -0
- package/dist/formSubmission.js.map +1 -0
- package/dist/handbookPage.d.ts +52 -0
- package/dist/handbookPage.d.ts.map +1 -0
- package/dist/handbookPage.js +56 -0
- package/dist/handbookPage.js.map +1 -0
- package/dist/importExport.d.ts +143 -0
- package/dist/importExport.d.ts.map +1 -0
- package/dist/importExport.js +239 -0
- package/dist/importExport.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/invite.d.ts +35 -0
- package/dist/invite.d.ts.map +1 -0
- package/dist/invite.js +31 -0
- package/dist/invite.js.map +1 -0
- package/dist/mcpTool.d.ts +26 -0
- package/dist/mcpTool.d.ts.map +1 -0
- package/dist/mcpTool.js +13 -0
- package/dist/mcpTool.js.map +1 -0
- package/dist/member.d.ts +26 -0
- package/dist/member.d.ts.map +1 -0
- package/dist/member.js +24 -0
- package/dist/member.js.map +1 -0
- package/dist/moduleSetting.d.ts +19 -0
- package/dist/moduleSetting.d.ts.map +1 -0
- package/dist/moduleSetting.js +16 -0
- package/dist/moduleSetting.js.map +1 -0
- package/dist/note.d.ts +34 -0
- package/dist/note.d.ts.map +1 -0
- package/dist/note.js +35 -0
- package/dist/note.js.map +1 -0
- package/dist/opportunity.d.ts +30 -0
- package/dist/opportunity.d.ts.map +1 -0
- package/dist/opportunity.js +41 -0
- package/dist/opportunity.js.map +1 -0
- package/dist/partnership.d.ts +36 -0
- package/dist/partnership.d.ts.map +1 -0
- package/dist/partnership.js +50 -0
- package/dist/partnership.js.map +1 -0
- package/dist/person.d.ts +45 -0
- package/dist/person.d.ts.map +1 -0
- package/dist/person.js +60 -0
- package/dist/person.js.map +1 -0
- package/dist/pipelineStage.d.ts +29 -0
- package/dist/pipelineStage.d.ts.map +1 -0
- package/dist/pipelineStage.js +38 -0
- package/dist/pipelineStage.js.map +1 -0
- package/dist/planItem.d.ts +43 -0
- package/dist/planItem.d.ts.map +1 -0
- package/dist/planItem.js +44 -0
- package/dist/planItem.js.map +1 -0
- package/dist/position.d.ts +22 -0
- package/dist/position.d.ts.map +1 -0
- package/dist/position.js +25 -0
- package/dist/position.js.map +1 -0
- package/dist/raise.d.ts +38 -0
- package/dist/raise.d.ts.map +1 -0
- package/dist/raise.js +53 -0
- package/dist/raise.js.map +1 -0
- package/dist/role.d.ts +21 -0
- package/dist/role.d.ts.map +1 -0
- package/dist/role.js +21 -0
- package/dist/role.js.map +1 -0
- package/dist/search.d.ts +41 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +41 -0
- package/dist/search.js.map +1 -0
- package/dist/session.d.ts +92 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +67 -0
- package/dist/session.js.map +1 -0
- package/dist/values.d.ts +216 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +329 -0
- package/dist/values.js.map +1 -0
- package/dist/webhook.d.ts +85 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +75 -0
- package/dist/webhook.js.map +1 -0
- package/dist/wire.d.ts +40 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +40 -0
- package/dist/wire.js.map +1 -0
- package/package.json +51 -0
- package/src/account.ts +149 -0
- package/src/activity.ts +52 -0
- package/src/agentTask.ts +254 -0
- package/src/candidate.ts +83 -0
- package/src/company.ts +98 -0
- package/src/dashboard.ts +307 -0
- package/src/deal.ts +102 -0
- package/src/decision.ts +106 -0
- package/src/form.ts +217 -0
- package/src/formSubmission.ts +99 -0
- package/src/handbookPage.ts +99 -0
- package/src/importExport.ts +339 -0
- package/src/index.ts +324 -0
- package/src/invite.ts +69 -0
- package/src/mcpTool.ts +32 -0
- package/src/member.ts +53 -0
- package/src/moduleSetting.ts +37 -0
- package/src/note.ts +73 -0
- package/src/opportunity.ts +74 -0
- package/src/partnership.ts +89 -0
- package/src/person.ts +116 -0
- package/src/pipelineStage.ts +71 -0
- package/src/planItem.ts +91 -0
- package/src/position.ts +50 -0
- package/src/raise.ts +94 -0
- package/src/role.ts +45 -0
- package/src/search.ts +86 -0
- package/src/session.ts +177 -0
- package/src/values.ts +427 -0
- package/src/webhook.ts +164 -0
- package/src/wire.ts +51 -0
package/src/values.ts
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed value sets shared by the wire schemas and by any UI that renders a
|
|
3
|
+
* dropdown over one.
|
|
4
|
+
*
|
|
5
|
+
* This is the only copy. `@kelpie/server`'s module schemas import from here and
|
|
6
|
+
* re-export, so one array drives a table's check constraint, the route's Zod
|
|
7
|
+
* enum, and the browser's decoder. The dependency runs one way: the server may
|
|
8
|
+
* import this package, and this package depends on Zod and nothing else, which
|
|
9
|
+
* is what keeps it usable from a browser bundle and from the cloud repo.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const PREFERRED_CHANNELS = ['email', 'call', 'linkedin'] as const
|
|
13
|
+
export const INFLUENCE_LEVELS = [
|
|
14
|
+
'champion',
|
|
15
|
+
'decision_maker',
|
|
16
|
+
'influencer',
|
|
17
|
+
'blocker',
|
|
18
|
+
'end_user',
|
|
19
|
+
] as const
|
|
20
|
+
export const RELATIONSHIP_LEVELS = ['cold', 'warm', 'strong'] as const
|
|
21
|
+
|
|
22
|
+
export type PreferredChannel = (typeof PREFERRED_CHANNELS)[number]
|
|
23
|
+
export type Influence = (typeof INFLUENCE_LEVELS)[number]
|
|
24
|
+
export type Relationship = (typeof RELATIONSHIP_LEVELS)[number]
|
|
25
|
+
|
|
26
|
+
export const COMPANY_STAGES = ['startup', 'growth', 'enterprise', 'other'] as const
|
|
27
|
+
export const SIZE_BANDS = ['1-10', '11-50', '51-200', '201+'] as const
|
|
28
|
+
export const ACCOUNT_TYPES = ['prospect', 'customer', 'partner', 'investor', 'other'] as const
|
|
29
|
+
export const ICP_FITS = ['high', 'medium', 'low', 'unknown'] as const
|
|
30
|
+
|
|
31
|
+
export type CompanyStage = (typeof COMPANY_STAGES)[number]
|
|
32
|
+
export type SizeBand = (typeof SIZE_BANDS)[number]
|
|
33
|
+
export type AccountType = (typeof ACCOUNT_TYPES)[number]
|
|
34
|
+
export type IcpFit = (typeof ICP_FITS)[number]
|
|
35
|
+
|
|
36
|
+
/** Networks a person can be linked on. One list beats a column per network. */
|
|
37
|
+
export const SOCIAL_NETWORK_IDS = [
|
|
38
|
+
'angellist',
|
|
39
|
+
'bluesky',
|
|
40
|
+
'crunchbase',
|
|
41
|
+
'facebook',
|
|
42
|
+
'github',
|
|
43
|
+
'instagram',
|
|
44
|
+
'linkedin',
|
|
45
|
+
'mastodon',
|
|
46
|
+
'medium',
|
|
47
|
+
'substack',
|
|
48
|
+
'threads',
|
|
49
|
+
'tiktok',
|
|
50
|
+
'twitter',
|
|
51
|
+
'youtube',
|
|
52
|
+
'other',
|
|
53
|
+
] as const
|
|
54
|
+
|
|
55
|
+
export type SocialNetworkId = (typeof SOCIAL_NETWORK_IDS)[number]
|
|
56
|
+
|
|
57
|
+
/** Display names for `SOCIAL_NETWORK_IDS`, in the same order. */
|
|
58
|
+
export const SOCIAL_NETWORK_LABELS: Readonly<Record<SocialNetworkId, string>> = {
|
|
59
|
+
angellist: 'AngelList',
|
|
60
|
+
bluesky: 'Bluesky',
|
|
61
|
+
crunchbase: 'Crunchbase',
|
|
62
|
+
facebook: 'Facebook',
|
|
63
|
+
github: 'GitHub',
|
|
64
|
+
instagram: 'Instagram',
|
|
65
|
+
linkedin: 'LinkedIn',
|
|
66
|
+
mastodon: 'Mastodon',
|
|
67
|
+
medium: 'Medium',
|
|
68
|
+
substack: 'Substack',
|
|
69
|
+
threads: 'Threads',
|
|
70
|
+
tiktok: 'TikTok',
|
|
71
|
+
twitter: 'X / Twitter',
|
|
72
|
+
youtube: 'YouTube',
|
|
73
|
+
other: 'Other',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The record types a detail page exists for, and therefore the ones a UI module
|
|
78
|
+
* can add a tab or a sidebar card to. Lived in `@kelpie/ui` until this package
|
|
79
|
+
* gave it a home both the UI and the cloud repo can import.
|
|
80
|
+
*
|
|
81
|
+
* Narrower than the server's `RECORD_OBJECT_TYPES`, which is the list a
|
|
82
|
+
* `record.*` event may carry. A Position, a Form, and a Handbook page are all
|
|
83
|
+
* written and all publish events, but none of them has a detail page for a
|
|
84
|
+
* module to hang anything off.
|
|
85
|
+
*/
|
|
86
|
+
export const EXTENSIBLE_RECORD_TYPES = [
|
|
87
|
+
'person',
|
|
88
|
+
'company',
|
|
89
|
+
'deal',
|
|
90
|
+
'opportunity',
|
|
91
|
+
'partnership',
|
|
92
|
+
'raise',
|
|
93
|
+
'role',
|
|
94
|
+
'candidate',
|
|
95
|
+
] as const
|
|
96
|
+
|
|
97
|
+
export type ExtensibleRecordType = (typeof EXTENSIBLE_RECORD_TYPES)[number]
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The record types a note, activity, decision, or plan item attaches to.
|
|
101
|
+
*
|
|
102
|
+
* Not the same list as `EXTENSIBLE_RECORD_TYPES`: a Role is a detail page a UI
|
|
103
|
+
* module can extend, but nothing attaches a note to it. Interview notes go on
|
|
104
|
+
* the Candidate, which is the person-and-role link.
|
|
105
|
+
*/
|
|
106
|
+
export const RECORD_TARGET_TYPES = [
|
|
107
|
+
'person',
|
|
108
|
+
'company',
|
|
109
|
+
'deal',
|
|
110
|
+
'opportunity',
|
|
111
|
+
'partnership',
|
|
112
|
+
'raise',
|
|
113
|
+
'candidate',
|
|
114
|
+
] as const
|
|
115
|
+
|
|
116
|
+
export type RecordTargetType = (typeof RECORD_TARGET_TYPES)[number]
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* What `GET /v1/search` looks through, and the order its groups come back in.
|
|
120
|
+
*
|
|
121
|
+
* Handbook leads because a question phrased in words rather than names is usually
|
|
122
|
+
* a question the handbook answers. The rest follow the sidebar.
|
|
123
|
+
*
|
|
124
|
+
* Neither of the lists above fits. `RECORD_OBJECT_TYPES` on the server has no
|
|
125
|
+
* Decision and does have Position, Candidate and Form: a Position is reachable
|
|
126
|
+
* only as the person holding it, and nobody searches for a Candidate by name
|
|
127
|
+
* without finding the Person first.
|
|
128
|
+
*/
|
|
129
|
+
export const SEARCH_COLLECTIONS = [
|
|
130
|
+
'handbook_page',
|
|
131
|
+
'person',
|
|
132
|
+
'role',
|
|
133
|
+
'company',
|
|
134
|
+
'deal',
|
|
135
|
+
'opportunity',
|
|
136
|
+
'raise',
|
|
137
|
+
'partnership',
|
|
138
|
+
'decision',
|
|
139
|
+
] as const
|
|
140
|
+
|
|
141
|
+
export type SearchCollection = (typeof SEARCH_COLLECTIONS)[number]
|
|
142
|
+
|
|
143
|
+
/** Whether a Role is still being hired for. */
|
|
144
|
+
export const ROLE_STATUSES = ['open', 'closed'] as const
|
|
145
|
+
|
|
146
|
+
export type RoleStatus = (typeof ROLE_STATUSES)[number]
|
|
147
|
+
|
|
148
|
+
export const ROLE_STATUS_LABELS: Readonly<Record<RoleStatus, string>> = {
|
|
149
|
+
open: 'Open',
|
|
150
|
+
closed: 'Closed',
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Where a Candidate stands with the Role they are attached to.
|
|
155
|
+
*
|
|
156
|
+
* This is the Person↔Role link's state, never a column on Person: the same
|
|
157
|
+
* person can be in process for one role and in the nurture pile for another.
|
|
158
|
+
*/
|
|
159
|
+
export const CANDIDATE_STATUSES = [
|
|
160
|
+
'in_process',
|
|
161
|
+
'nurture',
|
|
162
|
+
'hired',
|
|
163
|
+
'passed',
|
|
164
|
+
'withdrawn',
|
|
165
|
+
] as const
|
|
166
|
+
|
|
167
|
+
export type CandidateStatus = (typeof CANDIDATE_STATUSES)[number]
|
|
168
|
+
|
|
169
|
+
export const CANDIDATE_STATUS_LABELS: Readonly<Record<CandidateStatus, string>> = {
|
|
170
|
+
in_process: 'In process',
|
|
171
|
+
nurture: 'Nurture',
|
|
172
|
+
hired: 'Hired',
|
|
173
|
+
passed: 'Passed',
|
|
174
|
+
withdrawn: 'Withdrawn',
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The one status that carries an interview stage. Every other status clears it. */
|
|
178
|
+
export const IN_PROCESS: CandidateStatus = 'in_process'
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* How far through interviewing a candidate is, in order. Meaningful only while
|
|
182
|
+
* the candidate is in process, which is why `interview_stage` is nullable.
|
|
183
|
+
*/
|
|
184
|
+
export const INTERVIEW_STAGES = ['sourced', 'screen', 'interview', 'offer'] as const
|
|
185
|
+
|
|
186
|
+
export type InterviewStage = (typeof INTERVIEW_STAGES)[number]
|
|
187
|
+
|
|
188
|
+
export const INTERVIEW_STAGE_LABELS: Readonly<Record<InterviewStage, string>> = {
|
|
189
|
+
sourced: 'Sourced',
|
|
190
|
+
screen: 'Screen',
|
|
191
|
+
interview: 'Interview',
|
|
192
|
+
offer: 'Offer',
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Where a candidate enters the process when no stage is named. */
|
|
196
|
+
export const FIRST_INTERVIEW_STAGE: InterviewStage = INTERVIEW_STAGES[0]
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The four pipelines whose board columns live in `pipeline_stages`. A Deal moves
|
|
200
|
+
* through `deal` stages and so on; the kinds are fixed even though the stages
|
|
201
|
+
* within each are workspace-configurable.
|
|
202
|
+
*/
|
|
203
|
+
export const PIPELINE_KINDS = ['deal', 'opportunity', 'raise', 'partnership'] as const
|
|
204
|
+
|
|
205
|
+
export type PipelineKind = (typeof PIPELINE_KINDS)[number]
|
|
206
|
+
|
|
207
|
+
/** Display names for `PIPELINE_KINDS`. "Fundraising" is what the nav calls a Raise. */
|
|
208
|
+
export const PIPELINE_KIND_LABELS: Readonly<Record<PipelineKind, string>> = {
|
|
209
|
+
deal: 'Deal',
|
|
210
|
+
opportunity: 'Opportunity',
|
|
211
|
+
raise: 'Fundraising',
|
|
212
|
+
partnership: 'Partnership',
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* How far along a plan item is. Stored, never derived: whether something is
|
|
217
|
+
* overdue is a question about its date, and whether it is finished is a question
|
|
218
|
+
* about this column, and conflating the two would make a late-but-done item
|
|
219
|
+
* shout for attention forever.
|
|
220
|
+
*/
|
|
221
|
+
export const PLAN_ITEM_STATUSES = ['todo', 'in_progress', 'done'] as const
|
|
222
|
+
|
|
223
|
+
export type PlanItemStatus = (typeof PLAN_ITEM_STATUSES)[number]
|
|
224
|
+
|
|
225
|
+
export const PLAN_ITEM_STATUS_LABELS: Readonly<Record<PlanItemStatus, string>> = {
|
|
226
|
+
todo: 'To do',
|
|
227
|
+
in_progress: 'In progress',
|
|
228
|
+
done: 'Done',
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The statuses that still need doing. `plan.completed` fires on leaving this set. */
|
|
232
|
+
export const OPEN_PLAN_ITEM_STATUSES = ['todo', 'in_progress'] as const
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* What an activity says happened. `created`, `updated`, `stage_changed`,
|
|
236
|
+
* `note_added`, `linked` and `unlinked` are emitted by the server; `email`,
|
|
237
|
+
* `call` and `meeting` are logged history an integration or an agent supplies.
|
|
238
|
+
*
|
|
239
|
+
* `unlinked` is only filed when a link is deleted through its own route. A link
|
|
240
|
+
* that dies with either of its ends never reaches a service, so the timeline
|
|
241
|
+
* that survives keeps the `linked` row without a counterpart. That row is
|
|
242
|
+
* history rather than a claim about the present, so it stays true either way.
|
|
243
|
+
*/
|
|
244
|
+
export const ACTIVITY_KINDS = [
|
|
245
|
+
'created',
|
|
246
|
+
'updated',
|
|
247
|
+
'stage_changed',
|
|
248
|
+
'note_added',
|
|
249
|
+
'email',
|
|
250
|
+
'call',
|
|
251
|
+
'meeting',
|
|
252
|
+
'linked',
|
|
253
|
+
'unlinked',
|
|
254
|
+
] as const
|
|
255
|
+
|
|
256
|
+
export type ActivityKind = (typeof ACTIVITY_KINDS)[number]
|
|
257
|
+
|
|
258
|
+
export const MEMBER_ROLES = ['owner', 'admin', 'member'] as const
|
|
259
|
+
|
|
260
|
+
export type MemberRole = (typeof MEMBER_ROLES)[number]
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Roles an invitation may offer. Ownership is created with the workspace or
|
|
264
|
+
* transferred between members; it is never invited, and the `invites.role` check
|
|
265
|
+
* constraint says the same.
|
|
266
|
+
*/
|
|
267
|
+
export const INVITABLE_ROLES = ['admin', 'member'] as const
|
|
268
|
+
|
|
269
|
+
export type InvitableRole = (typeof INVITABLE_ROLES)[number]
|
|
270
|
+
|
|
271
|
+
/** What a pending invitation is called once its `expires_at` has passed. */
|
|
272
|
+
export const INVITE_STATUSES = ['pending', 'expired'] as const
|
|
273
|
+
|
|
274
|
+
export type InviteStatus = (typeof INVITE_STATUSES)[number]
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Light, dark, or whatever the operating system says.
|
|
278
|
+
*
|
|
279
|
+
* `system` is a stored answer rather than the absence of one: a reader who has
|
|
280
|
+
* chosen to follow the machine has expressed a preference, and it has to survive
|
|
281
|
+
* a move to a browser whose machine currently says something else.
|
|
282
|
+
*/
|
|
283
|
+
export const THEME_PREFERENCES = ['system', 'light', 'dark'] as const
|
|
284
|
+
|
|
285
|
+
export type ThemePreference = (typeof THEME_PREFERENCES)[number]
|
|
286
|
+
|
|
287
|
+
/** A paused form still exists and still renders; its submit answers 409 (`forms.md`). */
|
|
288
|
+
export const FORM_STATUSES = ['active', 'paused'] as const
|
|
289
|
+
|
|
290
|
+
export type FormStatus = (typeof FORM_STATUSES)[number]
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* What a field renders as in the embed. Deliberately short: `forms.md` puts file
|
|
294
|
+
* uploads, multi-page forms and branching out of scope, and every type here is
|
|
295
|
+
* one `<input>`, `<textarea>` or `<select>`.
|
|
296
|
+
*/
|
|
297
|
+
export const FORM_FIELD_TYPES = ['text', 'email', 'textarea', 'select'] as const
|
|
298
|
+
|
|
299
|
+
export type FormFieldType = (typeof FORM_FIELD_TYPES)[number]
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Where a field's answer lands on submit.
|
|
303
|
+
*
|
|
304
|
+
* `position.title` rather than a person field, because a job title belongs to the
|
|
305
|
+
* Person↔Company link and nowhere else. `submission` stores the answer without
|
|
306
|
+
* writing any CRM record, which is what a free-text "How can we help?" wants.
|
|
307
|
+
*/
|
|
308
|
+
export const FORM_FIELD_MAP_TARGETS = [
|
|
309
|
+
'person.name',
|
|
310
|
+
'person.email',
|
|
311
|
+
'company.name',
|
|
312
|
+
'company.domain',
|
|
313
|
+
'position.title',
|
|
314
|
+
'deal.name',
|
|
315
|
+
'submission',
|
|
316
|
+
] as const
|
|
317
|
+
|
|
318
|
+
export type FormFieldMapTarget = (typeof FORM_FIELD_MAP_TARGETS)[number]
|
|
319
|
+
|
|
320
|
+
export const FORM_FIELD_MAP_TARGET_LABELS: Readonly<Record<FormFieldMapTarget, string>> = {
|
|
321
|
+
'person.name': 'Person · name',
|
|
322
|
+
'person.email': 'Person · email',
|
|
323
|
+
'company.name': 'Company · name',
|
|
324
|
+
'company.domain': 'Company · domain',
|
|
325
|
+
'position.title': 'Position · title',
|
|
326
|
+
'deal.name': 'Deal · name',
|
|
327
|
+
submission: 'Submission only',
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** The one mapping a form cannot process without, and may carry at most once. */
|
|
331
|
+
export const PERSON_EMAIL_TARGET: FormFieldMapTarget = 'person.email'
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* How a select option's stored key should be read back.
|
|
335
|
+
*
|
|
336
|
+
* The answer map is `fieldId → string` on the wire either way; this says what
|
|
337
|
+
* the string means, so a consumer knows `"true"` was a checkbox and not a word.
|
|
338
|
+
*/
|
|
339
|
+
export const FORM_OPTION_VALUE_TYPES = ['string', 'number', 'boolean'] as const
|
|
340
|
+
|
|
341
|
+
export type FormOptionValueType = (typeof FORM_OPTION_VALUE_TYPES)[number]
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The domain events a webhook can subscribe to.
|
|
345
|
+
*
|
|
346
|
+
* A subset of the server's event catalog on purpose: the ticket's minimum
|
|
347
|
+
* viable set, and the events whose payloads describe something a receiver
|
|
348
|
+
* outside Kelpie can act on. The rest of the catalog (`stage.changed`,
|
|
349
|
+
* `note.added`, membership and workspace events) is not deliverable yet, so it
|
|
350
|
+
* is not offered rather than accepted and silently never sent.
|
|
351
|
+
*/
|
|
352
|
+
export const WEBHOOK_EVENTS = [
|
|
353
|
+
'record.created',
|
|
354
|
+
'record.updated',
|
|
355
|
+
'record.deleted',
|
|
356
|
+
'form.submitted',
|
|
357
|
+
] as const
|
|
358
|
+
|
|
359
|
+
export type WebhookEvent = (typeof WEBHOOK_EVENTS)[number]
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* `failing` is the delivery engine's, not the customer's: it means the last
|
|
363
|
+
* delivery exhausted its attempts. `paused` is the customer's, and stops
|
|
364
|
+
* delivery entirely. A failing webhook keeps being tried, which is what lets it
|
|
365
|
+
* return to `active` on its own once the endpoint recovers.
|
|
366
|
+
*/
|
|
367
|
+
export const WEBHOOK_STATUSES = ['active', 'failing', 'paused'] as const
|
|
368
|
+
|
|
369
|
+
export type WebhookStatus = (typeof WEBHOOK_STATUSES)[number]
|
|
370
|
+
|
|
371
|
+
/** What a `PATCH` may set. `failing` is a report on the endpoint, not a request. */
|
|
372
|
+
export const WEBHOOK_SETTABLE_STATUSES = ['active', 'paused'] as const
|
|
373
|
+
|
|
374
|
+
export type WebhookSettableStatus = (typeof WEBHOOK_SETTABLE_STATUSES)[number]
|
|
375
|
+
|
|
376
|
+
/** A delivery is only logged once it has settled, so there is no pending value. */
|
|
377
|
+
export const WEBHOOK_DELIVERY_STATUSES = ['success', 'failed'] as const
|
|
378
|
+
|
|
379
|
+
export type WebhookDeliveryStatus = (typeof WEBHOOK_DELIVERY_STATUSES)[number]
|
|
380
|
+
|
|
381
|
+
export const WEBHOOK_STATUS_LABELS: Readonly<Record<WebhookStatus, string>> = {
|
|
382
|
+
active: 'Active',
|
|
383
|
+
failing: 'Failing',
|
|
384
|
+
paused: 'Paused',
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* What an agent task can point at: the seven attachable record types, plus
|
|
389
|
+
* three surfaces that carry tasks without being note targets — a Role, a
|
|
390
|
+
* Handbook page, and the workspace itself (`agent-tasks.md`).
|
|
391
|
+
*/
|
|
392
|
+
export const AGENT_TASK_TARGET_TYPES = [
|
|
393
|
+
'person',
|
|
394
|
+
'company',
|
|
395
|
+
'deal',
|
|
396
|
+
'opportunity',
|
|
397
|
+
'partnership',
|
|
398
|
+
'raise',
|
|
399
|
+
'candidate',
|
|
400
|
+
'role',
|
|
401
|
+
'handbook',
|
|
402
|
+
'workspace',
|
|
403
|
+
] as const
|
|
404
|
+
|
|
405
|
+
export type AgentTaskTargetType = (typeof AGENT_TASK_TARGET_TYPES)[number]
|
|
406
|
+
|
|
407
|
+
/** `primary` shows as a compact action; `overflow` lives under "More". */
|
|
408
|
+
export const AGENT_TASK_PLACEMENTS = ['primary', 'overflow'] as const
|
|
409
|
+
|
|
410
|
+
export type AgentTaskPlacement = (typeof AGENT_TASK_PLACEMENTS)[number]
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Where a run's dispatch stands. The lifecycle describes the POST to the
|
|
414
|
+
* registered agent, not the agent's own work: Kelpie hands the prompt over and
|
|
415
|
+
* records whether the handover landed. `agent-tasks.md` defines no callback for
|
|
416
|
+
* an agent to report completion, so anything past the dispatch would be a guess.
|
|
417
|
+
*/
|
|
418
|
+
export const AGENT_RUN_STATUSES = ['queued', 'running', 'succeeded', 'failed'] as const
|
|
419
|
+
|
|
420
|
+
export type AgentRunStatus = (typeof AGENT_RUN_STATUSES)[number]
|
|
421
|
+
|
|
422
|
+
export const AGENT_RUN_STATUS_LABELS: Readonly<Record<AgentRunStatus, string>> = {
|
|
423
|
+
queued: 'Queued',
|
|
424
|
+
running: 'Running',
|
|
425
|
+
succeeded: 'Succeeded',
|
|
426
|
+
failed: 'Failed',
|
|
427
|
+
}
|
package/src/webhook.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { WEBHOOK_DELIVERY_STATUSES, WEBHOOK_EVENTS, WEBHOOK_STATUSES } from './values.ts'
|
|
4
|
+
import type {
|
|
5
|
+
WebhookDeliveryStatus,
|
|
6
|
+
WebhookEvent,
|
|
7
|
+
WebhookSettableStatus,
|
|
8
|
+
WebhookStatus,
|
|
9
|
+
} from './values.ts'
|
|
10
|
+
import { definedFields, idSchema, nullableTimestampSchema, timestampSchema } from './wire.ts'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wire and write shapes for `/v1/webhooks`.
|
|
14
|
+
*
|
|
15
|
+
* The signing secret appears exactly once, in the `201` that creates the
|
|
16
|
+
* webhook, and never again — the same contract API keys have. Every later read
|
|
17
|
+
* carries only `secret_prefix`, which is enough to tell two registrations
|
|
18
|
+
* apart in a list and useless to anyone who intercepts it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface Webhook {
|
|
22
|
+
readonly id: string
|
|
23
|
+
readonly url: string
|
|
24
|
+
readonly events: readonly WebhookEvent[]
|
|
25
|
+
/** The leading characters of the secret, for recognising a registration. */
|
|
26
|
+
readonly secretPrefix: string
|
|
27
|
+
readonly status: WebhookStatus
|
|
28
|
+
/** Derived from the delivery log, not stored: null until the first delivery settles. */
|
|
29
|
+
readonly lastDeliveryAt: Date | null
|
|
30
|
+
readonly lastDeliveryStatus: WebhookDeliveryStatus | null
|
|
31
|
+
readonly createdAt: Date
|
|
32
|
+
readonly updatedAt: Date
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The one response that carries the signing secret. Nothing can retrieve it later. */
|
|
36
|
+
export interface CreatedWebhook extends Webhook {
|
|
37
|
+
readonly secret: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One settled delivery: an event sent to one webhook, after its retries. */
|
|
41
|
+
export interface WebhookDelivery {
|
|
42
|
+
readonly id: string
|
|
43
|
+
readonly webhookId: string
|
|
44
|
+
readonly event: WebhookEvent
|
|
45
|
+
/** Exactly the JSON that was signed and sent. */
|
|
46
|
+
readonly payload: unknown
|
|
47
|
+
readonly status: WebhookDeliveryStatus
|
|
48
|
+
/** How many HTTP requests it took. At least 1; more means retries. */
|
|
49
|
+
readonly attempts: number
|
|
50
|
+
/** When it succeeded, null when it never did. */
|
|
51
|
+
readonly deliveredAt: Date | null
|
|
52
|
+
readonly createdAt: Date
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const webhookWire = {
|
|
56
|
+
id: idSchema,
|
|
57
|
+
url: z.string(),
|
|
58
|
+
events: z.array(z.enum(WEBHOOK_EVENTS)),
|
|
59
|
+
secret_prefix: z.string(),
|
|
60
|
+
status: z.enum(WEBHOOK_STATUSES),
|
|
61
|
+
last_delivery_at: nullableTimestampSchema,
|
|
62
|
+
last_delivery_status: z.enum(WEBHOOK_DELIVERY_STATUSES).nullable(),
|
|
63
|
+
created_at: timestampSchema,
|
|
64
|
+
updated_at: timestampSchema,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const webhookWireSchema = z.object(webhookWire)
|
|
68
|
+
|
|
69
|
+
function toWebhook(wire: z.output<typeof webhookWireSchema>): Webhook {
|
|
70
|
+
return {
|
|
71
|
+
id: wire.id,
|
|
72
|
+
url: wire.url,
|
|
73
|
+
events: wire.events,
|
|
74
|
+
secretPrefix: wire.secret_prefix,
|
|
75
|
+
status: wire.status,
|
|
76
|
+
lastDeliveryAt: wire.last_delivery_at,
|
|
77
|
+
lastDeliveryStatus: wire.last_delivery_status,
|
|
78
|
+
createdAt: wire.created_at,
|
|
79
|
+
updatedAt: wire.updated_at,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const webhookSchema: z.ZodType<Webhook, unknown> = webhookWireSchema.transform(toWebhook)
|
|
84
|
+
|
|
85
|
+
export const createdWebhookSchema: z.ZodType<CreatedWebhook, unknown> = z
|
|
86
|
+
.object({ ...webhookWire, secret: z.string() })
|
|
87
|
+
.transform((wire): CreatedWebhook => ({ ...toWebhook(wire), secret: wire.secret }))
|
|
88
|
+
|
|
89
|
+
export const webhookDeliverySchema: z.ZodType<WebhookDelivery, unknown> = z
|
|
90
|
+
.object({
|
|
91
|
+
id: idSchema,
|
|
92
|
+
webhook_id: idSchema,
|
|
93
|
+
event: z.enum(WEBHOOK_EVENTS),
|
|
94
|
+
payload: z.unknown(),
|
|
95
|
+
status: z.enum(WEBHOOK_DELIVERY_STATUSES),
|
|
96
|
+
attempts: z.number().int(),
|
|
97
|
+
delivered_at: nullableTimestampSchema,
|
|
98
|
+
created_at: timestampSchema,
|
|
99
|
+
})
|
|
100
|
+
.transform(
|
|
101
|
+
(wire): WebhookDelivery => ({
|
|
102
|
+
id: wire.id,
|
|
103
|
+
webhookId: wire.webhook_id,
|
|
104
|
+
event: wire.event,
|
|
105
|
+
payload: wire.payload,
|
|
106
|
+
status: wire.status,
|
|
107
|
+
attempts: wire.attempts,
|
|
108
|
+
deliveredAt: wire.delivered_at,
|
|
109
|
+
createdAt: wire.created_at,
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
export interface CreateWebhookInput {
|
|
114
|
+
readonly url: string
|
|
115
|
+
readonly events: readonly WebhookEvent[]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* `status` takes only the two values a customer controls. `failing` is what the
|
|
120
|
+
* delivery engine reports about the endpoint, so setting it by hand would be
|
|
121
|
+
* asserting something only the engine can know.
|
|
122
|
+
*/
|
|
123
|
+
export interface WebhookInput {
|
|
124
|
+
readonly url?: string
|
|
125
|
+
readonly events?: readonly WebhookEvent[]
|
|
126
|
+
readonly status?: WebhookSettableStatus
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* How long a rotation may sign under both secrets.
|
|
131
|
+
*
|
|
132
|
+
* Long enough to cover a working day, because the window has to span a customer
|
|
133
|
+
* noticing, changing their configuration and getting a deploy out, and short
|
|
134
|
+
* enough that a leaked secret is not honoured for a week.
|
|
135
|
+
*
|
|
136
|
+
* Shared rather than duplicated: the service computes the expiry from it and the
|
|
137
|
+
* browser tells the customer what they are choosing, and the two disagreeing
|
|
138
|
+
* would mean a checkbox that promises a window nothing implements.
|
|
139
|
+
*/
|
|
140
|
+
export const WEBHOOK_SECRET_OVERLAP_HOURS = 24
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Replacing a webhook's signing secret.
|
|
144
|
+
*
|
|
145
|
+
* `overlap` keeps the old secret valid for a further
|
|
146
|
+
* `WEBHOOK_SECRET_OVERLAP_HOURS`, so a delivery is signed under both and an
|
|
147
|
+
* endpoint that has not been redeployed still verifies. Off means the old secret
|
|
148
|
+
* stops working at once, and deliveries fail until the new one is live.
|
|
149
|
+
*/
|
|
150
|
+
export interface RotateWebhookSecretInput {
|
|
151
|
+
readonly overlap: boolean
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function rotateWebhookSecretBody(input: RotateWebhookSecretInput): unknown {
|
|
155
|
+
return { overlap: input.overlap }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function createWebhookBody(input: CreateWebhookInput): unknown {
|
|
159
|
+
return { url: input.url, events: input.events }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function webhookBody(input: WebhookInput): unknown {
|
|
163
|
+
return definedFields({ url: input.url, events: input.events, status: input.status })
|
|
164
|
+
}
|
package/src/wire.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pieces every resource schema in this package is built from.
|
|
5
|
+
*
|
|
6
|
+
* `api.md` puts `snake_case` on the wire and `camelCase` in TypeScript, and says
|
|
7
|
+
* the mapping happens at the boundary. For a client, this package is that
|
|
8
|
+
* boundary: each resource declares a schema over the wire shape and transforms
|
|
9
|
+
* it into the record the UI holds, so no component ever reads a `snake_case`
|
|
10
|
+
* key.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** An `id` from `api.md`: `<prefix>_<ulid>`. Only checked for non-emptiness. */
|
|
14
|
+
export const idSchema = z.string().min(1)
|
|
15
|
+
|
|
16
|
+
/** An ISO 8601 UTC timestamp, read as a `Date`. */
|
|
17
|
+
export const timestampSchema = z.iso.datetime().transform((value) => new Date(value))
|
|
18
|
+
|
|
19
|
+
/** A nullable timestamp: `null` on the wire stays `null`, it does not become an epoch. */
|
|
20
|
+
export const nullableTimestampSchema = z.iso
|
|
21
|
+
.datetime()
|
|
22
|
+
.nullable()
|
|
23
|
+
.transform((value) => (value === null ? null : new Date(value)))
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fields every CRM record carries. Spread into a resource's wire object rather
|
|
27
|
+
* than merged after the transform, because a transformed schema can no longer
|
|
28
|
+
* be extended.
|
|
29
|
+
*/
|
|
30
|
+
export const recordTimestamps = {
|
|
31
|
+
created_at: timestampSchema,
|
|
32
|
+
updated_at: timestampSchema,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** What `recordTimestamps` becomes once parsed. */
|
|
36
|
+
export interface RecordTimestamps {
|
|
37
|
+
readonly createdAt: Date
|
|
38
|
+
readonly updatedAt: Date
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Drops keys whose value is `undefined`.
|
|
43
|
+
*
|
|
44
|
+
* `PATCH` sends only what changed, and `null` clears a nullable field, so an
|
|
45
|
+
* `undefined` in a request body would be indistinguishable from "clear this"
|
|
46
|
+
* once `JSON.stringify` removed it. Building bodies through this makes the
|
|
47
|
+
* omission deliberate instead of incidental.
|
|
48
|
+
*/
|
|
49
|
+
export function definedFields(fields: Record<string, unknown>): Record<string, unknown> {
|
|
50
|
+
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined))
|
|
51
|
+
}
|