Add Remote session foundations and feasibility checks
Add opt-in Tentacle protocol 1.1, explicit remote grants, lifecycle IPC and expiring core leases in both SDKs. Revoke access before update preparation, including failed updates. Add transient secret entries, shared Braille cell export and isolated Xorg recovery experiments with focused tests and documented limitations. Production consent, desktop services and live interoperability remain pending.
This commit is contained in:
1 parent
a47253d697
commit
573d15d7b4
45 files changed
+3634
-80
No files matched your search
@@ -38,6 +38,8 @@ type Element struct {
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
MaxLength int `json:"max_length,omitempty"`
|
||||
// Secret is supported only on transient entries, never preference pages.
|
||||
Secret bool `json:"secret,omitempty"`
|
||||
}
|
||||
type Form struct {
|
||||
Title string `json:"title"`
|
||||
@@ -107,6 +109,11 @@ func NewSession(form Form) (*Session, error) {
|
||||
// ValidatePreferences shares control validation with internal forms, while
|
||||
// allowing a native, scrolling page to contain several full voice editors.
|
||||
func ValidatePreferences(form Form) error {
|
||||
for _, element := range form.Elements {
|
||||
if element.Secret {
|
||||
return errors.New("secret entries are not supported in preferences")
|
||||
}
|
||||
}
|
||||
_, err := new_session(form, 256, 256)
|
||||
return err
|
||||
}
|
||||
@@ -132,6 +139,9 @@ func new_session(form Form, maxItems, maxElements int) (*Session, error) {
|
||||
return nil, errors.New("controls need unique IDs, labels and supported kinds")
|
||||
}
|
||||
ids[element.ID] = true
|
||||
if element.Secret && element.Kind != Entry {
|
||||
return nil, errors.New("only entries may be secret")
|
||||
}
|
||||
if element.MaxLength == 0 {
|
||||
element.MaxLength = maxTextLength
|
||||
}
|
||||
@@ -190,8 +200,12 @@ func (session *Session) Presentation() Presentation {
|
||||
case List:
|
||||
text += fmt.Sprintf(": %s, %d of %d", element.Items[state.selected].Label, state.selected+1, len(element.Items))
|
||||
case Entry:
|
||||
text += ", edit: " + element.Text
|
||||
cursor = len([]rune(element.Label+", edit: ")) + state.caret
|
||||
prefix, value := ", edit: ", element.Text
|
||||
if element.Secret {
|
||||
prefix, value = ", password edit: ", strings.Repeat("*", utf8.RuneCountInString(element.Text))
|
||||
}
|
||||
text += prefix + value
|
||||
cursor = len([]rune(element.Label+prefix)) + state.caret
|
||||
if state.caret != state.anchor {
|
||||
text += fmt.Sprintf(" (selected %d characters)", abs(state.caret-state.anchor))
|
||||
}
|
||||
@@ -205,6 +219,9 @@ func (session *Session) Presentation() Presentation {
|
||||
text += ", button"
|
||||
}
|
||||
speech := text
|
||||
if element.Secret {
|
||||
speech = element.Label + ", password edit, " + characterCount(utf8.RuneCountInString(element.Text))
|
||||
}
|
||||
if element.Kind == List {
|
||||
speech = element.Items[state.selected].Label
|
||||
}
|
||||
@@ -225,7 +242,12 @@ func (session *Session) Opening() Presentation {
|
||||
return output
|
||||
}
|
||||
|
||||
// Values includes plaintext secrets for the submitting caller. It must never
|
||||
// be used for presentation, logging or generic settings persistence.
|
||||
func (session *Session) Values() map[string]Value {
|
||||
if session.closed {
|
||||
return nil
|
||||
}
|
||||
if session.grid != nil {
|
||||
return session.grid.values()
|
||||
}
|
||||
@@ -240,6 +262,17 @@ func (session *Session) Values() map[string]Value {
|
||||
return values
|
||||
}
|
||||
|
||||
// Close drops the editor's references to secrets on submission, cancellation
|
||||
// or capture revocation. Go strings do not provide guaranteed memory erasure.
|
||||
func (session *Session) Close() {
|
||||
session.closed = true
|
||||
for index := range session.elements {
|
||||
if session.elements[index].element.Secret {
|
||||
session.elements[index].element.Text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (session *Session) Handle(event Event) Effect {
|
||||
if session.grid != nil {
|
||||
return session.grid.handle(event)
|
||||
@@ -248,7 +281,7 @@ func (session *Session) Handle(event Event) Effect {
|
||||
return Effect{}
|
||||
}
|
||||
if event.Key == Escape {
|
||||
session.closed = true
|
||||
session.Close()
|
||||
return Effect{Cancelled: true}
|
||||
}
|
||||
if event.Key == Tab {
|
||||
@@ -372,6 +405,13 @@ func abs(value int) int {
|
||||
return value
|
||||
}
|
||||
|
||||
func characterCount(count int) string {
|
||||
if count == 1 {
|
||||
return "1 character"
|
||||
}
|
||||
return fmt.Sprintf("%d characters", count)
|
||||
}
|
||||
|
||||
func (session *Session) edit(event Event) Effect {
|
||||
state := &session.elements[session.focus]
|
||||
characters := []rune(state.element.Text)
|
||||
@@ -386,6 +426,11 @@ func (session *Session) edit(event Event) Effect {
|
||||
case "v":
|
||||
return Effect{Paste: true}
|
||||
case "c", "x":
|
||||
if state.element.Secret {
|
||||
output := session.Presentation()
|
||||
output.Speech = "Copy and cut are unavailable for password fields"
|
||||
return Effect{Presentation: output}
|
||||
}
|
||||
if start == end {
|
||||
return Effect{}
|
||||
}
|
||||
@@ -439,6 +484,8 @@ func (session *Session) edit(event Event) Effect {
|
||||
output := session.Presentation()
|
||||
if next == len(characters) {
|
||||
output.Speech = "End of text"
|
||||
} else if state.element.Secret {
|
||||
output.Speech = "Hidden character"
|
||||
} else {
|
||||
output.Speech = string(characters[next])
|
||||
}
|
||||
@@ -462,7 +509,11 @@ func (session *Session) edit(event Event) Effect {
|
||||
state.element.Text = string(append(characters[:start], characters[end:]...))
|
||||
state.caret, state.anchor = start, start
|
||||
output := session.Presentation()
|
||||
output.Speech = "Deleted " + removed
|
||||
if state.element.Secret {
|
||||
output.Speech = "Deleted " + characterCount(end-start)
|
||||
} else {
|
||||
output.Speech = "Deleted " + removed
|
||||
}
|
||||
return Effect{Presentation: output}
|
||||
}
|
||||
if event.Key == Text && event.Text != "" && !event.Control {
|
||||
@@ -501,6 +552,10 @@ func (session *Session) Insert(text string) Effect {
|
||||
state.element.Text = string(characters[:start]) + text + string(characters[end:])
|
||||
state.caret, state.anchor = start+len(inserted), start+len(inserted)
|
||||
output := session.Presentation()
|
||||
output.Speech = text
|
||||
if state.element.Secret {
|
||||
output.Speech = "Inserted " + characterCount(len(inserted))
|
||||
} else {
|
||||
output.Speech = text
|
||||
}
|
||||
return Effect{Presentation: output}
|
||||
}
|
||||
Reference in new issue
Block a user