musereelsdk

package module
v0.0.0-...-0052a8a Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 27, 2026 License: Apache-2.0 Imports: 39 Imported by: 0

README

musereel-sdk

English | 简体中文

musereel-sdk is the Go SDK boundary for controlled MuseReel workbench instances and the backends that own or operate them. This repository is intentionally private until the public-release decision is made.

“Public SDK” here describes the S31 boundary for third-party backends; it does not announce a public repository release. This change makes no external publication or hosting claim.

Installation

The module path is:

go get github.com/emiya-dev/musereel-sdk

The repository remains private, so the command requires the caller's normal private-module access to be configured. The module declares Go 1.25.

Quick start

The compile-checked starting points are in example_test.go. They use only exported SDK APIs, generate signing keys in memory, and use placeholder configuration.

Every example carries an // Output: directive, which is what makes go test actually run it rather than merely compile it. That is deliberate: an example that is only compiled proves that the API names exist, not that the code works. ExampleGatewayCreateRequest calls Validate on the request it shows, so an invalid example request fails the package tests instead of misleading a reader who copies it.

Because they really run, the examples never open a network connection, read a real certificate, or depend on environment variables. ExampleRequestFingerprint and ExampleCanonicalGatewayPath are fully deterministic and therefore assert their exact output; the fingerprint value pins the JCS canonicalization result, which cannot change without being a breaking change under CONTRIBUTING.md.

The examples cover these first steps:

  • ExampleMTLSConfig shows the local mTLS file configuration. A real client passes it to NewTLSConfig or NewMTLSCredentials, then uses DialRuntime.
  • ExampleNewEd25519Signer, ExampleNewEd25519SignerFromPEM, ExampleNewES256Signer, and ExampleNewES256SignerFromPEM show the registered signing-key forms.
  • ExampleNewToken and ExampleNewCachedTokenSource show a local TokenSource; a runtime connection normally uses NewGRPCTokenSource to exchange the mTLS identity for a short-lived token.
  • ExampleNewAuthenticatedClient, ExampleNewGatewayClient, ExampleGatewayCreateRequest, and ExampleNewRuntimeClient show construction of the client surfaces and a validated request without making a request.
  • ExampleRequestFingerprint, ExampleCanonicalGatewayPath, and ExampleSignAssertion and ExampleSignActorAssertion show the request identity that must remain stable across an authenticated retry.

API overview

The package is deliberately a transport and control-plane boundary:

Area Main exported API
mTLS and transport MTLSConfig, NewTLSConfig, NewMTLSCredentials, DialRuntime
Runtime tokens Token, NewToken, TokenSource, CachedTokenSource, GRPCTokenSource, NewCachedTokenSource, NewGRPCTokenSource, WithClock
Assertions and identity Signer, NewEd25519Signer, NewEd25519SignerFromPEM, NewES256Signer, NewES256SignerFromPEM, AssertionInput, SignAssertion, SignActorAssertion, RequestFingerprint, CanonicalGatewayPath
Authenticated gRPC AuthenticatedClient, NewAuthenticatedClient, AssertionCall
Runtime control plane RuntimeClient, NewRuntimeClient, WithRuntimeAssertion, typed runtime.v1 methods, and RuntimeRPCError
Gateway invocation surface GatewayClient, NewGatewayClient, GatewayCreateRequest, GatewayInvocationSpec, CreateAsync, CreateStream, Get, GetWithETag, Cancel, DownloadArtifact, and NewPoller
Canonical JSON jcs.CanonicalizeJSON from the github.com/emiya-dev/musereel-sdk/jcs subpackage

The generated protobuf messages and service clients live under the runtime subpackage. ExchangeRuntimeToken is intentionally kept behind GRPCTokenSource; RuntimeClient is for the typed runtime control plane after the token exchange.

Core concepts

mTLS bootstrap

MTLSConfig names the client certificate, private key, CA bundle, and optional server name. NewTLSConfig validates the current pair and re-reads the pair for each handshake so an atomic file replacement can rotate the certificate. The private key is kept inside the TLS stack and is not included in errors or formatted configuration output. DialRuntime opens the gRPC connection with these transport credentials.

Runtime tokens and authenticated gRPC

GRPCTokenSource sends the generated empty ExchangeRuntimeTokenRequest over the mTLS connection and caches the returned short-lived Bearer token. CachedTokenSource provides a fixed 60-second refresh window and single-flight exchange behavior; Token remains opaque by default and NewToken is available for custom local TokenSource implementations. The exchange request carries no tenant, instance, or scope fields.

AuthenticatedClient attaches the Bearer token to generic unary gRPC calls and retries exactly once after the stable runtime_unauthenticated code. It passes the same business arguments through the retry, preserving caller-owned idempotency keys and request fingerprints. RuntimeClient uses this boundary for its typed runtime methods.

GetSkuCatalog is actor-scoped. Callers pass a non-nil generated GetSkuCatalogRequest with the same canonical actor used by the workbench and configure RuntimeClient with WithRuntimeAssertion. The SDK signs catalog:get over /runtime.v1.RuntimeService/GetSkuCatalog, JCS {}, and an empty idempotency key. Each logical call and each transparent token-refresh attempt receives a fresh nonce; the SDK does not turn the server's strict query replay rule into idempotent safe retry.

Actor assertions and JCS fingerprints

Signer supports the registered EdDSA and ES256 forms. SignAssertion produces a compact JWS with a fresh nonce and a maximum validity window of 60 seconds; SignActorAssertion is the transport-only convenience form. The AssertionInput identity context includes the already-bound instance, tenant, session, and actor values.

RequestFingerprint computes the frozen SHA-256, unpadded base64url request fingerprint from the method, canonical path, actor, idempotency key, and JCS body. Empty body is canonicalized as {}. The jcs subpackage implements the server's RFC 8785 subset and orders object property names by UTF-16 code units, not Go's UTF-8 sort.Strings order.

Idempotency and invocation delivery

Mutation calls use the caller's idempotency key; query calls do not carry one. The assertion-aware retry signs a fresh nonce while the business identity and fingerprint remain fixed. GatewayClient keeps async and stream creation as separate methods, leaves delivery_mode out of the create body, and exposes ETag-aware reads, cancellation, SSE handling, and artifact download with Content-Digest verification.

Actor-scoped catalog and quality tiers

The generated catalog exposes each public tier as quality, parameter_schema_jcs, dispatchable, display_name, and a typed SkuPublicPrice. Amounts remain strings and the SDK does not interpret pricing or dispatch policy. A minimal selection flow is:

catalog, err := runtimeClient.GetSkuCatalog(ctx, &runtimepb.GetSkuCatalogRequest{
    Actor: actor,
})
if err != nil {
    return err
}

qualityTier := catalog.GetSkus()[0].GetQualityTiers()[0].GetQuality()
request := musereelsdk.GatewayCreateRequest{
    SKU:     "image.generate.v1",
    TaskRef: "task-01",
    Spec: musereelsdk.GatewayInvocationSpec{
        SchemaVersion: "1",
        Input:          map[string]string{"prompt": "a lighthouse"},
        Parameters:     map[string]string{"image_count": "1", "resolution": "512", "quality": qualityTier},
    },
    ModerationReceipt: receipt,
    QualityTier:       &qualityTier,
}

GatewayCreateRequest.QualityTier is optional: nil preserves the legacy absent field, while a pointer to "" explicitly selects the platform-default empty tier. Non-empty public keys use q followed by 1–3 digits (q[0-9]{1,3}), including q0, q00, q01, and q001. The key is opaque: the SDK does not parse or normalize it as an integer, so q01 remains q01. Merchant-private opaque values use mq1_ plus 32 lowercase hexadecimal characters. For image and video, when QualityTier is set, spec.parameters must be a JSON object whose quality exists, is a non-empty string, and matches the top-level value byte-for-byte. Moderation permits only the explicit empty value. Other capabilities may carry the top-level selector without the SDK inventing a per-SKU schema copy.

Variable Offers and order assertions

OfferCatalogItem exposes purchase_kind, min_payable_amount, max_payable_amount, payable_step, and units_per_currency_unit as public strings. The SDK passes these catalog values through unchanged and does not parse or normalize money or units.

CreateOrderRequest.RequestedPayableAmount is an optional string pointer. A nil pointer omits the field and preserves the fixed-Offer assertion body {"offer_price_id":"..."} byte-for-byte. A non-nil pointer adds requested_payable_amount to the JCS body, including when its value is the explicit empty string; the server owns the resulting fixed/variable and amount validation. The operation, canonical path, and GRPC assertion method stay unchanged. A token-refresh retry keeps the same business fingerprint and signs a new nonce.

ResolveRegistrationRequest.domain is supplied by the frontend and is passed through unchanged. The SDK does not derive, normalize, or complete it from Host, Origin, or configuration. invite_code is the frozen wire field for a channel identifier only.

S90 breaking migration

