mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-03 16:41:27 +01:00
capture: a token writes down from outside the browser
POST /api/capture takes a line of text and puts it on a card in an inbox, or at the end of today's diary entry, starting the entry when there is none yet. It answers a logged-in browser, and also a request that carries an Authorization: Bearer token from :token add, so a phone shortcut, a script or curl can capture with no tab open. A token is nwc_ and 32 random bytes. It is shown once, copied, and stored only as its SHA-256, the way a share key is. Only the capture route reads the header. The Session extractor never does, so a token opens nothing else. Wrong tokens go through the login's backoff on a throttle of their own. A removed token is refused from the next request on, because each capture looks the hash up again. A card lands where :card puts one outside the board: the inbox of the space named by "space", or else of the first space with a board. That rule now lives server-side as boards::inbox_of. The first line of the text becomes the title and the rest becomes the notes. Diary text becomes "- HH:MM text" in WORKSPACE_TIMEZONE, appended to the first document written today at the top of the diary. That is the entry :today opens. The append keeps a revision and indexes links like any other save. :token lists, adds and removes tokens, and completes their names after rm. The message that shows a new token stays in the status bar instead of fading, because it cannot be shown again.
This commit is contained in:
1 parent
a42f9074a2
commit
f629f5af97
13 files changed
+758
-9
No files matched your search
@@ -153,6 +153,9 @@ const folderNames = () => [
|
||||
|
||||
const spaceNames = () => workspace.spaces.map((space) => space.slug)
|
||||
|
||||
/** The capture tokens' names, for Tab after `:token rm`; null until fetched. */
|
||||
let tokenNames: string[] | null = null
|
||||
|
||||
/** `:encrypt` and `:decrypt`: a file, or everything under a folder. */
|
||||
const sealTarget = (encrypt: boolean) =>
|
||||
workspace.guard(async () => {
|
||||
@@ -1169,6 +1172,56 @@ export const commands: CommandSpec[] = [
|
||||
workspace.say('usage: :sshkey [add <name> <key> | rm <name> | vault <name> on|off]', 'error')
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'token',
|
||||
args: '[add <name> | rm <name>]',
|
||||
group: 'view',
|
||||
help: 'list, add or remove tokens that capture into the workspace from outside it — add shows the token once and copies it',
|
||||
complete: (arg) => {
|
||||
// Tab cannot wait for the server, so the names are fetched the first
|
||||
// time it is pressed here and offered from then on.
|
||||
if (tokenNames === null) {
|
||||
tokenNames = []
|
||||
void api.listTokens().then((tokens) => (tokenNames = tokens.map((t) => t.name)), () => (tokenNames = null))
|
||||
}
|
||||
return arg.startsWith('rm ') ? tokenNames.map((name) => `rm ${name}`) : ['add ', 'rm ']
|
||||
},
|
||||
run: (arg) =>
|
||||
workspace.guard(async () => {
|
||||
const trimmed = arg.trim()
|
||||
if (trimmed === '' || trimmed === 'ls' || trimmed === 'list') {
|
||||
const tokens = await api.listTokens()
|
||||
tokenNames = tokens.map((t) => t.name)
|
||||
if (tokens.length === 0) return workspace.say('no tokens — :token add <name> makes one')
|
||||
return workspace.say(
|
||||
tokens
|
||||
.map((t) => `${t.name}, ${t.last_used_at === null ? 'never used' : `used ${relative(t.last_used_at)}`}`)
|
||||
.join(' · '),
|
||||
)
|
||||
}
|
||||
const addMatch = /^add\s+(.+)$/.exec(trimmed)
|
||||
if (addMatch) {
|
||||
const added = await api.addToken(addMatch[1].trim())
|
||||
tokenNames = null
|
||||
const copied = await workspace.copy(added.token)
|
||||
return workspace.say(
|
||||
`token ${added.name}: ${added.token} — ${copied ? 'copied; ' : ''}shown this once, the server keeps only its hash`,
|
||||
'info',
|
||||
true,
|
||||
)
|
||||
}
|
||||
const rmMatch = /^rm\s+(.+)$/.exec(trimmed)
|
||||
if (rmMatch) {
|
||||
const tokens = await api.listTokens()
|
||||
const target = tokens.find((t) => t.name === rmMatch[1].trim())
|
||||
if (!target) return workspace.say(`no token named ${rmMatch[1].trim()}`, 'error')
|
||||
await api.removeToken(target.id)
|
||||
tokenNames = null
|
||||
return workspace.say(`removed ${target.name} — it captures nothing from now on`)
|
||||
}
|
||||
workspace.say('usage: :token [add <name> | rm <name>]', 'error')
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'vault',
|
||||
group: 'vault',
|
||||
|
||||
Reference in new issue
Block a user