@fourier-labs/harbour-contracts 0.77.0 → 0.78.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.
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/integration-execute-result.schema.json",
4
4
  "title": "IntegrationExecuteResult",
5
- "description": "The `data` member of a successful execute response `{data, requestId}`; the SDK unwraps `data`. Exactly one of the operation results (twenty-four operations since 0.76.0, ADR 0018 and ADR 0022): slack.channel.history -> {messages, hasMore, nextCursor?} (plain text/IDs/time, plus the display name and emoji icon a per-app-named app post carries \u2014 authorName/authorIcon, 0.55.0 \u2014 never the provider payload; `nextCursor` when hasMore, to pass as the next read's `cursor`); slack.message.post, slack.message.update, slack.message.delete -> {messageId} (the Slack ts); slack.dm.send -> {messageId, channelId} (the ts and the DM channel it went to); slack.user.lookup -> {userId, displayName}; gmail.thread.list -> {threads, hasMore, nextPageToken?} (newest matching threads; each `messageId` is the thread's newest message, what gmail.message.read takes; `nextPageToken` when hasMore); gmail.thread.read -> {id, emails} (every message of the thread, oldest first, each shaped like a gmail.message.read result); gmail.message.read -> {id, threadId, subject, from, to, date, text, truncated, attachments?} (headers plus the plain text with html stripped, cut at 16 KiB with `truncated` set; `attachments` lists the files without their bytes, which gmail.attachment.read fetches by id); gmail.attachment.read -> {filename, mimeType, size, dataBase64}; gmail.message.send -> {messageId, threadId} (the Gmail ids of the sent message and its thread); warehouse.view.read -> {rows, truncated} (rows keyed by the selected columns, or for an aggregate read by the groupBy columns then the aggregate aliases; decimal/64-bit values as strings, dates as ISO strings; since 0.70.0 every server sends `truncated`, true when more rows matched than `limit` \u2014 the server reads limit+1 rows to know \u2014 so the rows are incomplete and must not be totalled or averaged; a server before 0.70.0 omitted it). Basecamp (0.76.0, ADR 0022; every id a string of digits, every page at most 100 items, `nextPageToken` only when hasMore, text cut at 16 KiB like Gmail): basecamp.project.list -> {projects, hasMore, nextPageToken?}; basecamp.todolist.list -> {todolists, hasMore, nextPageToken?}; basecamp.todo.list -> {todos, hasMore, nextPageToken?}; basecamp.message.list -> {boardMessages, hasMore, nextPageToken?} (`boardMessages`, not `messages`, which is the Slack history shape); basecamp.comment.list -> {comments, hasMore, nextPageToken?}; basecamp.people.list -> {people, hasMore, nextPageToken?}; basecamp.todo.create, basecamp.message.post, basecamp.comment.post, basecamp.campfire.post -> {id, appUrl} (the new recording and its Basecamp page); basecamp.todo.complete -> {id, completed} with completed always true. An unknown send outcome is never a result; it is an error with details.code SEND_OUTCOME_UNKNOWN. A whole result over 1 MiB is rejected, never cut down to fit \u2014 except a gmail.attachment.read, whose single attachment is bounded by its own 2 MiB.",
5
+ "description": "The `data` member of a successful execute response `{data, requestId}`; the SDK unwraps `data`. Exactly one of the operation results (thirty-three operations since 0.78.0, ADR 0018, ADR 0022 and ADR 0024): slack.channel.history -> {messages, hasMore, nextCursor?} (plain text/IDs/time, plus the display name and emoji icon a per-app-named app post carries \u2014 authorName/authorIcon, 0.55.0 \u2014 never the provider payload; `nextCursor` when hasMore, to pass as the next read's `cursor`); slack.message.post, slack.message.update, slack.message.delete -> {messageId} (the Slack ts); slack.dm.send -> {messageId, channelId} (the ts and the DM channel it went to); slack.user.lookup -> {userId, displayName}; gmail.thread.list -> {threads, hasMore, nextPageToken?} (newest matching threads; each `messageId` is the thread's newest message, what gmail.message.read takes; `nextPageToken` when hasMore); gmail.thread.read -> {id, emails} (every message of the thread, oldest first, each shaped like a gmail.message.read result); gmail.message.read -> {id, threadId, subject, from, to, date, text, truncated, attachments?} (headers plus the plain text with html stripped, cut at 16 KiB with `truncated` set; `attachments` lists the files without their bytes, which gmail.attachment.read fetches by id); gmail.attachment.read -> {filename, mimeType, size, dataBase64}; gmail.message.send -> {messageId, threadId} (the Gmail ids of the sent message and its thread); warehouse.view.read -> {rows, truncated} (rows keyed by the selected columns, or for an aggregate read by the groupBy columns then the aggregate aliases; decimal/64-bit values as strings, dates as ISO strings; since 0.70.0 every server sends `truncated`, true when more rows matched than `limit` \u2014 the server reads limit+1 rows to know \u2014 so the rows are incomplete and must not be totalled or averaged; a server before 0.70.0 omitted it). Basecamp (0.76.0, ADR 0022; every id a string of digits, every page at most 100 items, `nextPageToken` only when hasMore, text cut at 16 KiB like Gmail): basecamp.project.list -> {projects, hasMore, nextPageToken?}; basecamp.todolist.list -> {todolists, hasMore, nextPageToken?}; basecamp.todo.list -> {todos, hasMore, nextPageToken?}; basecamp.message.list -> {boardMessages, hasMore, nextPageToken?} (`boardMessages`, not `messages`, which is the Slack history shape); basecamp.comment.list -> {comments, hasMore, nextPageToken?}; basecamp.people.list -> {people, hasMore, nextPageToken?}; basecamp.todo.create, basecamp.message.post, basecamp.comment.post, basecamp.campfire.post -> {id, appUrl} (the new recording and its Basecamp page); basecamp.todo.complete -> {id, completed} with completed always true. SharePoint (0.78.0, ADR 0024; every id a Microsoft Graph id, `nextPageToken` only when hasMore and up to 2048 characters for a Graph `$skiptoken`): sharepoint.list.list -> {lists, hasMore, nextPageToken?} (at most 100, hidden lists dropped); sharepoint.item.list -> {items, hasMore, nextPageToken?} (at most 50; each item's `fields` are the column values as Graph returns them with the system columns dropped, the whole object cut at 16 KiB); sharepoint.drive.list -> {drives, hasMore, nextPageToken?}; sharepoint.file.list and sharepoint.file.search -> {files, hasMore, nextPageToken?} (at most 100 entries, folders included with isFolder); sharepoint.file.read -> {id, filename, mimeType, size, dataBase64} (the Gmail attachment shape plus the file's id; at most 2 MiB of bytes); sharepoint.item.create -> {id, webUrl} (the new item and its SharePoint page); sharepoint.item.update -> {id}; sharepoint.file.upload -> {id, filename, webUrl} (the stored file, under the name `conflict` rename may have given it). An unknown send outcome is never a result; it is an error with details.code SEND_OUTCOME_UNKNOWN. A whole result over 1 MiB is rejected, never cut down to fit \u2014 except a gmail.attachment.read or a sharepoint.file.read, whose single file is bounded by its own 2 MiB.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "properties": {
@@ -53,13 +53,14 @@
53
53
  "nextPageToken": {
54
54
  "type": "string",
55
55
  "minLength": 1,
56
- "maxLength": 256,
57
- "description": "gmail.thread.list (0.71.0): the token of the next page, present only when hasMore."
56
+ "maxLength": 2048,
57
+ "description": "gmail.thread.list (0.71.0), the basecamp.* lists (0.76.0) and the paged sharepoint.* reads (0.78.0): the token of the next page, present only when hasMore; up to 2048 characters since 0.78.0 for a Graph $skiptoken."
58
58
  },