The old RuntimeClient.GetSkuCatalog(ctx) form is intentionally removed. Migrate at compile time to GetSkuCatalog(ctx, &runtimepb.GetSkuCatalogRequest{Actor: actor}) and provide WithRuntimeAssertion; a Bearer-only catalog call is no longer valid. The generated QualityTier no longer exposes the former tag-3 field: tag 3 and its name are reserved, while public presentation and per-tier price are available at tags 5 and 6. This is the dated pre-launch exception recorded in CONTRIBUTING.md, not permission for future runtime.v1 breaks.

Contract synchronization

contract-input/ is the frozen contract layout. It has exactly two kinds of file:

  • Mirrors are copies of Sluice-owned files. runtime.proto is the frozen runtime mirror, and frozen_public_error_codes.json mirrors backend/service/gateway/frozen_public_error_codes.json. Every mirror must be hashed by scripts/check-contract-pin.sh.
  • Pin records carry expected values and are the gate's input rather than its subject: SOURCE.txt and GATEWAY_HTTP_ANCHOR.txt. They are not self-hashed. There is no third kind of file.

SOURCE.txt pins the source repository, source path, source commit, SHA-256, freeze date, and the pinned code-generation toolchain metadata.

contract-input/runtime.proto is a frozen mirror, not a second fact source. The sole source of truth is in the sluice repository. Hand-editing the mirror is a violation. A refresh must come from the pinned Sluice source and update the source commit, SHA-256, and freeze date together as one reviewed change. The local gate recomputes the mirror SHA-256 and fails unless it equals the pinned value; it does not fetch the internal source repository.

The gateway HTTP surface is anchored by contract-input/GATEWAY_HTTP_ANCHOR.txt. That file alone carries the frozen gateway chapter, source commit, route count, and freeze date. Read those values there; do not copy them into this README.

That rule was paid for. Earlier revisions of this README and of CONTRIBUTING.md did restate the anchor's document version, baseline date, and route count, and those copies stayed wrong for months after the anchor itself moved — nothing compares prose against the anchor file, so a restated constant rots silently and still reads as authoritative. The specific stale values are deliberately not repeated here either: quoting them back would recreate exactly the failure this paragraph exists to prevent. The route contract remains owned by Sluice rather than becoming a second HTTP contract source here.

contract-input/reference/jcs-server-reference.go.txt was the warning example of an unhashed mirror: it looked authoritative while nobody kept it fresh. It used sort.Strings (UTF-8 byte order) for object keys, while jcs/jcs.go and the live Sluice implementation at backend/pkg/app/core/jcs.go use the RFC 8785 §3.2.3 UTF-16 code-unit order. The two orders disagree on non-BMP property names. Reimplementing from that stale file therefore produces actor_assertion_invalid fingerprints with no useful clue. The file has been deleted; the JCS behavior source is now jcs/jcs.go plus the UTF-16 assertion in jcs/jcs_test.go.

runtime/runtime.pb.go and runtime/runtime_grpc.pb.go are generated from the frozen contract-input/runtime.proto with the pinned local protoc toolchain. ExchangeRuntimeToken uses generated protobuf messages and the standard gRPC protobuf codec. The hand-written transition codec was removed, and its golden-byte assertions were moved to the generated types.

Conformance

Conformance uses a manual conformance build tag. The default local check uses go test -tags conformance -short ./...; run the real compose environment separately with:

go build -tags conformance ./...
go test -tags conformance ./conformance

The required environment variables are:

MUSEREEL_CONFORMANCE_GATEWAY_URL, MUSEREEL_CONFORMANCE_RUNTIME_TARGET, MUSEREEL_CONFORMANCE_MTLS_CERT_FILE, MUSEREEL_CONFORMANCE_MTLS_KEY_FILE, MUSEREEL_CONFORMANCE_MTLS_CA_FILE, MUSEREEL_CONFORMANCE_SIGNING_PRIVATE_KEY_FILE, MUSEREEL_CONFORMANCE_SIGNING_KID, MUSEREEL_CONFORMANCE_INSTANCE_ID, MUSEREEL_CONFORMANCE_TENANT_ID, MUSEREEL_CONFORMANCE_SESSION_ID, MUSEREEL_CONFORMANCE_ACTOR, MUSEREEL_CONFORMANCE_SKU_ID, MUSEREEL_CONFORMANCE_TASK_REF, and MUSEREEL_CONFORMANCE_DELIVERY_MODE (async or stream).

Optional variables are:

MUSEREEL_CONFORMANCE_MTLS_SERVER_NAME, MUSEREEL_CONFORMANCE_SPEC_SCHEMA_VERSION, MUSEREEL_CONFORMANCE_SPEC_INPUT_JSON, MUSEREEL_CONFORMANCE_SPEC_PARAMETERS_JSON, and MUSEREEL_CONFORMANCE_EVENT_ID.

There is deliberately no environment variable for the moderation receipt. A receipt cannot be supplied from outside: the harness mints one by first running a moderation.generate.v1 invocation and reading the receipt out of that call's terminal result (mintModerationReceipt in conformance.go).

MUSEREEL_CONFORMANCE_SPEC_SCHEMA_VERSION has no single default. When it is empty, conformance.go's conformanceSchemaVersionBySKU supplies 3 for video.generate.v1 and 1 for each of the other six SKUs.

Artifact IDs are not supplied through an environment variable. The contract requires {artifact_id} to be server-issued; invocation_artifact.id is a random UUID, so a client-preseeded value cannot match. The harness parses the ID from the terminal snapshot.result, then uses the SDK download interface to verify Content-Digest.

The machine-readable stdout markers are:

ARTIFACT_LEG=downloaded sku=<sku_id> count=<n>
ARTIFACT_LEG=skipped sku=<sku_id>

⚠ Running one non-artifact SKU produces only skipped; that does not prove that the artifact transport leg works. skipped is for non-artifact text, lyrics, and moderation SKUs; a downloaded marker must have a count of at least one. To cover that leg, the driver matrix must produce downloaded once each for video, image, music, and speech.

The target is the Sluice-side compose environment with its E14 fixture. If the environment is missing, TestSluiceComposeConformance fails fast instead of skipping. The only exception is -short, which skips the compose leg so the offline tests in the same package can enter the default check.

S31 boundary

This SDK is for controlled workbench instances running on an owning or third-party backend. It must not be embedded in a browser, a mobile client, or a customer-controlled frontend.

The SDK boundary may carry authentication materials, actor assertions, idempotency, and safe-retry behavior. Ledger, pricing, compliance, and supplier logic permanently do not belong in this SDK, even when implementing them would be convenient for callers. The SDK must not provide any capability that bypasses server-side validation.

The package contains typed transport and control-plane wrappers. The runtime client passes server-provided string amounts and units through without numeric interpretation; ledger, pricing, compliance, and supplier decisions remain on the server side.

Local gate

Run the complete local baseline with:

./scripts/ci.sh check

The gate checks formatting, builds the default and conformance-tagged packages, vets both build surfaces, runs the default and short conformance tests, and verifies the contract pins. The pin-only check is:

./scripts/check-contract-pin.sh

The repository has no hosted workflow in this milestone; the local shell gate is the current CI shape.

Project status and license

  • Module: github.com/emiya-dev/musereel-sdk
  • Go language version: 1.25
  • Runtime contract: runtime.v1, frozen by contract-input/SOURCE.txt
  • SDK-002 provides mTLS loading and rotation, short-lived runtime-token caching, actor assertions, and the generic authenticated gRPC boundary.
  • SDK-003 owns invocation wrappers.
  • SDK-004 adds committed generated protobuf/gRPC code and the typed runtime control-plane client.
  • External dependencies: grpc-go v1.80.0, protobuf v1.36.11, and the locked indirect closure recorded in go.mod/go.sum.
  • License: Apache-2.0. The copyright-holder line is intentionally left for the owner to fill in in LICENSE.
  • Hosted CI/workflow wiring: TBD; the local shell gate is the only CI shape in this milestone.

Documentation

Overview

Package musereelsdk is the public SDK boundary for controlled MuseReel workbench instances. It provides the authentication, assertion-signing, Gateway HTTP, and runtime control-plane clients used by an authorized workbench process.

"Public" here names the S31 boundary offered to third-party backends. It is not a statement about publication: this repository stays private until the owner makes a public-release decision.

This package is intended for a controlled workbench instance, not for code embedded in a browser, mobile application, or other customer-controlled frontend. The main entry points are NewTLSConfig or DialRuntime for mTLS, NewGRPCTokenSource and NewRuntimeClient for runtime operations, and NewGatewayClient for authenticated invocation operations.

Index

Examples

Constants

View Source
const (
	RuntimeUnauthenticated = "runtime_unauthenticated"
	ActorAssertionInvalid  = "actor_assertion_invalid"
	ActorAssertionReplayed = "actor_assertion_replayed"
)

RuntimeUnauthenticated, ActorAssertionInvalid, and ActorAssertionReplayed are stable runtime error codes. SDK branches use these codes rather than unstable human-readable status text or transport status alone.

