Workflows
Below you can find some typical workflows for using the TMC and example command lines. Refer to the detailed documentation of each command for a complete list of available flags and arguments.
tmc <command> --help
Create a Repository
To create an empty repository named my-catalog in the folder tm-catalog under your user home directory, execute
tmc repo add --type file my-catalog ~/tm-catalog
If the directory does not exist, it will be created when you import the first TM into the repository.
See repo add for more details on how to create repositories.
Manage The List of Repositories
Most subcommands of tmc operate by default on the list of named repositories stored in its config.json file, unless a single one of them is selected
by --repo flag or a local repository is defined by --directory flag. The default location of the config.json file is ~/.tm-catalog. You can
override the default config directory with the --config flag. There must be at least one repository to serve specified in config.json file or with the --repo or --directory flags.
To view and modify the list of repositories in the config file, use command repo and its subcommands. For example,
tmc repo list
tmc repo show my-catalog
tmc repo toggle-enaled my-catalog
tmc repo remove my-catalog
You can also use any directory as a storage space for an unnamed local repository. You will need to pass a --directory flag to most commands.
Import Thing Models
To be imported into a catalog, a TM must be valid according to the W3C Thing Model schema. In addition to that, some minimal key fields defined by schema.org are required. The fields are:
schema:author/schema:name(https://schema.org/author)schema:manufacturer/schema:name(https://schema.org/manufacturer)schema:mpn(https://schema.org/mpn)
These fields together build the mandatory part of TM name.
You may also want to set the field version/model (https://www.w3.org/TR/wot-thing-description11/#versioninfo) to track
and communicate the extent of changes between versions of TMs with the same TM name.
You can check if a TM can be imported by validating it beforehand:
tmc validate my-tm.json
Import a TM or a folder with multiple TMs into the catalog:
tmc import my-tm.json
tmc import ./my-tms
Attachments
When importing a folder, the import command can be used with the --with-attachments flag to import attachments along with the TMs. An attachment is linked to a TM by placing it into a subfolder whose name exactly matches the TM’s filename (including its extension).
For example:
- If your TM file is:
../example-catalog/.tmc/omniuser/omnicorp/senseall/v1.0.0-20241008124326-15af48381cf7.tm.json - Then an attachment (e.g.,
readme.md) for this TM would be placed at:../example-catalog/.tmc/omniuser/omnicorp/senseall/.attachments/v1.0.0-20241008124326-15af48381cf7.tm.json/readme.md
Input Sanitization
Please pay attention to the values of manufacturer, author, and mpn as they will be sanitized following the rules below:
- spaces will be removed
- all letters will become lowercase
- characters below will be replaced with
-:_+&=:/
- all characters with an accent will be replaced with their versions without an accent, e.g.,
öwill becomeoe,àwill becomea.
In the end, there will be only letters, numbers and - remaining.
You can refer to SanitizeName function at internal/utils/util.go.
Find and fetch a TM
tmc list --filter.mpn poc1000
tmc fetch siemens/siemens/poc1000/v1.0.1-20240407094932-5a3840060b05.tm.json
You can fetch a specific version of a TM by fetching by ID as above, or you can fetch the latest TM that matches a given name and, optionally, a part of semantic version.
Examples:
tmc fetch siemens/siemens/poc1000
tmc fetch siemens/siemens/poc1000:v1
tmc fetch siemens/siemens/poc1000:v1.0.1
Publish a Catalog to a Git Forge
Initialize the directory where your file repository is located as a git repository and use the git workflows to commit and push it to your git forge, like GitHub or GitLab.
You may want to add the *.lock files to your .gitignore, but it’s not mandatory
echo "*.lock" >> .gitignore
To use the published catalog as a “http” repository on the consumer side, you have to use the URL, under which all files
of the git repository can be retrieved by HTTP GET request by either appending their relative paths to the URL or
substituting a placeholder with the relative path. This URL will differ between different git forges.
For example, for GitHub, it has this form https://raw.githubusercontent.com/<group>/<repository>/refs/heads/main,
where you should substitute <group> and <repository> with actual names. For GitLab, you can use the REST API
endpoint
https://gitlab.example.com/api/v4/projects/<project-id>/repository/files/{{ID}}?ref=main, where
you replace <project-id> with the numeric id of the GitLab project.
If the git repository is private, an access token needs to be configured using tmc remote set-auth
This method has the advantage that the infrastructure of the forges is used and no custom infrastructure needs to be maintained by the creator. The downside is that any contribution has to go through a git workflow, which might not be an accessible option for system integrators. In addition, products will most likely want to deploy a private catalog with a curated list of TMs, without relying on a forge.
Expose a Catalog for HTTP Clients
To expose a catalog over HTTP, start a server:
tmc serve
An OpenAPI description of the API is available for ease of integration.
Once a catalog is exposed with tmc serve, it can be configured as a repository of type tmc on other clients. Users
can push to a hosted catalog using the REST API, without using git workflow and hosting can happen on the edge within a
product.
To make things easier, we build a tmc container image which runs the cli as a server. That image doesn’t
have any TMs inside it. A creator can then simply serve a file or local repository, by mapping its directory
or volume into the container as follows:
docker run --rm --name tm-catalog -p 8080:8080 -v$(pwd):/thingmodels ghcr.io/wot-oss/tmc:latest
Catalog as S3 bucket
In order to quickly getting started with S3, we recommend to use localstack (requires docker) and awslocal for local developments. Once installed:
- start localstack:
localstack start - create a bucket in s3 by running:
awslocal s3api create-bucket --bucket tmc-bucket --region eu-central-1 --create-bucket-configuration LocationConstraint=eu-central-1 - copy the tmc into the newly created bucket:
awslocal s3 cp <local_repo_folder> s3://tmc-bucket --recursive --endpoint-url=http://localhost:4566 - create the S3 repo configuration in config.json:
{ "s3repo": { "description": "", "aws_bucket": "tmc-bucket", "aws_region": "eu-central-1", "aws_endpoint": "http://localhost:4566", "aws_access_key_id":"some access key", "aws_secret_access_key":"some secret", "type": "s3" } } - run tmc. the s3 repo should be accessible just as any other repo, you’ve been using before.
JWT Validation for API Requests
The serve command can be configured with the --jwtValidation flag to enforce security by requiring valid JWTs for incoming API requests.
Configuration
--jwtValidation- Enables JWT-based access control for the API server.
--jwksURL=<url>- Specifies the JWKS URL to retrieve public keys for verifying JWT signatures.
- Example:
http://127.0.0.1:8100/.well-known/jwks.json.
--jwtServiceID=<serviceID>- String that represents the audience (
audclaim) required in valid JWTs. - Example:
"myServiceID".
- String that represents the audience (
Behavior with JWT Validation
When the --jwtValidation flag is provided:
1. Bearer Token Requirement
- All incoming requests must include a valid Bearer token in the
Authorizationheader.
2. JWKS Validation
- Incoming tokens are validated against the JSON Web Key Sets (JWKS) at the URL specified in
--jwksURL. - The server checks the following:
- JWT signature is valid and matches the public key(s) defined in the JWKS.
- JWT is issued by the issuer URL corresponding to
--jwksURL. - JWT audience (
audclaim) matches the value specified in--jwtServiceID.
3. Scope-Based Access Control
- The JWT must include a
scopeclaim, which is an array of strings defining user permissions. - Each scope string determines the user’s access rights to specific endpoints, as defined in the Scope Table (explained below).
- Example Scope Claim:
"scope":["tmc.ns.myNamespace.read","tmc.ns.myNamespace.write"] - Default Scopes Configuration: a set of default scopes can be defined in a separate JSON file, specified with the
--defaultScopesPathflag. These scopes are automatically added to any user request, effectively extending the user’s token scopes. This allows for defining baseline access that applies to all requests, regardless of the scopes present in the user’s individual JWT.
For example, a default_scopes.json file might look like this:
{
"scopes": [
"tmc.ns.*.read",
"tmc.ns.omnicorp.write"
]
}
With this configuration, all users would implicitly gain tmc.ns.*.read (read access across all namespaces) and tmc.ns.omnicorp.write permissions, in addition to any scopes explicitly granted in their JWT. With a default configuration file, the catalog when run with --jwtValidation flag still requires a token, but the scopes array may be empty. In this case, the user’s access will be limited solely to the endpoints defined in that default configuration file.
- Additionally, the scope prefix can be configured by setting
--jwtScopesPrefixflag. E.g., when set to--jwtScopesPrefix "myScopePrefix", all scopes are expected to bemyScopePrefix.tmc.*
4. Token Validation Details
The token is confirmed to be valid if:
Its signature is verified using a public key from the JWKS. The token has not expired (checks the exp claim). It is issued by the expected issuer URL. The aud claim equals the value of –jwtServiceID. The scope claim contains sufficient permissions for the requested endpoint.
5. Error Handling
Requests without a valid Bearer token will result in an HTTP 401 Unauthorized error.
6. Scope Table
| Scope | /thing-models/{tmID}/attachments/{attachmentFileName} (GET) | /thing-models/{tmID}/attachments/{attachmentFileName} (PUT) | /thing-models/{tmID}/attachments/{attachmentFileName} (DELETE) | /thing-models/{tmName}/{tmVersion}/attachments/{attachmentFileName} (GET) | /thing-models/{tmName}/{tmVersion}/attachments/{attachmentFileName} (PUT) | /thing-models/{tmName}/{tmVersion}/attachments/{attachmentFileName} (DELETE) | Inventory (GET) | /thing-models/{tmID} (GET) | /thing-models/{tmID} (DELETE) | /thing-models/.latest/{fetchName} (GET) | /thing-models/.latest/{fetchName} (POST) | /thing-models (POST) | /repos (GET) | /info* (GET) | /health* (GET) |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| tmc.ns.{namespace}.read: Reading TMs, their metadata and attachments | if tmID == namespace | no | no | if tmID == namespace | no | no | if tmID == namespace | if tmID == namespace | no | if tmID == namespace | no | no | no | no | no |
| tmc.ns.{namespace}.write: Adding new TMs and attachments | no | if tmID == namespace | no | no | if tmID == namespace | no | no | no | no | no | if tmID == namespace | if tmID == namespace | no | no | no |
| tmc.ns.{namespace}.attachments.delete: Deleting attachments | no | no | if tmID == namespace | no | no | if tmID == namespace | no | no | no | no | no | no | no | no | |
| tmc.ns.{namespace}.thingmodels.delete: Deleting TMs (not desired, thus separate scope) | no | no | no | no | no | no | no | no | if tmID == namespace | no | no | no | no | no | no |
| tmc.repos.read: Reading /repos | no | no | no | no | no | no | no | no | no | no | no | no | yes | no | no |
| tmc.internal.read: Reading everything under /info | no | no | no | no | no | no | no | no | no | no | no | no | no | yes | no |
| tmc.health.read: Reading everything under /healthz | no | no | no | no | no | no | no | no | no | no | no | no | no | no | yes |
| tmc.admin: everything's allowed | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
* can be used as a wildcard at the place of {namespace} in scopes to access all namespaces in tmc. (e.g., tm.ns.*.read)