diff --git a/src/cli/broker.toit b/src/cli/broker.toit index af303b87e..cad7299d0 100644 --- a/src/cli/broker.toit +++ b/src/cli/broker.toit @@ -146,7 +146,6 @@ class Broker: --part-id=id cli_.cache.get-file-path key: | store/FileStore | broker-connection_.pod-registry-upload-pod-part contents --part-id=id - --scope=server-config.scope store.save contents key := cache-key-pod-manifest --broker-config=server-config @@ -154,12 +153,10 @@ class Broker: cli_.cache.get-file-path key: | store/FileStore | encoded := ubjson.encode manifest broker-connection_.pod-registry-upload-pod-manifest encoded --pod-id=pod.id - --scope=server-config.scope store.save encoded description-ids := broker-connection_.pod-registry-descriptions --fleet-id=fleet-id - --scope=server-config.scope --names=[pod.name] --create-if-absent @@ -219,7 +216,6 @@ class Broker: cli_.cache.get cache-key: | store/FileStore | trivial := build-trivial-patch patch.bits_ broker-connection_.upload-firmware trivial - --scope=server-config.scope --firmware-id=trivial-id store.save-via-writer: | writer/io.Writer | trivial.do: writer.write it @@ -235,7 +231,6 @@ class Broker: trivial-old := cli_.cache.get cache-key: | store/FileStore | downloaded := null catch: downloaded = broker-connection_.download-firmware - --scope=server-config.scope --id=old-id if not downloaded: cli_.ui.emit --warning "Failed to download old firmware for patch $old-id -> $trivial-id." @@ -273,7 +268,6 @@ class Broker: to64 := base64.encode patch.to_ --url-mode cli_.ui.emit --info "Uploading patch $from64 -> $to64 ($diff-size)." broker-connection_.upload-firmware diff - --scope=server-config.scope --firmware-id=diff-id store.save-via-writer: | writer/io.Writer | diff.do: writer.write it @@ -306,7 +300,6 @@ class Broker: encoded-manifest := cli_.cache.get manifest-key: | store/FileStore | bytes := broker-connection_.pod-registry-download-pod-manifest --pod-id=pod-id - --scope=server-config.scope store.save bytes manifest := ubjson.decode encoded-manifest return Pod.from-manifest @@ -319,7 +312,6 @@ class Broker: cli_.cache.get key: | store/FileStore | bytes := broker-connection_.pod-registry-download-pod-part part-id - --scope=server-config.scope store.save bytes list-pods --names/List -> Map: @@ -329,7 +321,6 @@ class Broker: else: descriptions = broker-connection_.pod-registry-descriptions --fleet-id=fleet-id - --scope=server-config.scope --names=names --no-create-if-absent result := {:} @@ -341,7 +332,6 @@ class Broker: delete --description-names/List: descriptions := broker-connection_.pod-registry-descriptions --fleet-id=fleet-id - --scope=server-config.scope --names=description-names --no-create-if-absent unknown-pod-descriptions := [] @@ -410,7 +400,6 @@ class Broker: descriptions := broker-connection_.pod-registry-descriptions --fleet-id=fleet-id - --scope=server-config.scope --names=names.to-list --no-create-if-absent @@ -754,14 +743,14 @@ class Broker: store.with-tmp-directory: | tmp-dir | // TODO(florian): do we want to rely on the cache, or should we // do a check to see if the files are really uploaded? - device-scope := Scope.from-organization-id device.organization-id + // Note: every device in this fleet uses the broker's scope. + // device.organization-id is guaranteed to equal the broker's + // configured scope, so we don't need to pass it explicitly. broker-connection_.upload-image program.image32 --app-id=id - --scope=device-scope --word-size=32 file.write-contents program.image32 --path="$tmp-dir/image32.bin" broker-connection_.upload-image program.image64 - --scope=device-scope --app-id=id --word-size=64 file.write-contents program.image64 --path="$tmp-dir/image64.bin" diff --git a/src/cli/brokers/broker.toit b/src/cli/brokers/broker.toit index 302057a72..194db00af 100644 --- a/src/cli/brokers/broker.toit +++ b/src/cli/brokers/broker.toit @@ -98,30 +98,30 @@ interface BrokerCli implements Authenticatable: update-goals --device-ids/List --goals/List -> none /** - Uploads an application image with the given $app-id so that a device in - $scope can fetch it. + Uploads an application image with the given $app-id. + + Each $BrokerCli instance is bound to a single $Scope (through its + server config); the image is uploaded for devices in that scope. There may be multiple images for the same $app-id, that differ in the $word-size. Generally $word-size is either 32 or 64. */ upload-image - --scope/Scope --app-id/Uuid --word-size/int contents/ByteArray -> none /** - Uploads a firmware with the given $firmware-id so that a device in - $scope can fetch it. + Uploads a firmware with the given $firmware-id. The $chunks are a list of byte arrays. */ - upload-firmware --scope/Scope --firmware-id/string chunks/List -> none + upload-firmware --firmware-id/string chunks/List -> none /** - Downloads a firmware chunk inside the given $scope. + Downloads a firmware chunk. */ - download-firmware --scope/Scope --id/string -> ByteArray + download-firmware --id/string -> ByteArray /** Informs the broker that a device with the given $device-id has been provisioned. @@ -155,7 +155,6 @@ interface BrokerCli implements Authenticatable: */ pod-registry-description-upsert -> int --fleet-id/Uuid - --scope/Scope --name/string --description/string? @@ -218,7 +217,6 @@ interface BrokerCli implements Authenticatable: */ pod-registry-descriptions -> List --fleet-id/Uuid - --scope/Scope --names/List --create-if-absent/bool @@ -250,14 +248,13 @@ interface BrokerCli implements Authenticatable: Uploads a pod part to the registry. */ pod-registry-upload-pod-part -> none - --scope/Scope --part-id/string contents/ByteArray /** Downloads a pod part from the registry. */ - pod-registry-download-pod-part part-id/string --scope/Scope -> ByteArray + pod-registry-download-pod-part part-id/string -> ByteArray /** Saves the manifest of a pod. @@ -266,7 +263,6 @@ interface BrokerCli implements Authenticatable: a pod from its parts. */ pod-registry-upload-pod-manifest -> none - --scope/Scope --pod-id/Uuid contents/ByteArray @@ -274,7 +270,6 @@ interface BrokerCli implements Authenticatable: Downloads the manifest of a pod. */ pod-registry-download-pod-manifest -> ByteArray - --scope/Scope --pod-id/Uuid with-broker server-config/ServerConfig --cli/Cli [block]: diff --git a/src/cli/brokers/http/base.toit b/src/cli/brokers/http/base.toit index 08cfeef09..577bd2a3e 100644 --- a/src/cli/brokers/http/base.toit +++ b/src/cli/brokers/http/base.toit @@ -184,29 +184,28 @@ class BrokerCliHttp implements BrokerCli: return result upload-image -> none - --scope/Scope --app-id/Uuid --word-size/int contents/ByteArray: - organization-id := scope.as-uuid + scope := server-config_.scope.to-json send-request_ COMMAND-UPLOAD_ { - "path": "/toit-artemis-assets/$organization-id/images/$app-id.$word-size", + "path": "/toit-artemis-assets/$scope/images/$app-id.$word-size", "content": contents, } - upload-firmware --scope/Scope --firmware-id/string chunks/List -> none: - organization-id := scope.as-uuid + upload-firmware --firmware-id/string chunks/List -> none: + scope := server-config_.scope.to-json firmware := #[] chunks.do: firmware += it send-request_ COMMAND-UPLOAD_ { - "path": "/toit-artemis-assets/$organization-id/firmware/$firmware-id", + "path": "/toit-artemis-assets/$scope/firmware/$firmware-id", "content": firmware, } - download-firmware --scope/Scope --id/string -> ByteArray: - organization-id := scope.as-uuid + download-firmware --id/string -> ByteArray: + scope := server-config_.scope.to-json return send-request_ COMMAND-DOWNLOAD_ { - "path": "/toit-artemis-assets/$organization-id/firmware/$id", + "path": "/toit-artemis-assets/$scope/firmware/$id", } notify-created --device-id/Uuid --state/Map -> none: @@ -245,13 +244,12 @@ class BrokerCliHttp implements BrokerCli: /** See $BrokerCli.pod-registry-description-upsert. */ pod-registry-description-upsert -> int --fleet-id/Uuid - --scope/Scope --name/string --description/string?: - organization-id := scope.as-uuid + scope := server-config_.scope.to-json return send-request_ COMMAND-POD-REGISTRY-DESCRIPTION-UPSERT_ { "_fleet_id": "$fleet-id", - "_organization_id": "$organization-id", + "_organization_id": scope, "_name": name, "_description": description, } @@ -315,16 +313,15 @@ class BrokerCliHttp implements BrokerCli: } return response.map: PodRegistryDescription.from-map it - /** See $(BrokerCli.pod-registry-descriptions --fleet-id --scope --names --create-if-absent). */ + /** See $(BrokerCli.pod-registry-descriptions --fleet-id --names --create-if-absent). */ pod-registry-descriptions -> List --fleet-id/Uuid - --scope/Scope --names/List --create-if-absent/bool: - organization-id := scope.as-uuid + scope := server-config_.scope.to-json response := send-request_ COMMAND-POD-REGISTRY-DESCRIPTIONS-BY-NAMES_ { "_fleet_id": "$fleet-id", - "_organization_id": "$organization-id", + "_organization_id": scope, "_names": names, "_create_if_absent": create-if-absent, } @@ -371,36 +368,34 @@ class BrokerCliHttp implements BrokerCli: /** See $BrokerCli.pod-registry-upload-pod-part. */ pod-registry-upload-pod-part -> none - --scope/Scope --part-id/string contents/ByteArray: - organization-id := scope.as-uuid + scope := server-config_.scope.to-json send-request_ COMMAND-UPLOAD_ { - "path": "/toit-artemis-pods/$organization-id/part/$part-id", + "path": "/toit-artemis-pods/$scope/part/$part-id", "content": contents, } /** See $BrokerCli.pod-registry-download-pod-part. */ - pod-registry-download-pod-part part-id/string --scope/Scope -> ByteArray: - organization-id := scope.as-uuid + pod-registry-download-pod-part part-id/string -> ByteArray: + scope := server-config_.scope.to-json return send-request_ COMMAND-DOWNLOAD-PRIVATE_ { - "path": "/toit-artemis-pods/$organization-id/part/$part-id", + "path": "/toit-artemis-pods/$scope/part/$part-id", } /** See $BrokerCli.pod-registry-upload-pod-manifest. */ pod-registry-upload-pod-manifest -> none - --scope/Scope --pod-id/Uuid contents/ByteArray: - organization-id := scope.as-uuid + scope := server-config_.scope.to-json send-request_ COMMAND-UPLOAD_ { - "path": "/toit-artemis-pods/$organization-id/manifest/$pod-id", + "path": "/toit-artemis-pods/$scope/manifest/$pod-id", "content": contents, } /** See $BrokerCli.pod-registry-download-pod-manifest. */ - pod-registry-download-pod-manifest --scope/Scope --pod-id/Uuid -> ByteArray: - organization-id := scope.as-uuid + pod-registry-download-pod-manifest --pod-id/Uuid -> ByteArray: + scope := server-config_.scope.to-json return send-request_ COMMAND-DOWNLOAD-PRIVATE_ { - "path": "/toit-artemis-pods/$organization-id/manifest/$pod-id", + "path": "/toit-artemis-pods/$scope/manifest/$pod-id", } diff --git a/src/cli/brokers/supabase/supabase.toit b/src/cli/brokers/supabase/supabase.toit index 500819768..853a6aacc 100644 --- a/src/cli/brokers/supabase/supabase.toit +++ b/src/cli/brokers/supabase/supabase.toit @@ -34,6 +34,7 @@ create-broker-cli-supabase-http server-config/ServerConfigSupabase --cli/Cli -> --use-tls=server-config.use-tls --root-certificate-ders=server-config.root-certificate-der ? [server-config.root-certificate-der] : null --poll-interval=server-config.poll-interval + --scope=server-config.scope return BrokerCliSupabase --id=id supabase-client http-config diff --git a/src/cli/cache.toit b/src/cli/cache.toit index 0e3910a1e..7ea25c8bb 100644 --- a/src/cli/cache.toit +++ b/src/cli/cache.toit @@ -16,17 +16,17 @@ cache-key-application-image id/Uuid --broker-config/ServerConfig -> string: cache-key-pod-parts -> string --broker-config/ServerConfig --part-id/string: - return "$broker-config.cache-key/$broker-config.scope.as-uuid/pod/parts/$part-id" + return "$broker-config.cache-key/$broker-config.scope.to-json/pod/parts/$part-id" cache-key-pod-manifest -> string --broker-config/ServerConfig --pod-id/Uuid: - return "$broker-config.cache-key/$broker-config.scope.as-uuid/pod/manifest/$pod-id" + return "$broker-config.cache-key/$broker-config.scope.to-json/pod/manifest/$pod-id" cache-key-patch -> string --broker-config/ServerConfig --patch-id/string: - return "$broker-config.cache-key/$broker-config.scope.as-uuid/patches/$patch-id" + return "$broker-config.cache-key/$broker-config.scope.to-json/patches/$patch-id" CACHE-ARTIFACT-KIND-ENVELOPE ::= "envelope" CACHE-ARTIFACT-KIND-PARTITION-TABLE ::= "partitions" diff --git a/src/cli/fleet.toit b/src/cli/fleet.toit index 18b4bd007..6f953f762 100644 --- a/src/cli/fleet.toit +++ b/src/cli/fleet.toit @@ -109,7 +109,7 @@ class FleetFile: (which is still org-id concrete). */ organization-id -> Uuid: - return broker-scope.as-uuid + return Uuid.parse broker-scope.to-json static parse path/string --default-broker-config/ServerConfig --cli/Cli -> FleetFile: ui := cli.ui @@ -198,12 +198,12 @@ class FleetFile: if is-new-format: if not broker-server.scope: ui.abort "Fleet file '$path' is missing 'scope' on broker server '$broker-name'." - organization-id = broker-server.scope.as-uuid + organization-id = Uuid.parse broker-server.scope.to-json else: // Legacy format: the top-level organization-id was the same for // every server. Pin it onto every entry so the new in-memory // shape is consistent. - legacy-scope := Scope.from-organization-id organization-id + legacy-scope := Scope "$organization-id" servers.map --in-place: | _ server-config/ServerConfig | server-config.with --scope=legacy-scope @@ -222,7 +222,7 @@ class FleetFile: broker-name = default-broker-config.name // Very-legacy fleet file with no broker/servers entry. Attach the // legacy top-level organization-id to the default broker config. - legacy-scope := Scope.from-organization-id organization-id + legacy-scope := Scope "$organization-id" servers = { default-broker-config.name: default-broker-config.with --scope=legacy-scope, } @@ -437,7 +437,7 @@ class Fleet: (which is still org-id concrete). */ organization-id -> Uuid: - return broker-scope.as-uuid + return Uuid.parse broker-scope.to-json static load-fleet-file -> FleetFile fleet-root-or-ref/string @@ -640,7 +640,7 @@ class FleetWithDevices extends Fleet: recovery-urls := recovery-url-prefixes.map: | prefix | "$prefix/recover-$(fleet-id).json" scoped-broker-config := broker-config.with - --scope=(Scope.from-organization-id organization-id) + --scope=(Scope "$organization-id") fleet-file := FleetFile --path="$fleet-root/$FLEET-FILE_" --id=fleet-id diff --git a/src/shared/scope.toit b/src/shared/scope.toit index 1b9aad84c..13d2e9051 100644 --- a/src/shared/scope.toit +++ b/src/shared/scope.toit @@ -1,7 +1,5 @@ // Copyright (C) 2026 Toit contributors. -import uuid show Uuid - /** A per-service authentication scope. @@ -10,33 +8,33 @@ A $Scope is the additional bit of information a service needs, on top of the The user's identity comes from their auth provider session (stored in the CLI's config); the scope comes from the fleet file. -For now a $Scope always wraps an organization-id UUID. In the future scopes - will become opaque, JSON-encodable blobs that each service's auth provider - interprets independently. Calling code that needs a UUID today should go - through $as-uuid so the conversion point is greppable when the underlying - representation broadens. +A scope wraps a JSON-encodable value (string, Map, List, etc.). Each + service's auth provider issues scopes in whatever shape makes sense for + its backend; the consuming backend knows that shape and interprets the + $to-json value accordingly. Scopes are opaque to anyone else in the + pipeline. + +For the current Toit-hosted setup, scopes wrap an organization-id UUID as + a string. A hypothetical GitHub-backed pod-store might wrap + `{"owner": "toit", "repo": "fleet-pods"}` instead. */ class Scope: - organization-id_/Uuid - - constructor.from-organization-id organization-id/Uuid: - organization-id_ = organization-id + json_/any /** - Returns the scope as a UUID. + Constructs a scope from any JSON-encodable value. - Today the scope is always a UUID; the throw is here for the future - when scopes can also be other shapes. + The $value is taken as-is; no validation. It is the caller's + responsibility to ensure the value is encodable by the JSON encoder. */ - as-uuid -> Uuid: - return organization-id_ + constructor value/any: + json_ = value - operator == other -> bool: - if other is not Scope: return false - return organization-id_ == (other as Scope).organization-id_ - - hash-code -> int: - return organization-id_.hash-code + /** + Returns the scope's JSON-encodable value. + */ + to-json -> any: + return json_ stringify -> string: - return "Scope($organization-id_)" + return "Scope($json_)" diff --git a/src/shared/server-config.toit b/src/shared/server-config.toit index ceaf6331f..79d07419f 100644 --- a/src/shared/server-config.toit +++ b/src/shared/server-config.toit @@ -141,7 +141,7 @@ class ServerConfigSupabase extends ServerConfig implements supabase.ServerConfig use-tls := json.get "use_tls" if use-tls == null: use-tls = json.contains "root_certificate_name" scope-value := json.get "scope" - scope/Scope? := scope-value and (Scope.from-organization-id (Uuid.parse scope-value)) + scope/Scope? := scope-value and (Scope scope-value) return ServerConfigSupabase name --host=json["host"] @@ -188,7 +188,7 @@ class ServerConfigSupabase extends ServerConfig implements supabase.ServerConfig if serialized: result["root_certificate_der_id"] = serialized if scope: - result["scope"] = "$scope.as-uuid" + result["scope"] = scope.to-json return result to-service-json [--der-serializer] --base64/bool=false -> Map: @@ -265,7 +265,7 @@ class ServerConfigHttp extends ServerConfig: use-tls := config.get "use_tls" if use-tls == null: use-tls = config.contains "root_certificate_names" scope-value := config.get "scope" - scope/Scope? := scope-value and (Scope.from-organization-id (Uuid.parse scope-value)) + scope/Scope? := scope-value and (Scope scope-value) return ServerConfigHttp name --host=config["host"] --port=config.get "port" @@ -317,7 +317,7 @@ class ServerConfigHttp extends ServerConfig: if admin-headers: result["admin_headers"] = admin-headers if scope: - result["scope"] = "$scope.as-uuid" + result["scope"] = scope.to-json return result to-service-json [--der-serializer] --base64/bool=false -> Map: diff --git a/tests/broker-test.toit b/tests/broker-test.toit index 6c66a739a..d1b9dc7d5 100644 --- a/tests/broker-test.toit +++ b/tests/broker-test.toit @@ -166,11 +166,9 @@ test-image broker-cli/broker.BrokerCli broker-service/broker.BrokerService --net contents-64 = ("test-image 64" * 10_000).to-byte-array broker-cli.upload-image contents-32 - --scope=TEST-SCOPE --app-id=APP-ID --word-size=32 broker-cli.upload-image contents-64 - --scope=TEST-SCOPE --app-id=APP-ID --word-size=64 @@ -209,11 +207,9 @@ test-firmware broker-cli/broker.BrokerCli broker-service/broker.BrokerService -- broker-cli.upload-firmware chunks --firmware-id=FIRMWARE-ID - --scope=TEST-SCOPE downloaded-bytes := broker-cli.download-firmware --id=FIRMWARE-ID - --scope=TEST-SCOPE expect-bytes-equal contents downloaded-bytes broker-connection := broker-service.connect --network=network --device=DEVICE1 diff --git a/tests/broker.toit b/tests/broker.toit index 0a912cc83..632383c4d 100644 --- a/tests/broker.toit +++ b/tests/broker.toit @@ -34,7 +34,11 @@ class TestBroker: with-tmp-config-cli: | cli/Cli | broker-cli/BrokerCli? := null try: - broker-cli = BrokerCli server-config --cli=cli + // The BrokerCli operates inside a fleet's scope; attach + // TEST-SCOPE here rather than on the bare server-config (which + // is also reused as a global-config entry in the tests). + scoped-config := server-config.with --scope=TEST-SCOPE + broker-cli = BrokerCli scoped-config --cli=cli block.call broker-cli finally: if broker-cli: broker-cli.close diff --git a/tests/pod-registry-test.toit b/tests/pod-registry-test.toit index 344f2325a..11c509a43 100644 --- a/tests/pod-registry-test.toit +++ b/tests/pod-registry-test.toit @@ -40,7 +40,6 @@ test-pod-registry --test-broker/TestBroker broker-cli/broker.BrokerCli: // Create a description. description-id := broker-cli.pod-registry-description-upsert --fleet-id=fleet-id - --scope=TEST-SCOPE --name="pod1" --description=null descriptions = broker-cli.pod-registry-descriptions --fleet-id=fleet-id @@ -52,7 +51,6 @@ test-pod-registry --test-broker/TestBroker broker-cli/broker.BrokerCli: // Create the same description again. description-id-received := broker-cli.pod-registry-description-upsert --fleet-id=fleet-id - --scope=TEST-SCOPE --name="pod1" --description=null expect-equals description-id description-id-received @@ -60,7 +58,6 @@ test-pod-registry --test-broker/TestBroker broker-cli/broker.BrokerCli: // Create another description. description-id2 := broker-cli.pod-registry-description-upsert --fleet-id=fleet-id - --scope=TEST-SCOPE --name="pod2" --description="description2" descriptions = broker-cli.pod-registry-descriptions --fleet-id=fleet-id @@ -85,7 +82,6 @@ test-pod-registry --test-broker/TestBroker broker-cli/broker.BrokerCli: // Get the descriptions by name. descriptions = broker-cli.pod-registry-descriptions --fleet-id=fleet-id - --scope=TEST-SCOPE --names=["pod1"] --no-create-if-absent expect-equals 1 descriptions.size @@ -95,7 +91,6 @@ test-pod-registry --test-broker/TestBroker broker-cli/broker.BrokerCli: // Do the same but create the missing description. descriptions = broker-cli.pod-registry-descriptions --fleet-id=fleet-id - --scope=TEST-SCOPE --names=["pod1", "pod3"] --create-if-absent names = descriptions.map: it.name @@ -266,13 +261,11 @@ test-pod-registry --test-broker/TestBroker broker-cli/broker.BrokerCli: description-id3 := broker-cli.pod-registry-description-upsert --fleet-id=fleet-id - --scope=TEST-SCOPE --name="pod3" --description=null description-id4 := broker-cli.pod-registry-description-upsert --fleet-id=fleet-id - --scope=TEST-SCOPE --name="pod4" --description=null @@ -322,20 +315,17 @@ test-pods --test-broker/TestBroker broker-cli/broker.BrokerCli: pod-contents.do: | key/string value/string | broker-cli.pod-registry-upload-pod-part - --scope=TEST-SCOPE --part-id=key value.to-byte-array // Upload the keys as a manifest. manifest := ubjson.encode pod broker-cli.pod-registry-upload-pod-manifest - --scope=TEST-SCOPE --pod-id=pod-id manifest // Download the manifest. downloaded-manifest := broker-cli.pod-registry-download-pod-manifest - --scope=TEST-SCOPE --pod-id=pod-id expect-equals manifest downloaded-manifest decoded := ubjson.decode downloaded-manifest @@ -345,5 +335,4 @@ test-pods --test-broker/TestBroker broker-cli/broker.BrokerCli: // Download the parts. decoded.do: | _ id/string | downloaded-part := broker-cli.pod-registry-download-pod-part id - --scope=TEST-SCOPE expect-equals pod-contents[id] downloaded-part.to-string diff --git a/tests/utils.toit b/tests/utils.toit index 69e4054d3..1ff859314 100644 --- a/tests/utils.toit +++ b/tests/utils.toit @@ -60,7 +60,7 @@ ADMIN-NAME ::= "Admin User" /** Preseeded "Test Organization". */ TEST-ORGANIZATION-NAME ::= "Test Organization" TEST-ORGANIZATION-UUID ::= Uuid.parse "4b6d9e35-cae9-44c0-8da0-6b0e485987e2" -TEST-SCOPE ::= cli-scope.Scope.from-organization-id TEST-ORGANIZATION-UUID +TEST-SCOPE ::= cli-scope.Scope "$TEST-ORGANIZATION-UUID" /** Preseeded test device in $TEST-ORGANIZATION-UUID. */ TEST-DEVICE-UUID ::= Uuid.parse "eb45c662-356c-4bea-ad8c-ede37688fddf"