From a8eaaa15817ed53a56671896754eef12cf6148fa Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 21:29:35 +0000 Subject: [PATCH] refactor(ax): tighten AX-facing docs --- datanode/client.go | 21 ++++----------------- doc.go | 6 +----- io.go | 40 +++++++++++++--------------------------- local/client.go | 14 ++++++-------- node/node.go | 13 +++---------- s3/s3.go | 12 +++++------- sigil/crypto_sigil.go | 38 ++++++++++++++------------------------ sigil/sigil.go | 42 ++++++------------------------------------ sqlite/sqlite.go | 9 +++------ store/doc.go | 5 +---- store/medium.go | 5 +---- store/store.go | 18 ++++++------------ workspace/doc.go | 5 +---- workspace/service.go | 8 ++------ 14 files changed, 66 insertions(+), 170 deletions(-) diff --git a/datanode/client.go b/datanode/client.go index b2c00cd..f94cccf 100644 --- a/datanode/client.go +++ b/datanode/client.go @@ -1,14 +1,9 @@ -// Package datanode provides an in-memory io.Medium backed by Borg's DataNode. +// Package datanode keeps io.Medium data in Borg's DataNode. // // medium := datanode.New() // _ = medium.Write("jobs/run.log", "started") // snapshot, _ := medium.Snapshot() // restored, _ := datanode.FromTar(snapshot) -// -// DataNode is an in-memory fs.FS that serialises to tar. Wrapping it as a -// Medium lets any code that works with io.Medium transparently operate on an -// in-memory filesystem that can be snapshotted, shipped as a crash report, or -// wrapped in a TIM container for runc execution. package datanode import ( @@ -39,16 +34,12 @@ var ( // Example: medium := datanode.New() // _ = medium.Write("jobs/run.log", "started") // snapshot, _ := medium.Snapshot() -// -// All paths are relative (no leading slash). Thread-safe via RWMutex. type Medium struct { dataNode *borgdatanode.DataNode directorySet map[string]bool // explicit directories that exist without file contents mu sync.RWMutex } -// Example: medium := datanode.New() -// _ = medium.Write("jobs/run.log", "started") func New() *Medium { return &Medium{ dataNode: borgdatanode.New(), @@ -56,11 +47,9 @@ func New() *Medium { } } -// FromTar restores a Medium from tar bytes. -// -// sourceMedium := datanode.New() -// snapshot, _ := sourceMedium.Snapshot() -// restored, _ := datanode.FromTar(snapshot) +// Example: sourceMedium := datanode.New() +// snapshot, _ := sourceMedium.Snapshot() +// restored, _ := datanode.FromTar(snapshot) func FromTar(data []byte) (*Medium, error) { dataNode, err := borgdatanode.FromTar(data) if err != nil { @@ -73,7 +62,6 @@ func FromTar(data []byte) (*Medium, error) { } // Example: snapshot, _ := medium.Snapshot() -// Use this for crash reports, workspace packaging, or TIM creation. func (m *Medium) Snapshot() ([]byte, error) { m.mu.RLock() defer m.mu.RUnlock() @@ -98,7 +86,6 @@ func (m *Medium) Restore(data []byte) error { } // Example: dataNode := medium.DataNode() -// Use this to wrap the filesystem in a TIM container. func (m *Medium) DataNode() *borgdatanode.DataNode { m.mu.RLock() defer m.mu.RUnlock() diff --git a/doc.go b/doc.go index 3b300c1..14eb1cb 100644 --- a/doc.go +++ b/doc.go @@ -1,11 +1,7 @@ -// Package io defines the storage boundary used across CoreGO. +// Package io gives CoreGO a single storage surface. // // medium, _ := io.NewSandboxed("/srv/app") // _ = medium.Write("config/app.yaml", "port: 8080") // backup, _ := io.NewSandboxed("/srv/backup") // _ = io.Copy(medium, "data/report.json", backup, "daily/report.json") -// -// Callers work against Medium so the same code can read and write state from -// sandboxed local paths, in-memory nodes, SQLite, S3, or other backends -// without changing application logic. package io diff --git a/io.go b/io.go index 1ab88a7..57362a1 100644 --- a/io.go +++ b/io.go @@ -10,12 +10,10 @@ import ( "dappco.re/go/core/io/local" ) -// Medium is the storage boundary used across CoreGO. -// -// medium, _ := io.NewSandboxed("/srv/app") -// _ = medium.Write("config/app.yaml", "port: 8080") -// backup, _ := io.NewSandboxed("/srv/backup") -// _ = io.Copy(medium, "data/report.json", backup, "daily/report.json") +// Example: medium, _ := io.NewSandboxed("/srv/app") +// _ = medium.Write("config/app.yaml", "port: 8080") +// backup, _ := io.NewSandboxed("/srv/backup") +// _ = io.Copy(medium, "data/report.json", backup, "daily/report.json") type Medium interface { Read(path string) (string, error) @@ -61,7 +59,7 @@ type Medium interface { IsDir(path string) bool } -// FileInfo provides a simple implementation of fs.FileInfo for mock testing. +// FileInfo is a test helper that satisfies fs.FileInfo. type FileInfo struct { name string size int64 @@ -82,7 +80,7 @@ func (fi FileInfo) IsDir() bool { return fi.isDir } func (fi FileInfo) Sys() any { return nil } -// DirEntry provides a simple implementation of fs.DirEntry for mock testing. +// DirEntry is a test helper that satisfies fs.DirEntry. type DirEntry struct { name string isDir bool @@ -98,9 +96,7 @@ func (de DirEntry) Type() fs.FileMode { return de.mode.Type() } func (de DirEntry) Info() (fs.FileInfo, error) { return de.info, nil } -// Local is the unsandboxed filesystem medium rooted at "/". -// -// io.Local.Read("/etc/hostname") +// Example: io.Local.Read("/etc/hostname") var Local Medium var _ Medium = (*local.Medium)(nil) @@ -113,14 +109,8 @@ func init() { } } -// Use NewSandboxed to confine file operations to a root directory. -// All file operations are restricted to paths within the root, and the root -// directory will be created if it does not exist. -// -// Example usage: -// -// medium, _ := io.NewSandboxed("/srv/app") -// _ = medium.Write("config/app.yaml", "port: 8080") +// Example: medium, _ := io.NewSandboxed("/srv/app") +// _ = medium.Write("config/app.yaml", "port: 8080") func NewSandboxed(root string) (Medium, error) { return local.New(root) } @@ -171,10 +161,8 @@ func Copy(source Medium, sourcePath string, destination Medium, destinationPath // --- MockMedium --- -// MockMedium is an in-memory Medium for tests. -// -// medium := io.NewMockMedium() -// _ = medium.Write("config/app.yaml", "port: 8080") +// Example: medium := io.NewMockMedium() +// _ = medium.Write("config/app.yaml", "port: 8080") type MockMedium struct { Files map[string]string Dirs map[string]bool @@ -183,10 +171,8 @@ type MockMedium struct { var _ Medium = (*MockMedium)(nil) -// Use NewMockMedium when tests need an in-memory Medium. -// -// medium := io.NewMockMedium() -// _ = medium.Write("config/app.yaml", "port: 8080") +// Example: medium := io.NewMockMedium() +// _ = medium.Write("config/app.yaml", "port: 8080") func NewMockMedium() *MockMedium { return &MockMedium{ Files: make(map[string]string), diff --git a/local/client.go b/local/client.go index 29ba6ec..81c7f9e 100644 --- a/local/client.go +++ b/local/client.go @@ -1,4 +1,4 @@ -// Package local provides the local filesystem implementation of io.Medium. +// Package local binds io.Medium to the local filesystem. // // medium, _ := local.New("/srv/app") // _ = medium.Write("config/app.yaml", "port: 8080") @@ -13,18 +13,16 @@ import ( core "dappco.re/go/core" ) -// Medium is the local filesystem backend returned by New. +// Example: medium, _ := local.New("/srv/app") +// _ = medium.Write("config/app.yaml", "port: 8080") type Medium struct { filesystemRoot string } var unrestrictedFileSystem = (&core.Fs{}).NewUnrestricted() -// local.New("/") exposes the full filesystem. -// local.New("/srv/app") confines access to a project root. -// -// medium, _ := local.New("/srv/app") -// _ = medium.Write("config/app.yaml", "port: 8080") +// Example: medium, _ := local.New("/srv/app") +// _ = medium.Write("config/app.yaml", "port: 8080") func New(root string) (*Medium, error) { absoluteRoot := absolutePath(root) // Resolve symlinks so sandbox checks compare like-for-like. @@ -179,7 +177,7 @@ func logSandboxEscape(root, path, attempted string) { core.Security("sandbox escape detected", "root", root, "path", path, "attempted", attempted, "user", username) } -// sandboxedPath sanitises and returns the full filesystem path. +// sandboxedPath resolves a path inside the filesystem root. // Absolute paths are sandboxed under root (unless root is "/"). func (m *Medium) sandboxedPath(path string) string { if path == "" { diff --git a/node/node.go b/node/node.go index 71fd217..36f491a 100644 --- a/node/node.go +++ b/node/node.go @@ -1,12 +1,9 @@ -// Package node provides an in-memory filesystem implementation of io.Medium. +// Package node keeps io.Medium data in memory. // // nodeTree := node.New() // nodeTree.AddData("config/app.yaml", []byte("port: 8080")) // snapshot, _ := nodeTree.ToTar() // restored, _ := node.FromTar(snapshot) -// -// It stores files in memory with implicit directory structure and supports -// tar serialisation. package node import ( @@ -27,18 +24,14 @@ import ( // nodeTree.AddData("config/app.yaml", []byte("port: 8080")) // snapshot, _ := nodeTree.ToTar() // restored, _ := node.FromTar(snapshot) -// -// Directories are implicit: they exist whenever a file path contains a "/". type Node struct { files map[string]*dataFile } -// compile-time interface checks +// Compile-time interface checks. var _ coreio.Medium = (*Node)(nil) var _ fs.ReadFileFS = (*Node)(nil) -// Example: nodeTree := node.New() -// nodeTree.AddData("config/app.yaml", []byte("port: 8080")) func New() *Node { return &Node{files: make(map[string]*dataFile)} } @@ -89,7 +82,7 @@ func (n *Node) ToTar() ([]byte, error) { return buf.Bytes(), nil } -// Use FromTar(data) to restore an in-memory tree from tar bytes. +// Example: restored, _ := node.FromTar(snapshot) func FromTar(data []byte) (*Node, error) { n := New() if err := n.LoadTar(data); err != nil { diff --git a/s3/s3.go b/s3/s3.go index 4884522..055c949 100644 --- a/s3/s3.go +++ b/s3/s3.go @@ -1,4 +1,4 @@ -// Package s3 provides an S3-backed io.Medium. +// Package s3 stores io.Medium data in S3 objects. // // client := awss3.NewFromConfig(aws.Config{Region: "us-east-1"}) // medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"}) @@ -21,8 +21,8 @@ import ( coreio "dappco.re/go/core/io" ) -// Client is the subset of the AWS S3 client API used by this package. -// Tests can provide any mock that satisfies the same method set. +// Example: client := awss3.NewFromConfig(aws.Config{Region: "us-east-1"}) +// medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"}) type Client interface { GetObject(ctx context.Context, params *awss3.GetObjectInput, optFns ...func(*awss3.Options)) (*awss3.GetObjectOutput, error) PutObject(ctx context.Context, params *awss3.PutObjectInput, optFns ...func(*awss3.Options)) (*awss3.PutObjectOutput, error) @@ -33,10 +33,8 @@ type Client interface { CopyObject(ctx context.Context, params *awss3.CopyObjectInput, optFns ...func(*awss3.Options)) (*awss3.CopyObjectOutput, error) } -// Medium is the S3-backed io.Medium returned by New. -// -// medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"}) -// _ = medium.Write("reports/daily.txt", "done") +// Example: medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"}) +// _ = medium.Write("reports/daily.txt", "done") type Medium struct { client Client bucket string diff --git a/sigil/crypto_sigil.go b/sigil/crypto_sigil.go index 8903868..802878a 100644 --- a/sigil/crypto_sigil.go +++ b/sigil/crypto_sigil.go @@ -1,11 +1,8 @@ -// Package sigil provides pre-obfuscation helpers for XChaCha20-Poly1305. +// Package sigil wraps XChaCha20-Poly1305 with deterministic pre-obfuscation. // // cipherSigil, _ := sigil.NewChaChaPolySigil([]byte("0123456789abcdef0123456789abcdef")) // ciphertext, _ := cipherSigil.In([]byte("payload")) // plaintext, _ := cipherSigil.Out(ciphertext) -// -// Use NewChaChaPolySigilWithObfuscator when you want ShuffleMaskObfuscator -// instead of the default XOR pre-obfuscation layer. package sigil import ( @@ -32,10 +29,7 @@ var ( NoKeyConfiguredError = core.E("sigil.NoKeyConfiguredError", "no encryption key configured", nil) ) -// PreObfuscator is the hook ChaChaPolySigil uses before and after encryption. -// -// XORObfuscator is the default. ShuffleMaskObfuscator is available when you -// want byte shuffling as well as masking. +// PreObfuscator customises the bytes mixed in before and after encryption. type PreObfuscator interface { // Obfuscate transforms plaintext before encryption using the provided entropy. // The entropy is typically the encryption nonce, ensuring the transformation @@ -47,7 +41,7 @@ type PreObfuscator interface { Deobfuscate(data []byte, entropy []byte) []byte } -// XORObfuscator is the default pre-obfuscator returned by NewChaChaPolySigil. +// Example: cipherSigil, _ := sigil.NewChaChaPolySigil(key) type XORObfuscator struct{} // Obfuscate XORs the data with a key stream derived from the entropy. @@ -215,11 +209,9 @@ type ChaChaPolySigil struct { randomReader goio.Reader // for testing injection } -// NewChaChaPolySigil returns a ChaChaPolySigil backed by a 32-byte key. -// -// cipherSigil, _ := sigil.NewChaChaPolySigil([]byte("0123456789abcdef0123456789abcdef")) -// ciphertext, _ := cipherSigil.In([]byte("payload")) -// plaintext, _ := cipherSigil.Out(ciphertext) +// Example: cipherSigil, _ := sigil.NewChaChaPolySigil([]byte("0123456789abcdef0123456789abcdef")) +// ciphertext, _ := cipherSigil.In([]byte("payload")) +// plaintext, _ := cipherSigil.Out(ciphertext) func NewChaChaPolySigil(key []byte) (*ChaChaPolySigil, error) { if len(key) != 32 { return nil, InvalidKeyError @@ -235,14 +227,14 @@ func NewChaChaPolySigil(key []byte) (*ChaChaPolySigil, error) { }, nil } -// NewChaChaPolySigilWithObfuscator returns a ChaChaPolySigil with a custom pre-obfuscator. +// Example: cipherSigil, _ := sigil.NewChaChaPolySigilWithObfuscator( // -// cipherSigil, _ := sigil.NewChaChaPolySigilWithObfuscator( -// []byte("0123456789abcdef0123456789abcdef"), -// &sigil.ShuffleMaskObfuscator{}, -// ) -// ciphertext, _ := cipherSigil.In([]byte("payload")) -// plaintext, _ := cipherSigil.Out(ciphertext) +// []byte("0123456789abcdef0123456789abcdef"), +// &sigil.ShuffleMaskObfuscator{}, +// +// ) +// ciphertext, _ := cipherSigil.In([]byte("payload")) +// plaintext, _ := cipherSigil.Out(ciphertext) func NewChaChaPolySigilWithObfuscator(key []byte, obfuscator PreObfuscator) (*ChaChaPolySigil, error) { cipherSigil, err := NewChaChaPolySigil(key) if err != nil { @@ -334,9 +326,7 @@ func (s *ChaChaPolySigil) Out(data []byte) ([]byte, error) { return plaintext, nil } -// GetNonceFromCiphertext returns the nonce embedded in ciphertext. -// -// nonce, _ := sigil.GetNonceFromCiphertext(ciphertext) +// Example: nonce, _ := sigil.GetNonceFromCiphertext(ciphertext) func GetNonceFromCiphertext(ciphertext []byte) ([]byte, error) { nonceSize := chacha20poly1305.NonceSizeX if len(ciphertext) < nonceSize { diff --git a/sigil/sigil.go b/sigil/sigil.go index 5336648..e12d847 100644 --- a/sigil/sigil.go +++ b/sigil/sigil.go @@ -1,49 +1,23 @@ -// Package sigil provides the Sigil transformation framework for composable, -// reversible data transformations. +// Package sigil chains reversible byte transformations. // // hexSigil, _ := sigil.NewSigil("hex") // gzipSigil, _ := sigil.NewSigil("gzip") // encoded, _ := sigil.Transmute([]byte("payload"), []sigil.Sigil{hexSigil, gzipSigil}) // decoded, _ := sigil.Untransmute(encoded, []sigil.Sigil{hexSigil, gzipSigil}) -// -// Sigils are the core abstraction - each sigil implements a specific -// transformation (encoding, compression, hashing, encryption) with a uniform -// interface. Sigils can be chained together to create transformation pipelines. package sigil import core "dappco.re/go/core" -// Sigil defines the interface for a data transformer. -// -// A Sigil represents a single transformation unit that can be applied to byte data. -// Sigils may be reversible (encoding, compression, encryption) or irreversible (hashing). -// -// For reversible sigils: Out(In(x)) == x for all valid x -// For irreversible sigils: Out returns the input unchanged -// For symmetric sigils: In(x) == Out(x) -// -// Implementations must handle nil input by returning nil without error, -// and empty input by returning an empty slice without error. +// Sigil transforms byte slices. type Sigil interface { - // In applies the forward transformation to the data. - // For encoding sigils, this encodes the data. - // For compression sigils, this compresses the data. - // For hash sigils, this computes the digest. + // Example: encoded, _ := hexSigil.In([]byte("payload")) In(data []byte) ([]byte, error) - // Out applies the reverse transformation to the data. - // For reversible sigils, this recovers the original data. - // For irreversible sigils (e.g., hashing), this returns the input unchanged. + // Example: decoded, _ := hexSigil.Out(encoded) Out(data []byte) ([]byte, error) } -// Transmute applies a series of sigils to data in sequence. -// -// Each sigil's In method is called in order, with the output of one sigil -// becoming the input of the next. If any sigil returns an error, Transmute -// stops immediately and returns nil with that error. -// -// To reverse a transmutation, call each sigil's Out method in reverse order. +// Example: encoded, _ := sigil.Transmute([]byte("payload"), []sigil.Sigil{hexSigil, gzipSigil}) func Transmute(data []byte, sigils []Sigil) ([]byte, error) { var err error for _, s := range sigils { @@ -55,11 +29,7 @@ func Transmute(data []byte, sigils []Sigil) ([]byte, error) { return data, nil } -// Untransmute reverses a transmutation by applying Out in reverse order. -// -// Each sigil's Out method is called in reverse order, with the output of one sigil -// becoming the input of the next. If any sigil returns an error, Untransmute -// stops immediately and returns nil with that error. +// Example: decoded, _ := sigil.Untransmute(encoded, []sigil.Sigil{hexSigil, gzipSigil}) func Untransmute(data []byte, sigils []Sigil) ([]byte, error) { var err error for i := len(sigils) - 1; i >= 0; i-- { diff --git a/sqlite/sqlite.go b/sqlite/sqlite.go index 9fd936d..87a5d99 100644 --- a/sqlite/sqlite.go +++ b/sqlite/sqlite.go @@ -1,4 +1,4 @@ -// Package sqlite persists io.Medium content in a SQLite database. +// Package sqlite stores io.Medium content in SQLite. // // medium, _ := sqlite.New(sqlite.Options{Path: ":memory:"}) // _ = medium.Write("config/app.yaml", "port: 8080") @@ -18,10 +18,8 @@ import ( _ "modernc.org/sqlite" // Pure Go SQLite driver ) -// Medium stores filesystem-shaped content in SQLite. -// -// medium, _ := sqlite.New(sqlite.Options{Path: ":memory:"}) -// _ = medium.Write("config/app.yaml", "port: 8080") +// Example: medium, _ := sqlite.New(sqlite.Options{Path: ":memory:"}) +// _ = medium.Write("config/app.yaml", "port: 8080") type Medium struct { database *sql.DB table string @@ -29,7 +27,6 @@ type Medium struct { var _ coreio.Medium = (*Medium)(nil) -// Example: medium, _ := sqlite.New(sqlite.Options{Path: ":memory:", Table: "files"}) type Options struct { // Path is the SQLite database path. Use ":memory:" for tests. Path string diff --git a/store/doc.go b/store/doc.go index abfa5b7..5101af0 100644 --- a/store/doc.go +++ b/store/doc.go @@ -1,10 +1,7 @@ -// Package store provides a SQLite-backed group-namespaced key-value store. +// Package store maps grouped keys onto SQLite rows. // // keyValueStore, _ := store.New(store.Options{Path: ":memory:"}) // _ = keyValueStore.Set("app", "theme", "midnight") // medium := keyValueStore.AsMedium() // _ = medium.Write("app/theme", "midnight") -// -// It also exposes an io.Medium adapter so grouped values can participate in -// the same storage workflows as filesystem-backed mediums. package store diff --git a/store/medium.go b/store/medium.go index 3a548d3..1d5feca 100644 --- a/store/medium.go +++ b/store/medium.go @@ -12,11 +12,8 @@ import ( // Example: medium, _ := store.NewMedium(store.Options{Path: "config.db"}) // _ = medium.Write("app/theme", "midnight") +// entries, _ := medium.List("") // entries, _ := medium.List("app") -// -// Paths are mapped as group/key - the first segment is the group, -// the rest is the key. List("") returns groups as directories, -// List("group") returns keys as files. type Medium struct { store *Store } diff --git a/store/store.go b/store/store.go index 1929093..4d43f30 100644 --- a/store/store.go +++ b/store/store.go @@ -10,17 +10,15 @@ import ( _ "modernc.org/sqlite" ) -// NotFoundError is returned when a key does not exist in the store. +// Example: _, err := keyValueStore.Get("app", "theme") +// err matches store.NotFoundError when the key is missing. var NotFoundError = errors.New("key not found") -// Store is the grouped key/value database returned by New. -// -// keyValueStore, _ := store.New(store.Options{Path: ":memory:"}) +// Example: keyValueStore, _ := store.New(store.Options{Path: ":memory:"}) type Store struct { database *sql.DB } -// Example: keyValueStore, _ := store.New(store.Options{Path: ":memory:"}) type Options struct { // Path is the SQLite database path. Use ":memory:" for tests. Path string @@ -134,13 +132,9 @@ func (s *Store) GetAll(group string) (map[string]string, error) { return result, nil } -// Render loads all key-value pairs from a group and renders a Go template. -// -// Example usage: -// -// keyValueStore, _ := store.New(store.Options{Path: ":memory:"}) -// _ = keyValueStore.Set("user", "name", "alice") -// out, _ := keyValueStore.Render("hello {{ .name }}", "user") +// Example: keyValueStore, _ := store.New(store.Options{Path: ":memory:"}) +// _ = keyValueStore.Set("user", "name", "alice") +// out, _ := keyValueStore.Render("hello {{ .name }}", "user") func (s *Store) Render(templateText, group string) (string, error) { rows, err := s.database.Query("SELECT key, value FROM kv WHERE grp = ?", group) if err != nil { diff --git a/workspace/doc.go b/workspace/doc.go index 3c2140d..a0ea740 100644 --- a/workspace/doc.go +++ b/workspace/doc.go @@ -1,10 +1,7 @@ -// Package workspace provides encrypted user workspaces backed by io.Medium. +// Package workspace creates encrypted workspaces on top of io.Medium. // // service, _ := workspace.New(workspace.Options{Core: core.New(), Crypt: cryptProvider}) // workspaceID, _ := service.CreateWorkspace("alice", "pass123") // _ = service.SwitchWorkspace(workspaceID) // _ = service.WorkspaceFileSet("notes/todo.txt", "ship it") -// -// Workspaces are rooted under the caller's configured home directory and keep -// file access constrained to the active workspace. package workspace diff --git a/workspace/service.go b/workspace/service.go index c2bfc88..a55c8c1 100644 --- a/workspace/service.go +++ b/workspace/service.go @@ -11,9 +11,7 @@ import ( "dappco.re/go/core/io" ) -// Workspace is the workspace service interface returned by New. -// -// service, _ := workspace.New(workspace.Options{Core: core.New(), Crypt: cryptProvider}) +// Example: service, _ := workspace.New(workspace.Options{Core: core.New(), Crypt: cryptProvider}) type Workspace interface { CreateWorkspace(identifier, password string) (string, error) SwitchWorkspace(workspaceID string) error @@ -34,7 +32,7 @@ type Options struct { Crypt CryptProvider } -// Service is the Workspace implementation returned by New. +// Example: service, _ := workspace.New(workspace.Options{Core: core.New(), Crypt: cryptProvider}) type Service struct { core *core.Core crypt CryptProvider @@ -77,8 +75,6 @@ func New(options Options) (*Service, error) { } // Example: workspaceID, _ := service.CreateWorkspace("alice", "pass123") -// Identifier is hashed (SHA-256) to create the directory name. -// A PGP keypair is generated using the password. func (s *Service) CreateWorkspace(identifier, password string) (string, error) { s.mu.Lock() defer s.mu.Unlock()