View Source
const (
	GatewayInvalidInvocationRequest       = "invalid_invocation_request"
	GatewayModerationInvalidRequest       = "moderation_invalid_request"
	GatewayRuntimeUnauthenticated         = RuntimeUnauthenticated
	GatewayActorAssertionInvalid          = ActorAssertionInvalid
	GatewayActorAssertionReplayed         = ActorAssertionReplayed
	GatewayRuntimeForbidden               = "runtime_forbidden"
	GatewaySKUNotAllowed                  = "sku_not_allowed"
	GatewayComplianceRejected             = "compliance_rejected"
	GatewayInvocationNotFound             = "invocation_not_found"
	GatewayInvocationArtifactNotFound     = "invocation_artifact_not_found"
	GatewayInvocationArtifactExpired      = "invocation_artifact_expired"
	GatewayInvocationDeliveryModeMismatch = "invocation_delivery_mode_mismatch"
	GatewayInvocationIdempotencyConflict  = "invocation_idempotency_conflict"
	GatewayInvocationTransitionConflict   = "invocation_transition_conflict"
	GatewayInsufficientQuota              = "insufficient_quota"
	GatewayMemberLimitExceeded            = "member_limit_exceeded"
	GatewayRateLimited                    = "rate_limited"
	GatewayUpstreamUnavailable            = "upstream_unavailable"
	GatewayInternalError                  = "internal_error"
)

Gateway error codes are the HTTP SDK's only stable values for branching; human-readable messages are for diagnostics only.

View Source
const (
	// RuntimeRegistrationUnavailable is the stable retryable error code for
	// registration resolution being temporarily unavailable.
	RuntimeRegistrationUnavailable = "registration_unavailable"
	// RuntimeRegistrationCodeInvalid is the stable non-retryable error code for
	// an invite code that is not usable at this site.
	RuntimeRegistrationCodeInvalid = "registration_code_invalid"
	// RuntimeRegistrationCodeExpired is the stable non-retryable error code for
	// an invite code that was usable but can no longer be used.
	RuntimeRegistrationCodeExpired = "registration_code_expired"
	// RuntimeRegistrationCodeNotFound is the stable non-retryable error code for
	// an invite code that does not exist.
	//
	// Deprecated: As of hub S78 the server no longer exposes
	// registration_code_not_found at the transport boundary; use
	// RuntimeRegistrationCodeInvalid for an invite code that is unusable under
	// the current contract.
	RuntimeRegistrationCodeNotFound = "registration_code_not_found"
	// RuntimeRegistrationCodeMerchantMismatch is the stable non-retryable error
	// code for an invite code that does not match the site's merchant.
	//
	// Deprecated: As of hub S78 the server no longer exposes
	// registration_code_merchant_mismatch at the transport boundary; use
	// RuntimeRegistrationCodeInvalid for an invite code that is unusable under
	// the current contract.
	RuntimeRegistrationCodeMerchantMismatch = "registration_code_merchant_mismatch"
	// RuntimeQueryInvalid is the stable error code for an invalid balance or
	// ledger query request.
	RuntimeQueryInvalid = "runtime_query_invalid"
	// RuntimeSubjectUnavailable is the stable error code for a runtime subject
	// that is temporarily unavailable.
	RuntimeSubjectUnavailable = "runtime_subject_unavailable"
	// RuntimeIdentityInactive is the stable error code for an inactive identity.
	RuntimeIdentityInactive = "identity_inactive"
)
View Source
const MinimumTLSVersion uint16 = tls.VersionTLS12

MinimumTLSVersion is the explicit lower bound for SDK mTLS connections.

Variables

This section is empty.

Functions

func CanonicalGatewayPath

func CanonicalGatewayPath(route GatewayRoute, ids ...string) (string, error)

CanonicalGatewayPath builds and validates a registered invocation path. The route-specific IDs are server-issued path segments and therefore must already be URL-safe; the SDK never URL-escapes them as part of fingerprint construction. Create accepts no IDs, get/cancel accept one invocation ID, and get_artifact accepts an invocation ID followed by an artifact ID.

Example
package main

import (
	"fmt"

	musereelsdk "github.com/emiya-dev/musereel-sdk"
)

func main() {
	path, err := musereelsdk.CanonicalGatewayPath(
		musereelsdk.GatewayInvocationGetArtifact,
		"invocation-example",
		"artifact-example",
	)
	if err != nil {
		panic(err)
	}
	fmt.Println(path)
}
Output:
/runtime/v1/invocations/invocation-example/artifacts/artifact-example

func CanonicalPath

func CanonicalPath(path string) (string, error)

CanonicalPath validates an already assembled canonical path and returns it unchanged. Canonicalization here is validation, not normalization: any spelling that would require normalization is rejected to keep fingerprints stable across clients.

func DialRuntime

func DialRuntime(ctx context.Context, target string, config MTLSConfig, options ...grpc.DialOption) (*grpc.ClientConn, error)

DialRuntime opens an mTLS grpc connection. ExchangeRuntimeToken selects its temporary internal wire codec per call, so this connection remains usable by future generated protobuf clients as well.

func ErrorCode

func ErrorCode(err error) string

ErrorCode returns a frozen Sluice error code when one is present.

func IsRuntimeUnauthenticated

func IsRuntimeUnauthenticated(err error) bool

IsRuntimeUnauthenticated reports only the stable runtime code. A generic codes.Unauthenticated status is deliberately insufficient to trigger a credential refresh.

func NewMTLSCredentials

func NewMTLSCredentials(config MTLSConfig) (credentials.TransportCredentials, error)

NewMTLSCredentials returns grpc transport credentials using NewTLSConfig.

func NewTLSConfig

func NewTLSConfig(config MTLSConfig) (*tls.Config, error)

NewTLSConfig loads the CA and validates the current client key pair. The returned config deliberately leaves Certificates empty: GetClientCertificate re-reads the pair for every handshake, allowing atomic file replacement to rotate the certificate without a watcher or background goroutine.

func RequestFingerprint

func RequestFingerprint(method, canonicalPath, actor, idempotencyKey string, body []byte) (string, error)

RequestFingerprint computes the frozen SHA-256 fingerprint. Empty body is canonicalized as {}, and the returned base64url has no padding.

Example
package main

import (
	"fmt"

	musereelsdk "github.com/emiya-dev/musereel-sdk"
)

func main() {
	fingerprint, err := musereelsdk.RequestFingerprint(
		"POST",
		"/runtime/v1/invocations",
		"actor@example",
		"idempotency-example",
		[]byte(`{"prompt":"hello"}`),
	)
	if err != nil {
		panic(err)
	}
	fmt.Println(fingerprint)
}
Output:
3YxxQLRqVlYoGoNNxmyczn1pDG7af8dsKcxKqqF9kqU

func RetryableGatewayCode

func RetryableGatewayCode(code string) bool

RetryableGatewayCode returns the frozen code table's default retryability decision.

It cannot see this response's wire retryable value, so it always returns true for internal_error. When a *GatewayError is available, use RetryableByCode instead; see GatewayError for the distinction.

func SignAssertion

func SignAssertion(signer Signer, input AssertionInput) (JWS, AssertionClaims, error)

SignAssertion creates a compact JWS with a fresh random nonce on every invocation. Its maximum validity window is the frozen 60 seconds.

Example
package main

import (
	"crypto/ed25519"
	"crypto/rand"
	"time"

	musereelsdk "github.com/emiya-dev/musereel-sdk"
)

func main() {
	_, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		panic(err)
	}
	signer, err := musereelsdk.NewEd25519Signer("example-assertion-kid", privateKey)
	if err != nil {
		panic(err)
	}
	assertion, claims, err := musereelsdk.SignAssertion(signer, musereelsdk.AssertionInput{
		InstanceID:     "instance-example",
		TenantID:       "tenant-example",
		SessionID:      "session-example",
		Actor:          "actor@example",
		Operation:      string(musereelsdk.GatewayInvocationCreate),
		Method:         "POST",
		CanonicalPath:  "/runtime/v1/invocations",
		Body:           []byte(`{"sku_id":"text.generate.v1"}`),
		IdempotencyKey: "idempotency-example",
		IssuedAt:       time.Unix(1700000000, 0),
		TTL:            time.Minute,
	})
	if err != nil {
		panic(err)
	}
	_ = assertion.Compact()
	_ = claims.RequestFingerprint
}

func ValidateCanonicalPath

func ValidateCanonicalPath(path string) error

ValidateCanonicalPath accepts exactly registered gateway invocation paths and runtime.v1.RuntimeService full method paths.

Types

type AssertionCall

type AssertionCall struct {
	Args               any
	IdempotencyKey     string
	RequestFingerprint string
	Sign               func(Token) (JWS, error)
	ApplyAssertion     func(args any, assertion JWS) error
	ReadIdentity       func(args any) (idempotencyKey, requestFingerprint string, err error)
}

AssertionCall describes the small amount of request-specific behavior needed when a retry must receive a fresh nonce. ApplyAssertion changes only the actor_assertion field in Args. ReadIdentity is an optional negative control: when supplied, the client proves that idempotency key and request fingerprint did not change across the refresh retry.

