Skip to content

Commit a60c036

Browse files
feat(cat-voices): expose collaborators in metadata (#3750)
* Add collaborators to DocumentDataMetadata and add mapper * update DTO * fix: failing tests * fix: spelling
1 parent bd072eb commit a60c036

File tree

6 files changed

+464
-174
lines changed

6 files changed

+464
-174
lines changed

catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/data/document_data_metadata.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,16 @@ final class DocumentDataMetadata extends Equatable {
2727
/// A reference to a section of a document.
2828
final String? section;
2929

30+
/// A list of allowed Collaborators on the next subsequent version of a document.
31+
final List<CatalystId>? collaborators;
32+
3033
/// A list of referenced parameters like brand, category or campaign.
3134
final DocumentParameters parameters;
3235

33-
/// List of authors represented by CatalystId
36+
/// List of authors represented by CatalystId.
37+
///
38+
/// Note. This list just represents who signed this version
39+
/// Note. Can change from version to version when [collaborators] are non empty.
3440
final List<CatalystId>? authors;
3541

3642
/// The default constructor for the [DocumentDataMetadata].
@@ -42,6 +48,7 @@ final class DocumentDataMetadata extends Equatable {
4248
this.template,
4349
this.reply,
4450
this.section,
51+
this.collaborators,
4552
this.parameters = const DocumentParameters(),
4653
this.authors,
4754
}) : assert(
@@ -80,6 +87,7 @@ final class DocumentDataMetadata extends Equatable {
8087
required SignedDocumentRef template,
8188
required DocumentParameters parameters,
8289
required List<CatalystId> authors,
90+
List<CatalystId>? collaborators,
8391
}) {
8492
return DocumentDataMetadata(
8593
type: DocumentType.proposalDocument,
@@ -88,6 +96,7 @@ final class DocumentDataMetadata extends Equatable {
8896
template: template,
8997
parameters: parameters,
9098
authors: authors,
99+
collaborators: collaborators,
91100
);
92101
}
93102

@@ -134,10 +143,14 @@ final class DocumentDataMetadata extends Equatable {
134143
template,
135144
reply,
136145
section,
146+
collaborators,
137147
parameters,
138148
authors,
139149
];
140150

151+
/// Who signed this document version. Can change from version to version.
152+
List<CatalystId>? get signers => authors;
153+
141154
String get version => selfRef.version!;
142155

143156
DocumentDataMetadata copyWith({
@@ -148,6 +161,7 @@ final class DocumentDataMetadata extends Equatable {
148161
Optional<SignedDocumentRef>? template,
149162
Optional<SignedDocumentRef>? reply,
150163
Optional<String>? section,
164+
Optional<List<CatalystId>>? collaborators,
151165
DocumentParameters? parameters,
152166
Optional<List<CatalystId>>? authors,
153167
}) {
@@ -159,6 +173,7 @@ final class DocumentDataMetadata extends Equatable {
159173
template: template.dataOr(this.template),
160174
reply: reply.dataOr(this.reply),
161175
section: section.dataOr(this.section),
176+
collaborators: collaborators.dataOr(this.collaborators),
162177
parameters: parameters ?? this.parameters,
163178
authors: authors.dataOr(this.authors),
164179
);

catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/document_data_dto.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ final class DocumentDataMetadataDto {
9292
final DocumentRefDto? template;
9393
final DocumentRefDto? reply;
9494
final String? section;
95+
final List<String>? collaborators;
9596
final List<DocumentRefDto> parameters;
9697
final List<String>? authors;
9798

@@ -103,6 +104,7 @@ final class DocumentDataMetadataDto {
103104
this.template,
104105
this.reply,
105106
this.section,
107+
this.collaborators,
106108
this.parameters = const [],
107109
this.authors,
108110
});
@@ -125,6 +127,7 @@ final class DocumentDataMetadataDto {
125127
template: data.template?.toDto(),
126128
reply: data.reply?.toDto(),
127129
section: data.section,
130+
collaborators: data.collaborators?.map((e) => e.toString()).toList(),
128131
parameters: data.parameters.set.map((e) => e.toDto()).toList(),
129132
authors: data.authors?.map((e) => e.toString()).toList(),
130133
);
@@ -140,6 +143,7 @@ final class DocumentDataMetadataDto {
140143
template: template?.toModel().toSignedDocumentRef(),
141144
reply: reply?.toModel().toSignedDocumentRef(),
142145
section: section,
146+
collaborators: collaborators?.map((e) => CatalystId.fromUri(e.getUri())).toList(),
143147
parameters: DocumentParameters(
144148
parameters.map((e) => e.toModel().toSignedDocumentRef()).toSet(),
145149
),
Lines changed: 36 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import 'dart:convert';
21
import 'dart:typed_data';
32

43
import 'package:catalyst_compression/catalyst_compression.dart';
54
import 'package:catalyst_cose/catalyst_cose.dart';
65
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
7-
import 'package:catalyst_voices_repositories/src/document/exception/document_exception.dart';
86
import 'package:catalyst_voices_repositories/src/signed_document/signed_document_manager.dart';
7+
import 'package:catalyst_voices_repositories/src/signed_document/signed_document_mapper.dart';
98
import 'package:cbor/cbor.dart';
109
import 'package:equatable/equatable.dart';
1110

@@ -21,28 +20,10 @@ final class SignedDocumentManagerImpl implements SignedDocumentManager {
2120
@override
2221
Future<SignedDocument> parseDocument(Uint8List bytes) async {
2322
final coseSign = CoseSign.fromCbor(cbor.decode(bytes));
24-
final signers = _CatalystIdListExt.fromCose(
25-
coseSign.signatures.map((e) => e.protectedHeaders.kid).nonNulls.toList().cast(),
26-
);
27-
28-
final metadata = _DocumentMetadataExt.fromCose(
29-
protectedHeaders: coseSign.protectedHeaders,
30-
unprotectedHeaders: coseSign.unprotectedHeaders,
31-
signers: signers,
32-
);
3323

34-
final payloadBytes = await _brotliDecompressPayload(coseSign);
35-
final payload = SignedDocumentPayload.fromBytes(
36-
payloadBytes,
37-
contentType: metadata.contentType,
38-
);
24+
final rawPayload = await _decompressPayload(coseSign);
3925

40-
return _CoseSignedDocument(
41-
coseSign: coseSign,
42-
payload: payload,
43-
metadata: metadata,
44-
signers: signers,
45-
);
26+
return _CoseSignedDocument.fromCose(coseSign, rawPayload: rawPayload);
4627
}
4728

4829
@override
@@ -52,11 +33,11 @@ final class SignedDocumentManagerImpl implements SignedDocumentManager {
5233
required CatalystId catalystId,
5334
required CatalystPrivateKey privateKey,
5435
}) async {
55-
final compressedPayload = await _brotliCompressPayload(document.toBytes());
36+
final compressedPayload = await _compressPayload(document.toBytes());
5637

5738
final coseSign = await CoseSign.sign(
58-
protectedHeaders: metadata.asCoseProtectedHeaders,
59-
unprotectedHeaders: metadata.asCoseUnprotectedHeaders,
39+
protectedHeaders: SignedDocumentMapper.buildCoseProtectedHeaders(metadata),
40+
unprotectedHeaders: const CoseHeaders.unprotected(),
6041
payload: compressedPayload,
6142
signers: [_CatalystSigner(catalystId, privateKey)],
6243
);
@@ -69,12 +50,12 @@ final class SignedDocumentManagerImpl implements SignedDocumentManager {
6950
);
7051
}
7152

72-
Future<Uint8List> _brotliCompressPayload(Uint8List payload) async {
53+
Future<Uint8List> _compressPayload(Uint8List payload) async {
7354
final compressed = await brotli.compress(payload);
7455
return Uint8List.fromList(compressed);
7556
}
7657

77-
Future<Uint8List> _brotliDecompressPayload(CoseSign coseSign) async {
58+
Future<Uint8List> _decompressPayload(CoseSign coseSign) async {
7859
if (coseSign.protectedHeaders.contentEncoding == CoseHttpContentEncoding.brotli) {
7960
final decompressed = await brotli.decompress(coseSign.payload);
8061
return Uint8List.fromList(decompressed);
@@ -149,6 +130,34 @@ final class _CoseSignedDocument with EquatableMixin implements SignedDocument {
149130
required this.signers,
150131
}) : _coseSign = coseSign;
151132

133+
factory _CoseSignedDocument.fromCose(
134+
CoseSign coseSign, {
135+
required Uint8List rawPayload,
136+
}) {
137+
final signers = coseSign.signatures
138+
.map((e) => e.protectedHeaders.kid)
139+
.nonNulls
140+
.cast<CatalystIdKid>()
141+
.toList();
142+
143+
final metadata = SignedDocumentMapper.buildMetadata(
144+
protectedHeaders: coseSign.protectedHeaders,
145+
unprotectedHeaders: coseSign.unprotectedHeaders,
146+
signers: signers,
147+
);
148+
final payload = SignedDocumentPayload.fromBytes(
149+
rawPayload,
150+
contentType: metadata.contentType,
151+
);
152+
153+
return _CoseSignedDocument(
154+
coseSign: coseSign,
155+
payload: payload,
156+
metadata: metadata,
157+
signers: metadata.signers ?? [],
158+
);
159+
}
160+
152161
@override
153162
List<Object?> get props => [_coseSign, payload, metadata, signers];
154163

@@ -163,148 +172,3 @@ final class _CoseSignedDocument with EquatableMixin implements SignedDocument {
163172
return _coseSign.verify(verifier: _CatalystVerifier(catalystId));
164173
}
165174
}
166-
167-
extension _CatalystIdExt on CatalystId {
168-
static CatalystId? fromCose(CatalystIdKid kid) {
169-
final string = utf8.decode(kid.bytes);
170-
final uri = Uri.tryParse(string);
171-
if (uri == null) return null;
172-
173-
return CatalystId.fromUri(uri);
174-
}
175-
}
176-
177-
extension _CatalystIdListExt on List<CatalystId> {
178-
static List<CatalystId> fromCose(List<CatalystIdKid> list) {
179-
return list.map(_CatalystIdExt.fromCose).nonNulls.toList();
180-
}
181-
}
182-
183-
extension _DocumentMetadataExt on DocumentDataMetadata {
184-
CoseHeaders get asCoseProtectedHeaders {
185-
final id = this.id;
186-
final version = this.version;
187-
final ref = this.ref;
188-
final template = this.template;
189-
final reply = this.reply;
190-
final section = this.section;
191-
192-
return CoseHeaders.protected(
193-
mediaType: contentType.asCose,
194-
contentEncoding: CoseHttpContentEncoding.brotli,
195-
type: CoseDocumentType(type.uuid.asUuidV4),
196-
id: CoseDocumentId(id.asUuidV7),
197-
ver: CoseDocumentVer(version.asUuidV7),
198-
ref: ref == null ? null : [ref].asCose,
199-
template: template == null ? null : [template].asCose,
200-
reply: reply == null ? null : [reply].asCose,
201-
section: section == null ? null : CoseSectionRef(CoseJsonPointer(section)),
202-
parameters: parameters.set.toList().asCose,
203-
);
204-
}
205-
206-
CoseHeaders get asCoseUnprotectedHeaders {
207-
return const CoseHeaders.unprotected();
208-
}
209-
210-
static DocumentDataMetadata fromCose({
211-
required CoseHeaders protectedHeaders,
212-
required CoseHeaders unprotectedHeaders,
213-
required List<CatalystId> signers,
214-
}) {
215-
final type = protectedHeaders.type?.value.format();
216-
final id = protectedHeaders.id?.value?.format();
217-
final ver = protectedHeaders.ver?.value?.format();
218-
final ref = protectedHeaders.ref;
219-
final template = protectedHeaders.template;
220-
final reply = protectedHeaders.reply;
221-
final parameters = protectedHeaders.parameters;
222-
223-
final malformedReasons = <String>[];
224-
if (id == null) {
225-
malformedReasons.add('id is missing');
226-
}
227-
if (ver == null) {
228-
malformedReasons.add('version is missing');
229-
}
230-
231-
if (malformedReasons.isNotEmpty) {
232-
throw DocumentMetadataMalformedException(reasons: malformedReasons);
233-
}
234-
235-
return DocumentDataMetadata(
236-
contentType: _SignedDocumentContentTypeExt.fromCose(protectedHeaders.mediaType),
237-
type: type == null ? DocumentType.unknown : DocumentType.fromJson(type),
238-
selfRef: SignedDocumentRef(id: id!, version: ver),
239-
ref: ref == null ? null : _DocumentRefsExt.fromCose(ref).firstOrNull,
240-
template: template == null ? null : _DocumentRefsExt.fromCose(template).firstOrNull,
241-
reply: reply == null ? null : _DocumentRefsExt.fromCose(reply).firstOrNull,
242-
section: protectedHeaders.section?.value.text,
243-
parameters: parameters == null
244-
? const DocumentParameters()
245-
: DocumentParameters(_DocumentRefsExt.fromCose(parameters).toSet()),
246-
authors: signers,
247-
);
248-
}
249-
}
250-
251-
extension _DocumentRefExt on DocumentRef {
252-
CoseDocumentRef get asCose => CoseDocumentRef.optional(
253-
documentId: id.asUuidV7,
254-
documentVer: (version ?? id).asUuidV7,
255-
documentLocator: CoseDocumentLocator.fallback(),
256-
);
257-
258-
static SignedDocumentRef fromCose(CoseDocumentRef cose) {
259-
return SignedDocumentRef(
260-
id: cose.documentId.format(),
261-
version: cose.documentVer.format(),
262-
);
263-
}
264-
}
265-
266-
extension _DocumentRefsExt on List<DocumentRef> {
267-
CoseDocumentRefs get asCose => CoseDocumentRefs(map((e) => e.asCose).toList());
268-
269-
static List<SignedDocumentRef> fromCose(CoseDocumentRefs cose) {
270-
return cose.refs.map(_DocumentRefExt.fromCose).toList();
271-
}
272-
}
273-
274-
extension _SignedDocumentContentTypeExt on DocumentContentType {
275-
/// Maps the [DocumentContentType] into COSE representation.
276-
CoseMediaType? get asCose {
277-
switch (this) {
278-
case DocumentContentType.json:
279-
return CoseMediaType.json;
280-
case DocumentContentType.unknown:
281-
return null;
282-
}
283-
}
284-
285-
static DocumentContentType fromCose(CoseMediaType? mediaType) {
286-
switch (mediaType) {
287-
case CoseMediaType.json:
288-
return DocumentContentType.json;
289-
case CoseMediaType.cbor:
290-
case CoseMediaType.cddl:
291-
case CoseMediaType.schemaJson:
292-
case CoseMediaType.css:
293-
case CoseMediaType.cssHandlebars:
294-
case CoseMediaType.html:
295-
case CoseMediaType.htmlHandlebars:
296-
case CoseMediaType.markdown:
297-
case CoseMediaType.markdownHandlebars:
298-
case CoseMediaType.plain:
299-
case CoseMediaType.plainHandlebars:
300-
case null:
301-
return DocumentContentType.unknown;
302-
}
303-
}
304-
}
305-
306-
extension _UuidExt on String {
307-
CoseUuidV4 get asUuidV4 => CoseUuidV4.fromString(this);
308-
309-
CoseUuidV7 get asUuidV7 => CoseUuidV7.fromString(this);
310-
}

0 commit comments

Comments
 (0)