59
59
  "id": {
60
60
  "type": "string",
61
61
  "minLength": 1,
62
- "maxLength": 128
62
+ "maxLength": 256,
63
+ "description": "gmail.thread.read / gmail.message.read: the Gmail id; the Basecamp writes and complete (0.76.0): the recording's id; the SharePoint writes and sharepoint.file.read (0.78.0): the Graph id of the item or file (up to 256 characters since 0.78.0, the GraphId bound)."
63
64
  },
64
65
  "threadId": {
65
66
  "type": "string",
@@ -198,6 +199,45 @@
198
199
  "type": "boolean",
199
200
  "description": "basecamp.todo.complete (0.76.0): always true; the to-do is complete whether this call or an earlier one completed it."
200
201
  },
202
+ "lists": {
203
+ "type": "array",
204
+ "maxItems": 100,
205
+ "items": {
206
+ "$ref": "#/definitions/SharePointList"
207
+ },
208
+ "description": "sharepoint.list.list (0.78.0): one page of the site's lists the signed-in person can see, hidden lists dropped."
209
+ },
210
+ "items": {
211
+ "type": "array",
212
+ "maxItems": 50,
213
+ "items": {
214
+ "$ref": "#/definitions/SharePointListItem"
215
+ },
216
+ "description": "sharepoint.item.list (0.78.0): one page of the list's items (Graph pages of 50), filtered when the read said `filter`."
217
+ },
218
+ "drives": {
219
+ "type": "array",
220
+ "maxItems": 100,
221
+ "items": {
222
+ "$ref": "#/definitions/SharePointDrive"
223
+ },
224
+ "description": "sharepoint.drive.list (0.78.0): one page of the site's document libraries."
225
+ },
226
+ "files": {
227
+ "type": "array",
228
+ "maxItems": 100,
229
+ "items": {
230
+ "$ref": "#/definitions/SharePointFile"
231
+ },
232
+ "description": "sharepoint.file.list / sharepoint.file.search (0.78.0): one page of a folder's entries (files and folders) or of the files matching the search."
233
+ },
234
+ "webUrl": {
235
+ "type": "string",
236
+ "format": "uri",
237
+ "minLength": 1,
238
+ "maxLength": 2048,
239
+ "description": "sharepoint.item.create / sharepoint.file.upload (0.78.0): the SharePoint page of the new item or file, for a person to open."
240
+ },
201
241
  "status": {
202
242
  "type": "integer"
203
243
  },