type AssertionClaims

type AssertionClaims struct {
	Issuer             string `json:"iss"`
	Subject            string `json:"sub"`
	Audience           string `json:"aud"`
	TenantID           string `json:"tenant_id"`
	SessionID          string `json:"session_id"`
	Operation          string `json:"operation"`
	RequestFingerprint string `json:"request_fingerprint"`
	IssuedAt           int64  `json:"iat"`
	ExpiresAt          int64  `json:"exp"`
	Nonce              string `json:"nonce"`
}

AssertionClaims is the fixed actor assertion payload.

func VerifyCompactJWS

func VerifyCompactJWS(compact string, publicKey crypto.PublicKey) (AssertionClaims, error)

VerifyCompactJWS verifies an SDK compact JWS and returns its fixed claims. It is intended for tests and local self-checks; server registration remains the source of truth for kid authorization.

func VerifyJWS

func VerifyJWS(jws JWS, publicKey crypto.PublicKey) (AssertionClaims, error)

VerifyJWS is the typed wrapper around VerifyCompactJWS.

type AssertionInput

type AssertionInput struct {
	InstanceID     string
	TenantID       string
	SessionID      string
	Actor          string
	Operation      string
	Method         string
	CanonicalPath  string
	Body           []byte
	IdempotencyKey string
	IssuedAt       time.Time
	TTL            time.Duration
}

AssertionInput contains token-bound identity context and the current operation request. InstanceID and TenantID are assertion claims supplied by the caller's already-bound runtime-token context; they are never put into the empty ExchangeRuntimeTokenRequest.

type AuthenticatedClient

type AuthenticatedClient struct {
	// contains filtered or unexported fields
}

AuthenticatedClient adds a Bearer token to generic unary grpc calls and retries exactly once after the stable runtime_unauthenticated code. It does not inspect or rewrite business request fields.

func NewAuthenticatedClient

func NewAuthenticatedClient(connection grpc.ClientConnInterface, tokens TokenSource) *AuthenticatedClient

NewAuthenticatedClient constructs a generic authenticated unary caller.

Example
package main

import (
	"context"
	"time"

	musereelsdk "github.com/emiya-dev/musereel-sdk"
)

func main() {
	now := time.Unix(1700000000, 0)
	tokens := musereelsdk.NewCachedTokenSource(
		func(context.Context) (musereelsdk.Token, error) {
			return musereelsdk.NewToken("example-token", "Bearer", now.Add(5*time.Minute))
		},
		musereelsdk.WithClock(func() time.Time { return now }),
	)
	client := musereelsdk.NewAuthenticatedClient(nil, tokens)
	_ = client
}

func (*AuthenticatedClient) Invoke

func (client *AuthenticatedClient) Invoke(ctx context.Context, method string, args, reply any, options ...grpc.CallOption) error

Invoke calls method with a Bearer token. The same args and reply values are passed to the one allowed retry, preserving caller-owned idempotency keys and request fingerprints.

func (*AuthenticatedClient) InvokeWithAssertion

func (client *AuthenticatedClient) InvokeWithAssertion(ctx context.Context, method string, call AssertionCall, reply any, options ...grpc.CallOption) error

InvokeWithAssertion is the assertion-aware form of Invoke. It signs once per attempt, so a token-refresh retry gets a new nonce while the supplied business identity remains fixed.

type CachedTokenSource

type CachedTokenSource struct {
	// contains filtered or unexported fields
}

CachedTokenSource provides lazy refresh, a fixed 60-second refresh window, and single-flight exchange behavior for any exchange function.

func NewCachedTokenSource

func NewCachedTokenSource(exchange TokenExchangeFunc, options ...TokenSourceOption) *CachedTokenSource

NewCachedTokenSource constructs a cache around an exchange function.

Example
package main

import (
	"context"
	"time"

	musereelsdk "github.com/emiya-dev/musereel-sdk"
)

func main() {
	now := time.Unix(1700000000, 0)
	source := musereelsdk.NewCachedTokenSource(
		func(context.Context) (musereelsdk.Token, error) {
			return musereelsdk.NewToken("example-token", "Bearer", now.Add(5*time.Minute))
		},
		musereelsdk.WithClock(func() time.Time { return now }),
	)
	token, err := source.Token(context.Background())
	if err != nil {
		panic(err)
	}
	_ = token.AccessToken()
}

func (*CachedTokenSource) Invalidate

func (source *CachedTokenSource) Invalidate()

Invalidate discards the cached token. An exchange already in flight is not cancelled; its result is still the single-flight result for waiting callers.

func (*CachedTokenSource) Token

func (source *CachedTokenSource) Token(ctx context.Context) (Token, error)

Token returns the cached token when more than 60 seconds remain. At or below the threshold, exactly one caller exchanges while other callers wait for that same result.

type Clock

type Clock func() time.Time

Clock is injectable for deterministic token lifetime tests and callers with a controlled time source.

type ECDSAP256Signer

type ECDSAP256Signer struct {
	// contains filtered or unexported fields
}

ECDSAP256Signer implements ES256 with the JWS-required fixed-width R||S signature encoding rather than ASN.1 DER.

func NewES256Signer

func NewES256Signer(kid string, key *ecdsa.PrivateKey) (*ECDSAP256Signer, error)

NewES256Signer validates and copies the P-256 private key.

Example
package main

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"

	musereelsdk "github.com/emiya-dev/musereel-sdk"
)

func main() {
	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		panic(err)
	}
	signer, err := musereelsdk.NewES256Signer("example-es256-kid", privateKey)
	if err != nil {
		panic(err)
	}
	_ = signer
}

func NewES256SignerFromPEM

func NewES256SignerFromPEM(kid string, pemBytes []byte) (*ECDSAP256Signer, error)

NewES256SignerFromPEM parses a PKCS#8 or SEC1 PEM private key.

Example
package main

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
	"encoding/pem"

	musereelsdk "github.com/emiya-dev/musereel-sdk"
)

func main() {
	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		panic(err)
	}
	der, err := x509.MarshalPKCS8PrivateKey(privateKey)
	if err != nil {
		panic(err)
	}
	pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der})
	signer, err := musereelsdk.NewES256SignerFromPEM("example-es256-kid", pemBytes)
	if err != nil {
		panic(err)
	}
	_ = signer
}

func (*ECDSAP256Signer) Algorithm

func (signer *ECDSAP256Signer) Algorithm() string

Algorithm returns the registered JWS algorithm name, "ES256".

func (*ECDSAP256Signer) Format

func (signer *ECDSAP256Signer) Format(state fmt.State, verb rune)

Format writes the fixed "[REDACTED_PRIVATE_KEY]" placeholder for every formatting verb, so formatting cannot expose the signer's private key.

func (*ECDSAP256Signer) GoString

func (signer *ECDSAP256Signer) GoString() string

GoString returns the fixed "[REDACTED_PRIVATE_KEY]" placeholder for %#v formatting. It intentionally never formats the signer's private key.

func (*ECDSAP256Signer) KeyID

func (signer *ECDSAP256Signer) KeyID() string

KeyID returns the key identifier supplied when the signer was constructed. It returns an empty string for a nil receiver.

func (*ECDSAP256Signer) MarshalJSON

func (signer *ECDSAP256Signer) MarshalJSON() ([]byte, error)

MarshalJSON encodes the fixed "[REDACTED_PRIVATE_KEY]" placeholder instead of the signer's private key.

func (*ECDSAP256Signer) Sign

func (signer *ECDSAP256Signer) Sign(message []byte) ([]byte, error)

Sign returns an ES256 signature for message using SHA-256 and the JWS fixed-width R||S encoding. It returns an error when the signer is nil or does not contain a configured P-256 private key.

func (*ECDSAP256Signer) String

func (signer *ECDSAP256Signer) String() string

String returns the fixed "[REDACTED_PRIVATE_KEY]" placeholder. It intentionally never formats the signer's private key.

type Ed25519Signer

type Ed25519Signer struct {
	// contains filtered or unexported fields
}

Ed25519Signer implements the registered EdDSA algorithm.

func NewEd25519Signer

func NewEd25519Signer(kid string, key ed25519.PrivateKey) (*Ed25519Signer, error)

NewEd25519Signer validates and copies an Ed25519 private key.

Example
package main

import (
	"crypto/ed25519"
	"crypto/rand"

	musereelsdk "github.com/emiya-dev/musereel-sdk"
)

func main() {
	_, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		panic(err)
	}
	signer, err := musereelsdk.NewEd25519Signer("example-ed25519-kid", privateKey)
	if err != nil {
		panic(err)
	}
	_ = signer
}

func NewEd25519SignerFromPEM

func NewEd25519SignerFromPEM(kid string, pemBytes []byte) (*Ed25519Signer, error)

NewEd25519SignerFromPEM parses a PKCS#8 PEM private key.

Example
package main

import (
	"crypto/ed25519"
	"crypto/rand"
	"crypto/x509"
	"encoding/pem"

	musereelsdk "github.com/emiya-dev/musereel-sdk"
)

func main() {
	_, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		panic(err)
	}
	der, err := x509.MarshalPKCS8PrivateKey(privateKey)
	if err != nil {
		panic(err)
	}
	pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der})
	signer, err := musereelsdk.NewEd25519SignerFromPEM("example-ed25519-kid", pemBytes)
	if err != nil {
		panic(err)
	}
	_ = signer
}

func (*Ed25519Signer) Algorithm

func (signer *Ed25519Signer) Algorithm() string

Algorithm returns the registered JWS algorithm name, "EdDSA".

func (*Ed25519Signer) Format

func (signer *Ed25519Signer) Format(state fmt.State, verb rune)

Format writes the fixed "[REDACTED_PRIVATE_KEY]" placeholder for every formatting verb, so formatting cannot expose the signer's private key.

func (*Ed25519Signer) GoString

func (signer *Ed25519Signer) GoString() string

GoString returns the fixed "[REDACTED_PRIVATE_KEY]" placeholder for %#v formatting. It intentionally never formats the signer's private key.

func (*Ed25519Signer) KeyID

func (signer *Ed25519Signer) KeyID() string

KeyID returns the key identifier supplied when the signer was constructed. It returns an empty string for a nil receiver.

func (*Ed25519Signer) MarshalJSON

func (signer *Ed25519Signer) MarshalJSON() ([]byte, error)

MarshalJSON encodes the fixed "[REDACTED_PRIVATE_KEY]" placeholder instead of the signer's private key.

func (*Ed25519Signer) Sign

func (signer *Ed25519Signer) Sign(message []byte) ([]byte, error)

Sign returns an Ed25519 signature for message. It returns an error when the signer is nil or does not contain a correctly sized private key.

func (*Ed25519Signer) String

func (signer *Ed25519Signer) String() string

String returns the fixed "[REDACTED_PRIVATE_KEY]" placeholder. It intentionally never formats the signer's private key.

type ErrorCodeProvider

type ErrorCodeProvider interface {
	ErrorCode() string
}

ErrorCodeProvider is an optional application error shape for stable Sluice codes. grpc status errors are also recognized when their status message is exactly a frozen code or starts with that code followed by a delimiter.

type GRPCTokenSource

type GRPCTokenSource struct {
	// contains filtered or unexported fields
}

GRPCTokenSource exchanges the empty runtime token request through the generated protobuf client message, then applies CachedTokenSource lifetime and single-flight semantics.

func NewGRPCTokenSource

func NewGRPCTokenSource(connection grpc.ClientConnInterface, options ...TokenSourceOption) *GRPCTokenSource

NewGRPCTokenSource constructs a token source backed by RuntimeService.

func (*GRPCTokenSource) Invalidate

func (source *GRPCTokenSource) Invalidate()

Invalidate discards the gRPC source's cached token.

func (*GRPCTokenSource) Token

func (source *GRPCTokenSource) Token(ctx context.Context) (Token, error)

Token delegates to the cached gRPC source.

type GatewayActorFunc

type GatewayActorFunc func(context.Context) (string, error)

GatewayActorFunc supplies the actor at request time. It is resolved once per logical request, and a token-refresh retry reuses the same value.

type GatewayCancelResponse

type GatewayCancelResponse struct {
	StatusCode   int
	RequestID    string
	InvocationID string
	Accepted     bool
	Snapshot     *GatewayInvocationSnapshot
}

GatewayCancelResponse represents either a newly accepted cancellation intent (202) or the current snapshot (200).

type GatewayClient

type GatewayClient struct {
	// contains filtered or unexported fields
}

GatewayClient wraps the four authenticated Gateway invocation routes.

func NewGatewayClient

func NewGatewayClient(baseURL string, tlsConfig *tls.Config, tokens TokenSource, signer Signer, identity GatewayIdentity, options ...GatewayClientOption) (*GatewayClient, error)

NewGatewayClient constructs an authenticated Gateway HTTP client. Callers provide TLS configuration from NewTLSConfig and the token and signer abstractions shared by the rest of the SDK.

Example
package main

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"crypto/tls"
	"time"

	musereelsdk "github.com/emiya-dev/musereel-sdk"
)

func main() {
	_, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		panic(err)
	}
	signer, err := musereelsdk.NewEd25519Signer("example-gateway-kid", privateKey)
	if err != nil {
		panic(err)
	}
	now := time.Unix(1700000000, 0)
	tokens := musereelsdk.NewCachedTokenSource(
		func(context.Context) (musereelsdk.Token, error) {
			return musereelsdk.NewToken("example-token", "Bearer", now.Add(5*time.Minute))
		},
		musereelsdk.WithClock(func() time.Time { return now }),
	)
	client, err := musereelsdk.NewGatewayClient(
		"https://gateway.example.invalid",
		&tls.Config{MinVersion: musereelsdk.MinimumTLSVersion},
		tokens,
		signer,
		musereelsdk.GatewayIdentity{
			InstanceID: "instance-example",
			TenantID:   "tenant-example",
			SessionID:  "session-example",
			Actor:      "actor@example",
		},
	)
	if err != nil {
		panic(err)
	}
	_ = client
}

func (*GatewayClient) Cancel

func (client *GatewayClient) Cancel(ctx context.Context, invocationID, idempotencyKey string) (GatewayCancelResponse, error)

Cancel sends a bodyless DELETE using the idempotency key held by the caller.

func (*GatewayClient) CreateAsync

func (client *GatewayClient) CreateAsync(ctx context.Context, request GatewayCreateRequest, idempotencyKey string) (GatewayCreateResponse, error)

CreateAsync sends an async-mode create request and returns its snapshot.

func (*GatewayClient) CreateStream

func (client *GatewayClient) CreateStream(ctx context.Context, request GatewayCreateRequest, idempotencyKey string) (GatewayCreateResponse, error)

CreateStream sends a stream-mode create request and returns its SSE stream.

func (*GatewayClient) DownloadArtifact

func (client *GatewayClient) DownloadArtifact(ctx context.Context, invocationID, artifactID string, dst io.Writer) error

DownloadArtifact verifies Content-Digest before writing any bytes to dst.

func (*GatewayClient) Get

func (client *GatewayClient) Get(ctx context.Context, invocationID string) (GatewayGetResponse, error)

Get retrieves the current snapshot without an idempotency key.

This method is intentionally not dead code. An SDK-013 reconciliation review once misclassified it as unused: Sluice's integration rehearsal module (test/rehearsal, a separate Go module that replaces this repository through a sibling directory) has real call sites in golden/golden_test.go and golden/completed_test.go, and Sluice's ci.sh full harness compiles and runs that module. Removing Get would not fail this repository's own gates, but it would make another repository's gate fail to compile. It therefore remains public because it has a consumer.

func (*GatewayClient) GetWithETag

func (client *GatewayClient) GetWithETag(ctx context.Context, invocationID, etag string) (GatewayGetResponse, error)

GetWithETag sends If-None-Match when etag is non-empty. A 304 response is returned with NotModified=true rather than as an error.

func (*GatewayClient) NewPoller

func (client *GatewayClient) NewPoller(invocationID string) (*GatewayPoller, error)

NewPoller creates an ETag-aware poller for an invocation.

type GatewayClientOption

type GatewayClientOption func(*gatewayClientConfig)

GatewayClientOption customizes SDK-local behavior only.

func WithGatewayClock

func WithGatewayClock(now Clock) GatewayClientOption

WithGatewayClock injects the clock used for assertion issuance and polling metadata tests. It does not change the maximum 60-second wire TTL.

type GatewayCreateRequest

type GatewayCreateRequest struct {
	SKU               string                `json:"sku_id"`
	TaskRef           string                `json:"task_ref"`
	Spec              GatewayInvocationSpec `json:"spec"`
	ModerationReceipt string                `json:"moderation_receipt"`
	// QualityTier distinguishes the legacy absent field (nil) from an explicit
	// platform-default empty tier. The SDK never accepts supplier or model
	// selectors here.
	QualityTier *string `json:"quality_tier,omitempty"`
}

GatewayCreateRequest contains the four required create-body fields plus the optional public quality_tier selector. It does not contain delivery_mode; the delivery mode is a SKU catalog attribute.

Example
package main

import (
	musereelsdk "github.com/emiya-dev/musereel-sdk"
)

func main() {
	qualityTier := "q2"
	request := musereelsdk.GatewayCreateRequest{
		SKU:     "image.generate.v1",
		TaskRef: "task-example",
		Spec: musereelsdk.GatewayInvocationSpec{
			SchemaVersion: "1",
			Input:         map[string]string{"prompt": "hello"},
			Parameters:    map[string]string{"image_count": "1", "resolution": "512", "quality": qualityTier},
		},
		ModerationReceipt: "opaque-receipt",
		QualityTier:       &qualityTier,
	}
	if err := request.Validate(); err != nil {
		panic(err)
	}
}

