From f6b27565cd9e7c7a9d3a5c7cda987a02dbf2e438 Mon Sep 17 00:00:00 2001 From: Foxyz Date: Fri, 20 Feb 2026 03:44:02 +0300 Subject: [PATCH] Implement chat server auth, models, handlers, and docs context --- context/objectbox_entity_annotations.json | 79 +++ context/objectbox_property_types.json | 79 +++ context/relic_docs.json | 81 +++ context/relic_hello.json | 83 +++ lib/config/config.dart | 4 +- lib/models.dart | 25 +- lib/server.dart | 40 +- lib/src/auth/auth_middleware.dart | 2 - lib/src/auth/crypto_service.dart | 42 +- lib/src/handlers/auth_handlers.dart | 284 ++++++++-- lib/src/handlers/channel_handlers.dart | 18 +- lib/src/handlers/message_handlers.dart | 34 +- lib/src/handlers/public_handlers.dart | 43 +- lib/src/utils/extensions.dart | 2 - pubspec.lock | 597 ++++++++++++++++++++++ pubspec.yaml | 24 +- 16 files changed, 1285 insertions(+), 152 deletions(-) create mode 100644 context/objectbox_entity_annotations.json create mode 100644 context/objectbox_property_types.json create mode 100644 context/relic_docs.json create mode 100644 context/relic_hello.json create mode 100644 pubspec.lock diff --git a/context/objectbox_entity_annotations.json b/context/objectbox_entity_annotations.json new file mode 100644 index 0000000..20458fb --- /dev/null +++ b/context/objectbox_entity_annotations.json @@ -0,0 +1,79 @@ +{ + "code": 200, + "data": { + "description": "Instead of SQL, you define your data model by annotating your persistent object types on the programming language level.", + "external": { + "alternate": { + "https://docs.objectbox.io/entity-annotations.md": { + "type": "text/markdown" + }, + "https://docs.objectbox.io/entity-annotations/rss.xml": { + "title": "RSS Feed", + "type": "application/rss+xml" + } + }, + "apple-touch-icon": { + "https://docs.objectbox.io/~gitbook/image?url=https%3A%2F%2F4174656640-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-legacy-files%2Fo%2Fspaces%252F-LETufmyus5LFkviJjr4%252Favatar.png%3Fgeneration%3D1528453316288221%26alt%3Dmedia&width=180&height=180&sign=96b28d45&sv=2": { + "media": "(prefers-color-scheme: dark)", + "type": "image/png" + } + }, + "canonical": { + "https://docs.objectbox.io/entity-annotations": {} + }, + "icon": { + "https://docs.objectbox.io/~gitbook/image?url=https%3A%2F%2F4174656640-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-legacy-files%2Fo%2Fspaces%252F-LETufmyus5LFkviJjr4%252Favatar.png%3Fgeneration%3D1528453316288221%26alt%3Dmedia&width=48&height=48&sign=96b28d45&sv=2": { + "media": "(prefers-color-scheme: dark)", + "type": "image/png" + } + }, + "preconnect": { + "https://api.gitbook.com/cache/": {}, + "https://ka-p.fontawesome.com/releases/v7.1.0/": {}, + "https://static-2v.gitbook.com/": { + "crossorigin": "" + } + }, + "preload": { + "https://static-2v.gitbook.com/_next/static/chunks/webpack-3f8d6b18f6ab2c58.js?dpl=59ffb95417795c0d96c480da62fd9440881efa4b": { + "as": "script", + "fetchpriority": "low" + } + }, + "privacy-policy": { + "https://objectbox.io/wordpress/wp-content/uploads/2022/08/Object-Box-Privacy-Notice.pdf": {} + } + }, + "html": "Entity Annotations | ObjectBox Docs
search

Entity Annotations

Instead of SQL, you define your data model by annotating your persistent object types on the programming language level.

ObjectBox - Database Persistence with Entity Annotations

ObjectBox is a database that persists objects, which we sometimes also call entities.

To let ObjectBox know which classes are entities you annotate them with @Entity. This annotation identifies the class User in the following example as a persistable entity. This will trigger ObjectBox to generate persistence code tailored for this class:

@Entity\npublic class User {\n    \n    @Id\n    private long id;\n    \n    private String name;\n    \n    // Not persisted:\n    @Transient\n    private int tempUsageCount;\n    \n   // TODO: getters and setters.\n}
circle-info

Note:

  • It’s often good practice to model entities as “dumb” data classes (POJOs) with just properties.

  • Entities must have a no-args constructor, or for better performance, a constructor with all properties as arguments. In the above example, a default, no-args constructor is generated by the compiler.

Object IDs: @Id

In ObjectBox, entities must have one 64-bit integer ID property with non-private visibility (or non-private getter and setter method) to efficiently get or reference objects.

@Entity\npublic class User {\n    @Id public long id;\n    // Note: You can use the nullable java.lang.Long, but we do not recommend it.\n    \n    ...\n}

If you need to use another type for IDs (such as a string UID given by a server), model them as regular properties and use queries to look up objects by your application-specific ID. Also, make sure to index the property, and if it's a string use a case-sensitive condition, to speed up lookups. To prevent duplicates it is also possible to enforce a unique value for this secondary ID.

@Entity\nclass StringIdEntity {\n    @Id public long id;\n    @Index public String uid;\n    // Alternatively:\n    // @Unique String uid;\n}\n\nStringIdEntity entity = box.query()\n    .equal(StringIdEntity_.uid, uid, StringOrder.CASE_SENSITIVE)\n    .build().findUnique()

ID properties are unique and indexed by default.

When you put a new object you do not assign an ID. By default IDs for new objects are assigned by ObjectBox. See the page on Object IDs for details.

circle-info

If you need to assign IDs by yourself have a look at how to switch to self-assigned IDs and what side effects apply.

Make entity data accessible

ObjectBox needs to access the data of your entity’s properties (e.g. in the generated code). You have two options:

  1. Make sure properties do not have private visibility.

  2. Provide standard getters (your IDE can generate them easily).

To improve performance when ObjectBox constructs your entities, you might also want to provide an all-properties constructor.

@Entity\npublic class Order {\n    \n    // Option 1: field is not private.\n    @Id long id;\n    \n    // Option 2: field is private, but getter is available.\n    private String name;\n    \n    public ToOne<Customer> customer;\n    public ToMany<Order> relatedOrders;\n    \n    // At minimum, provide a default constructor for ObjectBox.\n    public Order() {\n    }\n\n    // Optional: all-properties constructor for better performance.\n    // - make sure type matches exactly,\n    // - for ToOne add its virtual ID property instead,\n    // - for ToMany add no parameter.\n    public Order(long id, String name, long customerId) {\n        this.id = id;\n        this.name = name;\n        this.customer.setTargetId(customerId);\n    }\n    \n    public String getName() {\n        return this.name;\n    }\n}

Supported property types

ObjectBox supports a wide range of property types including integers, floats, strings, dates, lists, and flex properties for dynamic data. See the dedicated page for details:

Property Typeschevron-right

If the built-in types don't cover your needs, you can use custom types and converters.

Basic annotations for entity properties

Transient

@Transient marks properties that should not be persisted. In Java static or transient properties will also not be persisted.

@Transient\nprivate int notPersisted;

NameInDb

circle-info

Only available for Java/Kotlin at the moment

@NameInDb lets you define a name on the database level for a property. This allows you to rename the property without affecting the property name on the database level.

circle-exclamation
  • We recommend using @Uid annotations to rename properties and even entities instead.

  • @NameInDb only works with inline constants to specify a column name.

@NameInDb(\"username\")\nprivate String name;

Property Indexes

Annotate a property with @Index to create a database index for the corresponding database column. This can improve performance when querying for that property.

@Index\nprivate String name;
circle-exclamation

@Index is currently not supported for String[], byte[], float and double

An index stores additional information in the database to make lookups faster. As an analogy, we could look at Java-like programming languages where you store objects in a list. For example, you could store persons using a List<Person>. Now, you want to search for all persons with a specific name so you would iterate through the list and check for the name property of each object. This is an O(N) operation and thus doesn't scale well with an increasing number of objects. To make this more scalable you can introduce a second data structure Map<String, Person> with the name as a key. This will give you a constant lookup time (O(1)). The downside of this is that it needs more resources (here: RAM) and slows down add/remove operations on the list a bit. These principles can be transferred to database indexes, just that the primary resource consumed is disk space.

Index types (String)

For scalar properties, ObjectBox uses a value-based index. Because String properties typically require more storage space than scalar values, by default ObjectBox uses a hash index for strings instead.

To override the default and use a value-based index for a String property, specify the index type:

@Index(type = IndexType.VALUE)\nprivate String name;

Keep in mind that for String, depending on the length of your values, a value-based index may require more storage space than the default hash-based index.

ObjectBox supports these index types:

  • Not specified or DEFAULT Uses the best index based on property type (HASH for String, VALUE for others).

  • VALUE Uses property values to build the index. For String, this may require more storage than a hash-based index.

  • HASH Uses a 32-bit hash of property values to build the index. Occasional collisions may occur which should not have any performance impact in practice. Usually, a better choice than HASH64, as it requires less storage.

  • HASH64 Uses a 64-bit hash of property values to build the index. Requires more storage than HASH and thus should not be the first choice in most cases.

circle-info

Limits of hash-based indexes: Hashes work great for equality checks, but not for \"starts with\" type conditions. If you frequently use those, you should use value-based indexes instead.

Vector Index for Nearest Neighbor Search

To enable nearest neighbor search, a special index type for vector properties is available:

On-Device Vector Searchchevron-right

Unique constraints

Annotate a property with @Unique to enforce that values are unique before an entity is put:

@Unique\nprivate String name;

A put() operation will abort and throw a UniqueViolationException if the unique constraint is violated:

try {\n    box.put(new User(\"Sam Flynn\"));\n} catch (UniqueViolationException e) {\n    // A User with that name already exists.\n}

For a single property it is possible to specify that a conflicting object should be replaced instead of an exception being thrown:

@Unique(onConflict = ConflictStrategy.REPLACE)\nprivate String name;
circle-exclamation

The REPLACE strategy will add a new object with a different ID. As relations (ToOne/ToMany) reference objects by ID, if the previous object was referenced in any relations, these need to be updated manually.

circle-info

Unique constraints are based on an index, so it is possible to further configure the index with an @Index annotation.

Change database type

Use @Type in Java/Kotlin or the type attribute on @Property in Dart to override how the value of a property is stored and interpreted in the database.

// Store 64-bit integer as time in nanoseconds.\n@Type(DatabaseType.DateNano)\nprivate long timeInNanos;

Relations

Creating to-one and to-many relations between objects is possible as well, see the Relations documentation for details.

Triggering code generation

Once your entity schema is in place, you can trigger the code generation process.

Last updated

Was this helpful?

This site uses cookies to deliver its service and to analyze traffic. By browsing this site, you accept the privacy policy.

", + "metadata": { + "apple-mobile-web-app-status-bar-style": "default", + "apple-mobile-web-app-title": "ObjectBox Docs", + "color-scheme": "light", + "description": "Instead of SQL, you define your data model by annotating your persistent object types on the programming language level.", + "generator": "GitBook (59ffb95)", + "lang": "en", + "mobile-web-app-capable": "yes", + "next-size-adjust": "", + "og:description": "Instead of SQL, you define your data model by annotating your persistent object types on the programming language level.", + "og:image": "https://docs.objectbox.io/~gitbook/ogimage/-LEUYsjP1BaBoJ_OPUD3", + "og:title": "Entity Annotations | ObjectBox Docs", + "robots": "index, follow", + "twitter:card": "summary_large_image", + "twitter:description": "Instead of SQL, you define your data model by annotating your persistent object types on the programming language level.", + "twitter:image": "https://docs.objectbox.io/~gitbook/ogimage/-LEUYsjP1BaBoJ_OPUD3", + "twitter:title": "Entity Annotations | ObjectBox Docs", + "viewport": "width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" + }, + "title": "Entity Annotations | ObjectBox Docs", + "url": "https://docs.objectbox.io/entity-annotations", + "usage": { + "tokens": 221469 + } + }, + "meta": { + "usage": { + "tokens": 221469 + } + }, + "status": 20000 +} \ No newline at end of file diff --git a/context/objectbox_property_types.json b/context/objectbox_property_types.json new file mode 100644 index 0000000..64c442f --- /dev/null +++ b/context/objectbox_property_types.json @@ -0,0 +1,79 @@ +{ + "code": 200, + "data": { + "description": "ObjectBox supports a variety of data types inside the stored objects including various list types and even special \"flex\" types for dynamic and schema-less data.", + "external": { + "alternate": { + "https://docs.objectbox.io/property-types.md": { + "type": "text/markdown" + }, + "https://docs.objectbox.io/property-types/rss.xml": { + "title": "RSS Feed", + "type": "application/rss+xml" + } + }, + "apple-touch-icon": { + "https://docs.objectbox.io/~gitbook/image?url=https%3A%2F%2F4174656640-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-legacy-files%2Fo%2Fspaces%252F-LETufmyus5LFkviJjr4%252Favatar.png%3Fgeneration%3D1528453316288221%26alt%3Dmedia&width=180&height=180&sign=96b28d45&sv=2": { + "media": "(prefers-color-scheme: dark)", + "type": "image/png" + } + }, + "canonical": { + "https://docs.objectbox.io/property-types": {} + }, + "icon": { + "https://docs.objectbox.io/~gitbook/image?url=https%3A%2F%2F4174656640-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-legacy-files%2Fo%2Fspaces%252F-LETufmyus5LFkviJjr4%252Favatar.png%3Fgeneration%3D1528453316288221%26alt%3Dmedia&width=48&height=48&sign=96b28d45&sv=2": { + "media": "(prefers-color-scheme: dark)", + "type": "image/png" + } + }, + "preconnect": { + "https://api.gitbook.com/cache/": {}, + "https://ka-p.fontawesome.com/releases/v7.1.0/": {}, + "https://static-2v.gitbook.com/": { + "crossorigin": "" + } + }, + "preload": { + "https://static-2v.gitbook.com/_next/static/chunks/webpack-3f8d6b18f6ab2c58.js?dpl=59ffb95417795c0d96c480da62fd9440881efa4b": { + "as": "script", + "fetchpriority": "low" + } + }, + "privacy-policy": { + "https://objectbox.io/wordpress/wp-content/uploads/2022/08/Object-Box-Privacy-Notice.pdf": {} + } + }, + "html": "Property Types | ObjectBox Docs
search

Property Types

ObjectBox supports a variety of data types inside the stored objects including various list types and even special \"flex\" types for dynamic and schema-less data.

ObjectBox stores data as objects, which are instances of entity classes you define. Objects consist of properties, which are the individual fields that hold your data, like a user's name or age. This page covers the property types ObjectBox supports:

  • Standard types for single values of a fixed type

  • List/Vector types for collections of values of a fixed type

  • The flex type for dynamic and schema-less data

Standard Types

Standard types include boolean, integers, floating point types, string, and date types. These are the most common types you will use in typical applications.

Quick Reference

Category
ObjectBox Type
Stored as

Boolean

Bool

1 byte

Integer (8-bit)

Byte

1 byte

Integer (16-bit)

Short

2 bytes

Integer (32-bit)

Int

4 bytes

Integer (64-bit)

Long

8 bytes

To-One Relation

Relation

8 bytes

Float (32-bit)

Float

4 bytes

Float (64-bit)

Double

8 bytes

Text Character

Char

2 bytes

String

String

UTF-8 bytes

Date (ms)

Date

8 bytes (long)

Date (ns)

DateNano

8 bytes (long)

circle-info

Nullable types: All ObjectBox property types support null values. In Java, use wrapper classes (Integer vs int) for nullable types. In Kotlin, Dart, and Python, nullability is part of the type system.

Integers

ObjectBox supports all standard integer types, from 8-bit to 64-bit. Rule of thumb: use large enough integer types that are safe for your use case in the future. Use smaller types only if you are certain about the range of values as you cannot change the type of an existing property later.

@Entity\npublic class Sensor {\n    @Id public long id;\n    \n    public byte status;        // -128 to 127\n    public short temperature;  // -32,768 to 32,767  \n    public int count;          // ~2 billion range\n    public long timestamp;     // Large numbers, IDs\n}

Floating Point

Choose 32-bit floats for memory efficiency when ~7 digits of precision is enough. Use 64-bit doubles when you need ~15 digits.

public float score;      // 32-bit, ~7 significant digits\npublic double precise;   // 64-bit, ~15 significant digits

Strings

Strings are stored as UTF-8 encoded bytes without a length limit.

public String name;\npublic String description;  // Can be very long

Dates and Times

ObjectBox stores dates as 64-bit integers (\"timestamps\") representing time since the Unix epoch (January 1, 1970, 00:00:00 UTC). Choose your precision:

Precision
ObjectBox Type
Use case

Milliseconds

Date (the default)

Most applications

Nanoseconds

DateNano

High-precision timestamps

Note: while DateNano values are always stored as nanoseconds, the actual precision of the values depends on the platform and programming language. For example, Dart uses only microsecond precision (the last three digits of the nanosecond are \"truncated\").

// Millisecond precision (default)\npublic Date createdAt;\n\n// Nanosecond precision\n@Type(DatabaseType.DateNano)\npublic long preciseTimestamp;

IDs and Relations

Every ObjectBox entity must have exactly one ID property of type Long (64-bit integer). See Entity Annotations for details.

\"Relation\" is used for one of two relation types available in ObjectBox. It's a 64-bit integer that references the ID of another object. Depending on the programming language you use, it may translate to a \"to-one\" construct. See Relations for complete documentation.

Lists and Arrays

Store collections of values directly; no special tables or joins required. The internal type names are \"vectors\", but they may translate to lists or arrays in the programming language you use.

circle-info

Binary data is stored as a ByteVector. In other databases, this is may be called a \"BLOB\" (Binary Large Object).

Quick Reference

ObjectBox Type
Description

BoolVector

Bool values (stored using one byte per value)

ByteVector

Byte values (8-bit integers), binary data, BLOB

ShortVector

Short values (16-bit integers)

CharVector

Char values (16-bit characters)

IntVector

Int values (32-bit integers)

LongVector

Long values (64-bit integers)

FloatVector

Float values (32-bit floating point)

DoubleVector

Double values (64-bit floating point)

StringVector

String values (UTF-8 encoded strings)

DateVector

Date values (64-bit timestamp)

DateNanoVector

DateNano values (high precision 64-bit timestamp)

Language Examples

// Primitive arrays\npublic byte[] imageData;\npublic int[] scores;\npublic double[] measurements;\n\n// String collections\npublic String[] tags;\npublic List<String> categories;
circle-info

For vector search: Float vectors used for embeddings and similarity search have special indexing support. See On-Device Vector Search.

Flex Properties

Flex properties can hold data values of various types, including complex structures, in a single property. They serve two main purposes:

  • Dynamic data: you want to process JSON-like data dynamically

  • Nested objects: as an alternative to relations, you can embed map-like data directly

circle-info

When syncing with MongoDB, flex properties can hold nested documents.

@Entity\npublic class Customer {\n    @Id long id;\n\n    // Stores any supported type at runtime\n    @Nullable Object tag;\n\n    // Or explicitly use a String map\n    @Nullable Map<String, Object> stringMap;\n\n    // Or a list\n    @Nullable List<Object> flexList;\n    \n    public Customer(Object tag) {\n        this.id = 0;\n        this.tag = tag;\n    }\n    \n    public Customer() {} // For ObjectBox\n}\n\nCustomer customerStrTag = new Customer(\"string-tag\");\nCustomer customerIntTag = new Customer(1234);\nbox.put(customerStrTag, customerIntTag);

See FlexObjectConverterarrow-up-right for Java/Kotlin additional notes.

Custom Types

If the existing property types do not fully cover your use case, check custom types and converters.

Last updated

Was this helpful?

This site uses cookies to deliver its service and to analyze traffic. By browsing this site, you accept the privacy policy.

", + "metadata": { + "apple-mobile-web-app-status-bar-style": "default", + "apple-mobile-web-app-title": "ObjectBox Docs", + "color-scheme": "light", + "description": "ObjectBox supports a variety of data types inside the stored objects including various list types and even special \"flex\" types for dynamic and schema-less data.", + "generator": "GitBook (59ffb95)", + "lang": "en", + "mobile-web-app-capable": "yes", + "next-size-adjust": "", + "og:description": "ObjectBox supports a variety of data types inside the stored objects including various list types and even special \"flex\" types for dynamic and schema-less data.", + "og:image": "https://docs.objectbox.io/~gitbook/ogimage/kSqRmDrNicE3QmU2oyBf", + "og:title": "Property Types | ObjectBox Docs", + "robots": "index, follow", + "twitter:card": "summary_large_image", + "twitter:description": "ObjectBox supports a variety of data types inside the stored objects including various list types and even special \"flex\" types for dynamic and schema-less data.", + "twitter:image": "https://docs.objectbox.io/~gitbook/ogimage/kSqRmDrNicE3QmU2oyBf", + "twitter:title": "Property Types | ObjectBox Docs", + "viewport": "width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" + }, + "title": "Property Types | ObjectBox Docs", + "url": "https://docs.objectbox.io/property-types", + "usage": { + "tokens": 175515 + } + }, + "meta": { + "usage": { + "tokens": 175515 + } + }, + "status": 20000 +} \ No newline at end of file diff --git a/context/relic_docs.json b/context/relic_docs.json new file mode 100644 index 0000000..e755bf3 --- /dev/null +++ b/context/relic_docs.json @@ -0,0 +1,81 @@ +{ + "code": 200, + "data": { + "description": "Relic is a modern, high-performance web server framework inspired by Shelf and built by the Serverpod team to meet the demanding requirements of modern web applications. Relic provides a robust, type-safe, and efficient foundation for building scalable web services.", + "external": { + "alternate": { + "https://docs.dartrelic.dev/": { + "data-rh": "true", + "hreflang": "x-default" + } + }, + "canonical": { + "https://docs.dartrelic.dev/": { + "data-rh": "true" + } + }, + "icon": { + "https://docs.dartrelic.dev/img/favicon.png": { + "data-rh": "true" + } + }, + "preconnect": { + "https://www.google-analytics.com/": {}, + "https://www.googletagmanager.com/": {} + }, + "prefetch": { + "https://docs.dartrelic.dev/assets/js/0ca634c5.c40e5698.js": {}, + "https://docs.dartrelic.dev/assets/js/17896441.35fb8100.js": {}, + "https://docs.dartrelic.dev/assets/js/22dd74f7.32b10889.js": {}, + "https://docs.dartrelic.dev/assets/js/2c10348c.c38ae9dc.js": {}, + "https://docs.dartrelic.dev/assets/js/2dc195cc.c7b109bb.js": {}, + "https://docs.dartrelic.dev/assets/js/441e1055.6248ef14.js": {}, + "https://docs.dartrelic.dev/assets/js/4edc808e.d18ceb40.js": {}, + "https://docs.dartrelic.dev/assets/js/5b00fbf1.0fa3fba9.js": {}, + "https://docs.dartrelic.dev/assets/js/5e95c892.753674ac.js": {}, + "https://docs.dartrelic.dev/assets/js/6783a3b2.0aa1428a.js": {}, + "https://docs.dartrelic.dev/assets/js/9412f430.c708638d.js": {}, + "https://docs.dartrelic.dev/assets/js/9576f52c.f029d9c3.js": {}, + "https://docs.dartrelic.dev/assets/js/a7bd4aaa.b870ce93.js": {}, + "https://docs.dartrelic.dev/assets/js/a94703ab.055f41b9.js": {}, + "https://docs.dartrelic.dev/assets/js/a9685393.8fdc4764.js": {}, + "https://docs.dartrelic.dev/assets/js/aba21aa0.1d722264.js": {}, + "https://docs.dartrelic.dev/assets/js/aca04f59.28672d9f.js": {}, + "https://docs.dartrelic.dev/assets/js/b4e2adc5.e5a3b2b2.js": {}, + "https://docs.dartrelic.dev/assets/js/b685e31b.f5eb8692.js": {}, + "https://docs.dartrelic.dev/assets/js/be2b5817.53d9f078.js": {}, + "https://docs.dartrelic.dev/assets/js/c644a7fe.ad3a69e0.js": {}, + "https://docs.dartrelic.dev/assets/js/c678908c.c170f033.js": {} + } + }, + "html": "\n\n\n🎯 Introduction | Relic\n\n\n\n\n\n\n\n\n\n\n
Skip to main content
Version: 1.0.0

Introduction

\n

Relic is a modern, high-performance web server framework inspired by Shelf and built by the Serverpod team to meet the demanding requirements of modern web applications. Relic provides a robust, type-safe, and efficient foundation for building scalable web services.

\n

Why Relic?​

\n

While Shelf has been a solid foundation for Dart web applications, several areas for improvement were identified. Rather than work around these limitations, Relic was created as a next-generation framework that maintains Shelf's familiar structure while delivering significant enhancements.

\n

Features​

\n

Relic aims to be a comprehensive web server framework for Dart. It provides a wide range of features out of the box and allows you to extend it with middleware. Here are some of the key features:

\n\n

🔒 Type Safe

Strongly typed APIs with validated HTTP headers.

⚡ Fast

High performance through trie-based routing and LRU caching.

🔌 WebSockets

Built-in WebSocket support for real time communication.

🔧 Middleware

Composable middleware pipeline with router integration.

👨‍💻 Dev Ready

Great IDE support, clear errors, and full documentation.

🚀 Production

Production-ready with high test coverage (3,000+ tests).

\n

Comparison with Shelf​

\n

Relic started out as a clone of Shelf, but has since diverged. An adapter is being built so that you can use Shelf middleware with Relic. Here is a comparison of some of the key differences:

\n
FeatureShelfRelic
Type Safety❌ Uses dynamic types✅ Strongly typed APIs with validated HTTP headers
Routing❌ Linear scan over routes✅ Built-in trie-based router
WebSockets❌ External packages✅ Built-in support
Performance✅ Good✅ Optimized (trie routing, LRU cache)
Path Parameters❌ String-based✅ Symbol-based
Static Files❌ Limited✅ Includes cache busting & etag support
Control Flow❌ Exceptions for control flow✅ Explicit state machine
Request State❌ Untyped context bag✅ Context properties with IDE discoverability
Body Model❌ Body and encoding are separate✅ Unified
Copyright © 2026 Serverpod authors.
\n\n", + "metadata": { + "description": "Relic is a modern, high-performance web server framework inspired by Shelf and built by the Serverpod team to meet the demanding requirements of modern web applications. Relic provides a robust, type-safe, and efficient foundation for building scalable web services.", + "docsearch:docusaurus_tag": "docs-default-1.0.0", + "docsearch:language": "en", + "docsearch:version": "1.0.0", + "docusaurus_locale": "en", + "docusaurus_tag": "docs-default-1.0.0", + "docusaurus_version": "1.0.0", + "generator": "Docusaurus v3.9.2", + "lang": "en", + "og:description": "Relic is a modern, high-performance web server framework inspired by Shelf and built by the Serverpod team to meet the demanding requirements of modern web applications. Relic provides a robust, type-safe, and efficient foundation for building scalable web services.", + "og:locale": "en", + "og:title": "🎯 Introduction | Relic", + "og:url": "https://docs.dartrelic.dev/", + "twitter:card": "summary_large_image", + "viewport": "width=device-width, initial-scale=1.0" + }, + "title": "🎯 Introduction | Relic", + "url": "https://docs.dartrelic.dev/", + "usage": { + "tokens": 14117 + } + }, + "meta": { + "usage": { + "tokens": 14117 + } + }, + "status": 20000 +} \ No newline at end of file diff --git a/context/relic_hello.json b/context/relic_hello.json new file mode 100644 index 0000000..ccb4d3d --- /dev/null +++ b/context/relic_hello.json @@ -0,0 +1,83 @@ +{ + "code": 200, + "data": { + "description": "Once you have Dart installed, it only takes a few lines of code to set up your Relic server. These are the steps you need to take to get a simple Hello world server up and running.", + "external": { + "alternate": { + "https://docs.dartrelic.dev/getting-started/hello-world-example": { + "data-rh": "true", + "hreflang": "x-default" + } + }, + "canonical": { + "https://docs.dartrelic.dev/getting-started/hello-world-example": { + "data-rh": "true" + } + }, + "icon": { + "https://docs.dartrelic.dev/img/favicon.png": { + "data-rh": "true" + } + }, + "preconnect": { + "https://www.google-analytics.com/": {}, + "https://www.googletagmanager.com/": {} + }, + "prefetch": { + "https://docs.dartrelic.dev/assets/js/0ca634c5.c40e5698.js": {}, + "https://docs.dartrelic.dev/assets/js/17896441.35fb8100.js": {}, + "https://docs.dartrelic.dev/assets/js/22dd74f7.32b10889.js": {}, + "https://docs.dartrelic.dev/assets/js/2c10348c.c38ae9dc.js": {}, + "https://docs.dartrelic.dev/assets/js/2dc195cc.c7b109bb.js": {}, + "https://docs.dartrelic.dev/assets/js/441e1055.6248ef14.js": {}, + "https://docs.dartrelic.dev/assets/js/4edc808e.d18ceb40.js": {}, + "https://docs.dartrelic.dev/assets/js/5b00fbf1.0fa3fba9.js": {}, + "https://docs.dartrelic.dev/assets/js/5e95c892.753674ac.js": {}, + "https://docs.dartrelic.dev/assets/js/6783a3b2.0aa1428a.js": {}, + "https://docs.dartrelic.dev/assets/js/9412f430.c708638d.js": {}, + "https://docs.dartrelic.dev/assets/js/9576f52c.f029d9c3.js": {}, + "https://docs.dartrelic.dev/assets/js/a7bd4aaa.b870ce93.js": {}, + "https://docs.dartrelic.dev/assets/js/a94703ab.055f41b9.js": {}, + "https://docs.dartrelic.dev/assets/js/a9685393.8fdc4764.js": {}, + "https://docs.dartrelic.dev/assets/js/aba21aa0.1d722264.js": {}, + "https://docs.dartrelic.dev/assets/js/aca04f59.28672d9f.js": {}, + "https://docs.dartrelic.dev/assets/js/b4e2adc5.e5a3b2b2.js": {}, + "https://docs.dartrelic.dev/assets/js/b685e31b.f5eb8692.js": {}, + "https://docs.dartrelic.dev/assets/js/be2b5817.53d9f078.js": {}, + "https://docs.dartrelic.dev/assets/js/c644a7fe.ad3a69e0.js": {}, + "https://docs.dartrelic.dev/assets/js/c678908c.c170f033.js": {}, + "https://docs.dartrelic.dev/assets/js/f22cf6c3.3fb896a1.js": {} + } + }, + "html": "\n\n\nHello world | Relic\n\n\n\n\n\n\n\n\n\n\n
Skip to main content
Version: 1.0.0

Hello world

\n

Once you have Dart installed, it only takes a few lines of code to set up your Relic server. These are the steps you need to take to get a simple Hello world server up and running.

\n

Create a Dart package​

\n

First, you need to create a new Dart package for your Relic server.

\n
dart create -t console-full hello_world
\n

Add the Relic dependency​

\n

Next, add the relic package as a dependency to your pubspec.yaml file.

\n
cd hello_world
dart pub add relic
\n

Edit the main file​

\n

Edit the bin/hello_world.dart:

\n
Hello world server
import 'package:relic/relic.dart';

/// A simple 'Hello World' server demonstrating basic Relic usage.
Future<void> main() async {
// Setup the app.
final app = RelicApp()
// Route with parameters (:name & :age).
..get('/user/:name/age/:age', helloHandler)
// Middleware on all paths below '/'.
..use('/', logRequests())
// Custom fallback - optional (default is 404 Not Found).
..fallback = respondWith(
(_) => Response.notFound(
body: Body.fromString(\"Sorry, that doesn't compute.\\n\"),
),
);

// Start the server (defaults to using port 8080).
await app.serve();
}

/// Handles requests to the hello endpoint with path parameters.
Response helloHandler(final Request req) {
final name = req.rawPathParameters[#name];
final age = int.parse(req.rawPathParameters[#age]!);

return Response.ok(
body: Body.fromString('Hello, $name! To think you are $age years old.\\n'),
);
}
\n

What this code does:

\n
    \n
  1. Router and route: RelicApp() configures routing and registers a /user route.
  2. \n
  3. Middleware: use('/', logRequests()) logs each request for all paths under /.
  4. \n
  5. Server and fallback: app.serve() starts on port 8080; fallback handles unmatched routes and returns a 404 (not found).
  6. \n
\n

The result is a server that responds with a personalized greeting when you send a GET request matching /user/:name/age/:age.

\n

Running locally​

\n

Start your server with:

\n
dart bin/hello_world.dart
\n

Then, open your browser and visit http://localhost:8080/user/Nova/age/27, or use curl:

\n
curl http://localhost:8080/user/Nova/age/37
\n

You should see:

\n
Hello Nova! To think you are 27 years old.
\n

Congratulations! You just ran your first Relic server.

\n
tip

Relic works with hot reload out of the box. ⚡️ To use it, start your server in Debug mode from your IDE (or use dart run --enable-vm-service bin/hello_world.dart). Ensure that Hot Reload On Save is enabled in your IDE settings (typically, set it to manual which will trigger hot reload when you save your file).

Copyright © 2026 Serverpod authors.
\n\n", + "metadata": { + "description": "Once you have Dart installed, it only takes a few lines of code to set up your Relic server. These are the steps you need to take to get a simple Hello world server up and running.", + "docsearch:docusaurus_tag": "docs-default-1.0.0", + "docsearch:language": "en", + "docsearch:version": "1.0.0", + "docusaurus_locale": "en", + "docusaurus_tag": "docs-default-1.0.0", + "docusaurus_version": "1.0.0", + "generator": "Docusaurus v3.9.2", + "lang": "en", + "og:description": "Once you have Dart installed, it only takes a few lines of code to set up your Relic server. These are the steps you need to take to get a simple Hello world server up and running.", + "og:locale": "en", + "og:title": "Hello world | Relic", + "og:url": "https://docs.dartrelic.dev/getting-started/hello-world-example", + "twitter:card": "summary_large_image", + "viewport": "width=device-width, initial-scale=1.0" + }, + "publishedTime": "Wed, 18 Feb 2026 09:05:14 GMT", + "title": "Hello world | Relic", + "url": "https://docs.dartrelic.dev/getting-started/hello-world-example", + "usage": { + "tokens": 21616 + } + }, + "meta": { + "usage": { + "tokens": 21616 + } + }, + "status": 20000 +} \ No newline at end of file diff --git a/lib/config/config.dart b/lib/config/config.dart index a6cfdc4..4e71f19 100644 --- a/lib/config/config.dart +++ b/lib/config/config.dart @@ -1,12 +1,12 @@ class Config { // Server settings static const int port = 9870; - + // IMPORTANT: Set this to your actual server host/domain // Used for generating userId: "username@serverHost" static const String serverHost = 'localhost:9870'; static const String serverName = 'Chat Server'; - + // JWT settings // TODO: Move to environment variable in production static const String jwtSecret = 'your_super_secret_key_change_in_production'; diff --git a/lib/models.dart b/lib/models.dart index 84ca816..5a6a701 100644 --- a/lib/models.dart +++ b/lib/models.dart @@ -1,18 +1,18 @@ import 'package:objectbox/objectbox.dart'; /// User entity - works for both HOME and GUEST servers -/// +/// /// On HOME server: /// - isLocal = true /// - stores password, encrypted private key /// - this is where identity lives -/// +/// /// On GUEST server: /// - isLocal = false /// - homeServer contains the URL of user's home /// - publicKey cached for verification -@Entity() -class User { +@Entity(uid: 3848231140411393897) +class ChatUser { @Id() int id = 0; @@ -37,7 +37,8 @@ class User { // Server relationship bool isLocal; // true = this is their home server - String? homeServer; // if guest, where is their home? (e.g., "https://my.server.com") + String? + homeServer; // if guest, where is their home? (e.g., "https://my.server.com") // Server-specific List roles; // ["admin", "moderator", "member"] @@ -48,7 +49,7 @@ class User { @Property(type: PropertyType.date) DateTime? lastSyncedAt; // When profile was last refreshed from home - User({ + ChatUser({ required this.userId, required this.username, required this.publicKey, @@ -68,8 +69,8 @@ class User { lastSyncedAt = lastSyncedAt; } -@Entity() -class Channel { +@Entity(uid: 2663861829934850300) +class ChatChannel { @Id() int id = 0; @@ -81,15 +82,15 @@ class Channel { @Property(type: PropertyType.date) DateTime createdAt; - Channel({ + ChatChannel({ required this.name, this.description, DateTime? createdAt, }) : createdAt = createdAt ?? DateTime.now(); } -@Entity() -class Message { +@Entity(uid: 2697867424319168785) +class ChatMessage { @Id() int id = 0; @@ -112,7 +113,7 @@ class Message { @Property(type: PropertyType.date) DateTime? editedAt; - Message({ + ChatMessage({ required this.channelId, required this.authorLocalId, required this.authorUserId, diff --git a/lib/server.dart b/lib/server.dart index a25b0ea..6564ff8 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -1,5 +1,3 @@ -import 'package:chat_server/models.dart'; -import 'package:chat_server/objectbox.g.dart'; import 'package:hotreloader/hotreloader.dart'; import 'package:relic/relic.dart'; @@ -9,13 +7,7 @@ import 'src/handlers/auth_handlers.dart'; import 'src/handlers/channel_handlers.dart'; import 'src/handlers/message_handlers.dart'; import 'src/handlers/public_handlers.dart'; -import 'src/utils/extensions.dart'; - -// Global ObjectBox store and boxes -late final Store store; -late final Box userBox; -late final Box channelBox; -late final Box messageBox; +import 'src/state/app_state.dart'; RelicApp? _currentApp; @@ -24,34 +16,34 @@ Future startServer() async { _currentApp = RelicApp() // ============ PUBLIC ENDPOINTS (no auth) ============ - + // Auth ..post('/auth/register', registerHandler) ..post('/auth/login', loginHandler) ..post('/auth/join', joinServerHandler) - + // Public identity verification (for cross-server) - ..get('/.well-known/users/', publicUserHandler) - ..get('/.well-known/verify/', publicVerifyHandler) + ..get('/.well-known/users/:username', publicUserHandler) + ..get('/.well-known/verify/:userId', publicVerifyHandler) ..get('/.well-known/server', publicServerInfoHandler) - + // ============ PROTECTED ENDPOINTS (requires auth) ============ ..use('/api', jwtMiddleware) - + // Current user ..get('/api/me', getMeHandler) - + // Channels ..get('/api/channels', getChannelsHandler) ..post('/api/channels', createChannelHandler) - ..get('/api/channels/', getChannelHandler) - + ..get('/api/channels/:channelId', getChannelHandler) + // Messages ..get('/api/messages', getMessagesHandler) ..post('/api/messages', sendMessageHandler) - ..put('/api/messages/', editMessageHandler) - ..delete('/api/messages/', deleteMessageHandler) - + ..put('/api/messages/:messageId', editMessageHandler) + ..delete('/api/messages/:messageId', deleteMessageHandler) + // ============ LOGGING & FALLBACK ============ ..use('/', logRequests()) ..fallback = respondWith((_) => Response.notFound()); @@ -68,11 +60,7 @@ Future startServer() async { } void main() async { - // Initialize ObjectBox - store = openStore(); - userBox = store.box(); - channelBox = store.box(); - messageBox = store.box(); + initializeStore(); // Enable hot reload in development await HotReloader.create( diff --git a/lib/src/auth/auth_middleware.dart b/lib/src/auth/auth_middleware.dart index 427883c..e65a967 100644 --- a/lib/src/auth/auth_middleware.dart +++ b/lib/src/auth/auth_middleware.dart @@ -1,7 +1,5 @@ import 'package:relic/relic.dart'; -import '../../config/config.dart'; -import '../utils/extensions.dart'; import 'crypto_service.dart'; final userIdProperty = ContextProperty('userId'); diff --git a/lib/src/auth/crypto_service.dart b/lib/src/auth/crypto_service.dart index bd3457e..bb369a5 100644 --- a/lib/src/auth/crypto_service.dart +++ b/lib/src/auth/crypto_service.dart @@ -1,13 +1,13 @@ import 'dart:convert'; +import 'dart:math'; import 'dart:typed_data'; import 'package:bcrypt/bcrypt.dart'; -import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; -import 'package:crypton/crypton.dart'; -import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:crypton/crypton.dart' as crypton; +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart' as jwt; -import '../config/config.dart'; +import '../../config/config.dart'; class KeyPair { final String publicKey; @@ -31,7 +31,7 @@ class KeyPair { class CryptoService { /// Generate a new Ed25519 key pair static KeyPair generateKeyPair() { - final keyPair = ECKeypair.fromRandom(); + final keyPair = crypton.ECKeypair.fromRandom(); return KeyPair( publicKey: keyPair.publicKey.toString(), privateKey: keyPair.privateKey.toString(), @@ -40,17 +40,19 @@ class CryptoService { /// Sign data with private key static String sign(String data, String privateKey) { - final privKey = ECPrivateKey.fromString(privateKey); - final signature = privKey.createSignature(data); - return signature.toString(); + final privKey = crypton.ECPrivateKey.fromString(privateKey); + final message = Uint8List.fromList(utf8.encode(data)); + final signature = privKey.createSHA256Signature(message); + return base64Encode(signature); } /// Verify signature with public key static bool verify(String data, String signature, String publicKey) { try { - final pubKey = ECPublicKey.fromString(publicKey); - final sig = ECSignature.fromString(signature); - return pubKey.verifySignature(data, sig); + final pubKey = crypton.ECPublicKey.fromString(publicKey); + final message = Uint8List.fromList(utf8.encode(data)); + final signatureBytes = Uint8List.fromList(base64Decode(signature)); + return pubKey.verifySHA256Signature(message, signatureBytes); } catch (_) { return false; } @@ -118,6 +120,8 @@ class EncryptedKey { } class AuthService { + static final _nonceRandom = Random.secure(); + /// Generate JWT token for user static String generateToken(String userId, String username) { final payload = { @@ -125,9 +129,9 @@ class AuthService { 'username': username, }; - final jwt = JWT(payload); - return jwt.sign( - SecretKey(Config.jwtSecret), + final token = jwt.JWT(payload); + return token.sign( + jwt.SecretKey(Config.jwtSecret), expiresIn: Config.tokenExpiry, ); } @@ -135,8 +139,8 @@ class AuthService { /// Verify JWT token and return payload static Map? verifyToken(String token) { try { - final jwt = JWT.verify(token, SecretKey(Config.jwtSecret)); - return Map.from(jwt.payload as Map); + final verified = jwt.JWT.verify(token, jwt.SecretKey(Config.jwtSecret)); + return Map.from(verified.payload as Map); } catch (_) { return null; } @@ -154,8 +158,8 @@ class AuthService { /// Generate a nonce for signature verification static String generateNonce() { - final bytes = List.generate(32, (_) => DateTime.now().microsecondsSinceEpoch % 256); - final randomBytes = sha256.convert(bytes as Uint8List); - return randomBytes.toString().substring(0, 32); + final bytes = List.generate(32, (_) => _nonceRandom.nextInt(256)); + final hash = sha256.convert(bytes); + return hash.toString().substring(0, 32); } } diff --git a/lib/src/handlers/auth_handlers.dart b/lib/src/handlers/auth_handlers.dart index ed5f92e..226d0ed 100644 --- a/lib/src/handlers/auth_handlers.dart +++ b/lib/src/handlers/auth_handlers.dart @@ -1,11 +1,44 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:chat_server/models.dart'; import 'package:chat_server/objectbox.g.dart'; import 'package:relic/relic.dart'; +import '../../config/config.dart'; import '../auth/auth_middleware.dart'; import '../auth/crypto_service.dart'; +import '../state/app_state.dart'; import '../utils/extensions.dart'; +class RemoteIdentity { + final String userId; + final String username; + final String publicKey; + final String? displayName; + final String? avatarUrl; + final String? bio; + + RemoteIdentity({ + required this.userId, + required this.username, + required this.publicKey, + this.displayName, + this.avatarUrl, + this.bio, + }); +} + +class ParsedUserId { + final String username; + final String host; + + ParsedUserId({ + required this.username, + required this.host, + }); +} + /// Register on HOME server /// Creates user with keypair for identity Future registerHandler(Request req) async { @@ -25,8 +58,12 @@ Future registerHandler(Request req) async { ); } - // Check if username exists - final query = userBox.query(User_.username.equals(username)).build(); + // Check if username exists on this home server + final query = userBox + .query( + ChatUser_.username.equals(username) & ChatUser_.isLocal.equals(true), + ) + .build(); final existing = query.findFirst(); query.close(); @@ -51,7 +88,7 @@ Future registerHandler(Request req) async { // Create user with userId format: "username@this.server" final userId = '${username}@${Config.serverHost}'; - final user = User( + final user = ChatUser( userId: userId, username: username, publicKey: keyPair.publicKey, @@ -93,7 +130,11 @@ Future loginHandler(Request req) async { ); } - final query = userBox.query(User_.username.equals(username)).build(); + final query = userBox + .query( + ChatUser_.username.equals(username) & ChatUser_.isLocal.equals(true), + ) + .build(); final user = query.findFirst(); query.close(); @@ -106,7 +147,8 @@ Future loginHandler(Request req) async { // Check if this is actually their home server if (!user.isLocal) { return Response.badRequest( - body: Body.fromString('This is not your home server. Login there instead.'), + body: + Body.fromString('This is not your home server. Login there instead.'), ); } @@ -143,12 +185,33 @@ Future joinServerHandler(Request req) async { final homeServer = body['homeServer'] as String?; // "https://homeserver.com" final timestamp = body['timestamp'] as int?; final signature = body['signature'] as String?; - final username = body['username'] as String?; // Validate required fields - if (userId == null || homeServer == null || timestamp == null || signature == null) { + if (userId == null || timestamp == null || signature == null) { return Response.badRequest( - body: Body.fromString('userId, homeServer, timestamp, and signature required'), + body: Body.fromString( + 'userId, timestamp, and signature required (homeServer optional)'), + ); + } + + final parsedUser = parseUserId(userId); + if (parsedUser == null) { + return Response.badRequest( + body: Body.fromString('Invalid userId format. Expected username@host'), + ); + } + + final resolvedHomeServer = normalizeHomeServer(homeServer ?? parsedUser.host); + + if (resolvedHomeServer == null) { + return Response.badRequest( + body: Body.fromString('Invalid homeServer URL'), + ); + } + + if (!homeServerMatchesUserIdHost(resolvedHomeServer, parsedUser.host)) { + return Response.badRequest( + body: Body.fromString('homeServer does not match userId host'), ); } @@ -160,14 +223,54 @@ Future joinServerHandler(Request req) async { ); } + // Fetch current identity from home server + final identity = await fetchIdentityFromHomeServer( + resolvedHomeServer, + parsedUser.username, + ); + + if (identity == null) { + return Response.badRequest( + body: Body.fromString('Could not verify identity with home server'), + ); + } + + // Ensure fetched identity matches what caller claims + if (identity.userId != userId || identity.username != parsedUser.username) { + return Response.forbidden( + body: Body.fromString('Identity mismatch from home server'), + ); + } + + // Verify signature + // Signature is over: "{userId}:{timestamp}" + final messageToVerify = '$userId:$timestamp'; + final isValid = + CryptoService.verify(messageToVerify, signature, identity.publicKey); + + if (!isValid) { + return Response.forbidden( + body: Body.fromString('Invalid signature'), + ); + } + // Check if user already exists on this server - final existingQuery = userBox.query(User_.userId.equals(userId)).build(); + final existingQuery = userBox.query(ChatUser_.userId.equals(userId)).build(); final existingUser = existingQuery.findFirst(); existingQuery.close(); if (existingUser != null) { - // User already joined - just return a new token - final token = AuthService.generateToken(existingUser.userId, existingUser.username); + existingUser.username = identity.username; + existingUser.displayName = identity.displayName; + existingUser.avatarUrl = identity.avatarUrl; + existingUser.bio = identity.bio; + existingUser.publicKey = identity.publicKey; + existingUser.homeServer = resolvedHomeServer; + existingUser.lastSyncedAt = DateTime.now(); + userBox.put(existingUser); + + final token = + AuthService.generateToken(existingUser.userId, existingUser.username); userLocalIdProperty[req] = existingUser.id; return Response.ok( @@ -181,34 +284,18 @@ Future joinServerHandler(Request req) async { ); } - // Fetch public key from home server - final publicKey = await fetchPublicKeyFromHomeServer(homeServer, username ?? userId.split('@').first); - - if (publicKey == null) { - return Response.badRequest( - body: Body.fromString('Could not verify identity with home server'), - ); - } - - // Verify signature - // Signature is over: "{userId}:{timestamp}" - final messageToVerify = '$userId:$timestamp'; - final isValid = CryptoService.verify(messageToVerify, signature, publicKey); - - if (!isValid) { - return Response.forbidden( - body: Body.fromString('Invalid signature'), - ); - } - // Create local shadow account - final user = User( + final user = ChatUser( userId: userId, - username: username ?? userId.split('@').first, - publicKey: publicKey, + username: identity.username, + displayName: identity.displayName, + avatarUrl: identity.avatarUrl, + bio: identity.bio, + publicKey: identity.publicKey, isLocal: false, - homeServer: homeServer, + homeServer: resolvedHomeServer, roles: ['member'], + lastSyncedAt: DateTime.now(), ); final localId = userBox.put(user); @@ -227,37 +314,130 @@ Future joinServerHandler(Request req) async { ); } -/// Fetch public key from user's home server -Future fetchPublicKeyFromHomeServer( +/// Fetch public identity from user's home server +Future fetchIdentityFromHomeServer( String homeServer, String username, ) async { + final client = HttpClient(); + client.connectionTimeout = const Duration(seconds: 5); + try { - // Remove trailing slash final baseUrl = homeServer.replaceAll(RegExp(r'/$'), ''); + final endpoint = Uri.parse( + '$baseUrl/.well-known/users/${Uri.encodeComponent(username)}'); - // Use http client to fetch - // Endpoint: GET /.well-known/users/{username} - // Returns: { publicKey, userId, ... } + final request = await client.getUrl(endpoint); + request.headers.set(HttpHeaders.acceptHeader, 'application/json'); + final response = await request.close(); - // For now, return null to indicate we need http client - // This will be implemented with actual HTTP client - print('Would fetch: $baseUrl/.well-known/users/$username'); + if (response.statusCode != HttpStatus.ok) { + return null; + } - // TODO: Implement actual HTTP request - // final response = await http.get(Uri.parse('$baseUrl/.well-known/users/$username')); - // if (response.statusCode == 200) { - // final data = jsonDecode(response.body); - // return data['publicKey'] as String; - // } + final responseBody = await utf8.decodeStream(response); + final dynamic decoded = jsonDecode(responseBody); + if (decoded is! Map) { + return null; + } - return null; + final data = Map.from(decoded); + final userId = data['userId'] as String?; + final fetchedUsername = data['username'] as String?; + final publicKey = data['publicKey'] as String?; + + if (userId == null || fetchedUsername == null || publicKey == null) { + return null; + } + + return RemoteIdentity( + userId: userId, + username: fetchedUsername, + publicKey: publicKey, + displayName: data['displayName'] as String?, + avatarUrl: data['avatarUrl'] as String?, + bio: data['bio'] as String?, + ); } catch (e) { print('Error fetching public key: $e'); return null; + } finally { + client.close(force: true); } } +ParsedUserId? parseUserId(String userId) { + final parts = userId.split('@'); + if (parts.length != 2) { + return null; + } + + final username = parts[0].trim(); + final host = parts[1].trim(); + + if (username.isEmpty || host.isEmpty) { + return null; + } + + return ParsedUserId(username: username, host: host); +} + +String? normalizeHomeServer(String homeServerOrHost) { + final trimmed = homeServerOrHost.trim(); + if (trimmed.isEmpty) { + return null; + } + + final hasScheme = trimmed.contains('://'); + final candidate = + hasScheme ? trimmed : '${defaultSchemeForHost(trimmed)}://$trimmed'; + final uri = Uri.tryParse(candidate); + + if (uri == null || uri.host.isEmpty) { + return null; + } + + return uri + .replace(path: '', query: null, fragment: null) + .toString() + .replaceAll(RegExp(r'/$'), ''); +} + +String defaultSchemeForHost(String host) { + final lowerHost = host.toLowerCase(); + if (lowerHost.startsWith('localhost') || + lowerHost.startsWith('127.') || + lowerHost.startsWith('[::1]')) { + return 'http'; + } + + return 'https'; +} + +bool homeServerMatchesUserIdHost(String homeServerUrl, String userIdHost) { + final uri = Uri.tryParse(homeServerUrl); + if (uri == null || uri.host.isEmpty) { + return false; + } + + final normalizedUserIdHost = userIdHost.trim().toLowerCase(); + final urlHost = uri.host.toLowerCase(); + final urlHostWithPort = uri.hasPort ? '$urlHost:${uri.port}' : urlHost; + + if (normalizedUserIdHost == urlHostWithPort) { + return true; + } + + if (!uri.hasPort) { + final defaultPort = uri.scheme == 'http' ? 80 : 443; + if (normalizedUserIdHost == '$urlHost:$defaultPort') { + return true; + } + } + + return false; +} + /// Get current user's profile Future getMeHandler(Request req) async { final userId = userIdProperty[req]; @@ -268,7 +448,7 @@ Future getMeHandler(Request req) async { ); } - final query = userBox.query(User_.userId.equals(userId)).build(); + final query = userBox.query(ChatUser_.userId.equals(userId)).build(); final user = query.findFirst(); query.close(); diff --git a/lib/src/handlers/channel_handlers.dart b/lib/src/handlers/channel_handlers.dart index 5661db0..9c7e894 100644 --- a/lib/src/handlers/channel_handlers.dart +++ b/lib/src/handlers/channel_handlers.dart @@ -2,6 +2,7 @@ import 'package:chat_server/models.dart'; import 'package:chat_server/objectbox.g.dart'; import 'package:relic/relic.dart'; +import '../state/app_state.dart'; import '../utils/extensions.dart'; /// Get all channels @@ -25,7 +26,8 @@ Future getChannelsHandler(Request req) async { /// Create a channel (requires 'admin' role) Future createChannelHandler(Request req) async { final body = await req.asJson(); - final name = (body['name'] as String?)?.trim().toLowerCase().replaceAll(' ', '-'); + final name = + (body['name'] as String?)?.trim().toLowerCase().replaceAll(' ', '-'); final description = body['description'] as String?; if (name == null || name.isEmpty) { @@ -35,14 +37,15 @@ Future createChannelHandler(Request req) async { } // Check if channel exists - final existing = channelBox.query(Channel_.name.equals(name)).build().findFirst(); + final existing = + channelBox.query(ChatChannel_.name.equals(name)).build().findFirst(); if (existing != null) { return Response.badRequest( body: Body.fromString('Channel already exists'), ); } - final channel = Channel(name: name, description: description); + final channel = ChatChannel(name: name, description: description); final id = channelBox.put(channel); return Response.ok( @@ -56,7 +59,14 @@ Future createChannelHandler(Request req) async { } /// Get channel by ID -Future getChannelHandler(Request req, String channelIdStr) async { +Future getChannelHandler(Request req) async { + final channelIdStr = req.rawPathParameters[#channelId]; + if (channelIdStr == null) { + return Response.badRequest( + body: Body.fromString('Invalid channel ID'), + ); + } + final channelId = int.tryParse(channelIdStr); if (channelId == null) { return Response.badRequest( diff --git a/lib/src/handlers/message_handlers.dart b/lib/src/handlers/message_handlers.dart index da63d68..a8164e2 100644 --- a/lib/src/handlers/message_handlers.dart +++ b/lib/src/handlers/message_handlers.dart @@ -3,6 +3,7 @@ import 'package:chat_server/objectbox.g.dart'; import 'package:relic/relic.dart'; import '../auth/auth_middleware.dart'; +import '../state/app_state.dart'; import '../utils/extensions.dart'; /// Get messages for a channel @@ -36,11 +37,12 @@ Future getMessagesHandler(Request req) async { // Get messages final query = messageBox - .query(Message_.channelId.equals(channelId)) - .order(Message_.createdAt, flags: Order.descending) + .query(ChatMessage_.channelId.equals(channelId)) + .order(ChatMessage_.createdAt, flags: Order.descending) .build(); - final messages = query.find(limit: limit); + query.limit = limit; + final messages = query.find(); query.close(); return Response.ok( @@ -99,7 +101,7 @@ Future sendMessageHandler(Request req) async { } // Get user info for caching - final userQuery = userBox.query(User_.userId.equals(userId)).build(); + final userQuery = userBox.query(ChatUser_.userId.equals(userId)).build(); final user = userQuery.findFirst(); userQuery.close(); @@ -110,7 +112,7 @@ Future sendMessageHandler(Request req) async { } // Create message with cached author info - final message = Message( + final message = ChatMessage( channelId: channelId, authorLocalId: user.id, authorUserId: user.userId, @@ -136,7 +138,7 @@ Future sendMessageHandler(Request req) async { } /// Edit a message -Future editMessageHandler(Request req, String messageIdStr) async { +Future editMessageHandler(Request req) async { final userId = userIdProperty[req]; if (userId == null) { @@ -145,6 +147,13 @@ Future editMessageHandler(Request req, String messageIdStr) async { ); } + final messageIdStr = req.rawPathParameters[#messageId]; + if (messageIdStr == null) { + return Response.badRequest( + body: Body.fromString('Invalid message ID'), + ); + } + final messageId = int.tryParse(messageIdStr); if (messageId == null) { return Response.badRequest( @@ -160,7 +169,7 @@ Future editMessageHandler(Request req, String messageIdStr) async { } // Get user to verify ownership - final userQuery = userBox.query(User_.userId.equals(userId)).build(); + final userQuery = userBox.query(ChatUser_.userId.equals(userId)).build(); final user = userQuery.findFirst(); userQuery.close(); @@ -193,7 +202,7 @@ Future editMessageHandler(Request req, String messageIdStr) async { } /// Delete a message -Future deleteMessageHandler(Request req, String messageIdStr) async { +Future deleteMessageHandler(Request req) async { final userId = userIdProperty[req]; if (userId == null) { @@ -202,6 +211,13 @@ Future deleteMessageHandler(Request req, String messageIdStr) async { ); } + final messageIdStr = req.rawPathParameters[#messageId]; + if (messageIdStr == null) { + return Response.badRequest( + body: Body.fromString('Invalid message ID'), + ); + } + final messageId = int.tryParse(messageIdStr); if (messageId == null) { return Response.badRequest( @@ -217,7 +233,7 @@ Future deleteMessageHandler(Request req, String messageIdStr) async { } // Get user to verify ownership - final userQuery = userBox.query(User_.userId.equals(userId)).build(); + final userQuery = userBox.query(ChatUser_.userId.equals(userId)).build(); final user = userQuery.findFirst(); userQuery.close(); diff --git a/lib/src/handlers/public_handlers.dart b/lib/src/handlers/public_handlers.dart index 9ddd232..2609d01 100644 --- a/lib/src/handlers/public_handlers.dart +++ b/lib/src/handlers/public_handlers.dart @@ -1,18 +1,28 @@ -import 'package:chat_server/models.dart'; import 'package:chat_server/objectbox.g.dart'; import 'package:relic/relic.dart'; +import '../../config/config.dart'; +import '../state/app_state.dart'; import '../utils/extensions.dart'; /// Public endpoint for cross-server identity verification /// Called by other servers to verify a user's public key -/// +/// /// GET /.well-known/users/{username} /// Returns: { userId, publicKey, username, displayName, avatarUrl } -Future publicUserHandler(Request req, String username) async { - final query = userBox.query( - User_.username.equals(username) & User_.isLocal.equals(true), - ).build(); +Future publicUserHandler(Request req) async { + final username = req.rawPathParameters[#username]; + if (username == null) { + return Response.badRequest( + body: Body.fromString('Username required'), + ); + } + + final query = userBox + .query( + ChatUser_.username.equals(username) & ChatUser_.isLocal.equals(true), + ) + .build(); final user = query.findFirst(); query.close(); @@ -39,13 +49,22 @@ Future publicUserHandler(Request req, String username) async { /// Public endpoint to verify a user exists /// Lightweight check without full profile -/// +/// /// GET /.well-known/verify/{userId} /// Returns: { exists, userId, publicKey } -Future publicVerifyHandler(Request req, String userId) async { - final query = userBox.query( - User_.userId.equals(userId) & User_.isLocal.equals(true), - ).build(); +Future publicVerifyHandler(Request req) async { + final userId = req.rawPathParameters[#userId]; + if (userId == null) { + return Response.badRequest( + body: Body.fromString('userId required'), + ); + } + + final query = userBox + .query( + ChatUser_.userId.equals(userId) & ChatUser_.isLocal.equals(true), + ) + .build(); final user = query.findFirst(); query.close(); @@ -70,7 +89,7 @@ Future publicVerifyHandler(Request req, String userId) async { } /// Public server info -/// +/// /// GET /.well-known/server /// Returns: { serverHost, serverName, version } Future publicServerInfoHandler(Request req) async { diff --git a/lib/src/utils/extensions.dart b/lib/src/utils/extensions.dart index 9675b66..39d2b20 100644 --- a/lib/src/utils/extensions.dart +++ b/lib/src/utils/extensions.dart @@ -2,8 +2,6 @@ import 'dart:convert'; import 'package:relic/relic.dart'; -import '../config/config.dart'; - extension RequestHelper on Request { /// Parses the request body as JSON Future> asJson() async { diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..39d8412 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,597 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" + url: "https://pub.dev" + source: hosted + version: "1.6.5" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + bcrypt: + dependency: "direct main" + description: + name: bcrypt + sha256: "6073a700cbbc59f1d4ab27cd532755e3de5e676c4941f535f351374df849270b" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.dev" + source: hosted + version: "4.1.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + url: "https://pub.dev" + source: hosted + version: "2.4.13" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" + url: "https://pub.dev" + source: hosted + version: "8.12.3" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.dev" + source: hosted + version: "4.11.1" + collection: + dependency: "direct main" + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + crypton: + dependency: "direct main" + description: + name: crypton + sha256: "17b6631fbf89e389d421b46629132287ed37d601b2ad1357445826ab85022271" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + dart_jsonwebtoken: + dependency: "direct main" + description: + name: dart_jsonwebtoken + sha256: b268ff9bb87a16360a397f1accb50e7c781384e1119010b34f6d7c25820c44d8 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" + source: hosted + version: "2.3.6" + ed25519_edwards: + dependency: transitive + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flat_buffers: + dependency: transitive + description: + name: flat_buffers + sha256: "380bdcba5664a718bfd4ea20a45d39e13684f5318fcd8883066a55e21f37f4c3" + url: "https://pub.dev" + source: hosted + version: "23.5.26" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hotreloader: + dependency: "direct main" + description: + name: hotreloader + sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b + url: "https://pub.dev" + source: hosted + version: "4.3.0" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.0" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + meta: + dependency: transitive + description: + name: meta + sha256: "9f29b9bcc8ee287b1a31e0d01be0eae99a930dbffdaecf04b3f3d82a969f296f" + url: "https://pub.dev" + source: hosted + version: "1.18.1" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + objectbox: + dependency: "direct main" + description: + name: objectbox + sha256: "3d1cb5f9aa564f95c76ba251299f6cb1591c3dd8ff05fd76fa0549d899d9fe31" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + objectbox_generator: + dependency: "direct dev" + description: + name: objectbox_generator + sha256: "777924e14daf18603a023b346333acac1c4fce9fe4a715b85d98cfcc723d9a1a" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + relic: + dependency: "direct main" + description: + name: relic + sha256: d87e88a2dd758b67a03bfbedd7f62ef81f44734a4851ec51547b51502cd2f114 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + relic_core: + dependency: transitive + description: + name: relic_core + sha256: caefeef3a85580357cf2d6f70c4a3f9de3e2375ec22196aa983b84c8f463c960 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + relic_io: + dependency: transitive + description: + name: relic_io + sha256: "9f2148ae0b8ca60e241c980e37216eb416e1342d51992266fcd5d18b8092b820" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.9.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6112caa..3a219a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,19 +3,19 @@ description: A self-hosted chat server with home server identity model. version: 0.1.0 environment: - sdk: '>=3.0.0 <4.0.0' + sdk: ">=3.0.0 <4.0.0" dependencies: - relic: ^1.0.0 - objectbox: ^4.0.0 - dart_jsonwebtoken: ^2.14.0 - bcrypt: ^1.1.3 - crypton: ^2.2.1 - crypto: ^3.0.3 - collection: ^1.18.0 - hotreloader: ^1.0.6 + relic: + objectbox: + dart_jsonwebtoken: + bcrypt: + crypton: + crypto: + collection: + hotreloader: dev_dependencies: - objectbox_generator: ^4.0.0 - build_runner: ^2.4.0 - lints: ^4.0.0 + objectbox_generator: + build_runner: + lints: