@tellescope/schema 1.250.2 → 1.251.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/src/schema.ts CHANGED
@@ -90,7 +90,11 @@ import {
90
90
  AutomationAction,
91
91
  TimeTrackTimestamp,
92
92
  BelugaPharmacyMapping,
93
+ BelugaAutomationMappingEntry,
94
+ BelugaUpdateVisitPatientPreferenceItem,
93
95
  TimeTrack,
96
+ LinkedAccount,
97
+ LinkedAccountAccessEntry,
94
98
  } from "@tellescope/types-models"
95
99
 
96
100
  import {
@@ -261,6 +265,7 @@ import {
261
265
  exactMatchValidator,
262
266
  exactMatchValidatorOptional,
263
267
  listOfMongoIdStringValidatorOptionalOrEmptyOk,
268
+ linkedAccountAccessValidator,
264
269
  listOfStringsValidatorOptionalOrEmptyOk,
265
270
  stringValidatorOptionalEmptyOkay,
266
271
  analyticsQueryResultsValidator,
@@ -934,6 +939,9 @@ export type CustomActions = {
934
939
  consent: CustomAction<{ termsVersion: string, }, { user: User, authToken: string }>,
935
940
  get_users_for_groups: CustomAction<{ groups: string[] }, { userIds: string[] }>,
936
941
  play_phone_message: CustomAction<{ userId: string, message: string, enduserId?: string, journeyContext?: JourneyContext }, { }>,
942
+ get_linked_accounts: CustomAction<{}, { linkedAccounts: LinkedAccount[] }>,
943
+ switch_account: CustomAction<{ targetUserId: string }, { authToken: string, user: User }>,
944
+ request_linked_account_access: CustomAction<{ targetEmail: string }, { }>,
937
945
  },
938
946
  chat_rooms: {
939
947
  join_room: CustomAction<{ id: string }, { room: ChatRoom }>,
@@ -3558,7 +3566,73 @@ export const schema: SchemaV1 = build_schema({
3558
3566
 
3559
3567
  return "User organizationIds are readonly"
3560
3568
  }
3561
- }
3569
+ },
3570
+ {
3571
+ explanation: "linkedAccountAccess mutations are constrained to the owner with allowlisted transitions",
3572
+ evaluate: (record, _, session, method, { updates, original }) => {
3573
+ if (!updates || !('linkedAccountAccess' in updates)) return
3574
+ if (method === 'create') {
3575
+ if (updates.linkedAccountAccess && (updates.linkedAccountAccess as any[]).length > 0) {
3576
+ return "linkedAccountAccess cannot be set on user creation"
3577
+ }
3578
+ return
3579
+ }
3580
+
3581
+ // Grant management is reserved to the actor's own session. From a switched session
3582
+ // (session.actorUserId set), even targeting the proxy identity's own record is rejected —
3583
+ // otherwise A-as-B could self-approve other pending requests on B, or delete B's existing
3584
+ // grants and silently revoke other grantees.
3585
+ if ((session as any).actorUserId) {
3586
+ return "Cannot update linkedAccountAccess from a switched session"
3587
+ }
3588
+
3589
+ // self-update only — record carries the post-merge updated document; original is the prior state.
3590
+ const ownerId = (record as any)._id?.toString() ?? (original as any)?._id?.toString() ?? (original as any)?.id
3591
+ if (!(ownerId && ownerId === session.id)) {
3592
+ return "Only the account owner can update linkedAccountAccess"
3593
+ }
3594
+
3595
+ const oldEntries: LinkedAccountAccessEntry[] = (original as any)?.linkedAccountAccess ?? []
3596
+ const newEntries: LinkedAccountAccessEntry[] = (updates.linkedAccountAccess as any) ?? []
3597
+
3598
+ for (const newEntry of newEntries) {
3599
+ const oldMatch = oldEntries.find(e => e.userId === newEntry.userId)
3600
+ if (!oldMatch) {
3601
+ return "Cannot add entries to linkedAccountAccess via direct update; use request_linked_account_access"
3602
+ }
3603
+
3604
+ if (newEntry.email !== oldMatch.email) return "linkedAccountAccess entry email is immutable"
3605
+ if ((newEntry.fname ?? null) !== (oldMatch.fname ?? null)) return "linkedAccountAccess entry fname is immutable"
3606
+ if ((newEntry.lname ?? null) !== (oldMatch.lname ?? null)) return "linkedAccountAccess entry lname is immutable"
3607
+ if ((newEntry.orgName ?? null) !== (oldMatch.orgName ?? null)) return "linkedAccountAccess entry orgName is immutable"
3608
+ if (new Date(newEntry.createdAt).getTime() !== new Date(oldMatch.createdAt).getTime()) return "linkedAccountAccess entry createdAt is immutable"
3609
+ if (new Date(newEntry.requestExpiresAt).getTime() !== new Date(oldMatch.requestExpiresAt).getTime()) return "linkedAccountAccess entry requestExpiresAt is immutable"
3610
+
3611
+ if (newEntry.status !== oldMatch.status) {
3612
+ if (!(oldMatch.status === 'pending' && newEntry.status === 'accepted')) {
3613
+ return "linkedAccountAccess status can only transition from pending to accepted"
3614
+ }
3615
+ // Reject approval of an expired pending request — owner must wait for the requester
3616
+ // to re-issue. requestExpiresAt is immutable per the rule above; the only way for a
3617
+ // pending entry to refresh is request_linked_account_access replacing the expired entry.
3618
+ if (new Date(oldMatch.requestExpiresAt).getTime() < Date.now()) {
3619
+ return "linkedAccountAccess request has expired and cannot be approved; requester must re-request"
3620
+ }
3621
+ }
3622
+ }
3623
+
3624
+ return
3625
+ }
3626
+ },
3627
+ {
3628
+ explanation: "Legacy accountAccessGrantedTo field is no longer accepted",
3629
+ evaluate: (_, __, ___, ____, { updates }) => {
3630
+ if (updates && 'accountAccessGrantedTo' in updates) {
3631
+ return "accountAccessGrantedTo has been replaced by linkedAccountAccess"
3632
+ }
3633
+ return
3634
+ }
3635
+ },
3562
3636
  ],
3563
3637
  },