func (GatewayCreateRequest) Validate

func (request GatewayCreateRequest) Validate() error

Validate checks the request shape before the SDK creates a token or assertion for the request.

type GatewayCreateResponse

type GatewayCreateResponse struct {
	StatusCode    int
	RequestID     string
	InvocationID  string
	Location      string
	AlreadyExists bool
	Snapshot      *GatewayInvocationSnapshot
	Stream        *GatewaySSEStream
}

GatewayCreateResponse is shared by both create modes. A 303 response is represented by AlreadyExists=true and a non-empty InvocationID with a nil error; for stream mode, the caller owns and must close Stream.

type GatewayDeliveryMode

type GatewayDeliveryMode string

GatewayDeliveryMode is determined by the Gateway-side SKU catalog. The SDK exposes separate methods for the delivery modes; delivery_mode is not put in the request body and is never silently overridden by the SDK.

const (
	GatewayDeliveryStream GatewayDeliveryMode = "stream"
	GatewayDeliveryAsync  GatewayDeliveryMode = "async"
)

type GatewayError

type GatewayError struct {
	Code    string `json:"code"`
	Message string `json:"message"`

	// Retryable is a direct read of the wire value for diagnostics; do not use it
	// to decide whether to retry. When the server omits retryable, this field is
	// false, while the contract's conservative default for an unregistered
	// internal code is true (06:611-619). This one field therefore gives the
	// opposite answer to the effective decision. Always use RetryableByCode or
	// IsRetryable: only they can see whether the wire carried the field at all.
	Retryable    bool           `json:"retryable"`
	RetryAfterMS *int64         `json:"retry_after_ms"`
	Details      map[string]any `json:"details"`

	HTTPStatus   int    `json:"-"`
	RequestID    string `json:"-"`
	InvocationID string `json:"-"`
	// contains filtered or unexported fields
}

GatewayError is the stable shape of a Gateway HTTP error. Retryable preserves the wire value for diagnostics; SDK-created errors use the frozen code table as their default.

Call RetryableByCode or IsRetryable for retry decisions; do not call RetryableGatewayCode directly when you have a GatewayError. The two once happened to be equivalent, but they are not now: RetryableGatewayCode takes only a code string and cannot see this response's retryable value, so it always returns true for internal_error. The contract (06:611-619) says the retryable value of internal_error is not a constant: a deterministic deployment-configuration failure arrives as internal_error with retryable=false, and the caller must stop retrying. Only RetryableByCode and IsRetryable can see the wire value. RetryableGatewayCode remains for the conservative default when the caller has only a code string.

func (GatewayError) Error

func (err GatewayError) Error() string

Error intentionally omits Message so that server diagnostic text cannot accidentally carry a full token or assertion into an error string.

func (GatewayError) ErrorCode

func (err GatewayError) ErrorCode() string

ErrorCode implements the SDK's ErrorCodeProvider error shape.

func (GatewayError) IsRetryable

func (err GatewayError) IsRetryable() bool

IsRetryable is the explicit method form of RetryableByCode.

func (GatewayError) RetryableByCode

func (err GatewayError) RetryableByCode() bool

RetryableByCode returns the effective retryability decision for an invocation error without changing the GatewayError.Retryable field. For an internal_error received over HTTP, an explicit server value is used instead of the code-table default.

func (*GatewayError) UnmarshalJSON

func (err *GatewayError) UnmarshalJSON(data []byte) error

UnmarshalJSON preserves numeric values in Details without loss by using json.Number and extracts InvocationID from the frozen details field.

type GatewayGetResponse

type GatewayGetResponse struct {
	StatusCode  int
	RequestID   string
	Snapshot    *GatewayInvocationSnapshot
	NotModified bool
	ETag        string
	RetryAfter  time.Duration
}

GatewayGetResponse contains either a new snapshot or a 304 result. ETag and RetryAfter are metadata used by GatewayPoller.

type GatewayIdentity

type GatewayIdentity struct {
	InstanceID string
	TenantID   string
	SessionID  string
	Actor      string
	ActorFunc  GatewayActorFunc
}

GatewayIdentity is the token-bound identity context required by SignAssertion for Gateway requests.

type GatewayInvocationSnapshot

type GatewayInvocationSnapshot struct {
	ID string `json:"id"`
	// Version is int64 because the server's respond.go and the 06 contract
	// example ("version": 1) both encode it as a JSON number. Declaring it a string
	// made every real Gateway snapshot fail to decode (async create 202, GET 200,
	// and cancel all became protocol errors); the SDK fixtures once sent a quoted
	// value and therefore incorrectly reinforced that assumption.
	Version       int64                  `json:"version"`
	State         GatewayInvocationState `json:"state"`
	Terminal      bool                   `json:"terminal"`
	SKU           string                 `json:"sku_id"`
	TaskRef       string                 `json:"task_ref"`
	CreatedAtMS   int64                  `json:"created_at_ms"`
	UpdatedAtMS   int64                  `json:"updated_at_ms"`
	ReservedUnits *string                `json:"reserved_units"`
	SettledUnits  *string                `json:"settled_units"`
	Result        json.RawMessage        `json:"result"`
	Error         *GatewayError          `json:"error"`
	LotDeductions json.RawMessage        `json:"lot_deductions"`
}

GatewayInvocationSnapshot is shared by async create, GET, and cancel responses. Result and LotDeductions remain raw JSON; the SDK does not convert units or similar amount-like values to floating-point numbers.

type GatewayInvocationSpec

type GatewayInvocationSpec struct {
	SchemaVersion string `json:"schema_version"`
	Input         any    `json:"input"`
	Parameters    any    `json:"parameters"`
}

GatewayInvocationSpec is the frozen spec object. Input and Parameters use interface values, so callers may provide json.RawMessage or ordinary Go JSON values. Validate rejects JSON numbers that cannot be canonicalized (fractions, exponent notation, or values outside int64); integer forms are accepted.

type GatewayInvocationState

type GatewayInvocationState string

GatewayInvocationState is the closed set of states used by invocation snapshots.

const (
	GatewayStateAccepted            GatewayInvocationState = "accepted"
	GatewayStateRunning             GatewayInvocationState = "running"
	GatewayStateCancelPending       GatewayInvocationState = "cancel_pending"
	GatewayStateReconciling         GatewayInvocationState = "reconciling"
	GatewayStateSettlementShortfall GatewayInvocationState = "settlement_shortfall"
	GatewayStateCompleted           GatewayInvocationState = "completed"
	GatewayStateFailed              GatewayInvocationState = "failed"
	GatewayStateCancelled           GatewayInvocationState = "cancelled"
)

type GatewayPoller

type GatewayPoller struct {
	// contains filtered or unexported fields
}

GatewayPoller maintains the ETag and waits for the server's Retry-After before the next GET. An SSE disconnect does not cause it to POST or DELETE.

func (*GatewayPoller) ETag

func (poller *GatewayPoller) ETag() string

ETag returns the poller's current validator.

func (*GatewayPoller) Poll

func (poller *GatewayPoller) Poll(ctx context.Context) (GatewayGetResponse, error)

Poll performs one GET and updates the ETag. It first blocks until the previous response's Retry-After has elapsed, so a caller in a loop does not need to sleep itself.

type GatewayRoute

type GatewayRoute string

GatewayRoute identifies one of the four invocation routes frozen for the assertion fingerprint surface.

const (
	GatewayInvocationCreate      GatewayRoute = "invocation:create"
	GatewayInvocationGet         GatewayRoute = "invocation:get"
	GatewayInvocationGetArtifact GatewayRoute = "invocation:get_artifact"
	GatewayInvocationCancel      GatewayRoute = "invocation:cancel"
)

type GatewaySSEDisconnectError

type GatewaySSEDisconnectError struct{}

GatewaySSEDisconnectError indicates that the HTTP stream ended before a terminal event. Callers should resume the invocation with GatewayPoller or Get.

func (GatewaySSEDisconnectError) Error

Error returns the stable message used for a pre-terminal SSE disconnect.

func (GatewaySSEDisconnectError) Unwrap

Unwrap reports io.ErrUnexpectedEOF so callers can classify the disconnect with errors.Is.

type GatewaySSEEvent

type GatewaySSEEvent struct {
	ID           string
	Event        string
	RequestID    string
	InvocationID string
	Sequence     int64
	OccurredAtMS int64
	Payload      json.RawMessage
}

GatewaySSEEvent is a validated business event. Payload remains raw JSON; the SDK does not retype streaming results or units.

type GatewaySSEStream

type GatewaySSEStream struct {
	// contains filtered or unexported fields
}

GatewaySSEStream parses a text/event-stream response. A disconnect before a terminal event returns GatewaySSEDisconnectError and is never converted into a cancellation request.

func (*GatewaySSEStream) Close

func (stream *GatewaySSEStream) Close() error

Close releases the underlying HTTP response body.

func (*GatewaySSEStream) Next

func (stream *GatewaySSEStream) Next() (GatewaySSEEvent, error)

