What changed, and why it matters
This commit fixes how timestamps are stored for a built-in shopping/ticket feature. Previously, database columns used Drift's default dateTime type, which truncates fractional seconds. The patch switches those columns to store ISO 8601 UTC strings with millisecond precision and normalizes message timestamps to UTC. This is a data-integrity/correctness fix rather than a security vulnerability; it prevents subtle ordering or duplicate-detection bugs but does not create a direct path for an attacker to steal funds or run code.
Treat as a routine bugfix. Review the Iso8601UtcConverter for correctness and ensure existing database migrations handle the schema change safely. No urgent security response is warranted based on this diff alone.
Security signals we found
Data-type change from dateTime to text with custom converter
Timestamp normalization to UTC
No input validation, authentication, or cryptographic changes
No explicit security relevance stated in commit message
Evidence from the diff
The patch changes three DateTime columns in the ShopInBitTickets table from Drift dateTime to text-backed columns using a new Iso8601UtcConverter. Drift’s dateTime maps to SQLite’s limited datetime storage, which can drop sub-second precision. The new converter stores DateTime.fromMillisecondsSinceEpoch(…).toIso8601String(), preserving millisecond precision, and parses back with DateTime.parse(…).toUtc(). It also normalizes TicketMessage.timestamp to UTC on deserialization. The generated Drift code is updated accordingly. No SQL injection, buffer overflow, or cryptographic weakness is introduced or fixed.
Changed components
lib/db/drift/shared_db/tables/shopin_bit_tickets.dartlib/db/drift/shared_db/shared_database.g.dartlib/services/shopinbit/src/models/message.dartInspect captured patch +132 / −93
diff --git a/lib/db/drift/shared_db/shared_database.g.dart b/lib/db/drift/shared_db/shared_database.g.dart
index 67c9b7a..693442e 100644
--- a/lib/db/drift/shared_db/shared_database.g.dart
+++ b/lib/db/drift/shared_db/shared_database.g.dart
@@ -877,16 +877,17 @@ class $ShopInBitTicketsTable extends ShopInBitTickets
type: DriftSqlType.string,
requiredDuringInsert: false,
);
- static const VerificationMeta _lastAgentMessageAtMeta =
- const VerificationMeta('lastAgentMessageAt');
@override
- late final GeneratedColumn<DateTime> lastAgentMessageAt =
- GeneratedColumn<DateTime>(
+ late final GeneratedColumnWithTypeConverter<DateTime?, String>
+ lastAgentMessageAt =
+ GeneratedColumn<String>(
'last_agent_message_at',
aliasedName,
true,
- type: DriftSqlType.dateTime,
+ type: DriftSqlType.string,
requiredDuringInsert: false,
+ ).withConverter<DateTime?>(
+ $ShopInBitTicketsTable.$converterlastAgentMessageAtn,
);
static const VerificationMeta _feeTicketNumberMeta = const VerificationMeta(
'feeTicketNumber',
@@ -912,30 +913,28 @@ class $ShopInBitTicketsTable extends ShopInBitTickets
).withConverter<List<TicketMessage>>(
$ShopInBitTicketsTable.$convertermessages,
);
- static const VerificationMeta _createdAtMeta = const VerificationMeta(
- 'createdAt',
- );
@override
- late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
- 'created_at',
- aliasedName,
- false,
- type: DriftSqlType.dateTime,
- requiredDuringInsert: false,
- defaultValue: currentDateAndTime,
- );
- static const VerificationMeta _updatedAtMeta = const VerificationMeta(
- 'updatedAt',
- );
+ late final GeneratedColumnWithTypeConverter<DateTime, String> createdAt =
+ GeneratedColumn<String>(
+ 'created_at',
+ aliasedName,
+ false,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ clientDefault: () =>
+ ShopInBitTickets.dateConverter.toSql(DateTime.now()),
+ ).withConverter<DateTime>($ShopInBitTicketsTable.$convertercreatedAt);
@override
- late final GeneratedColumn<DateTime> updatedAt = GeneratedColumn<DateTime>(
- 'updated_at',
- aliasedName,
- false,
- type: DriftSqlType.dateTime,
- requiredDuringInsert: false,
- defaultValue: currentDateAndTime,
- );
+ late final GeneratedColumnWithTypeConverter<DateTime, String> updatedAt =
+ GeneratedColumn<String>(
+ 'updated_at',
+ aliasedName,
+ false,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ clientDefault: () =>
+ ShopInBitTickets.dateConverter.toSql(DateTime.now()),
+ ).withConverter<DateTime>($ShopInBitTicketsTable.$converterupdatedAt);
@override
List<GeneratedColumn> get $columns => [
apiTicketId,
@@ -1064,15 +1063,6 @@ class $ShopInBitTicketsTable extends ShopInBitTickets
),
);
}
- if (data.containsKey('last_agent_message_at')) {
- context.handle(
- _lastAgentMessageAtMeta,
- lastAgentMessageAt.isAcceptableOrUnknown(
- data['last_agent_message_at']!,
- _lastAgentMessageAtMeta,
- ),
- );
- }
if (data.containsKey('fee_ticket_number')) {
context.handle(
_feeTicketNumberMeta,
@@ -1082,18 +1072,6 @@ class $ShopInBitTicketsTable extends ShopInBitTickets
),
);
}
- if (data.containsKey('created_at')) {
- context.handle(
- _createdAtMeta,
- createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
- );
- }
- if (data.containsKey('updated_at')) {
- context.handle(
- _updatedAtMeta,
- updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
- );
- }
return context;
}
@@ -1155,10 +1133,13 @@ class $ShopInBitTicketsTable extends ShopInBitTickets
DriftSqlType.string,
data['${effectivePrefix}tracking_link'],
),
- lastAgentMessageAt: attachedDatabase.typeMapping.read(
- DriftSqlType.dateTime,
- data['${effectivePrefix}last_agent_message_at'],
- ),
+ lastAgentMessageAt: $ShopInBitTicketsTable.$converterlastAgentMessageAtn
+ .fromSql(
+ attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}last_agent_message_at'],
+ ),
+ ),
feeTicketNumber: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}fee_ticket_number'],
@@ -1169,14 +1150,18 @@ class $ShopInBitTicketsTable extends ShopInBitTickets
data['${effectivePrefix}messages'],
)!,
),
- createdAt: attachedDatabase.typeMapping.read(
- DriftSqlType.dateTime,
- data['${effectivePrefix}created_at'],
- )!,
- updatedAt: attachedDatabase.typeMapping.read(
- DriftSqlType.dateTime,
- data['${effectivePrefix}updated_at'],
- )!,
+ createdAt: $ShopInBitTicketsTable.$convertercreatedAt.fromSql(
+ attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}created_at'],
+ )!,
+ ),
+ updatedAt: $ShopInBitTicketsTable.$converterupdatedAt.fromSql(
+ attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}updated_at'],
+ )!,
+ ),
);
}
@@ -1193,8 +1178,16 @@ class $ShopInBitTicketsTable extends ShopInBitTickets
$converterstatus = const EnumNameConverter<ShopInBitOrderStatus>(
ShopInBitOrderStatus.values,
);
+ static TypeConverter<DateTime, String> $converterlastAgentMessageAt =
+ ShopInBitTickets.dateConverter;
+ static TypeConverter<DateTime?, String?> $converterlastAgentMessageAtn =
+ NullAwareTypeConverter.wrap($converterlastAgentMessageAt);
static TypeConverter<List<TicketMessage>, String> $convertermessages =
const MessagesConverter();
+ static TypeConverter<DateTime, String> $convertercreatedAt =
+ ShopInBitTickets.dateConverter;
+ static TypeConverter<DateTime, String> $converterupdatedAt =
+ ShopInBitTickets.dateConverter;
@override
bool get withoutRowId => true;
}
@@ -1268,7 +1261,11 @@ class ShopInBitTicket extends DataClass implements Insertable<ShopInBitTicket> {
map['tracking_link'] = Variable<String>(trackingLink);
}
if (!nullToAbsent || lastAgentMessageAt != null) {
- map['last_agent_message_at'] = Variable<DateTime>(lastAgentMessageAt);
+ map['last_agent_message_at'] = Variable<String>(
+ $ShopInBitTicketsTable.$converterlastAgentMessageAtn.toSql(
+ lastAgentMessageAt,
+ ),
+ );
}
if (!nullToAbsent || feeTicketNumber != null) {
map['fee_ticket_number'] = Variable<String>(feeTicketNumber);
@@ -1278,8 +1275,16 @@ class ShopInBitTicket extends DataClass implements Insertable<ShopInBitTicket> {
$ShopInBitTicketsTable.$convertermessages.toSql(messages),
);
}
- map['created_at'] = Variable<DateTime>(createdAt);
- map['updated_at'] = Variable<DateTime>(updatedAt);
+ {
+ map['created_at'] = Variable<String>(
+ $ShopInBitTicketsTable.$convertercreatedAt.toSql(createdAt),
+ );
+ }
+ {
+ map['updated_at'] = Variable<String>(
+ $ShopInBitTicketsTable.$converterupdatedAt.toSql(updatedAt),
+ );
+ }
return map;
}
@@ -1612,11 +1617,11 @@ class ShopInBitTicketsCompanion extends UpdateCompanion<ShopInBitTicket> {
Expression<String>? offerPrice,
Expression<String>? paymentInvoiceStatus,
Expression<String>? trackingLink,
- Expression<DateTime>? lastAgentMessageAt,
+ Expression<String>? lastAgentMessageAt,
Expression<String>? feeTicketNumber,
Expression<String>? messages,
- Expression<DateTime>? createdAt,
- Expression<DateTime>? updatedAt,
+ Expression<String>? createdAt,
+ Expression<String>? updatedAt,
}) {
return RawValuesInsertable({
if (apiTicketId != null) 'api_ticket_id': apiTicketId,
@@ -1727,8 +1732,10 @@ class ShopInBitTicketsCompanion extends UpdateCompanion<ShopInBitTicket> {
map['tracking_link'] = Variable<String>(trackingLink.value);
}
if (lastAgentMessageAt.present) {
- map['last_agent_message_at'] = Variable<DateTime>(
- lastAgentMessageAt.value,
+ map['last_agent_message_at'] = Variable<String>(
+ $ShopInBitTicketsTable.$converterlastAgentMessageAtn.toSql(
+ lastAgentMessageAt.value,
+ ),
);
}
if (feeTicketNumber.present) {
@@ -1740,10 +1747,14 @@ class ShopInBitTicketsCompanion extends UpdateCompanion<ShopInBitTicket> {
);
}
if (createdAt.present) {
- map['created_at'] = Variable<DateTime>(createdAt.value);
+ map['created_at'] = Variable<String>(
+ $ShopInBitTicketsTable.$convertercreatedAt.toSql(createdAt.value),
+ );
}
if (updatedAt.present) {
- map['updated_at'] = Variable<DateTime>(updatedAt.value);
+ map['updated_at'] = Variable<String>(
+ $ShopInBitTicketsTable.$converterupdatedAt.toSql(updatedAt.value),
+ );
}
return map;
}
@@ -2311,9 +2322,10 @@ class $$ShopInBitTicketsTableFilterComposer
builder: (column) => ColumnFilters(column),
);
- ColumnFilters<DateTime> get lastAgentMessageAt => $composableBuilder(
+ ColumnWithTypeConverterFilters<DateTime?, DateTime, String>
+ get lastAgentMessageAt => $composableBuilder(
column: $table.lastAgentMessageAt,
- builder: (column) => ColumnFilters(column),
+ builder: (column) => ColumnWithTypeConverterFilters(column),
);
ColumnFilters<String> get feeTicketNumber => $composableBuilder(
@@ -2331,15 +2343,17 @@ class $$ShopInBitTicketsTableFilterComposer
builder: (column) => ColumnWithTypeConverterFilters(column),
);
- ColumnFilters<DateTime> get createdAt => $composableBuilder(
- column: $table.createdAt,
- builder: (column) => ColumnFilters(column),
- );
+ ColumnWithTypeConverterFilters<DateTime, DateTime, String> get createdAt =>
+ $composableBuilder(
+ column: $table.createdAt,
+ builder: (column) => ColumnWithTypeConverterFilters(column),
+ );
- ColumnFilters<DateTime> get updatedAt => $composableBuilder(
- column: $table.updatedAt,
- builder: (column) => ColumnFilters(column),
- );
+ ColumnWithTypeConverterFilters<DateTime, DateTime, String> get updatedAt =>
+ $composableBuilder(
+ column: $table.updatedAt,
+ builder: (column) => ColumnWithTypeConverterFilters(column),
+ );
}
class $$ShopInBitTicketsTableOrderingComposer
@@ -2411,7 +2425,7 @@ class $$ShopInBitTicketsTableOrderingComposer
builder: (column) => ColumnOrderings(column),
);
- ColumnOrderings<DateTime> get lastAgentMessageAt => $composableBuilder(
+ ColumnOrderings<String> get lastAgentMessageAt => $composableBuilder(
column: $table.lastAgentMessageAt,
builder: (column) => ColumnOrderings(column),
);
@@ -2426,12 +2440,12 @@ class $$ShopInBitTicketsTableOrderingComposer
builder: (column) => ColumnOrderings(column),
);
- ColumnOrderings<DateTime> get createdAt => $composableBuilder(
+ ColumnOrderings<String> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => ColumnOrderings(column),
);
- ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
+ ColumnOrderings<String> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => ColumnOrderings(column),
);
@@ -2500,10 +2514,11 @@ class $$ShopInBitTicketsTableAnnotationComposer
builder: (column) => column,
);
- GeneratedColumn<DateTime> get lastAgentMessageAt => $composableBuilder(
- column: $table.lastAgentMessageAt,
- builder: (column) => column,
- );
+ GeneratedColumnWithTypeConverter<DateTime?, String> get lastAgentMessageAt =>
+ $composableBuilder(
+ column: $table.lastAgentMessageAt,
+ builder: (column) => column,
+ );
GeneratedColumn<String> get feeTicketNumber => $composableBuilder(
column: $table.feeTicketNumber,
@@ -2513,10 +2528,10 @@ class $$ShopInBitTicketsTableAnnotationComposer
GeneratedColumnWithTypeConverter<List<TicketMessage>, String> get messages =>
$composableBuilder(column: $table.messages, builder: (column) => column);
- GeneratedColumn<DateTime> get createdAt =>
+ GeneratedColumnWithTypeConverter<DateTime, String> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
- GeneratedColumn<DateTime> get updatedAt =>
+ GeneratedColumnWithTypeConverter<DateTime, String> get updatedAt =>
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
}
diff --git a/lib/db/drift/shared_db/tables/shopin_bit_tickets.dart b/lib/db/drift/shared_db/tables/shopin_bit_tickets.dart
index 83385eb..d18278b 100644
--- a/lib/db/drift/shared_db/tables/shopin_bit_tickets.dart
+++ b/lib/db/drift/shared_db/tables/shopin_bit_tickets.dart
@@ -7,6 +7,8 @@ import "../../../../services/shopinbit/src/models/message.dart";
import "../../../../utilities/logger.dart";
class ShopInBitTickets extends Table {
+ static const dateConverter = Iso8601UtcConverter();
+
IntColumn get apiTicketId => integer()();
TextColumn get customerKey => text()();
TextColumn get ticketNumber => text()();
@@ -23,15 +25,24 @@ class ShopInBitTickets extends Table {
TextColumn get paymentInvoiceStatus => text().nullable()();
TextColumn get trackingLink => text().nullable()();
- DateTimeColumn get lastAgentMessageAt => dateTime().nullable()();
+ TextColumn get lastAgentMessageAt =>
+ text().nullable().map(ShopInBitTickets.dateConverter)();
TextColumn get feeTicketNumber => text().nullable()();
TextColumn get messages =>
text().map(const MessagesConverter()).withDefault(const Constant("[]"))();
- DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
- DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
+ TextColumn get createdAt => text()
+ .map(ShopInBitTickets.dateConverter)
+ .clientDefault(
+ () => ShopInBitTickets.dateConverter.toSql(DateTime.now()),
+ )();
+ TextColumn get updatedAt => text()
+ .map(ShopInBitTickets.dateConverter)
+ .clientDefault(
+ () => ShopInBitTickets.dateConverter.toSql(DateTime.now()),
+ )();
@override
Set<Column<Object>> get primaryKey => {apiTicketId};
@@ -40,6 +51,19 @@ class ShopInBitTickets extends Table {
bool get withoutRowId => true;
}
+class Iso8601UtcConverter extends TypeConverter<DateTime, String> {
+ const Iso8601UtcConverter();
+
+ @override
+ DateTime fromSql(String fromDb) => DateTime.parse(fromDb).toUtc();
+
+ @override
+ String toSql(DateTime value) => DateTime.fromMillisecondsSinceEpoch(
+ value.toUtc().millisecondsSinceEpoch,
+ isUtc: true,
+ ).toIso8601String();
+}
+
/// Drift TypeConverter so `messages` round-trips between a JSON column and
/// `List<TicketMessage>` on the generated data class.
class MessagesConverter extends TypeConverter<List<TicketMessage>, String> {
diff --git a/lib/services/shopinbit/src/models/message.dart b/lib/services/shopinbit/src/models/message.dart
index 58e45cd..1251368 100644
--- a/lib/services/shopinbit/src/models/message.dart
+++ b/lib/services/shopinbit/src/models/message.dart
@@ -24,7 +24,7 @@ class TicketMessage {
factory TicketMessage.fromJson(Map<String, dynamic> json) {
return TicketMessage(
- timestamp: DateTime.parse(json['timestamp'] as String),
+ timestamp: DateTime.parse(json['timestamp'] as String).toUtc(),
fromAgent: json['from_agent'] as bool,
content: json['content'] as String,
);
Why this scored 20/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.