borgmcp-shared 0.13.1 → 1.0.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 +4 -3
- package/RELEASES.md +7 -0
- package/dist/conformance/adapter.d.ts +15 -2
- package/dist/conformance/adapter.d.ts.map +1 -1
- package/dist/conformance/adapter.js +187 -41
- package/dist/conformance/adapter.js.map +1 -1
- package/dist/conformance/index.d.ts +49 -3
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +95 -18
- package/dist/conformance/index.js.map +1 -1
- package/dist/protocol/contract.d.ts +1 -1
- package/dist/protocol/contract.d.ts.map +1 -1
- package/dist/protocol/contract.js +13 -18
- package/dist/protocol/contract.js.map +1 -1
- package/dist/protocol/coordination.d.ts +36 -1
- package/dist/protocol/coordination.d.ts.map +1 -1
- package/dist/protocol/coordination.js +72 -6
- package/dist/protocol/coordination.js.map +1 -1
- package/dist/protocol/errors.d.ts +1 -1
- package/dist/protocol/errors.d.ts.map +1 -1
- package/dist/protocol/errors.js +1 -1
- package/dist/protocol/errors.js.map +1 -1
- package/dist/protocol/types.d.ts +1 -5
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/protocol/version.d.ts +1 -1
- package/dist/protocol/version.js +1 -1
- package/dist/templates.d.ts +1 -3
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +8 -79
- package/dist/templates.js.map +1 -1
- package/docs/compatibility.md +15 -0
- package/docs/release-records.json +28 -0
- package/docs/releases/0.14.0.md +8 -0
- package/docs/releases/1.0.0.md +11 -0
- package/docs/template-lifecycle.md +1 -1
- package/package.json +1 -1
- package/src/conformance/adapter.ts +369 -50
- package/src/conformance/index.ts +122 -22
- package/src/protocol/contract.ts +15 -21
- package/src/protocol/coordination.ts +136 -7
- package/src/protocol/errors.ts +1 -1
- package/src/protocol/types.ts +1 -5
- package/src/protocol/version.ts +2 -2
- package/src/templates.ts +8 -81
package/src/conformance/index.ts
CHANGED
|
@@ -100,8 +100,6 @@ const APPEND_LOG_ENTRY: EnrichedStreamEntry = {
|
|
|
100
100
|
const APPEND_LOG_ROUTING = {
|
|
101
101
|
class: 'review',
|
|
102
102
|
recipients: ['one-of-one-reviewer'],
|
|
103
|
-
fellOpen: false,
|
|
104
|
-
message: 'Routing applied.',
|
|
105
103
|
} as const;
|
|
106
104
|
|
|
107
105
|
export interface AppendLogResultConformanceVector {
|
|
@@ -119,31 +117,71 @@ export interface AppendLogRequestConformanceVector {
|
|
|
119
117
|
export interface AppendLogIdempotencyConformanceVector {
|
|
120
118
|
name: string;
|
|
121
119
|
actor: 'same' | 'different';
|
|
122
|
-
mutation: 'none' | 'message' | '
|
|
120
|
+
mutation: 'none' | 'message' | 'audience' | 'recipient_set' | 'classification';
|
|
123
121
|
expected: 'deduplicated' | 'POST_ID_CONFLICT' | 'created';
|
|
124
122
|
}
|
|
125
123
|
|
|
126
|
-
/** Author-scoped post identity and exact resolved-
|
|
124
|
+
/** Author-scoped post identity and exact resolved-delivery retry outcomes. */
|
|
127
125
|
export const APPEND_LOG_IDEMPOTENCY_CONFORMANCE: readonly AppendLogIdempotencyConformanceVector[] = [
|
|
128
126
|
{ name: 'deduplicates an exact same-author retry', actor: 'same', mutation: 'none', expected: 'deduplicated' },
|
|
129
127
|
{ name: 'rejects a same-author message collision', actor: 'same', mutation: 'message', expected: 'POST_ID_CONFLICT' },
|
|
130
|
-
{ name: 'rejects a same-author
|
|
128
|
+
{ name: 'rejects a same-author audience collision', actor: 'same', mutation: 'audience', expected: 'POST_ID_CONFLICT' },
|
|
131
129
|
{ name: 'rejects a same-author recipient-set collision', actor: 'same', mutation: 'recipient_set', expected: 'POST_ID_CONFLICT' },
|
|
132
|
-
{ name: 'deduplicates
|
|
133
|
-
{ name: 'rejects a same-author resolved class-routing collision', actor: 'same', mutation: 'resolved_class_routing', expected: 'POST_ID_CONFLICT' },
|
|
130
|
+
{ name: 'deduplicates changed classification with identical explicit delivery', actor: 'same', mutation: 'classification', expected: 'deduplicated' },
|
|
134
131
|
{ name: 'creates an independent cross-author post', actor: 'different', mutation: 'none', expected: 'created' },
|
|
135
132
|
];
|
|
136
133
|
|
|
137
134
|
const APPEND_LOG_REQUEST = {
|
|
138
135
|
post_id: '00000000-0000-4000-8000-000000000205',
|
|
139
136
|
message: 'REVIEW-READY: example',
|
|
137
|
+
to: 'broadcast',
|
|
140
138
|
} satisfies AppendLogRequest;
|
|
141
139
|
|
|
142
140
|
export const APPEND_LOG_REQUEST_CONFORMANCE: readonly AppendLogRequestConformanceVector[] = [
|
|
143
141
|
{ name: 'accepts a canonical post UUID', request: APPEND_LOG_REQUEST, accepts: true },
|
|
144
|
-
{ name: 'rejects an omitted post UUID', request: { message: APPEND_LOG_REQUEST.message }, accepts: false },
|
|
142
|
+
{ name: 'rejects an omitted post UUID', request: { message: APPEND_LOG_REQUEST.message, to: 'broadcast' }, accepts: false },
|
|
145
143
|
{ name: 'rejects an invalid post UUID', request: { ...APPEND_LOG_REQUEST, post_id: 'not-a-uuid' }, accepts: false },
|
|
146
144
|
{ name: 'rejects unknown request fields', request: { ...APPEND_LOG_REQUEST, extra: true }, accepts: false },
|
|
145
|
+
{
|
|
146
|
+
name: 'rejects omitted addressing',
|
|
147
|
+
request: { post_id: APPEND_LOG_REQUEST.post_id, message: APPEND_LOG_REQUEST.message },
|
|
148
|
+
accepts: false,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'accepts explicit broadcast addressing',
|
|
152
|
+
request: APPEND_LOG_REQUEST,
|
|
153
|
+
accepts: true,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'accepts a non-empty recipient selector union branch',
|
|
157
|
+
request: { ...APPEND_LOG_REQUEST, to: ['Builder', 'id:3336cde1'] },
|
|
158
|
+
accepts: true,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'rejects null addressing',
|
|
162
|
+
request: { ...APPEND_LOG_REQUEST, to: null },
|
|
163
|
+
accepts: false,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'rejects an empty recipient selector array',
|
|
167
|
+
request: { ...APPEND_LOG_REQUEST, to: [] },
|
|
168
|
+
accepts: false,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'rejects an arbitrary scalar audience',
|
|
172
|
+
request: { ...APPEND_LOG_REQUEST, to: 'all' },
|
|
173
|
+
accepts: false,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'rejects retired visibility without falling open',
|
|
177
|
+
request: { ...APPEND_LOG_REQUEST, visibility: 'broadcast' },
|
|
178
|
+
accepts: false,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'rejects retired recipient ids without falling open',
|
|
182
|
+
request: { ...APPEND_LOG_REQUEST, recipientDroneIds: ['00000000-0000-4000-8000-000000000204'] },
|
|
183
|
+
accepts: false,
|
|
184
|
+
},
|
|
147
185
|
];
|
|
148
186
|
|
|
149
187
|
/** Runtime response vectors keep the append-log decoder aligned with its public type. */
|
|
@@ -214,13 +252,8 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
|
|
|
214
252
|
accepts: false,
|
|
215
253
|
},
|
|
216
254
|
{
|
|
217
|
-
name: 'rejects
|
|
218
|
-
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, fellOpen:
|
|
219
|
-
accepts: false,
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
name: 'rejects an invalid routing message',
|
|
223
|
-
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, message: 7 } },
|
|
255
|
+
name: 'rejects legacy fall-open routing metadata',
|
|
256
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, fellOpen: false } },
|
|
224
257
|
accepts: false,
|
|
225
258
|
},
|
|
226
259
|
{
|
|
@@ -883,7 +916,6 @@ export interface RoleDeleteConformanceVector {
|
|
|
883
916
|
| 'default'
|
|
884
917
|
| 'mandatory'
|
|
885
918
|
| 'human-seat'
|
|
886
|
-
| 'taxonomy-reference'
|
|
887
919
|
| 'unknown';
|
|
888
920
|
expected:
|
|
889
921
|
| {
|
|
@@ -895,7 +927,7 @@ export interface RoleDeleteConformanceVector {
|
|
|
895
927
|
}
|
|
896
928
|
| {
|
|
897
929
|
status: 409;
|
|
898
|
-
error: 'ROLE_IN_USE' | 'DEFAULT_ROLE_REQUIRED' | 'ROLE_REQUIRED'
|
|
930
|
+
error: 'ROLE_IN_USE' | 'DEFAULT_ROLE_REQUIRED' | 'ROLE_REQUIRED';
|
|
899
931
|
mutation: 'none';
|
|
900
932
|
message?: typeof ROLE_IN_USE_DELETE_MESSAGE;
|
|
901
933
|
}
|
|
@@ -945,11 +977,6 @@ export const ROLE_DELETE_CONFORMANCE: readonly RoleDeleteConformanceVector[] = [
|
|
|
945
977
|
fixture: 'human-seat',
|
|
946
978
|
expected: { status: 409, error: 'ROLE_REQUIRED', mutation: 'none' },
|
|
947
979
|
},
|
|
948
|
-
{
|
|
949
|
-
name: 'refuses a taxonomy default-recipient role',
|
|
950
|
-
fixture: 'taxonomy-reference',
|
|
951
|
-
expected: { status: 409, error: 'ROLE_REFERENCED', mutation: 'none' },
|
|
952
|
-
},
|
|
953
980
|
{
|
|
954
981
|
name: 'hides an unknown or inaccessible role',
|
|
955
982
|
fixture: 'unknown',
|
|
@@ -1012,6 +1039,79 @@ export const ROLE_RATIONALE_CONFORMANCE: readonly RoleRationaleConformanceVector
|
|
|
1012
1039
|
},
|
|
1013
1040
|
];
|
|
1014
1041
|
|
|
1042
|
+
export interface AckStatusConformanceVector {
|
|
1043
|
+
name: string;
|
|
1044
|
+
fixture: 'missing-ack' | 'ack-and-claim' | 'unread-preserved' | 'unknown-entry';
|
|
1045
|
+
expected:
|
|
1046
|
+
| { status: 200; acknowledged_at: null; mutation: 'none' }
|
|
1047
|
+
| { status: 200; acknowledgements: 1; claims: 1; distinct: true; mutation: 'none' }
|
|
1048
|
+
| { status: 200; unread_entry_available: true; mutation: 'none' }
|
|
1049
|
+
| { status: 404; error: 'NOT_FOUND'; mutation: 'none' };
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/** Portable acknowledgement-status outcomes for one exact log entry. */
|
|
1053
|
+
export const ACK_STATUS_CONFORMANCE: readonly AckStatusConformanceVector[] = [
|
|
1054
|
+
{
|
|
1055
|
+
name: 'reports an intended recipient with no acknowledgement',
|
|
1056
|
+
fixture: 'missing-ack',
|
|
1057
|
+
expected: { status: 200, acknowledged_at: null, mutation: 'none' },
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
name: 'keeps acknowledgements and claims in separate result collections',
|
|
1061
|
+
fixture: 'ack-and-claim',
|
|
1062
|
+
expected: { status: 200, acknowledgements: 1, claims: 1, distinct: true, mutation: 'none' },
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
name: 'does not consume the queried entry from unread delivery',
|
|
1066
|
+
fixture: 'unread-preserved',
|
|
1067
|
+
expected: { status: 200, unread_entry_available: true, mutation: 'none' },
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
name: 'returns not found for an unknown entry instead of missing acknowledgement state',
|
|
1071
|
+
fixture: 'unknown-entry',
|
|
1072
|
+
expected: { status: 404, error: 'NOT_FOUND', mutation: 'none' },
|
|
1073
|
+
},
|
|
1074
|
+
];
|
|
1075
|
+
|
|
1076
|
+
export interface EntryQueryConformanceVector {
|
|
1077
|
+
name: string;
|
|
1078
|
+
fixture: 'full-uuid' | 'unique-prefix' | 'unknown' | 'ambiguous-prefix' | 'read-only';
|
|
1079
|
+
expected:
|
|
1080
|
+
| { status: 200; exact_entry: true; mutation: 'none' }
|
|
1081
|
+
| { status: 404; error: 'NOT_FOUND'; mutation: 'none' }
|
|
1082
|
+
| { status: 409; error: 'LOG_ENTRY_PREFIX_AMBIGUOUS'; mutation: 'none' }
|
|
1083
|
+
| { status: 200; unread_entry_available: true; mutation: 'none' };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/** Portable exact-entry lookup outcomes for canonical UUID and short-prefix selectors. */
|
|
1087
|
+
export const ENTRY_QUERY_CONFORMANCE: readonly EntryQueryConformanceVector[] = [
|
|
1088
|
+
{
|
|
1089
|
+
name: 'resolves one canonical full UUID',
|
|
1090
|
+
fixture: 'full-uuid',
|
|
1091
|
+
expected: { status: 200, exact_entry: true, mutation: 'none' },
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
name: 'resolves one unique eight-hex prefix',
|
|
1095
|
+
fixture: 'unique-prefix',
|
|
1096
|
+
expected: { status: 200, exact_entry: true, mutation: 'none' },
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
name: 'hides an unknown or inaccessible selector',
|
|
1100
|
+
fixture: 'unknown',
|
|
1101
|
+
expected: { status: 404, error: 'NOT_FOUND', mutation: 'none' },
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
name: 'refuses an ambiguous eight-hex prefix',
|
|
1105
|
+
fixture: 'ambiguous-prefix',
|
|
1106
|
+
expected: { status: 409, error: 'LOG_ENTRY_PREFIX_AMBIGUOUS', mutation: 'none' },
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
name: 'does not advance unread or mutate acknowledgement, claim, log, or authority state',
|
|
1110
|
+
fixture: 'read-only',
|
|
1111
|
+
expected: { status: 200, unread_entry_available: true, mutation: 'none' },
|
|
1112
|
+
},
|
|
1113
|
+
];
|
|
1114
|
+
|
|
1015
1115
|
/** One canonical corpus consumed unchanged by shared, server, and client tests. */
|
|
1016
1116
|
export const RUNTIME_METADATA_REPOSITORY_CONFORMANCE:
|
|
1017
1117
|
readonly RuntimeMetadataRepositoryConformanceVector[] = [
|
package/src/protocol/contract.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
} from './types.js';
|
|
14
14
|
|
|
15
15
|
export const SHARED_PACKAGE_NAME = 'borgmcp-shared' as const;
|
|
16
|
-
export const SHARED_PACKAGE_VERSION = '0.
|
|
16
|
+
export const SHARED_PACKAGE_VERSION = '1.0.0' as const;
|
|
17
17
|
/** Maximum UTF-8 payload for each newly recorded decision text field. */
|
|
18
18
|
export const DECISION_TEXT_MAX_BYTES = 512 as const;
|
|
19
19
|
/** Maximum UTF-8 size of role detailed-description text and any returned section slice. */
|
|
@@ -590,7 +590,7 @@ export function decodeProtocolTagPreflight(value: unknown): ProtocolTagPreflight
|
|
|
590
590
|
exactKeys(input, ['protocol_version'], ['protocol_version']);
|
|
591
591
|
if (input.protocol_version !== PROTOCOL_VERSION) {
|
|
592
592
|
throw new ProtocolContractError(
|
|
593
|
-
'This client requires protocol
|
|
593
|
+
'This client requires protocol v12. The peer presents a different version. Update `borgmcp-server` and `borgmcp` to matching releases — server first, then client.',
|
|
594
594
|
ErrorCode.UNSUPPORTED_PROTOCOL_VERSION,
|
|
595
595
|
['protocol_version'],
|
|
596
596
|
);
|
|
@@ -1007,33 +1007,17 @@ export function decodeAppendLogRequest(value: unknown): import('./types.js').App
|
|
|
1007
1007
|
const input = record(value);
|
|
1008
1008
|
exactKeys(
|
|
1009
1009
|
input,
|
|
1010
|
-
['post_id', 'message', '
|
|
1011
|
-
['post_id', 'message'],
|
|
1010
|
+
['post_id', 'message', 'to', 'class', 'documents'],
|
|
1011
|
+
['post_id', 'message', 'to'],
|
|
1012
1012
|
);
|
|
1013
1013
|
const output: import('./types.js').AppendLogRequest = {
|
|
1014
1014
|
post_id: decodeUuid(input.post_id, ['post_id']),
|
|
1015
1015
|
message: boundedString(input.message, 1, PROTOCOL_LIMIT_CEILINGS.max_log_message_bytes, ['message']),
|
|
1016
|
+
to: input.to === 'broadcast' ? 'broadcast' : decodeRecipientSelectors(input.to),
|
|
1016
1017
|
};
|
|
1017
|
-
if (input.visibility !== undefined) {
|
|
1018
|
-
if (input.visibility !== 'broadcast' && input.visibility !== 'direct') {
|
|
1019
|
-
fail('Invalid log visibility.', ['visibility']);
|
|
1020
|
-
}
|
|
1021
|
-
output.visibility = input.visibility;
|
|
1022
|
-
}
|
|
1023
|
-
if (input.recipientDroneIds !== undefined) {
|
|
1024
|
-
if (!Array.isArray(input.recipientDroneIds) || input.recipientDroneIds.length === 0 || input.recipientDroneIds.length > 100) {
|
|
1025
|
-
fail('Expected recipientDroneIds to contain 1-100 UUIDs.', ['recipientDroneIds']);
|
|
1026
|
-
}
|
|
1027
|
-
output.recipientDroneIds = input.recipientDroneIds.map((id, index) =>
|
|
1028
|
-
decodeUuid(id, ['recipientDroneIds', index])
|
|
1029
|
-
);
|
|
1030
|
-
}
|
|
1031
1018
|
if (input.class !== undefined) {
|
|
1032
1019
|
output.class = boundedString(input.class, 1, 64, ['class']);
|
|
1033
1020
|
}
|
|
1034
|
-
if (input.to !== undefined) {
|
|
1035
|
-
output.to = decodeStringArray(input.to, 'to', 100, 120);
|
|
1036
|
-
}
|
|
1037
1021
|
if (input.documents !== undefined) {
|
|
1038
1022
|
output.documents = decodeStringArray(input.documents, 'documents', 100, 128)
|
|
1039
1023
|
.map((id, index) => decodeOpaqueIdentifier(id, ['documents', index]));
|
|
@@ -1041,6 +1025,16 @@ export function decodeAppendLogRequest(value: unknown): import('./types.js').App
|
|
|
1041
1025
|
return output;
|
|
1042
1026
|
}
|
|
1043
1027
|
|
|
1028
|
+
function decodeRecipientSelectors(value: unknown): string[] {
|
|
1029
|
+
const selectors = decodeStringArray(value, 'to', 100, 120);
|
|
1030
|
+
for (const [index, selector] of selectors.entries()) {
|
|
1031
|
+
if (selector !== selector.trim() || /[\u0000-\u001f\u007f-\u009f]/.test(selector)) {
|
|
1032
|
+
fail('Recipient selectors must be trimmed and control-free.', ['to', index]);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return selectors;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1044
1038
|
function decodeStringArray(value: unknown, field: string, maxItems: number, maxLength: number): string[] {
|
|
1045
1039
|
if (!Array.isArray(value) || value.length === 0 || value.length > maxItems) {
|
|
1046
1040
|
fail(`Expected ${field} to contain 1-${maxItems} values.`, [field]);
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
AppendLogResponse,
|
|
17
17
|
Decision,
|
|
18
18
|
EnrichedStreamEntry,
|
|
19
|
+
LogVisibility,
|
|
19
20
|
RoutingEcho,
|
|
20
21
|
} from './types.js';
|
|
21
22
|
import { isLabelLine, parseRoleSections } from '../role-section.js';
|
|
@@ -46,6 +47,39 @@ export interface ReadLogResult {
|
|
|
46
47
|
claims: ClaimRecord[];
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
export interface EntryQueryRequest {
|
|
51
|
+
entry_id: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface EntryQueryResult {
|
|
55
|
+
entry: EnrichedStreamEntry;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AckStatusRequest {
|
|
59
|
+
entry_id: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AckStatusRecipient {
|
|
63
|
+
drone_id: string;
|
|
64
|
+
drone_label: string | null;
|
|
65
|
+
drone_role: string | null;
|
|
66
|
+
acknowledged_at: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface AckStatusClaim {
|
|
70
|
+
drone_id: string;
|
|
71
|
+
drone_label: string | null;
|
|
72
|
+
drone_role: string | null;
|
|
73
|
+
claimed_at: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface AckStatusResult {
|
|
77
|
+
entry_id: string;
|
|
78
|
+
visibility: LogVisibility;
|
|
79
|
+
recipients: AckStatusRecipient[];
|
|
80
|
+
claims: AckStatusClaim[];
|
|
81
|
+
}
|
|
82
|
+
|
|
49
83
|
export interface ReassignDroneRequest {
|
|
50
84
|
role_id: string;
|
|
51
85
|
}
|
|
@@ -309,6 +343,106 @@ export function decodeReadLogRequestEnvelope(
|
|
|
309
343
|
return decodeProtocolEnvelope(value, decodeReadLogRequest);
|
|
310
344
|
}
|
|
311
345
|
|
|
346
|
+
export function decodeEntryQueryRequest(value: unknown): EntryQueryRequest {
|
|
347
|
+
const input = object(value);
|
|
348
|
+
exact(input, ['entry_id'], ['entry_id']);
|
|
349
|
+
const entryId = boundedString(input.entry_id, 'entry_id', 36);
|
|
350
|
+
if (/^[0-9a-f]{8}$/i.test(entryId)) return { entry_id: entryId.toLowerCase() };
|
|
351
|
+
return { entry_id: decodeUuid(entryId, ['entry_id']) };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function decodeEntryQueryRequestEnvelope(
|
|
355
|
+
value: unknown,
|
|
356
|
+
): ProtocolEnvelope<EntryQueryRequest> {
|
|
357
|
+
return decodeProtocolEnvelope(value, decodeEntryQueryRequest);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function decodeEntryQueryResult(value: unknown): EntryQueryResult {
|
|
361
|
+
const input = object(value);
|
|
362
|
+
exact(input, ['entry'], ['entry']);
|
|
363
|
+
return { entry: decodeEnrichedStreamEntry(input.entry) };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function decodeEntryQueryResultEnvelope(
|
|
367
|
+
value: unknown,
|
|
368
|
+
): ProtocolEnvelope<EntryQueryResult> {
|
|
369
|
+
return decodeProtocolEnvelope(value, decodeEntryQueryResult);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function decodeAckStatusRequest(value: unknown): AckStatusRequest {
|
|
373
|
+
const input = object(value);
|
|
374
|
+
exact(input, ['entry_id'], ['entry_id']);
|
|
375
|
+
return { entry_id: decodeUuid(input.entry_id, ['entry_id']) };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function decodeAckStatusRequestEnvelope(
|
|
379
|
+
value: unknown,
|
|
380
|
+
): ProtocolEnvelope<AckStatusRequest> {
|
|
381
|
+
return decodeProtocolEnvelope(value, decodeAckStatusRequest);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function decodeAckStatusRecipient(value: unknown): AckStatusRecipient {
|
|
385
|
+
const input = object(value);
|
|
386
|
+
exact(
|
|
387
|
+
input,
|
|
388
|
+
['drone_id', 'drone_label', 'drone_role', 'acknowledged_at'],
|
|
389
|
+
['drone_id', 'drone_label', 'drone_role', 'acknowledged_at'],
|
|
390
|
+
);
|
|
391
|
+
return {
|
|
392
|
+
drone_id: decodeUuid(input.drone_id, ['drone_id']),
|
|
393
|
+
drone_label: nullableString(input.drone_label, 'drone_label', 120),
|
|
394
|
+
drone_role: nullableString(input.drone_role, 'drone_role', 120),
|
|
395
|
+
acknowledged_at: input.acknowledged_at === null
|
|
396
|
+
? null
|
|
397
|
+
: decodeCanonicalTimestamp(input.acknowledged_at, ['acknowledged_at']),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function decodeAckStatusClaim(value: unknown): AckStatusClaim {
|
|
402
|
+
const input = object(value);
|
|
403
|
+
exact(
|
|
404
|
+
input,
|
|
405
|
+
['drone_id', 'drone_label', 'drone_role', 'claimed_at'],
|
|
406
|
+
['drone_id', 'drone_label', 'drone_role', 'claimed_at'],
|
|
407
|
+
);
|
|
408
|
+
return {
|
|
409
|
+
drone_id: decodeUuid(input.drone_id, ['drone_id']),
|
|
410
|
+
drone_label: nullableString(input.drone_label, 'drone_label', 120),
|
|
411
|
+
drone_role: nullableString(input.drone_role, 'drone_role', 120),
|
|
412
|
+
claimed_at: decodeCanonicalTimestamp(input.claimed_at, ['claimed_at']),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function decodeAckStatusResult(value: unknown): AckStatusResult {
|
|
417
|
+
const input = object(value);
|
|
418
|
+
exact(
|
|
419
|
+
input,
|
|
420
|
+
['entry_id', 'visibility', 'recipients', 'claims'],
|
|
421
|
+
['entry_id', 'visibility', 'recipients', 'claims'],
|
|
422
|
+
);
|
|
423
|
+
if (input.visibility !== 'broadcast' && input.visibility !== 'direct') {
|
|
424
|
+
throw new ProtocolContractError('Invalid acknowledgement-status visibility.');
|
|
425
|
+
}
|
|
426
|
+
if (!Array.isArray(input.recipients) || input.recipients.length > 500) {
|
|
427
|
+
throw new ProtocolContractError('Invalid acknowledgement-status recipients.');
|
|
428
|
+
}
|
|
429
|
+
if (!Array.isArray(input.claims) || input.claims.length > 500) {
|
|
430
|
+
throw new ProtocolContractError('Invalid acknowledgement-status claims.');
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
entry_id: decodeUuid(input.entry_id, ['entry_id']),
|
|
434
|
+
visibility: input.visibility,
|
|
435
|
+
recipients: input.recipients.map(decodeAckStatusRecipient),
|
|
436
|
+
claims: input.claims.map(decodeAckStatusClaim),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function decodeAckStatusResultEnvelope(
|
|
441
|
+
value: unknown,
|
|
442
|
+
): ProtocolEnvelope<AckStatusResult> {
|
|
443
|
+
return decodeProtocolEnvelope(value, decodeAckStatusResult);
|
|
444
|
+
}
|
|
445
|
+
|
|
312
446
|
function decodeClaimRecord(value: unknown): ClaimRecord {
|
|
313
447
|
const input = object(value);
|
|
314
448
|
exact(
|
|
@@ -345,22 +479,17 @@ function decodeRoutingEcho(value: unknown): RoutingEcho {
|
|
|
345
479
|
const input = object(value);
|
|
346
480
|
exact(
|
|
347
481
|
input,
|
|
348
|
-
['class', 'recipients'
|
|
349
|
-
['class', 'recipients'
|
|
482
|
+
['class', 'recipients'],
|
|
483
|
+
['class', 'recipients'],
|
|
350
484
|
);
|
|
351
485
|
if (!Array.isArray(input.recipients) || input.recipients.length > 100) {
|
|
352
486
|
throw new ProtocolContractError('Invalid routing recipient list.');
|
|
353
487
|
}
|
|
354
|
-
if (typeof input.fellOpen !== 'boolean') {
|
|
355
|
-
throw new ProtocolContractError('Invalid routing fell-open flag.');
|
|
356
|
-
}
|
|
357
488
|
return {
|
|
358
489
|
class: nullableString(input.class, 'routing.class', 64),
|
|
359
490
|
recipients: input.recipients.map((recipient) =>
|
|
360
491
|
boundedString(recipient, 'routing.recipients', 120)
|
|
361
492
|
),
|
|
362
|
-
fellOpen: input.fellOpen,
|
|
363
|
-
message: nullableString(input.message, 'routing.message', 512),
|
|
364
493
|
};
|
|
365
494
|
}
|
|
366
495
|
|
package/src/protocol/errors.ts
CHANGED
|
@@ -15,10 +15,10 @@ export enum ErrorCode {
|
|
|
15
15
|
REPOSITORY_ALREADY_ASSOCIATED = 'REPOSITORY_ALREADY_ASSOCIATED',
|
|
16
16
|
CUBE_ALREADY_ASSOCIATED = 'CUBE_ALREADY_ASSOCIATED',
|
|
17
17
|
POST_ID_CONFLICT = 'POST_ID_CONFLICT',
|
|
18
|
+
LOG_ENTRY_PREFIX_AMBIGUOUS = 'LOG_ENTRY_PREFIX_AMBIGUOUS',
|
|
18
19
|
ROLE_IN_USE = 'ROLE_IN_USE',
|
|
19
20
|
DEFAULT_ROLE_REQUIRED = 'DEFAULT_ROLE_REQUIRED',
|
|
20
21
|
ROLE_REQUIRED = 'ROLE_REQUIRED',
|
|
21
|
-
ROLE_REFERENCED = 'ROLE_REFERENCED',
|
|
22
22
|
ROLE_NOT_FOUND = 'ROLE_NOT_FOUND',
|
|
23
23
|
ROLE_SECTION_NOT_FOUND = 'ROLE_SECTION_NOT_FOUND',
|
|
24
24
|
ROLE_HAS_FROZEN_DRONES = 'ROLE_HAS_FROZEN_DRONES',
|
package/src/protocol/types.ts
CHANGED
|
@@ -168,17 +168,13 @@ export interface ReadLogResponse {
|
|
|
168
168
|
export interface RoutingEcho {
|
|
169
169
|
class: string | null;
|
|
170
170
|
recipients: string[];
|
|
171
|
-
fellOpen: boolean;
|
|
172
|
-
message: string | null;
|
|
173
171
|
}
|
|
174
172
|
|
|
175
173
|
export interface AppendLogRequest {
|
|
176
174
|
post_id: string;
|
|
177
175
|
message: string;
|
|
178
|
-
|
|
179
|
-
recipientDroneIds?: string[];
|
|
176
|
+
to: 'broadcast' | string[];
|
|
180
177
|
class?: string;
|
|
181
|
-
to?: string[];
|
|
182
178
|
documents?: string[];
|
|
183
179
|
}
|
|
184
180
|
|
package/src/protocol/version.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Current Borg coordination protocol generation. Clean-slate
|
|
2
|
-
export const PROTOCOL_VERSION = '
|
|
1
|
+
/** Current Borg coordination protocol generation. Clean-slate v12. */
|
|
2
|
+
export const PROTOCOL_VERSION = '12' as const;
|
|
3
3
|
|
|
4
4
|
export type ProtocolVersion = typeof PROTOCOL_VERSION;
|