Next returns the next business event. It skips comments such as : keep-alive and unknown SSE fields; a normal EOF after a terminal event is io.EOF.

func (*GatewaySSEStream) Pending

func (stream *GatewaySSEStream) Pending() bool

Pending reports whether the terminal event was invocation.pending. In that case, callers should switch to GET polling.

func (*GatewaySSEStream) Terminal

func (stream *GatewaySSEStream) Terminal() bool

Terminal reports whether a terminal or pending event has been observed.

type JWS

type JWS struct {
	// contains filtered or unexported fields
}

JWS is a compact actor assertion. Its compact value is redacted by default and can only be obtained through Compact for transmission.

func SignActorAssertion

func SignActorAssertion(signer Signer, input AssertionInput) (JWS, error)

SignActorAssertion is a convenience form when callers only need the transport value.

Example
package main

import (
	"crypto/ed25519"
	"crypto/rand"

	musereelsdk "github.com/emiya-dev/musereel-sdk"
)

func main() {
	_, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		panic(err)
	}
	signer, err := musereelsdk.NewEd25519Signer("example-actor-kid", privateKey)
	if err != nil {
		panic(err)
	}
	assertion, err := musereelsdk.SignActorAssertion(signer, musereelsdk.AssertionInput{
		InstanceID:     "instance-example",
		TenantID:       "tenant-example",
		SessionID:      "session-example",
		Actor:          "actor@example",
		Operation:      string(musereelsdk.GatewayInvocationCreate),
		Method:         "POST",
		CanonicalPath:  "/runtime/v1/invocations",
		Body:           []byte(`{"sku_id":"text.generate.v1"}`),
		IdempotencyKey: "idempotency-example",
	})
	if err != nil {
		panic(err)
	}
	_ = assertion.Bytes()
}

func (JWS) Bytes

func (jws JWS) Bytes() []byte

Bytes returns a copy of the compact JWS bytes for a protobuf bytes field.

func (JWS) Compact

func (jws JWS) Compact() string

Compact returns the compact JWS for an explicit transport operation.

func (JWS) Format

func (jws JWS) Format(state fmt.State, verb rune)

Format writes the fixed "[REDACTED]" placeholder for every formatting verb, so formatting cannot expose the compact JWS.

func (JWS) GoString

func (jws JWS) GoString() string

GoString returns the fixed "[REDACTED]" placeholder for %#v formatting instead of the compact JWS.

func (JWS) MarshalJSON

func (jws JWS) MarshalJSON() ([]byte, error)

MarshalJSON encodes the fixed "[REDACTED]" placeholder instead of the compact JWS, so ordinary JSON serialization cannot expose the assertion.

func (JWS) String

func (jws JWS) String() string

String returns the fixed "[REDACTED]" placeholder instead of the compact JWS. This is intentional: logging or printing a JWS must not expose its assertion claims or signature.

type MTLSConfig

type MTLSConfig struct {
	CertFile   string
	KeyFile    string
	CAFile     string
	ServerName string
}

MTLSConfig identifies the local client certificate, private key, and CA bundle. The private key is read only inside the TLS stack and is never included in errors or formatted configuration output.

Example
package main

import (
	musereelsdk "github.com/emiya-dev/musereel-sdk"
)

func main() {
	config := musereelsdk.MTLSConfig{
		CertFile:   "/path/to/client.crt",
		KeyFile:    "/path/to/client.key",
		CAFile:     "/path/to/ca.pem",
		ServerName: "gateway.example.invalid",
	}
	_ = config
}

type RuntimeAssertionConfig

type RuntimeAssertionConfig struct {
	Signer     Signer
	InstanceID string
	TenantID   string
	SessionID  string
}

RuntimeAssertionConfig holds the signing context for runtime actor assertions. Each RPC generates operation, path, body, and idempotency-key values according to the frozen contract; callers cannot override them.

type RuntimeClient

type RuntimeClient struct {
	// contains filtered or unexported fields
}

RuntimeClient is the typed control-plane wrapper for runtime.v1.RuntimeService. ExchangeRuntimeToken is not part of this client; GRPCTokenSource performs it through the mTLS bootstrap. Like the other RPCs without assertions, ResolveRegistration uses AuthenticatedClient to send a Bearer token and retains the single refresh retry for the stable unauthenticated code.

func NewRuntimeClient

func NewRuntimeClient(connection grpc.ClientConnInterface, tokens TokenSource, options ...RuntimeClientOption) *RuntimeClient

NewRuntimeClient constructs a runtime control-plane client. tokens is used by every RPC that requires a Bearer token, including ResolveRegistration.

Example
package main

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"time"

	musereelsdk "github.com/emiya-dev/musereel-sdk"
	runtimepb "github.com/emiya-dev/musereel-sdk/runtime"
)

func main() {
	_, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		panic(err)
	}
	signer, err := musereelsdk.NewEd25519Signer("example-runtime-kid", privateKey)
	if err != nil {
		panic(err)
	}
	now := time.Unix(1700000000, 0)
	tokens := musereelsdk.NewCachedTokenSource(
		func(context.Context) (musereelsdk.Token, error) {
			return musereelsdk.NewToken("example-token", "Bearer", now.Add(5*time.Minute))
		},
		musereelsdk.WithClock(func() time.Time { return now }),
	)
	client := musereelsdk.NewRuntimeClient(
		nil,
		tokens,
		musereelsdk.WithRuntimeAssertion(signer, "instance-example", "tenant-example", "session-example"),
	)
	catalogRequest := &runtimepb.GetSkuCatalogRequest{Actor: "actor@example"}
	_ = client
	_ = catalogRequest
}

func (*RuntimeClient) ConfirmRegistration

ConfirmRegistration builds the fixed JCS assertion body from the intent's fingerprint. The intent token remains only in the protobuf request and does not enter the assertion fingerprint. The actor itself is the idempotency-key exception for this RPC and is allowed to contain up to 256 bytes of UTF-8 under the contract.

func (*RuntimeClient) CreateOrder

func (client *RuntimeClient) CreateOrder(ctx context.Context, request *runtimepb.CreateOrderRequest, options ...grpc.CallOption) (*runtimepb.CreateOrderReply, error)

CreateOrder binds offer_price_id and, when present, the requested variable Offer amount into the signed request fingerprint. It protects one order creation with idempotency_key. It does not modify the price identifier or amount; binding means the assertion covers exactly the Offer price and caller-supplied amount this call was made against. Amounts are carried as strings without SDK parsing or reformatting, including an explicitly empty optional value so the server can reject it under its own contract.

func (*RuntimeClient) DisableIdentity

func (client *RuntimeClient) DisableIdentity(ctx context.Context, request *runtimepb.DisableIdentityRequest, options ...grpc.CallOption) (*runtimepb.IdentityReply, error)

DisableIdentity sends an instance-scoped disabled event.

func (*RuntimeClient) GetBalance

func (client *RuntimeClient) GetBalance(ctx context.Context, request *runtimepb.GetBalanceRequest, options ...grpc.CallOption) (*runtimepb.BalanceReply, error)

GetBalance uses an empty JSON body and a strict-nonce balance:get assertion.

func (*RuntimeClient) GetOfferCatalog

func (client *RuntimeClient) GetOfferCatalog(ctx context.Context, options ...grpc.CallOption) (*runtimepb.OfferCatalogReply, error)

GetOfferCatalog sends an empty protobuf message with a Bearer token and does not generate an assertion.

func (*RuntimeClient) GetOrder

func (client *RuntimeClient) GetOrder(ctx context.Context, request *runtimepb.GetOrderRequest, options ...grpc.CallOption) (*runtimepb.GetOrderReply, error)

GetOrder creates a query assertion with the fixed body {"order_id":"..."}.

func (*RuntimeClient) GetSkuCatalog

func (client *RuntimeClient) GetSkuCatalog(ctx context.Context, request *runtimepb.GetSkuCatalogRequest, options ...grpc.CallOption) (*runtimepb.SkuCatalogReply, error)

GetSkuCatalog returns the actor-scoped public SKU catalog. It signs the frozen empty JCS body with operation catalog:get and an empty idempotency key. Every invocation, including a transparent token-refresh attempt, gets a fresh nonce because catalog queries use strict replay semantics.

func (*RuntimeClient) ListBalanceLots

func (client *RuntimeClient) ListBalanceLots(ctx context.Context, request *runtimepb.ListBalanceLotsRequest, options ...grpc.CallOption) (*runtimepb.BalanceLotsReply, error)

ListBalanceLots passes through the opaque page_cursor. page_size=0 preserves the server default of 50; an explicit value must be 1-100. The strict-nonce body uses the request's original page_size.

func (*RuntimeClient) ListLedger

func (client *RuntimeClient) ListLedger(ctx context.Context, request *runtimepb.ListLedgerRequest, options ...grpc.CallOption) (*runtimepb.LedgerReply, error)

ListLedger passes through the opaque page_cursor. page_size=0 preserves the server default of 50; an explicit value must be 1-100. The strict-nonce body uses the request's original page_size.

