512 lines
12 KiB
Go
512 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/extism/go-pdk"
|
|
)
|
|
|
|
// GenerateInput represents the input for the generate function
|
|
type GenerateInput struct {
|
|
Credential string `json:"credential"` // Base64-encoded PublicKeyCredential
|
|
}
|
|
|
|
// GenerateOutput represents the output of the generate function
|
|
type GenerateOutput struct {
|
|
DID string `json:"did"`
|
|
Database []byte `json:"database"`
|
|
}
|
|
|
|
// LoadInput represents the input for the load function
|
|
type LoadInput struct {
|
|
Database []byte `json:"database"`
|
|
}
|
|
|
|
// LoadOutput represents the output of the load function
|
|
type LoadOutput struct {
|
|
Success bool `json:"success"`
|
|
DID string `json:"did,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// ExecInput represents the input for the exec function
|
|
type ExecInput struct {
|
|
Filter string `json:"filter"` // GitHub-style filter: "resource:accounts action:sign"
|
|
Token string `json:"token"` // UCAN token for authorization
|
|
}
|
|
|
|
// ExecOutput represents the output of the exec function
|
|
type ExecOutput struct {
|
|
Success bool `json:"success"`
|
|
Result json.RawMessage `json:"result,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// QueryInput represents the input for the query function
|
|
type QueryInput struct {
|
|
DID string `json:"did"`
|
|
}
|
|
|
|
// QueryOutput represents the output of the query function
|
|
type QueryOutput struct {
|
|
DID string `json:"did"`
|
|
Controller string `json:"controller"`
|
|
VerificationMethods []VerificationMethod `json:"verification_methods"`
|
|
Accounts []Account `json:"accounts"`
|
|
Credentials []Credential `json:"credentials"`
|
|
}
|
|
|
|
// VerificationMethod represents a DID verification method
|
|
type VerificationMethod struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Controller string `json:"controller"`
|
|
PublicKey string `json:"public_key"`
|
|
Purpose string `json:"purpose"`
|
|
}
|
|
|
|
// Account represents a derived blockchain account
|
|
type Account struct {
|
|
Address string `json:"address"`
|
|
ChainID string `json:"chain_id"`
|
|
CoinType int `json:"coin_type"`
|
|
AccountIndex int `json:"account_index"`
|
|
AddressIndex int `json:"address_index"`
|
|
Label string `json:"label"`
|
|
IsDefault bool `json:"is_default"`
|
|
}
|
|
|
|
// Credential represents a WebAuthn credential
|
|
type Credential struct {
|
|
CredentialID string `json:"credential_id"`
|
|
DeviceName string `json:"device_name"`
|
|
DeviceType string `json:"device_type"`
|
|
Authenticator string `json:"authenticator"`
|
|
Transports []string `json:"transports"`
|
|
CreatedAt string `json:"created_at"`
|
|
LastUsed string `json:"last_used"`
|
|
}
|
|
|
|
// FilterParams parsed from GitHub-style filter syntax
|
|
type FilterParams struct {
|
|
Resource string
|
|
Action string
|
|
Subject string
|
|
}
|
|
|
|
// Enclave holds the plugin state
|
|
type Enclave struct {
|
|
initialized bool
|
|
did string
|
|
}
|
|
|
|
var enclave = &Enclave{}
|
|
|
|
func main() {}
|
|
|
|
//go:wasmexport generate
|
|
func generate() int32 {
|
|
pdk.Log(pdk.LogInfo, "generate: starting database initialization")
|
|
|
|
var input GenerateInput
|
|
if err := pdk.InputJSON(&input); err != nil {
|
|
pdk.SetError(fmt.Errorf("generate: failed to parse input: %w", err))
|
|
return 1
|
|
}
|
|
|
|
if input.Credential == "" {
|
|
pdk.SetError(errors.New("generate: credential is required"))
|
|
return 1
|
|
}
|
|
|
|
credentialBytes, err := base64.StdEncoding.DecodeString(input.Credential)
|
|
if err != nil {
|
|
pdk.SetError(fmt.Errorf("generate: invalid base64 credential: %w", err))
|
|
return 1
|
|
}
|
|
|
|
did, err := initializeDatabase(credentialBytes)
|
|
if err != nil {
|
|
pdk.SetError(fmt.Errorf("generate: failed to initialize database: %w", err))
|
|
return 1
|
|
}
|
|
|
|
enclave.initialized = true
|
|
enclave.did = did
|
|
|
|
dbBytes, err := serializeDatabase()
|
|
if err != nil {
|
|
pdk.SetError(fmt.Errorf("generate: failed to serialize database: %w", err))
|
|
return 1
|
|
}
|
|
|
|
output := GenerateOutput{
|
|
DID: did,
|
|
Database: dbBytes,
|
|
}
|
|
|
|
if err := pdk.OutputJSON(output); err != nil {
|
|
pdk.SetError(fmt.Errorf("generate: failed to output result: %w", err))
|
|
return 1
|
|
}
|
|
|
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s", did))
|
|
return 0
|
|
}
|
|
|
|
//go:wasmexport load
|
|
func load() int32 {
|
|
pdk.Log(pdk.LogInfo, "load: loading database from buffer")
|
|
|
|
var input LoadInput
|
|
if err := pdk.InputJSON(&input); err != nil {
|
|
pdk.SetError(fmt.Errorf("load: failed to parse input: %w", err))
|
|
return 1
|
|
}
|
|
|
|
if len(input.Database) == 0 {
|
|
pdk.SetError(errors.New("load: database buffer is required"))
|
|
return 1
|
|
}
|
|
|
|
did, err := loadDatabase(input.Database)
|
|
if err != nil {
|
|
output := LoadOutput{
|
|
Success: false,
|
|
Error: err.Error(),
|
|
}
|
|
pdk.OutputJSON(output)
|
|
return 1
|
|
}
|
|
|
|
enclave.initialized = true
|
|
enclave.did = did
|
|
|
|
output := LoadOutput{
|
|
Success: true,
|
|
DID: did,
|
|
}
|
|
|
|
if err := pdk.OutputJSON(output); err != nil {
|
|
pdk.SetError(fmt.Errorf("load: failed to output result: %w", err))
|
|
return 1
|
|
}
|
|
|
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("load: loaded database for DID %s", did))
|
|
return 0
|
|
}
|
|
|
|
//go:wasmexport exec
|
|
func exec() int32 {
|
|
pdk.Log(pdk.LogInfo, "exec: executing action")
|
|
|
|
if !enclave.initialized {
|
|
pdk.SetError(errors.New("exec: database not initialized, call generate or load first"))
|
|
return 1
|
|
}
|
|
|
|
var input ExecInput
|
|
if err := pdk.InputJSON(&input); err != nil {
|
|
pdk.SetError(fmt.Errorf("exec: failed to parse input: %w", err))
|
|
return 1
|
|
}
|
|
|
|
if input.Filter == "" {
|
|
pdk.SetError(errors.New("exec: filter is required"))
|
|
return 1
|
|
}
|
|
|
|
params, err := parseFilter(input.Filter)
|
|
if err != nil {
|
|
pdk.SetError(fmt.Errorf("exec: invalid filter: %w", err))
|
|
return 1
|
|
}
|
|
|
|
if input.Token != "" {
|
|
if err := validateUCAN(input.Token, params); err != nil {
|
|
output := ExecOutput{
|
|
Success: false,
|
|
Error: fmt.Sprintf("authorization failed: %s", err.Error()),
|
|
}
|
|
pdk.OutputJSON(output)
|
|
return 1
|
|
}
|
|
}
|
|
|
|
result, err := executeAction(params)
|
|
if err != nil {
|
|
output := ExecOutput{
|
|
Success: false,
|
|
Error: err.Error(),
|
|
}
|
|
pdk.OutputJSON(output)
|
|
return 1
|
|
}
|
|
|
|
output := ExecOutput{
|
|
Success: true,
|
|
Result: result,
|
|
}
|
|
|
|
if err := pdk.OutputJSON(output); err != nil {
|
|
pdk.SetError(fmt.Errorf("exec: failed to output result: %w", err))
|
|
return 1
|
|
}
|
|
|
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("exec: completed %s on %s", params.Action, params.Resource))
|
|
return 0
|
|
}
|
|
|
|
//go:wasmexport query
|
|
func query() int32 {
|
|
pdk.Log(pdk.LogInfo, "query: resolving DID document")
|
|
|
|
if !enclave.initialized {
|
|
pdk.SetError(errors.New("query: database not initialized, call generate or load first"))
|
|
return 1
|
|
}
|
|
|
|
var input QueryInput
|
|
if err := pdk.InputJSON(&input); err != nil {
|
|
pdk.SetError(fmt.Errorf("query: failed to parse input: %w", err))
|
|
return 1
|
|
}
|
|
|
|
if input.DID == "" {
|
|
input.DID = enclave.did
|
|
}
|
|
|
|
if !strings.HasPrefix(input.DID, "did:") {
|
|
pdk.SetError(errors.New("query: invalid DID format"))
|
|
return 1
|
|
}
|
|
|
|
output, err := resolveDID(input.DID)
|
|
if err != nil {
|
|
pdk.SetError(fmt.Errorf("query: failed to resolve DID: %w", err))
|
|
return 1
|
|
}
|
|
|
|
if err := pdk.OutputJSON(output); err != nil {
|
|
pdk.SetError(fmt.Errorf("query: failed to output result: %w", err))
|
|
return 1
|
|
}
|
|
|
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("query: resolved DID %s", input.DID))
|
|
return 0
|
|
}
|
|
|
|
func initializeDatabase(credentialBytes []byte) (string, error) {
|
|
// TODO: Initialize SQLite database with schema
|
|
// TODO: Parse WebAuthn credential
|
|
// TODO: Generate MPC key shares
|
|
// TODO: Create DID document
|
|
// TODO: Insert initial records
|
|
|
|
did := fmt.Sprintf("did:sonr:%x", credentialBytes[:16])
|
|
|
|
pdk.Log(pdk.LogDebug, "initializeDatabase: created schema and initial records")
|
|
return did, nil
|
|
}
|
|
|
|
func serializeDatabase() ([]byte, error) {
|
|
// TODO: Serialize SQLite database to bytes
|
|
// TODO: Encrypt with WebAuthn-derived key
|
|
return []byte("placeholder_database"), nil
|
|
}
|
|
|
|
func loadDatabase(data []byte) (string, error) {
|
|
// TODO: Decrypt database with WebAuthn-derived key
|
|
// TODO: Load SQLite database from bytes
|
|
// TODO: Query for primary DID
|
|
|
|
if len(data) < 10 {
|
|
return "", errors.New("invalid database format")
|
|
}
|
|
|
|
did := "did:sonr:loaded"
|
|
pdk.Log(pdk.LogDebug, "loadDatabase: database loaded successfully")
|
|
return did, nil
|
|
}
|
|
|
|
func parseFilter(filter string) (*FilterParams, error) {
|
|
params := &FilterParams{}
|
|
parts := strings.Fields(filter)
|
|
|
|
for _, part := range parts {
|
|
kv := strings.SplitN(part, ":", 2)
|
|
if len(kv) != 2 {
|
|
continue
|
|
}
|
|
|
|
key, value := kv[0], kv[1]
|
|
switch key {
|
|
case "resource":
|
|
params.Resource = value
|
|
case "action":
|
|
params.Action = value
|
|
case "subject":
|
|
params.Subject = value
|
|
}
|
|
}
|
|
|
|
if params.Resource == "" {
|
|
return nil, errors.New("resource is required")
|
|
}
|
|
if params.Action == "" {
|
|
return nil, errors.New("action is required")
|
|
}
|
|
|
|
return params, nil
|
|
}
|
|
|
|
func validateUCAN(token string, params *FilterParams) error {
|
|
// TODO: Decode UCAN token
|
|
// TODO: Verify signature chain
|
|
// TODO: Check capabilities match params
|
|
// TODO: Verify not expired or revoked
|
|
|
|
if token == "" {
|
|
return errors.New("token is required")
|
|
}
|
|
|
|
pdk.Log(pdk.LogDebug, fmt.Sprintf("validateUCAN: validated token for %s:%s", params.Resource, params.Action))
|
|
return nil
|
|
}
|
|
|
|
func executeAction(params *FilterParams) (json.RawMessage, error) {
|
|
// TODO: Route to appropriate handler based on resource/action
|
|
// TODO: Execute database queries
|
|
// TODO: Return results
|
|
|
|
switch params.Resource {
|
|
case "accounts":
|
|
return executeAccountAction(params)
|
|
case "credentials":
|
|
return executeCredentialAction(params)
|
|
case "sessions":
|
|
return executeSessionAction(params)
|
|
case "grants":
|
|
return executeGrantAction(params)
|
|
default:
|
|
return nil, fmt.Errorf("unknown resource: %s", params.Resource)
|
|
}
|
|
}
|
|
|
|
func executeAccountAction(params *FilterParams) (json.RawMessage, error) {
|
|
switch params.Action {
|
|
case "list":
|
|
accounts := []Account{
|
|
{
|
|
Address: "sonr1abc123...",
|
|
ChainID: "sonr-mainnet-1",
|
|
CoinType: 118,
|
|
AccountIndex: 0,
|
|
AddressIndex: 0,
|
|
Label: "Primary",
|
|
IsDefault: true,
|
|
},
|
|
}
|
|
return json.Marshal(accounts)
|
|
case "sign":
|
|
return json.Marshal(map[string]string{"signature": "placeholder"})
|
|
default:
|
|
return nil, fmt.Errorf("unknown action for accounts: %s", params.Action)
|
|
}
|
|
}
|
|
|
|
func executeCredentialAction(params *FilterParams) (json.RawMessage, error) {
|
|
switch params.Action {
|
|
case "list":
|
|
credentials := []Credential{
|
|
{
|
|
CredentialID: "cred_abc123",
|
|
DeviceName: "MacBook Pro",
|
|
DeviceType: "platform",
|
|
Authenticator: "Touch ID",
|
|
Transports: []string{"internal"},
|
|
CreatedAt: "2025-01-01T00:00:00Z",
|
|
LastUsed: "2025-01-01T00:00:00Z",
|
|
},
|
|
}
|
|
return json.Marshal(credentials)
|
|
default:
|
|
return nil, fmt.Errorf("unknown action for credentials: %s", params.Action)
|
|
}
|
|
}
|
|
|
|
func executeSessionAction(params *FilterParams) (json.RawMessage, error) {
|
|
switch params.Action {
|
|
case "list":
|
|
return json.Marshal([]map[string]interface{}{})
|
|
case "create":
|
|
return json.Marshal(map[string]string{"session_id": "sess_placeholder"})
|
|
case "revoke":
|
|
return json.Marshal(map[string]bool{"revoked": true})
|
|
default:
|
|
return nil, fmt.Errorf("unknown action for sessions: %s", params.Action)
|
|
}
|
|
}
|
|
|
|
func executeGrantAction(params *FilterParams) (json.RawMessage, error) {
|
|
switch params.Action {
|
|
case "list":
|
|
return json.Marshal([]map[string]interface{}{})
|
|
case "create":
|
|
return json.Marshal(map[string]string{"grant_id": "grant_placeholder"})
|
|
case "revoke":
|
|
return json.Marshal(map[string]bool{"revoked": true})
|
|
default:
|
|
return nil, fmt.Errorf("unknown action for grants: %s", params.Action)
|
|
}
|
|
}
|
|
|
|
func resolveDID(did string) (*QueryOutput, error) {
|
|
// TODO: Query database for DID document
|
|
// TODO: Fetch verification methods
|
|
// TODO: Fetch associated accounts
|
|
// TODO: Fetch credentials
|
|
|
|
output := &QueryOutput{
|
|
DID: did,
|
|
Controller: did,
|
|
VerificationMethods: []VerificationMethod{
|
|
{
|
|
ID: did + "#key-1",
|
|
Type: "Ed25519VerificationKey2020",
|
|
Controller: did,
|
|
PublicKey: "placeholder_public_key",
|
|
Purpose: "authentication",
|
|
},
|
|
},
|
|
Accounts: []Account{
|
|
{
|
|
Address: "sonr1abc123...",
|
|
ChainID: "sonr-mainnet-1",
|
|
CoinType: 118,
|
|
AccountIndex: 0,
|
|
AddressIndex: 0,
|
|
Label: "Primary",
|
|
IsDefault: true,
|
|
},
|
|
},
|
|
Credentials: []Credential{
|
|
{
|
|
CredentialID: "cred_abc123",
|
|
DeviceName: "MacBook Pro",
|
|
DeviceType: "platform",
|
|
Authenticator: "Touch ID",
|
|
Transports: []string{"internal"},
|
|
CreatedAt: "2025-01-01T00:00:00Z",
|
|
LastUsed: "2025-01-01T00:00:00Z",
|
|
},
|
|
},
|
|
}
|
|
|
|
return output, nil
|
|
}
|