@@ -407,6 +447,87 @@
407
447
  "completed"
408
448
  ],
409
449
  "maxProperties": 2
450
+ },
451
+ {
452
+ "required": [
453
+ "lists",
454
+ "hasMore"
455
+ ],
456
+ "propertyNames": {
457
+ "enum": [
458
+ "lists",
459
+ "hasMore",
460
+ "nextPageToken"
461
+ ]
462
+ }
463
+ },
464
+ {
465
+ "required": [
466
+ "items",
467
+ "hasMore"
468
+ ],
469
+ "propertyNames": {
470
+ "enum": [
471
+ "items",
472
+ "hasMore",
473
+ "nextPageToken"
474
+ ]
475
+ }
476
+ },
477
+ {
478
+ "required": [
479
+ "drives",
480
+ "hasMore"
481
+ ],
482
+ "propertyNames": {
483
+ "enum": [
484
+ "drives",
485
+ "hasMore",
486
+ "nextPageToken"
487
+ ]
488
+ }
489
+ },
490
+ {
491
+ "required": [
492
+ "files",
493
+ "hasMore"
494
+ ],
495
+ "propertyNames": {
496
+ "enum": [
497
+ "files",
498
+ "hasMore",
499
+ "nextPageToken"
500
+ ]
501
+ }
502
+ },
503
+ {
504
+ "required": [
505
+ "id"
506
+ ],
507
+ "propertyNames": {
508
+ "enum": [
509
+ "id",
510
+ "webUrl"
511
+ ]
512
+ }
513
+ },
514
+ {
515
+ "required": [
516
+ "id",
517
+ "filename",
518
+ "webUrl"
519
+ ],
520
+ "maxProperties": 3
521
+ },
522
+ {
523
+ "required": [
524
+ "id",
525
+ "filename",
526
+ "mimeType",
527
+ "size",
528
+ "dataBase64"
529
+ ],
530
+ "maxProperties": 5
410
531
  }
411
532
  ],
412
533
  "definitions": {
@@ -852,6 +973,144 @@
852
973
  "description": "Whether the person is an administrator of the Basecamp account."
853
974
  }
854
975
  }
976
+ },
977
+ "SharePointList": {
978
+ "type": "object",
979
+ "additionalProperties": false,
980
+ "required": [
981
+ "id",
982
+ "name",
983
+ "webUrl"
984
+ ],
985
+ "description": "One list of a sharepoint.list.list result (0.78.0); `id` is what sharepoint.item.list, sharepoint.item.create and sharepoint.item.update take as `listId`. `template` is the list template Graph reports (genericList, documentLibrary, tasks, events...) when it does.",
986
+ "properties": {
987
+ "id": {
988
+ "$ref": "app-integrations-declaration.schema.json#/definitions/GraphId"
989
+ },
990
+ "name": {
991
+ "type": "string",
992
+ "maxLength": 256
993
+ },
994
+ "description": {
995
+ "type": "string",
996
+ "maxLength": 16384
997
+ },
998
+ "webUrl": {
999
+ "type": "string",
1000
+ "format": "uri",
1001
+ "minLength": 1,
1002
+ "maxLength": 2048
1003
+ },
1004
+ "template": {
1005
+ "type": "string",
1006
+ "maxLength": 64
1007
+ }
1008
+ }
1009
+ },
1010
+ "SharePointListItem": {
1011
+ "type": "object",
1012
+ "additionalProperties": false,
1013
+ "required": [
1014
+ "id",
1015
+ "fields",
1016
+ "webUrl",
1017
+ "createdAt",
1018
+ "modifiedAt"
1019
+ ],
1020
+ "description": "One item of a sharepoint.item.list result (0.78.0): `fields` are its column values keyed by internal column name, as Graph returns them with the system columns (@odata.etag, ContentType, Attachments, Edit, LinkTitle*, ItemChildCount, FolderChildCount, _UIVersionString, _ComplianceFlags...) dropped and the whole object cut at 16 KiB; `id` is what sharepoint.item.update takes as `itemId`.",
1021
+ "properties": {
1022
+ "id": {
1023
+ "$ref": "app-integrations-declaration.schema.json#/definitions/GraphId"
1024
+ },
1025
+ "fields": {
1026
+ "type": "object",
1027
+ "additionalProperties": {},
1028
+ "description": "Column values as Graph returns them (strings, numbers, booleans, and lookup or person values as Graph shapes them)."
1029
+ },
1030
+ "webUrl": {
1031
+ "type": "string",
1032
+ "format": "uri",
1033
+ "minLength": 1,
1034
+ "maxLength": 2048
1035
+ },
1036
+ "createdAt": {
1037
+ "type": "string",
1038
+ "format": "date-time"
1039
+ },
1040
+ "modifiedAt": {
1041
+ "type": "string",
1042
+ "format": "date-time"
1043
+ }
1044
+ }
1045
+ },
1046
+ "SharePointDrive": {
1047
+ "type": "object",
1048
+ "additionalProperties": false,
1049
+ "required": [
1050
+ "id",
1051
+ "name",
1052
+ "webUrl"
1053
+ ],
1054
+ "description": "One document library of a sharepoint.drive.list result (0.78.0); `id` is what the sharepoint.file.* operations take as `driveId`.",
1055
+ "properties": {
1056
+ "id": {
1057
+ "$ref": "app-integrations-declaration.schema.json#/definitions/GraphId"
1058
+ },
1059
+ "name": {
1060
+ "type": "string",
1061
+ "maxLength": 256
1062
+ },
1063
+ "webUrl": {
1064
+ "type": "string",
1065
+ "format": "uri",
1066
+ "minLength": 1,
1067
+ "maxLength": 2048
1068
+ }
1069
+ }
1070
+ },
1071
+ "SharePointFile": {
1072
+ "type": "object",
1073
+ "additionalProperties": false,
1074
+ "required": [
1075
+ "id",
1076
+ "name",
1077
+ "isFolder",
1078
+ "size",
1079
+ "webUrl",
1080
+ "modifiedAt"
1081
+ ],
1082
+ "description": "One entry of a sharepoint.file.list or sharepoint.file.search result (0.78.0): a file (`mimeType` set, `id` is what sharepoint.file.read takes as `fileId`) or a folder (`isFolder`, `id` is what sharepoint.file.list and sharepoint.file.upload take as `folderId`). `size` is in bytes; a folder's is the total of its contents.",
1083
+ "properties": {
1084
+ "id": {
1085
+ "$ref": "app-integrations-declaration.schema.json#/definitions/GraphId"
1086
+ },
1087
+ "name": {
1088
+ "type": "string",
1089
+ "maxLength": 256
1090
+ },
1091
+ "isFolder": {
1092
+ "type": "boolean"
1093
+ },
1094
+ "size": {
1095
+ "type": "integer",
1096
+ "minimum": 0
1097
+ },
1098
+ "mimeType": {
1099
+ "type": "string",
1100
+ "minLength": 1,
1101
+ "maxLength": 128
1102
+ },
1103
+ "webUrl": {
1104
+ "type": "string",
1105
+ "format": "uri",
1106
+ "minLength": 1,
1107
+ "maxLength": 2048
1108
+ },
1109
+ "modifiedAt": {
1110
+ "type": "string",
1111
+ "format": "date-time"
1112
+ }
1113
+ }
855
1114
  }