func (*RuntimeClient) ListSiteBranding

func (client *RuntimeClient) ListSiteBranding(ctx context.Context, options ...grpc.CallOption) (*runtimepb.ListSiteBrandingReply, error)

ListSiteBranding sends an empty protobuf message with a Bearer token and does not generate an assertion.

It is an instance-level query that returns brand references for all sites in the instance. The contract carries no actor assertion, so it has the same shape as the offer catalog method: it is registered in runtimeMethods and runtimeQueryMethods, but not in runtimeAssertionOperations.

func (*RuntimeClient) ResolveRegistration

func (client *RuntimeClient) ResolveRegistration(ctx context.Context, request *runtimepb.ResolveRegistrationRequest, options ...grpc.CallOption) (*runtimepb.RegistrationIntent, error)

ResolveRegistration resolves a registration intent in the authenticated mTLS and Bearer instance scope. request.Domain is a string supplied by the frontend; the SDK puts it in the protobuf request unchanged and does not derive, normalize, or complete it from Host, Origin, or configuration. request.InviteCode is only a channel identifier. An invite code that is not usable at the site is exposed at the transport boundary only as RuntimeRegistrationCodeInvalid; one that was usable but can no longer be used is exposed only as RuntimeRegistrationCodeExpired. Internal not_found and merchant_mismatch causes are not further distinguished at the SDK transport boundary because both are caller input errors and are not retryable.

An external caller should retry a server response with registration_unavailable at most once within a total two-second budget; the SDK does not implement that retry itself.

func (*RuntimeClient) SyncIdentity

func (client *RuntimeClient) SyncIdentity(ctx context.Context, request *runtimepb.SyncIdentityRequest, options ...grpc.CallOption) (*runtimepb.IdentityReply, error)

SyncIdentity sends an instance-scoped identity lifecycle event. The three identity RPCs share one event_id namespace; the SDK validates the format but does not deduplicate across RPCs.

func (*RuntimeClient) SyncVerificationStatus

func (client *RuntimeClient) SyncVerificationStatus(ctx context.Context, request *runtimepb.SyncVerificationStatusRequest, options ...grpc.CallOption) (*runtimepb.IdentityReply, error)

SyncVerificationStatus accepts only the proto-frozen verified, verified_at_ms, credential_ref, and issuer payload. It has no PII input. The client validates the ASCII format of credential_ref and issuer.

func (*RuntimeClient) VerifyAndConfirmPayment

VerifyAndConfirmPayment passes the payment proof through unchanged. The SDK does not parse signed_payload, signed_headers, or provider_query_ref, and it does not provide a "mark as paid" semantic.

type RuntimeClientOption

type RuntimeClientOption func(*RuntimeClient)

RuntimeClientOption configures local RuntimeClient behavior.

func WithRuntimeAssertion

func WithRuntimeAssertion(signer Signer, instanceID, tenantID, sessionID string) RuntimeClientOption

WithRuntimeAssertion is a convenience form of WithRuntimeAssertionConfig.

func WithRuntimeAssertionConfig

func WithRuntimeAssertionConfig(config RuntimeAssertionConfig) RuntimeClientOption

WithRuntimeAssertionConfig supplies the signer and token-bound identity context shared by the RPCs that require actor assertions.

type RuntimeRPCError

type RuntimeRPCError struct {
	// contains filtered or unexported fields
}

RuntimeRPCError preserves the underlying gRPC error while exposing the server's stable code and retryable semantics. It keeps an existing server status code and applies the contract's status mapping for frozen stable codes when necessary.

func (*RuntimeRPCError) Error

func (err *RuntimeRPCError) Error() string

Error returns the underlying gRPC error message, or an empty string for a nil receiver or a wrapper without an underlying error.

func (*RuntimeRPCError) ErrorCode

func (err *RuntimeRPCError) ErrorCode() string

ErrorCode implements ErrorCodeProvider without changing the frozen ErrorCode implementation in errors.go.

func (*RuntimeRPCError) GRPCStatus

func (err *RuntimeRPCError) GRPCStatus() *status.Status

GRPCStatus keeps status.Code and status.Convert useful after the stable-code wrapper is applied.

func (*RuntimeRPCError) Retryable

func (err *RuntimeRPCError) Retryable() bool

Retryable reports the frozen retryability of the returned runtime error. RuntimeClient itself does not retry registration_unavailable.

func (*RuntimeRPCError) Unwrap

func (err *RuntimeRPCError) Unwrap() error

Unwrap returns the underlying gRPC error so errors.Is and errors.As can inspect the original cause. It returns nil for a nil receiver.

type SecretString

type SecretString struct {
	// contains filtered or unexported fields
}

SecretString holds a value that must be explicitly revealed before it is used as a protocol string. Its default formatting and JSON encoding never include the value.

func (SecretString) Format

func (s SecretString) Format(state fmt.State, verb rune)

Format writes the fixed "[REDACTED]" placeholder for every formatting verb, so formatting cannot expose the stored value.

func (SecretString) GoString

func (s SecretString) GoString() string

GoString returns the fixed "[REDACTED]" placeholder for %#v formatting instead of the stored value.

func (SecretString) MarshalJSON

func (s SecretString) MarshalJSON() ([]byte, error)

MarshalJSON encodes the fixed "[REDACTED]" placeholder instead of the stored value.

func (SecretString) Reveal

func (s SecretString) Reveal() string

Reveal returns the secret for an explicit protocol operation. Callers should avoid storing or formatting the returned string.

func (SecretString) String

func (s SecretString) String() string

String returns the fixed "[REDACTED]" placeholder instead of the stored value.

type Signer

type Signer interface {
	Algorithm() string
	KeyID() string
	Sign(message []byte) ([]byte, error)
}

Signer signs a JWS signing input using one of the two algorithms registered for SDK-002. Implementations keep private key material private and redact it from all default formatting.

type Token

type Token struct {
	// contains filtered or unexported fields
}

Token is an opaque short-lived runtime access token. The raw token can only be obtained through the explicitly named AccessToken method; default formatting and JSON encoding are redacted.

func NewToken

func NewToken(accessToken, tokenType string, expiresAt time.Time) (Token, error)

NewToken constructs a token for custom TokenSource implementations. It does not accept or encode tenant, instance, or scope data.

Example
package main

import (
	"time"

	musereelsdk "github.com/emiya-dev/musereel-sdk"
)

func main() {
	token, err := musereelsdk.NewToken(
		"example-token",
		"Bearer",
		time.Unix(1700000000, 0).Add(5*time.Minute),
	)
	if err != nil {
		panic(err)
	}
	_ = token.TokenType()
}

func (Token) AccessToken

func (token Token) AccessToken() string

AccessToken reveals the opaque token for an Authorization header.

func (Token) ExpiresAt

func (token Token) ExpiresAt() time.Time

ExpiresAt returns the server-provided expiry instant.

func (Token) Format

func (token Token) Format(state fmt.State, verb rune)

Format writes the fixed "[REDACTED]" placeholder for every formatting verb, so formatting cannot expose the access token.

func (Token) GoString

func (token Token) GoString() string

GoString returns the fixed "[REDACTED]" placeholder for %#v formatting instead of the access token.

func (Token) MarshalJSON

func (token Token) MarshalJSON() ([]byte, error)

MarshalJSON encodes token metadata while replacing AccessToken with the fixed "[REDACTED]" placeholder. RequestID is omitted when empty, and the token type and expiry are retained.

func (Token) RequestID

func (token Token) RequestID() string

RequestID returns the exchange request identifier, when the server sent one.

func (Token) String

func (token Token) String() string

String returns the fixed "[REDACTED]" placeholder instead of the access token.

func (Token) TokenType

func (token Token) TokenType() string

TokenType returns the protocol token type. Valid SDK tokens always return Bearer.

type TokenExchangeFunc

type TokenExchangeFunc func(context.Context) (Token, error)

TokenExchangeFunc exchanges the bootstrap mTLS identity for a token.

type TokenInvalidator

type TokenInvalidator interface {
	Invalidate()
}

TokenInvalidator is implemented by SDK token sources whose cache can be discarded after a stable runtime_unauthenticated response.

type TokenSource

type TokenSource interface {
	Token(context.Context) (Token, error)
}

TokenSource supplies a currently usable runtime token.

type TokenSourceOption

type TokenSourceOption func(*tokenSourceConfig)

TokenSourceOption customizes local token-source behavior without changing the wire request. In particular, there is no tenant, instance, or scope field here because ExchangeRuntimeToken is intentionally an empty message.

func WithClock

func WithClock(now Clock) TokenSourceOption

WithClock injects the clock used for cache expiry decisions.

Directories

Path Synopsis
Package jcs implements the RFC 8785 subset frozen by the Sluice server, whose live implementation is backend/pkg/app/core/jcs.go in the sluice repository.
Package jcs implements the RFC 8785 subset frozen by the Sluice server, whose live implementation is backend/pkg/app/core/jcs.go in the sluice repository.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL