@medalsocial/sdk 1.7.0 → 1.8.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/README.md +38 -0
- package/dist/openapi/medal-social.openapi.json +686 -0
- package/dist/pilot/index.d.mts +2 -2
- package/dist/pilot/index.d.ts +2 -2
- package/dist/src/index.d.mts +230 -3
- package/dist/src/index.d.ts +230 -3
- package/dist/src/index.js +144 -19
- package/dist/src/index.js.map +1 -1
- package/dist/src/index.mjs +143 -19
- package/dist/src/index.mjs.map +1 -1
- package/dist/src/openapi.generated.d.mts +473 -0
- package/dist/src/openapi.generated.d.ts +473 -0
- package/dist/src/openapi.generated.js.map +1 -1
- package/openapi/medal-social.openapi.yaml +446 -0
- package/package.json +1 -1
- package/skills/client/SKILL.md +2 -0
- package/skills/resources/SKILL.md +50 -2
|
@@ -26,6 +26,11 @@ tags:
|
|
|
26
26
|
Appointment bookings — the service catalogue, free slots, staff actions
|
|
27
27
|
on a booking, and the customer-facing manage-token routes. Money is
|
|
28
28
|
always integer øre.
|
|
29
|
+
- name: Portal
|
|
30
|
+
description: >-
|
|
31
|
+
Customer self-service portal — e-mail one-time-code login, then
|
|
32
|
+
session-bound access to the signed-in contact's own profile, bookings,
|
|
33
|
+
data export and erasure. Session routes carry `X-Portal-Session`.
|
|
29
34
|
- name: GDPR
|
|
30
35
|
description: Manage exports, consent records, and cookie consent.
|
|
31
36
|
- name: Workspaces
|
|
@@ -898,6 +903,184 @@ paths:
|
|
|
898
903
|
$ref: "#/components/schemas/ApiResponse_BookingRescheduleResult"
|
|
899
904
|
default:
|
|
900
905
|
$ref: "#/components/responses/ApiError"
|
|
906
|
+
/api/v1/portal/login/start:
|
|
907
|
+
post:
|
|
908
|
+
tags: [Portal]
|
|
909
|
+
operationId: startPortalLogin
|
|
910
|
+
summary: E-mail a one-time login code
|
|
911
|
+
description: >-
|
|
912
|
+
Always answers 202 `{ status: "sent" }`, whether or not the address
|
|
913
|
+
belongs to a contact — enumeration-safe by design. Rate-limited per
|
|
914
|
+
address and per caller (`429 RATE_LIMITED`). Not idempotency-keyed.
|
|
915
|
+
Errors: `400 VALIDATION_ERROR`, `403 FORBIDDEN` (key lacks
|
|
916
|
+
`write:portal`), `429 RATE_LIMITED`.
|
|
917
|
+
requestBody:
|
|
918
|
+
required: true
|
|
919
|
+
content:
|
|
920
|
+
application/json:
|
|
921
|
+
schema:
|
|
922
|
+
$ref: "#/components/schemas/PortalLoginStartInput"
|
|
923
|
+
responses:
|
|
924
|
+
"202":
|
|
925
|
+
description: Code queued (or silently not, for an unknown address).
|
|
926
|
+
content:
|
|
927
|
+
application/json:
|
|
928
|
+
schema:
|
|
929
|
+
$ref: "#/components/schemas/ApiResponse_PortalLoginStartResult"
|
|
930
|
+
default:
|
|
931
|
+
$ref: "#/components/responses/ApiError"
|
|
932
|
+
/api/v1/portal/login/verify:
|
|
933
|
+
post:
|
|
934
|
+
tags: [Portal]
|
|
935
|
+
operationId: verifyPortalLogin
|
|
936
|
+
summary: Exchange the e-mailed code for a session
|
|
937
|
+
description: >-
|
|
938
|
+
A wrong, burned or expired code all answer `401 PORTAL_CODE_INVALID` —
|
|
939
|
+
the three are not distinguished, so the response is not an oracle for
|
|
940
|
+
which codes exist. The returned `session_token` is a bearer credential
|
|
941
|
+
for ONE contact: keep it in an HttpOnly cookie on the site's server and
|
|
942
|
+
never hand it to the browser. Not idempotency-keyed. Errors:
|
|
943
|
+
`400 VALIDATION_ERROR`, `401 PORTAL_CODE_INVALID`, `403 FORBIDDEN`,
|
|
944
|
+
`429 RATE_LIMITED`.
|
|
945
|
+
requestBody:
|
|
946
|
+
required: true
|
|
947
|
+
content:
|
|
948
|
+
application/json:
|
|
949
|
+
schema:
|
|
950
|
+
$ref: "#/components/schemas/PortalVerifyInput"
|
|
951
|
+
responses:
|
|
952
|
+
"200":
|
|
953
|
+
description: The new session.
|
|
954
|
+
content:
|
|
955
|
+
application/json:
|
|
956
|
+
schema:
|
|
957
|
+
$ref: "#/components/schemas/ApiResponse_PortalSession"
|
|
958
|
+
default:
|
|
959
|
+
$ref: "#/components/responses/ApiError"
|
|
960
|
+
/api/v1/portal/logout:
|
|
961
|
+
parameters:
|
|
962
|
+
- $ref: "#/components/parameters/PortalSession"
|
|
963
|
+
post:
|
|
964
|
+
tags: [Portal]
|
|
965
|
+
operationId: logoutPortal
|
|
966
|
+
summary: Revoke the session
|
|
967
|
+
description: >-
|
|
968
|
+
Revoking twice reaches the same state — the second call answers
|
|
969
|
+
`401 PORTAL_SESSION_INVALID`. Not idempotency-keyed. Errors:
|
|
970
|
+
`401 PORTAL_SESSION_REQUIRED` (header missing),
|
|
971
|
+
`401 PORTAL_SESSION_INVALID` (unknown, expired or revoked),
|
|
972
|
+
`403 FORBIDDEN`, `429 RATE_LIMITED`.
|
|
973
|
+
responses:
|
|
974
|
+
"204":
|
|
975
|
+
description: Session revoked.
|
|
976
|
+
default:
|
|
977
|
+
$ref: "#/components/responses/ApiError"
|
|
978
|
+
/api/v1/portal/me:
|
|
979
|
+
parameters:
|
|
980
|
+
- $ref: "#/components/parameters/PortalSession"
|
|
981
|
+
get:
|
|
982
|
+
tags: [Portal]
|
|
983
|
+
operationId: getPortalProfile
|
|
984
|
+
summary: The signed-in contact's profile
|
|
985
|
+
description: >-
|
|
986
|
+
Errors: `401 PORTAL_SESSION_REQUIRED`, `401 PORTAL_SESSION_INVALID`,
|
|
987
|
+
`403 FORBIDDEN` (key lacks `read:portal`), `429 RATE_LIMITED`.
|
|
988
|
+
responses:
|
|
989
|
+
"200":
|
|
990
|
+
description: Profile.
|
|
991
|
+
content:
|
|
992
|
+
application/json:
|
|
993
|
+
schema:
|
|
994
|
+
$ref: "#/components/schemas/ApiResponse_PortalProfile"
|
|
995
|
+
default:
|
|
996
|
+
$ref: "#/components/responses/ApiError"
|
|
997
|
+
patch:
|
|
998
|
+
tags: [Portal]
|
|
999
|
+
operationId: updatePortalProfile
|
|
1000
|
+
summary: Update the signed-in contact's profile
|
|
1001
|
+
description: >-
|
|
1002
|
+
Only the supplied fields change. `phone: null` clears the number,
|
|
1003
|
+
`family` replaces the whole list, and `marketing_consent` records a
|
|
1004
|
+
`marketing_email` consent decision with source `portal`. Answers the
|
|
1005
|
+
profile as it is after the change. Errors: `400 VALIDATION_ERROR`,
|
|
1006
|
+
`401 PORTAL_SESSION_REQUIRED`, `401 PORTAL_SESSION_INVALID`,
|
|
1007
|
+
`403 FORBIDDEN` (key lacks `write:portal`), `429 RATE_LIMITED`.
|
|
1008
|
+
requestBody:
|
|
1009
|
+
required: true
|
|
1010
|
+
content:
|
|
1011
|
+
application/json:
|
|
1012
|
+
schema:
|
|
1013
|
+
$ref: "#/components/schemas/PortalProfilePatch"
|
|
1014
|
+
responses:
|
|
1015
|
+
"200":
|
|
1016
|
+
description: Updated profile.
|
|
1017
|
+
content:
|
|
1018
|
+
application/json:
|
|
1019
|
+
schema:
|
|
1020
|
+
$ref: "#/components/schemas/ApiResponse_PortalProfile"
|
|
1021
|
+
default:
|
|
1022
|
+
$ref: "#/components/responses/ApiError"
|
|
1023
|
+
/api/v1/portal/me/bookings:
|
|
1024
|
+
parameters:
|
|
1025
|
+
- $ref: "#/components/parameters/PortalSession"
|
|
1026
|
+
get:
|
|
1027
|
+
tags: [Portal]
|
|
1028
|
+
operationId: listPortalBookings
|
|
1029
|
+
summary: The signed-in contact's bookings
|
|
1030
|
+
description: >-
|
|
1031
|
+
Split into `upcoming` and `past`. An upcoming booking still inside the
|
|
1032
|
+
workspace's policy windows carries `manage_token` and
|
|
1033
|
+
`can_manage: true`; the token opens the site's manage page. Errors:
|
|
1034
|
+
`401 PORTAL_SESSION_REQUIRED`, `401 PORTAL_SESSION_INVALID`,
|
|
1035
|
+
`403 FORBIDDEN`, `429 RATE_LIMITED`.
|
|
1036
|
+
responses:
|
|
1037
|
+
"200":
|
|
1038
|
+
description: Bookings.
|
|
1039
|
+
content:
|
|
1040
|
+
application/json:
|
|
1041
|
+
schema:
|
|
1042
|
+
$ref: "#/components/schemas/ApiResponse_PortalBookings"
|
|
1043
|
+
default:
|
|
1044
|
+
$ref: "#/components/responses/ApiError"
|
|
1045
|
+
/api/v1/portal/me/export:
|
|
1046
|
+
parameters:
|
|
1047
|
+
- $ref: "#/components/parameters/PortalSession"
|
|
1048
|
+
post:
|
|
1049
|
+
tags: [Portal]
|
|
1050
|
+
operationId: exportPortalData
|
|
1051
|
+
summary: Export everything held about the signed-in contact
|
|
1052
|
+
description: >-
|
|
1053
|
+
A synchronous GDPR Art. 15 export of the contact's profile, family,
|
|
1054
|
+
consents and bookings, as one JSON document. Read-only, so not
|
|
1055
|
+
idempotency-keyed. Errors: `401 PORTAL_SESSION_REQUIRED`,
|
|
1056
|
+
`401 PORTAL_SESSION_INVALID`, `403 FORBIDDEN`, `429 RATE_LIMITED`.
|
|
1057
|
+
responses:
|
|
1058
|
+
"200":
|
|
1059
|
+
description: The export.
|
|
1060
|
+
content:
|
|
1061
|
+
application/json:
|
|
1062
|
+
schema:
|
|
1063
|
+
$ref: "#/components/schemas/ApiResponse_PortalExport"
|
|
1064
|
+
default:
|
|
1065
|
+
$ref: "#/components/responses/ApiError"
|
|
1066
|
+
/api/v1/portal/me/delete:
|
|
1067
|
+
parameters:
|
|
1068
|
+
- $ref: "#/components/parameters/PortalSession"
|
|
1069
|
+
post:
|
|
1070
|
+
tags: [Portal]
|
|
1071
|
+
operationId: deletePortalAccount
|
|
1072
|
+
summary: Erase the signed-in contact
|
|
1073
|
+
description: >-
|
|
1074
|
+
GDPR Art. 17 erasure. The session is revoked as part of the deletion,
|
|
1075
|
+
so a retry answers `401 PORTAL_SESSION_INVALID` rather than deleting
|
|
1076
|
+
anything else. Not idempotency-keyed. Errors:
|
|
1077
|
+
`401 PORTAL_SESSION_REQUIRED`, `401 PORTAL_SESSION_INVALID`,
|
|
1078
|
+
`403 FORBIDDEN` (key lacks `write:portal`), `429 RATE_LIMITED`.
|
|
1079
|
+
responses:
|
|
1080
|
+
"204":
|
|
1081
|
+
description: Contact erased and session revoked.
|
|
1082
|
+
default:
|
|
1083
|
+
$ref: "#/components/responses/ApiError"
|
|
901
1084
|
/api/v1/gdpr/export:
|
|
902
1085
|
post:
|
|
903
1086
|
tags: [GDPR]
|
|
@@ -1541,6 +1724,18 @@ components:
|
|
|
1541
1724
|
Possession of it authorizes the customer's own cancel or reschedule.
|
|
1542
1725
|
schema:
|
|
1543
1726
|
type: string
|
|
1727
|
+
PortalSession:
|
|
1728
|
+
name: X-Portal-Session
|
|
1729
|
+
in: header
|
|
1730
|
+
required: true
|
|
1731
|
+
description: >-
|
|
1732
|
+
The `session_token` returned by `verifyPortalLogin`. A bearer
|
|
1733
|
+
credential for ONE contact — the site's server keeps it in an HttpOnly
|
|
1734
|
+
cookie and forwards it here. Missing answers
|
|
1735
|
+
`401 PORTAL_SESSION_REQUIRED`; unknown, expired or revoked answers
|
|
1736
|
+
`401 PORTAL_SESSION_INVALID`.
|
|
1737
|
+
schema:
|
|
1738
|
+
type: string
|
|
1544
1739
|
Cursor:
|
|
1545
1740
|
name: cursor
|
|
1546
1741
|
in: query
|
|
@@ -3615,6 +3810,16 @@ components:
|
|
|
3615
3810
|
$ref: "#/components/schemas/Envelope_BookingRescheduleResult"
|
|
3616
3811
|
ApiResponse_ManageSummary:
|
|
3617
3812
|
$ref: "#/components/schemas/Envelope_ManageSummary"
|
|
3813
|
+
ApiResponse_PortalLoginStartResult:
|
|
3814
|
+
$ref: "#/components/schemas/Envelope_PortalLoginStartResult"
|
|
3815
|
+
ApiResponse_PortalSession:
|
|
3816
|
+
$ref: "#/components/schemas/Envelope_PortalSession"
|
|
3817
|
+
ApiResponse_PortalProfile:
|
|
3818
|
+
$ref: "#/components/schemas/Envelope_PortalProfile"
|
|
3819
|
+
ApiResponse_PortalBookings:
|
|
3820
|
+
$ref: "#/components/schemas/Envelope_PortalBookings"
|
|
3821
|
+
ApiResponse_PortalExport:
|
|
3822
|
+
$ref: "#/components/schemas/Envelope_PortalExport"
|
|
3618
3823
|
ApiResponse_GdprExportRequest:
|
|
3619
3824
|
$ref: "#/components/schemas/Envelope_GdprExportRequest"
|
|
3620
3825
|
ApiResponse_GdprExportArray:
|
|
@@ -3946,6 +4151,247 @@ components:
|
|
|
3946
4151
|
properties:
|
|
3947
4152
|
data:
|
|
3948
4153
|
$ref: "#/components/schemas/ManageSummary"
|
|
4154
|
+
PortalLoginStartInput:
|
|
4155
|
+
type: object
|
|
4156
|
+
required: [email]
|
|
4157
|
+
properties:
|
|
4158
|
+
email:
|
|
4159
|
+
type: string
|
|
4160
|
+
format: email
|
|
4161
|
+
description: The address the code is sent to.
|
|
4162
|
+
locale:
|
|
4163
|
+
type: string
|
|
4164
|
+
description: Locale for the e-mail (e.g. `nb`, `en`); the workspace default when omitted.
|
|
4165
|
+
PortalLoginStartResult:
|
|
4166
|
+
description: >-
|
|
4167
|
+
Always `sent`, whether or not the address is a known contact — the
|
|
4168
|
+
route is enumeration-safe by design.
|
|
4169
|
+
type: object
|
|
4170
|
+
required: [status]
|
|
4171
|
+
properties:
|
|
4172
|
+
status:
|
|
4173
|
+
type: string
|
|
4174
|
+
const: sent
|
|
4175
|
+
PortalVerifyInput:
|
|
4176
|
+
type: object
|
|
4177
|
+
required: [email, code]
|
|
4178
|
+
properties:
|
|
4179
|
+
email:
|
|
4180
|
+
type: string
|
|
4181
|
+
format: email
|
|
4182
|
+
code:
|
|
4183
|
+
type: string
|
|
4184
|
+
description: The one-time code from the e-mail.
|
|
4185
|
+
PortalContactSummary:
|
|
4186
|
+
type: object
|
|
4187
|
+
required: [contact_id, first_name]
|
|
4188
|
+
properties:
|
|
4189
|
+
contact_id:
|
|
4190
|
+
type: string
|
|
4191
|
+
first_name:
|
|
4192
|
+
type: [string, "null"]
|
|
4193
|
+
PortalSession:
|
|
4194
|
+
description: >-
|
|
4195
|
+
A portal session. `session_token` is a bearer credential for ONE
|
|
4196
|
+
contact — keep it in an HttpOnly cookie on the site's server and never
|
|
4197
|
+
hand it to the browser.
|
|
4198
|
+
type: object
|
|
4199
|
+
required: [session_token, expires_at, contact]
|
|
4200
|
+
properties:
|
|
4201
|
+
session_token:
|
|
4202
|
+
type: string
|
|
4203
|
+
expires_at:
|
|
4204
|
+
type: integer
|
|
4205
|
+
description: Unix timestamp in milliseconds.
|
|
4206
|
+
contact:
|
|
4207
|
+
$ref: "#/components/schemas/PortalContactSummary"
|
|
4208
|
+
PortalFamilyMember:
|
|
4209
|
+
type: object
|
|
4210
|
+
required: [name, birth_year]
|
|
4211
|
+
properties:
|
|
4212
|
+
name:
|
|
4213
|
+
type: string
|
|
4214
|
+
birth_year:
|
|
4215
|
+
type: integer
|
|
4216
|
+
PortalProfile:
|
|
4217
|
+
type: object
|
|
4218
|
+
required:
|
|
4219
|
+
- contact_id
|
|
4220
|
+
- email
|
|
4221
|
+
- first_name
|
|
4222
|
+
- last_name
|
|
4223
|
+
- phone
|
|
4224
|
+
- family
|
|
4225
|
+
- marketing_consent
|
|
4226
|
+
- created_at
|
|
4227
|
+
properties:
|
|
4228
|
+
contact_id:
|
|
4229
|
+
type: string
|
|
4230
|
+
email:
|
|
4231
|
+
type: string
|
|
4232
|
+
first_name:
|
|
4233
|
+
type: [string, "null"]
|
|
4234
|
+
last_name:
|
|
4235
|
+
type: [string, "null"]
|
|
4236
|
+
phone:
|
|
4237
|
+
type: [string, "null"]
|
|
4238
|
+
family:
|
|
4239
|
+
type: array
|
|
4240
|
+
items:
|
|
4241
|
+
$ref: "#/components/schemas/PortalFamilyMember"
|
|
4242
|
+
marketing_consent:
|
|
4243
|
+
type: boolean
|
|
4244
|
+
created_at:
|
|
4245
|
+
type: integer
|
|
4246
|
+
description: Unix timestamp in milliseconds.
|
|
4247
|
+
PortalProfilePatch:
|
|
4248
|
+
description: Only the supplied fields change.
|
|
4249
|
+
type: object
|
|
4250
|
+
properties:
|
|
4251
|
+
first_name:
|
|
4252
|
+
type: string
|
|
4253
|
+
last_name:
|
|
4254
|
+
type: string
|
|
4255
|
+
phone:
|
|
4256
|
+
type: [string, "null"]
|
|
4257
|
+
description: "`null` clears the number."
|
|
4258
|
+
family:
|
|
4259
|
+
type: array
|
|
4260
|
+
description: Replaces the whole list.
|
|
4261
|
+
items:
|
|
4262
|
+
$ref: "#/components/schemas/PortalFamilyMember"
|
|
4263
|
+
marketing_consent:
|
|
4264
|
+
type: boolean
|
|
4265
|
+
description: Records a `marketing_email` consent change with source `portal`.
|
|
4266
|
+
PortalBooking:
|
|
4267
|
+
type: object
|
|
4268
|
+
required:
|
|
4269
|
+
- booking_id
|
|
4270
|
+
- status
|
|
4271
|
+
- start_ts
|
|
4272
|
+
- end_ts
|
|
4273
|
+
- service_id
|
|
4274
|
+
- service_name
|
|
4275
|
+
- resource_id
|
|
4276
|
+
- resource_name
|
|
4277
|
+
- booked_for_name
|
|
4278
|
+
- amount_ore
|
|
4279
|
+
- notes
|
|
4280
|
+
- manage_token
|
|
4281
|
+
- can_manage
|
|
4282
|
+
properties:
|
|
4283
|
+
booking_id:
|
|
4284
|
+
type: string
|
|
4285
|
+
status:
|
|
4286
|
+
$ref: "#/components/schemas/BookingStatus"
|
|
4287
|
+
start_ts:
|
|
4288
|
+
type: integer
|
|
4289
|
+
description: Unix timestamp in milliseconds.
|
|
4290
|
+
end_ts:
|
|
4291
|
+
type: integer
|
|
4292
|
+
description: Unix timestamp in milliseconds.
|
|
4293
|
+
service_id:
|
|
4294
|
+
type: [string, "null"]
|
|
4295
|
+
service_name:
|
|
4296
|
+
type: [string, "null"]
|
|
4297
|
+
resource_id:
|
|
4298
|
+
type: [string, "null"]
|
|
4299
|
+
resource_name:
|
|
4300
|
+
type: [string, "null"]
|
|
4301
|
+
booked_for_name:
|
|
4302
|
+
type: [string, "null"]
|
|
4303
|
+
amount_ore:
|
|
4304
|
+
type: [integer, "null"]
|
|
4305
|
+
description: Integer øre, or `null` when the service has no price.
|
|
4306
|
+
notes:
|
|
4307
|
+
type: [string, "null"]
|
|
4308
|
+
manage_token:
|
|
4309
|
+
type: [string, "null"]
|
|
4310
|
+
description: >-
|
|
4311
|
+
Present only while the booking is upcoming and manageable; opens
|
|
4312
|
+
the site's manage page.
|
|
4313
|
+
can_manage:
|
|
4314
|
+
type: boolean
|
|
4315
|
+
PortalBookings:
|
|
4316
|
+
type: object
|
|
4317
|
+
required: [upcoming, past]
|
|
4318
|
+
properties:
|
|
4319
|
+
upcoming:
|
|
4320
|
+
type: array
|
|
4321
|
+
items:
|
|
4322
|
+
$ref: "#/components/schemas/PortalBooking"
|
|
4323
|
+
past:
|
|
4324
|
+
type: array
|
|
4325
|
+
items:
|
|
4326
|
+
$ref: "#/components/schemas/PortalBooking"
|
|
4327
|
+
PortalConsentRecord:
|
|
4328
|
+
type: object
|
|
4329
|
+
required: [consent_type, granted, granted_at, revoked_at, source]
|
|
4330
|
+
properties:
|
|
4331
|
+
consent_type:
|
|
4332
|
+
type: string
|
|
4333
|
+
granted:
|
|
4334
|
+
type: boolean
|
|
4335
|
+
granted_at:
|
|
4336
|
+
type: [integer, "null"]
|
|
4337
|
+
description: Unix timestamp in milliseconds, or `null`.
|
|
4338
|
+
revoked_at:
|
|
4339
|
+
type: [integer, "null"]
|
|
4340
|
+
description: Unix timestamp in milliseconds, or `null`.
|
|
4341
|
+
source:
|
|
4342
|
+
type: string
|
|
4343
|
+
PortalExport:
|
|
4344
|
+
description: Everything the workspace holds about the signed-in contact (GDPR Art. 15).
|
|
4345
|
+
type: object
|
|
4346
|
+
required: [exported_at, contact, family, consents, bookings]
|
|
4347
|
+
properties:
|
|
4348
|
+
exported_at:
|
|
4349
|
+
type: integer
|
|
4350
|
+
description: Unix timestamp in milliseconds.
|
|
4351
|
+
contact:
|
|
4352
|
+
$ref: "#/components/schemas/PortalProfile"
|
|
4353
|
+
family:
|
|
4354
|
+
type: array
|
|
4355
|
+
items:
|
|
4356
|
+
$ref: "#/components/schemas/PortalFamilyMember"
|
|
4357
|
+
consents:
|
|
4358
|
+
type: array
|
|
4359
|
+
items:
|
|
4360
|
+
$ref: "#/components/schemas/PortalConsentRecord"
|
|
4361
|
+
bookings:
|
|
4362
|
+
type: array
|
|
4363
|
+
items:
|
|
4364
|
+
$ref: "#/components/schemas/PortalBooking"
|
|
4365
|
+
Envelope_PortalLoginStartResult:
|
|
4366
|
+
type: object
|
|
4367
|
+
required: [data]
|
|
4368
|
+
properties:
|
|
4369
|
+
data:
|
|
4370
|
+
$ref: "#/components/schemas/PortalLoginStartResult"
|
|
4371
|
+
Envelope_PortalSession:
|
|
4372
|
+
type: object
|
|
4373
|
+
required: [data]
|
|
4374
|
+
properties:
|
|
4375
|
+
data:
|
|
4376
|
+
$ref: "#/components/schemas/PortalSession"
|
|
4377
|
+
Envelope_PortalProfile:
|
|
4378
|
+
type: object
|
|
4379
|
+
required: [data]
|
|
4380
|
+
properties:
|
|
4381
|
+
data:
|
|
4382
|
+
$ref: "#/components/schemas/PortalProfile"
|
|
4383
|
+
Envelope_PortalBookings:
|
|
4384
|
+
type: object
|
|
4385
|
+
required: [data]
|
|
4386
|
+
properties:
|
|
4387
|
+
data:
|
|
4388
|
+
$ref: "#/components/schemas/PortalBookings"
|
|
4389
|
+
Envelope_PortalExport:
|
|
4390
|
+
type: object
|
|
4391
|
+
required: [data]
|
|
4392
|
+
properties:
|
|
4393
|
+
data:
|
|
4394
|
+
$ref: "#/components/schemas/PortalExport"
|
|
3949
4395
|
ScanCreateInput:
|
|
3950
4396
|
type: object
|
|
3951
4397
|
description: Provide exactly one of `url`, `orgnr`, or `name`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medalsocial/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "TypeScript SDK for Medal Social API — posts, emails, contacts, deals, helpdesk, webhooks, and GDPR compliance",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Medal Social / Ali Aljumaili",
|
package/skills/client/SKILL.md
CHANGED
|
@@ -57,6 +57,8 @@ Every request gets:
|
|
|
57
57
|
- `x-workspace-id: <workspaceId>` (only if `workspaceId` was set on the constructor)
|
|
58
58
|
- `User-Agent: medalsocial-sdk/<version>` (best-effort — browsers reject custom User-Agent; the SDK swallows that error silently)
|
|
59
59
|
|
|
60
|
+
Per-call extras go in `RequestOptions.headers` (accepted by `get`, `post`, `postOnce`, `patch`, `delete` on `BaseClient`). The named options win over a same-named bag entry: `{ headers: { "idempotency-key": "a" }, idempotencyKey: "b" }` sends `b`. The SDK uses this itself for the customer portal's `X-Portal-Session` header — you never set that one by hand; pass the session token to `medal.portal.*` instead. Bag keys are lower-cased before the protected names (`content-type`, the named options) are applied, so capitalisation cannot smuggle a duplicate past them. `retry: false` sends a request exactly once (no 429/5xx retry) on every verb (`post`, `patch`, `delete`) — used by `portal.login.verify`, `portal.logout` and `portal.deleteMe`, where the first attempt may have consumed the code or revoked the session and a retry would misreport success as failure, and by `portal.updateMe`, where a `marketing_consent` change records a consent event a retry would repeat.
|
|
61
|
+
|
|
60
62
|
## Retry behavior
|
|
61
63
|
|
|
62
64
|
`BaseClient.request` retries on **429 and 5xx** for up to **3 attempts total**:
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: resources
|
|
3
|
-
description: Use when calling any of the SDK resources (bookings, contacts, deals, emails, gdpr, posts, scan, workspaces) — listing with pagination, sending transactional or batch emails, scheduling and publishing posts, booking appointments or querying free slots, cancelling or rescheduling a booking as staff or on a customer's behalf, recording GDPR consent or running an export workflow, fetching a contact's activity timeline — or when needing OpenAPI-derived TypeScript types or the raw OpenAPI document from `@medalsocial/sdk`.
|
|
3
|
+
description: Use when calling any of the SDK resources (bookings, contacts, deals, emails, gdpr, portal, posts, scan, workspaces) — listing with pagination, sending transactional or batch emails, scheduling and publishing posts, booking appointments or querying free slots, cancelling or rescheduling a booking as staff or on a customer's behalf, signing a customer into the self-service portal and reading their own profile/bookings/export, recording GDPR consent or running an export workflow, fetching a contact's activity timeline — or when needing OpenAPI-derived TypeScript types or the raw OpenAPI document from `@medalsocial/sdk`.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Medal Social SDK — Resources
|
|
7
7
|
|
|
8
8
|
## When to load this skill
|
|
9
9
|
|
|
10
|
-
- Calling `medal.bookings.*`, `medal.contacts.*`, `medal.deals.*`, `medal.emails.*`, `medal.gdpr.*`, `medal.posts.*`, `medal.scan.*`, or `medal.workspaces.*`.
|
|
10
|
+
- Calling `medal.bookings.*`, `medal.contacts.*`, `medal.deals.*`, `medal.emails.*`, `medal.gdpr.*`, `medal.portal.*`, `medal.posts.*`, `medal.scan.*`, or `medal.workspaces.*`.
|
|
11
11
|
- Looking up an exact method signature or response shape.
|
|
12
12
|
- Building a list view that needs pagination.
|
|
13
13
|
- Sending a single transactional email or a bulk batch.
|
|
14
14
|
- Booking an appointment: reading the catalogue, querying free slots, creating a booking or party.
|
|
15
15
|
- Cancelling or rescheduling a booking — and deciding between the staff route and the customer manage-token route.
|
|
16
16
|
- Running a GDPR data-export workflow (request → poll → fetch).
|
|
17
|
+
- Building a customer self-service portal: e-mail code login, then the signed-in contact's own profile, bookings, export and erasure.
|
|
17
18
|
- Importing contacts from a CSV-like source.
|
|
18
19
|
- Needing OpenAPI-derived types for a custom fetch wrapper, generated mocks, or contract tests.
|
|
19
20
|
|
|
@@ -42,6 +43,8 @@ Errors throw `MedalApiError` (see the `client` skill for details).
|
|
|
42
43
|
| `medal.emails.templates` | `src/resources/emails.ts` (`EmailTemplates`) | `list()`, `get(slug, opts?)` |
|
|
43
44
|
| `medal.emails` | `src/resources/emails.ts` (`Emails`) | `send(input)`, `get(id)`, `batch(input)` |
|
|
44
45
|
| `medal.gdpr` | `src/resources/gdpr.ts` | `requestExport()`, `listExports()`, `getExport(id)`, `recordConsent(input)`, `getConsent(email)`, `cookieConsent(input)` |
|
|
46
|
+
| `medal.portal.login` | `src/resources/portal.ts` (`PortalLogin`) | `start({ email, locale? })` (always 202 `{ status: 'sent' }`), `verify({ email, code })` → `PortalSession` |
|
|
47
|
+
| `medal.portal` | `src/resources/portal.ts` | `me(session)`, `updateMe(session, patch)`, `myBookings(session)`, `exportMyData(session)`, `deleteMe(session)`, `logout(session)` — every one takes the `session_token` first and sends it as `X-Portal-Session`; `deleteMe`/`logout` resolve to `undefined` (204) |
|
|
45
48
|
| `medal.scan` | `src/resources/scan.ts` | `create(input)` (exactly one of `url`/`orgnr`/`name`; 202 async job), `get(id)`, `companies(q)` (Norwegian registry typeahead), `waitForResult(id, opts?)` (polls until done/failed; returns the job either way, throws only on deadline) |
|
|
46
49
|
| `medal.posts` | `src/resources/posts.ts` | `list(opts?)`, `create(input)`, `get(id)`, `update(id, input)`, `remove(id)`, `schedule(id, input)`, `publish(id)`, `channels()` |
|
|
47
50
|
| `medal.workspaces` | `src/resources/workspaces.ts` | `list()` |
|
|
@@ -223,6 +226,45 @@ const { data: summary } = await medal.emails.batch({
|
|
|
223
226
|
|
|
224
227
|
For more than 100 recipients, chunk into multiple `batch()` calls. There is no built-in chunker.
|
|
225
228
|
|
|
229
|
+
## Customer portal — e-mail code login, then session-bound self-service
|
|
230
|
+
|
|
231
|
+
`medal.portal` is for the workspace's **own customers**, not staff. A customer proves they own an e-mail address, gets a session, and can then see and change what the workspace holds about *them* — profile, family members, bookings, consents — export it, or erase it. The API key needs `read:portal` + `write:portal` (`403 FORBIDDEN` otherwise).
|
|
232
|
+
|
|
233
|
+
**The session token is a bearer credential for ONE contact.** `verify()` returns it once; your site's *server* keeps it in an HttpOnly, Secure cookie on the site's own domain and forwards it on every call. Never send it to the browser as JSON, never put it in a URL, and never let the browser call Medal directly — the API key would leak with it.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
// Step 1 — send the code. ALWAYS { status: 'sent' }, whether or not the address is a
|
|
237
|
+
// contact: enumeration-safe, so do not treat "sent" as "this customer exists".
|
|
238
|
+
await medal.portal.login.start({ email, locale: 'nb' });
|
|
239
|
+
|
|
240
|
+
// Step 2 — exchange the code. Wrong, burned and expired codes ALL answer
|
|
241
|
+
// 401 PORTAL_CODE_INVALID; there is no way to tell them apart, by design.
|
|
242
|
+
const { data: session } = await medal.portal.login.verify({ email, code });
|
|
243
|
+
cookies.set('portal_session', session.session_token, {
|
|
244
|
+
httpOnly: true, secure: true, sameSite: 'lax', expires: new Date(session.expires_at),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Step 3 — session-bound calls, token read back from the cookie
|
|
248
|
+
const token = cookies.get('portal_session');
|
|
249
|
+
const { data: me } = await medal.portal.me(token);
|
|
250
|
+
const { data: mine } = await medal.portal.myBookings(token); // { upcoming, past }
|
|
251
|
+
await medal.portal.updateMe(token, { family: [{ name: 'Ola', birth_year: 2018 }] });
|
|
252
|
+
const { data: exported } = await medal.portal.exportMyData(token); // GDPR Art. 15 — synchronous JSON
|
|
253
|
+
await medal.portal.logout(token); // 204 — revokes this session only
|
|
254
|
+
// …or, terminal (a later logout() on the same token answers 401 PORTAL_SESSION_INVALID):
|
|
255
|
+
await medal.portal.deleteMe(token); // GDPR Art. 17 — 204
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**`myBookings` hands you manage tokens.** An `upcoming` booking still inside the workspace's policy windows carries `manage_token` (string) and `can_manage: true`; everything else has `manage_token: null`. Use it with `medal.bookings.manage.*` — the customer routes, where the windows are enforced — never with the id-addressed staff routes.
|
|
259
|
+
|
|
260
|
+
**`updateMe` is a partial patch.** Only supplied fields change; `phone: null` clears the number; `family` replaces the whole list (send the full new list, not the delta); `marketing_consent` records a `marketing_email` consent decision with source `portal`, so it shows up in `medal.gdpr.getConsent(email)`.
|
|
261
|
+
|
|
262
|
+
**Two 401s, one meaning.** `PORTAL_SESSION_REQUIRED` (header missing) and `PORTAL_SESSION_INVALID` (unknown, expired, revoked — including after `deleteMe` or `logout`) both mean "sign in again": clear the cookie and send the customer back to step 1. Do not retry them.
|
|
263
|
+
|
|
264
|
+
**Nothing here is idempotency-keyed.** The login routes cannot duplicate anything (a retried `start` sends at most one more code; a retried `verify` meets a burned code), `me`/`myBookings`/`exportMyData` are reads, and `logout`/`deleteMe` are terminal — a retry meets a revoked session. Passing `idempotencyKey` is not possible on these methods and would change nothing if it were.
|
|
265
|
+
|
|
266
|
+
**Portal export vs. GDPR export.** `medal.portal.exportMyData(token)` is one contact's data, synchronous, returned inline. `medal.gdpr.requestExport()` is the whole *workspace*, asynchronous, polled via `getExport`. They are not interchangeable.
|
|
267
|
+
|
|
226
268
|
## GDPR — consent + export workflow
|
|
227
269
|
|
|
228
270
|
**Consent (per-contact):**
|
|
@@ -340,4 +382,10 @@ The `with { type: "json" }` import-attribute syntax requires Node 24+ or a bundl
|
|
|
340
382
|
| `medal.bookings.update(id, {})` | Rejected by the API; now also a compile error | Pass at least one of `notes` / `internal_notes` |
|
|
341
383
|
| Ignoring `pagination.truncated` on `bookings.list` | Matching bookings exist that no cursor reaches | Narrow `from_ts`/`to_ts` and page again |
|
|
342
384
|
| Converting `start_ts` to a fixed format before sending | The API takes Unix ms **or** ISO 8601 | Pass a slot's `start_ts` straight through |
|
|
385
|
+
| Sending `session_token` to the browser (JSON, URL, non-HttpOnly cookie) | It is a bearer credential for that contact — whoever holds it is them | HttpOnly, Secure cookie on the site's server; the server calls `medal.portal.*` |
|
|
386
|
+
| Treating `login.start` → `{ status: 'sent' }` as "this customer exists" | Enumeration-safe: unknown addresses answer `sent` too | Show "check your e-mail" unconditionally |
|
|
387
|
+
| Branching on why `verify` failed | Wrong, burned and expired codes all answer `PORTAL_CODE_INVALID` | One message: "that code did not work — request a new one" |
|
|
388
|
+
| Retrying a `PORTAL_SESSION_INVALID` | The session is gone (expired, revoked, or the contact was deleted) | Clear the cookie and restart the login |
|
|
389
|
+
| Passing a portal `manage_token` to `medal.bookings.cancel(id)` | Wrong route: staff semantics, and it takes an id not a token | `medal.bookings.manage.cancel(manage_token)` |
|
|
390
|
+
| `updateMe(token, { family: [newMember] })` to add one member | `family` REPLACES the list — the others are dropped | Send the full list: `[...me.family, newMember]` |
|
|
343
391
|
| Building a custom client when only types are needed | Reinventing the wheel | Import from `@medalsocial/sdk/openapi-types` |
|