856
1115
  },
857
1116
  "examples": [
@@ -1135,6 +1394,88 @@
1135
1394
  {
1136
1395
  "id": "1069479526",
1137
1396
  "completed": true
1397
+ },
1398
+ {
1399
+ "lists": [
1400
+ {
1401
+ "id": "c8f7a4d1-2b3e-4f5a-9c6d-7e8f9a0b1c2d",
1402
+ "name": "Facilities requests",
1403
+ "description": "Office and lease work.",
1404
+ "webUrl": "https://fourier.sharepoint.com/sites/intranet/Lists/Facilities%20requests",
1405
+ "template": "genericList"
1406
+ }
1407
+ ],
1408
+ "hasMore": false
1409
+ },
1410
+ {
1411
+ "items": [
1412
+ {
1413
+ "id": "42",
1414
+ "fields": {
1415
+ "Title": "Renew the office lease",
1416
+ "Status": "Open",
1417
+ "DueDate": "2026-10-31T00:00:00Z",
1418
+ "Priority": 2,
1419
+ "Urgent": false
1420
+ },
1421
+ "webUrl": "https://fourier.sharepoint.com/sites/intranet/Lists/Facilities%20requests/DispForm.aspx?ID=42",
1422
+ "createdAt": "2026-10-11T08:00:00Z",
1423
+ "modifiedAt": "2026-10-11T09:00:00Z"
1424
+ }
1425
+ ],
1426
+ "hasMore": true,
1427
+ "nextPageToken": "UGFnZWQ9VFJVRSZwX1NvcnRCZWhhdmlvcj0xJnBfSUQ9NDI"
1428
+ },
1429
+ {
1430
+ "drives": [
1431
+ {
1432
+ "id": "b!X1Y2Z3a4B5c6D7e8F9g0H1i2J3k4L5m6N7o8P9q0R1s2T3u4V5w6X7y8Z9a0B1c2",
1433
+ "name": "Documents",
1434
+ "webUrl": "https://fourier.sharepoint.com/sites/intranet/Shared%20Documents"
1435
+ }
1436
+ ],
1437
+ "hasMore": false
1438
+ },
1439
+ {
1440
+ "files": [
1441
+ {
1442
+ "id": "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36K",
1443
+ "name": "Reports",
1444
+ "isFolder": true,
1445
+ "size": 1048576,
1446
+ "webUrl": "https://fourier.sharepoint.com/sites/intranet/Shared%20Documents/Reports",
1447
+ "modifiedAt": "2026-10-10T17:00:00Z"
1448
+ },
1449
+ {
1450
+ "id": "01BYE5RZ5MYLM2SMX75ZBIPQZIHT6OAYPB",
1451
+ "name": "weekly-report.pdf",
1452
+ "isFolder": false,
1453
+ "size": 48213,
1454
+ "mimeType": "application/pdf",
1455
+ "webUrl": "https://fourier.sharepoint.com/sites/intranet/Shared%20Documents/Reports/weekly-report.pdf",
1456
+ "modifiedAt": "2026-10-11T08:00:00Z"
1457
+ }
1458
+ ],
1459
+ "hasMore": false
1460
+ },
1461
+ {
1462
+ "id": "01BYE5RZ5MYLM2SMX75ZBIPQZIHT6OAYPB",
1463
+ "filename": "weekly-report.pdf",
1464
+ "mimeType": "application/pdf",
1465
+ "size": 20,
1466
+ "dataBase64": "JVBERi0xLjQKJcOkw7zDtsOfCg=="
1467
+ },
1468
+ {
1469
+ "id": "43",
1470
+ "webUrl": "https://fourier.sharepoint.com/sites/intranet/Lists/Facilities%20requests/DispForm.aspx?ID=43"
1471
+ },
1472
+ {
1473
+ "id": "43"
1474
+ },
1475
+ {
1476
+ "id": "01BYE5RZ7KQJ2A3BUFOFD3GSPGOHDJD37L",
1477
+ "filename": "weekly-report 1.pdf",
1478
+ "webUrl": "https://fourier.sharepoint.com/sites/intranet/Shared%20Documents/Reports/weekly-report%201.pdf"
1138
1479
  }
1139
1480
  ]