3564
3638
  defaultActions: {
@@ -3731,7 +3805,7 @@ export const schema: SchemaV1 = build_schema({
3731
3805
  name: 'Play Phone Message',
3732
3806
  path: '/users/play-phone-message',
3733
3807
  description: "Calls the user and plays a recorded message",
3734
- parameters: {
3808
+ parameters: {
3735
3809
  userId: { validator: mongoIdStringValidator, required: true },
3736
3810
  message: { validator: stringValidator5000, required: true },
3737
3811
  enduserId: { validator: mongoIdStringValidator },
@@ -3739,6 +3813,39 @@ export const schema: SchemaV1 = build_schema({
3739
3813
  },
3740
3814
  returns: { },
3741
3815
  },
3816
+ get_linked_accounts: {
3817
+ op: "custom", access: 'read', method: "get",
3818
+ name: 'Get Linked Accounts',
3819
+ path: '/users/linked-accounts',
3820
+ description: "Returns accounts that have granted access to the caller",
3821
+ parameters: { },
3822
+ returns: {
3823
+ linkedAccounts: { validator: objectAnyFieldsAnyValuesValidator as any, required: true },
3824
+ },
3825
+ },
3826
+ switch_account: {
3827
+ op: "custom", access: 'update', method: "post",
3828
+ name: 'Switch Account',
3829
+ path: '/users/switch-account',
3830
+ description: "Switches the current session to a target account that has granted access",
3831
+ parameters: {
3832
+ targetUserId: { validator: mongoIdStringRequired, required: true },
3833
+ },
3834
+ returns: {
3835
+ authToken: { validator: stringValidator, required: true },
3836
+ user: { validator: 'user' as any, required: true },
3837
+ },
3838
+ },
3839
+ request_linked_account_access: {
3840
+ op: "custom", access: 'update', method: "post",
3841
+ name: 'Request Linked Account Access',
3842
+ path: '/users/request-linked-account-access',
3843
+ description: "Requests linked-account access from another user identified by email; the target user must accept before the requester can switch into the account",
3844
+ parameters: {
3845
+ targetEmail: { validator: emailValidator, required: true },
3846
+ },
3847
+ returns: { },
3848
+ },
3742
3849
  },
3743
3850
  publicActions: {
3744
3851
  begin_sso: {
@@ -3852,6 +3959,7 @@ export const schema: SchemaV1 = build_schema({
3852
3959
  email: {
3853
3960
  validator: emailValidator,
3854
3961
  required: true,
3962
+ updatesDisabled: true,
3855
3963
  examples: ['test@tellescope.com'],
3856
3964
  redactions: ['enduser'],
3857
3965
  },
@@ -3978,6 +4086,7 @@ export const schema: SchemaV1 = build_schema({
3978
4086
  dashboardView: { validator: customDashboardViewValidator },
3979
4087
  hideFromCalendarView: { validator: booleanValidator },
3980
4088
  requireSSO: { validator: listOfStringsValidatorUniqueOptionalOrEmptyOkay },
4089
+ linkedAccountAccess: { validator: linkedAccountAccessValidator },
3981
4090
  }
3982
4091
  },
3983
4092
  templates: {
@@ -7170,6 +7279,7 @@ export const schema: SchemaV1 = build_schema({
7170
7279
  canvasSyncPhoneConsent: { validator: booleanValidator },
7171
7280
  canvasStateToLocationId: { validator: objectAnyFieldsValidator(stringValidator100) },
7172
7281
  enforceMFA: { validator: booleanValidator },
7282
+ accountSwitchingEnabled: { validator: booleanValidator },
7173
7283
  replyToEnduserTransactionalEmails: { validator: emailValidator },
7174
7284
  customTermsOfService: { validator: stringValidator },
7175
7285
  customPrivacyPolicy: { validator: stringValidator },
@@ -7225,6 +7335,20 @@ export const schema: SchemaV1 = build_schema({
7225
7335
  summaryFormId: mongoIdStringOptional,
7226
7336
  }))
7227
7337
  },
7338
+ belugaAutomationMappings: {
7339
+ validator: listValidatorOptionalOrEmptyOk(objectValidator<BelugaAutomationMappingEntry>({
7340
+ enduserCondition: optionalAnyObjectValidator,
7341
+ patientPreferences: listValidator(objectValidator<BelugaUpdateVisitPatientPreferenceItem>({
7342
+ name: stringValidator,
7343
+ strength: stringValidator,
7344
+ refills: stringValidator,
7345
+ quantity: stringValidator,
7346
+ daysSupply: stringValidator,
7347
+ medId: stringValidator,
7348
+ })),
7349
+ pharmacyId: stringValidator,
7350
+ }))
7351
+ },
7228
7352
  },
7229
7353
  },
7230
7354
  databases: {
@@ -9734,7 +9858,7 @@ If a voicemail is left, it is indicated by recordingURI, transcription, or recor
9734
9858
  type: { validator: stringValidator100 }, // only used on creation
9735
9859
  maxTokens: { validator: positiveNumberValidator },
9736
9860
  conversationId: { validator: mongoIdStringValidator },
9737
- prompt: { validator: stringValidator25000 },
9861
+ prompt: { validator: stringValidator100000EmptyOkay },
9738
9862
  orchestrationId: { validator: stringValidatorOptional }, // optional ID to group multiple conversations as part of the same workflow
9739
9863
  },
9740
9864
  returns: {