1140
1481
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/saas-connection.schema.json",
4
4
  "title": "SaasConnection",
5
- "description": "Tenant-wide SaaS connection record (control table sk SAASCONN#<sourceId>, same tenant partition as database sources; alias uniqueness is enforced across both). One connection per tenant and provider: a single bot installation of the platform-owned provider app and a single logical channel map shared by every app environment (grants, requests and user consent links stay per environment). A Gmail connection has neither installation nor channels: user consent is the only step and its one resource is the fixed `inbox` alias. A Basecamp connection (0.76.0, ADR 0022) has no installation either: IT pins the company's Basecamp `account` (chosen from the accounts IT's own Basecamp sign-in belongs to) and maps project aliases to project ids in `channels`; basecamp.project.list runs on the fixed `account` alias. Contains references, never secrets: installation tokens live under their own token records. The pre-T25 per-environment shape (`clientId`, `clientSecretRef`, `environments`, `installations`) is deprecated: the control plane normalises such a stored record on read and never writes it.",
5
+ "description": "Tenant-wide SaaS connection record (control table sk SAASCONN#<sourceId>, same tenant partition as database sources; alias uniqueness is enforced across both). One connection per tenant and provider: a single bot installation of the platform-owned provider app and a single logical channel map shared by every app environment (grants, requests and user consent links stay per environment). A Gmail connection has neither installation nor channels: user consent is the only step and its one resource is the fixed `inbox` alias. A Basecamp connection (0.76.0, ADR 0022) has no installation either: IT pins the company's Basecamp `account` (chosen from the accounts IT's own Basecamp sign-in belongs to) and maps project aliases to project ids in `channels`; basecamp.project.list runs on the fixed `account` alias. A SharePoint connection (0.78.0, ADR 0024) is shaped the same way: IT pins the company's Microsoft 365 tenant in `account` (the Entra tenant id and the organisation's display name, from IT's own Microsoft sign-in) and maps site aliases to Microsoft Graph site ids (`hostname,siteCollectionGuid,webGuid`) in `channels`; every sharepoint.* operation runs on a site alias. Contains references, never secrets: installation tokens live under their own token records. The pre-T25 per-environment shape (`clientId`, `clientSecretRef`, `environments`, `installations`) is deprecated: the control plane normalises such a stored record on read and never writes it.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": ["schema", "tenantId", "sourceId", "kind", "provider", "displayName", "status", "version", "channels", "operations", "developmentPreapproval", "createdAt", "updatedAt"],
@@ -21,17 +21,17 @@
21
21
  "type": "object",
22
22
  "maxProperties": 64,
23
23
  "propertyNames": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationAlias" },
24
- "additionalProperties": { "type": "string", "pattern": "^[A-Za-z0-9]{1,32}$", "description": "A Slack channel id (public, non-archived) or a Basecamp project id (digits)." },
25
- "description": "Logical alias → provider resource id: a Slack channel id or a Basecamp project id. Always empty for Gmail."
24
+ "additionalProperties": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9.,_:-]{0,255}$", "description": "A Slack channel id (public, non-archived), a Basecamp project id (digits) or a SharePoint site id (0.78.0: the Microsoft Graph composite `hostname,siteCollectionGuid,webGuid`)." },
25
+ "description": "Logical alias → provider resource id: a Slack channel id, a Basecamp project id or a SharePoint site id. Always empty for Gmail."
26
26
  },
27
27
  "operations": {
28
28
  "type": "array",
29
29
  "minItems": 1,
30
- "maxItems": 22,
30
+ "maxItems": 31,
31
31
  "uniqueItems": true,
32
32
  "items": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationOperation" },
33
33
  "not": { "contains": { "const": "warehouse.view.read" } },
34
- "description": "Operations IT allows apps to request on this connection: the provider's own SaaS operations (Slack: slack.channel.history, slack.message.post, slack.message.update, slack.message.delete, slack.dm.send, slack.user.lookup; Gmail: gmail.thread.list, gmail.thread.read, gmail.message.read, gmail.message.send, gmail.attachment.read; Basecamp (0.76.0): basecamp.project.list, basecamp.todolist.list, basecamp.todo.list, basecamp.todo.create, basecamp.todo.complete, basecamp.message.list, basecamp.message.post, basecamp.comment.list, basecamp.comment.post, basecamp.campfire.post, basecamp.people.list). Never warehouse.view.read."
34
+ "description": "Operations IT allows apps to request on this connection: the provider's own SaaS operations (Slack: slack.channel.history, slack.message.post, slack.message.update, slack.message.delete, slack.dm.send, slack.user.lookup; Gmail: gmail.thread.list, gmail.thread.read, gmail.message.read, gmail.message.send, gmail.attachment.read; Basecamp (0.76.0): basecamp.project.list, basecamp.todolist.list, basecamp.todo.list, basecamp.todo.create, basecamp.todo.complete, basecamp.message.list, basecamp.message.post, basecamp.comment.list, basecamp.comment.post, basecamp.campfire.post, basecamp.people.list; SharePoint (0.78.0): sharepoint.list.list, sharepoint.item.list, sharepoint.item.create, sharepoint.item.update, sharepoint.drive.list, sharepoint.file.list, sharepoint.file.search, sharepoint.file.read, sharepoint.file.upload). Never warehouse.view.read."
35
35
  },
36
36
  "developmentPreapproval": { "$ref": "#/definitions/DevelopmentPreapproval" },
37
37
  "oauthClient": { "$ref": "#/definitions/SaasOauthClient" },
@@ -77,17 +77,17 @@
77
77
  "definitions": {
78
78
  "SaasProvider": {
79
79
  "type": "string",
80
- "enum": ["slack", "gmail", "basecamp"],
81
- "description": "Slack carries an installation and a channel map; Gmail is user-consent only; Basecamp is user-consent only with an IT-pinned account and a project map in `channels`."
80
+ "enum": ["slack", "gmail", "basecamp", "sharepoint"],
81
+ "description": "Slack carries an installation and a channel map; Gmail is user-consent only; Basecamp is user-consent only with an IT-pinned account and a project map in `channels`; SharePoint (0.78.0) is user-consent only, pinned to the company's Microsoft 365 tenant in `account`, with a site map in `channels`."
82
82
  },
83
83
  "BasecampAccount": {
84
84
  "type": "object",
85
85
  "additionalProperties": false,
86
86
  "required": ["id", "name"],
87
- "description": "The pinned Basecamp account (0.76.0, Basecamp only): the one account, among those IT's own Basecamp sign-in belongs to, that every user token on this connection must also belong to and that every project id in `channels` lives in. Absent until IT chooses it; governance refuses every basecamp.* call until then.",
87
+ "description": "The pinned provider account (0.76.0, Basecamp; 0.78.0, SharePoint — the name keeps its 0.76.0 spelling so consumers need not move): the one account, among those IT's own provider sign-in belongs to, that every user token on this connection must also belong to and that every resource id in `channels` lives in. For Basecamp the account (digits); for SharePoint the Microsoft 365 tenant (the Entra tenant GUID). Absent until IT chooses it; governance refuses every basecamp.* and sharepoint.* call until then.",
88
88
  "properties": {
89
- "id": { "type": "string", "pattern": "^[0-9]{1,20}$", "description": "The Basecamp account id, the first path segment of the API base URL." },
90
- "name": { "type": "string", "minLength": 1, "maxLength": 120, "description": "The account's name as Basecamp shows it, for IT to recognise." }
89
+ "id": { "type": "string", "pattern": "^[A-Za-z0-9-]{1,64}$", "description": "The Basecamp account id (digits, the first path segment of the API base URL) or the Microsoft Entra tenant id (a GUID, the `tid` claim)." },
90
+ "name": { "type": "string", "minLength": 1, "maxLength": 120, "description": "The account's name as the provider shows it (a Basecamp account name, a Microsoft 365 organisation display name), for IT to recognise." }
91
91
  }
92
92
  },
93
93
  "SaasOauthClient": {
@@ -265,6 +265,23 @@
265
265
  "userModeApproval": "automatic",
266
266
  "createdAt": "2026-10-11T08:00:00Z",
267
267
  "updatedAt": "2026-10-11T08:00:00Z"
268
+ },
269
+ {
270
+ "schema": "harbour.saas-connection/1.0",
271
+ "tenantId": "fourier",
272
+ "sourceId": "company-sharepoint",
273
+ "kind": "saas",
274
+ "provider": "sharepoint",
275
+ "displayName": "Company SharePoint",
276
+ "status": "ACTIVE",
277
+ "version": 2,
278
+ "account": { "id": "3f2c9b1e-7d4a-4c6b-9e8f-0a1b2c3d4e5f", "name": "Fourier Labs" },
279
+ "channels": { "intranet": "fourier.sharepoint.com,8a1c6f2e-3b4d-4e5f-9a0b-1c2d3e4f5a6b,0f1e2d3c-4b5a-4968-8776-655443322110" },
280
+ "operations": ["sharepoint.list.list", "sharepoint.item.list", "sharepoint.item.create", "sharepoint.item.update", "sharepoint.drive.list", "sharepoint.file.list", "sharepoint.file.search", "sharepoint.file.read", "sharepoint.file.upload"],
281
+ "developmentPreapproval": { "enabled": true, "hours": 24 },
282
+ "userModeApproval": "automatic",
283
+ "createdAt": "2026-10-11T08:00:00Z",
284
+ "updatedAt": "2026-10-11T08:00:00Z"
268
285
  }
269
286
  ]
270
287
  }