@sciilo.ai/codex-sidecar 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +332 -0
- package/bin/sciilo-sidecar.js +221 -0
- package/package.json +53 -0
- package/src/banner.js +63 -0
- package/src/bridge.js +844 -0
- package/src/codex-app-server.js +125 -0
- package/src/codex-cli.js +37 -0
- package/src/config.js +87 -0
- package/src/document-seal.js +195 -0
- package/src/vault.js +493 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { createInterface } from 'node:readline'
|
|
4
|
+
import { codexInvocation } from './codex-cli.js'
|
|
5
|
+
|
|
6
|
+
export class CodexAppServer extends EventEmitter {
|
|
7
|
+
constructor({ command, cwd, spawnProcess = spawn } = {}) {
|
|
8
|
+
super()
|
|
9
|
+
this.invocation = codexInvocation(command)
|
|
10
|
+
this.cwd = cwd
|
|
11
|
+
this.spawnProcess = spawnProcess
|
|
12
|
+
this.nextId = 1
|
|
13
|
+
this.pending = new Map()
|
|
14
|
+
this.child = null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async start() {
|
|
18
|
+
if (this.child) return
|
|
19
|
+
const child = this.spawnProcess(
|
|
20
|
+
this.invocation.command,
|
|
21
|
+
[...this.invocation.prefixArgs, 'app-server'],
|
|
22
|
+
{
|
|
23
|
+
cwd: this.cwd,
|
|
24
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
25
|
+
env: process.env,
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
this.child = child
|
|
29
|
+
child.once('error', error => this.failAll(error))
|
|
30
|
+
child.once('exit', (code, signal) => {
|
|
31
|
+
const error = new Error(`Codex App Server stopped (${signal || code}).`)
|
|
32
|
+
this.child = null
|
|
33
|
+
this.failAll(error)
|
|
34
|
+
this.emit('exit', error)
|
|
35
|
+
})
|
|
36
|
+
child.stderr.setEncoding('utf8')
|
|
37
|
+
child.stderr.on('data', chunk => this.emit('log', chunk.trimEnd()))
|
|
38
|
+
createInterface({ input: child.stdout }).on('line', line => this.receiveLine(line))
|
|
39
|
+
|
|
40
|
+
await this.request('initialize', {
|
|
41
|
+
clientInfo: {
|
|
42
|
+
name: 'sciilo-sidecar',
|
|
43
|
+
title: 'Sciilo Codex Sidecar',
|
|
44
|
+
version: '0.1.0',
|
|
45
|
+
},
|
|
46
|
+
capabilities: {
|
|
47
|
+
experimentalApi: true,
|
|
48
|
+
requestAttestation: false,
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
this.notify('initialized')
|
|
52
|
+
const account = await this.request('account/read', { refreshToken: false })
|
|
53
|
+
if (account?.requiresOpenaiAuth && !account?.account) {
|
|
54
|
+
throw new Error('Codex is not authenticated. Run `sciilo-sidecar setup`.')
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
request(method, params) {
|
|
59
|
+
if (!this.child?.stdin?.writable) {
|
|
60
|
+
return Promise.reject(new Error('Codex App Server is unavailable.'))
|
|
61
|
+
}
|
|
62
|
+
const id = this.nextId++
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
this.pending.set(String(id), { resolve, reject, method })
|
|
65
|
+
this.write({ method, id, params })
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
notify(method, params) {
|
|
70
|
+
this.write(params === undefined ? { method } : { method, params })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
respond(id, result) {
|
|
74
|
+
this.write({ id, result })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
respondError(id, message, code = -32603) {
|
|
78
|
+
this.write({ id, error: { code, message } })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
write(frame) {
|
|
82
|
+
this.child?.stdin?.write(`${JSON.stringify(frame)}\n`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
receiveLine(line) {
|
|
86
|
+
if (!line.trim()) return
|
|
87
|
+
let frame
|
|
88
|
+
try {
|
|
89
|
+
frame = JSON.parse(line)
|
|
90
|
+
} catch {
|
|
91
|
+
this.emit('log', `Ignored non-JSON Codex frame: ${line}`)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
if (frame.id !== undefined && !frame.method) {
|
|
95
|
+
const pending = this.pending.get(String(frame.id))
|
|
96
|
+
if (!pending) return
|
|
97
|
+
this.pending.delete(String(frame.id))
|
|
98
|
+
if (frame.error) {
|
|
99
|
+
const error = new Error(frame.error.message || `${pending.method} failed`)
|
|
100
|
+
error.code = frame.error.code
|
|
101
|
+
pending.reject(error)
|
|
102
|
+
} else {
|
|
103
|
+
pending.resolve(frame.result)
|
|
104
|
+
}
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
if (frame.method && frame.id !== undefined) {
|
|
108
|
+
this.emit('request', frame)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
if (frame.method) this.emit('notification', frame)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
stop() {
|
|
115
|
+
const child = this.child
|
|
116
|
+
this.child = null
|
|
117
|
+
if (child && !child.killed) child.kill('SIGTERM')
|
|
118
|
+
this.failAll(new Error('Codex App Server stopped.'))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
failAll(error) {
|
|
122
|
+
for (const pending of this.pending.values()) pending.reject(error)
|
|
123
|
+
this.pending.clear()
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/codex-cli.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url)
|
|
5
|
+
|
|
6
|
+
export function bundledCodexEntrypoint(resolvePackage = require.resolve) {
|
|
7
|
+
return resolvePackage('@openai/codex/bin/codex.js')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function codexInvocation(configuredCommand, resolvePackage) {
|
|
11
|
+
if (configuredCommand) {
|
|
12
|
+
return {
|
|
13
|
+
command: configuredCommand,
|
|
14
|
+
prefixArgs: [],
|
|
15
|
+
source: 'configured',
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
command: process.execPath,
|
|
20
|
+
prefixArgs: [bundledCodexEntrypoint(resolvePackage)],
|
|
21
|
+
source: 'bundled',
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function runCodexSync(
|
|
26
|
+
configuredCommand,
|
|
27
|
+
args,
|
|
28
|
+
options,
|
|
29
|
+
spawnProcess = spawnSync,
|
|
30
|
+
) {
|
|
31
|
+
const invocation = codexInvocation(configuredCommand)
|
|
32
|
+
return spawnProcess(
|
|
33
|
+
invocation.command,
|
|
34
|
+
[...invocation.prefixArgs, ...args],
|
|
35
|
+
options,
|
|
36
|
+
)
|
|
37
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_CODEX_MODEL = 'gpt-5.6-sol'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pinned rather than inherited.
|
|
9
|
+
*
|
|
10
|
+
* A thread started without this setting takes whatever `model_reasoning_effort`
|
|
11
|
+
* the machine's `~/.codex/config.toml` happens to hold — a personal CLI
|
|
12
|
+
* preference, set for an entirely different purpose. On a machine configured
|
|
13
|
+
* for `xhigh`, every answer paid for maximum reasoning before its first token,
|
|
14
|
+
* and the product behaved differently from one computer to the next for a
|
|
15
|
+
* reason no one could see from here.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_REASONING_EFFORT = 'medium'
|
|
18
|
+
|
|
19
|
+
export const defaultConfigPath = () => process.env.SCIILO_SIDECAR_CONFIG
|
|
20
|
+
|| join(homedir(), '.config', 'sciilo-sidecar', 'config.json')
|
|
21
|
+
|
|
22
|
+
export function resolveWorkspace(explicit) {
|
|
23
|
+
return resolve(explicit || process.cwd())
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function loadConfig(path = defaultConfigPath()) {
|
|
27
|
+
|
|
28
|
+
let raw
|
|
29
|
+
try {
|
|
30
|
+
raw = await readFile(path, 'utf8')
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code === 'ENOENT') return null
|
|
33
|
+
throw error
|
|
34
|
+
}
|
|
35
|
+
const config = JSON.parse(raw)
|
|
36
|
+
validateConfig(config)
|
|
37
|
+
return normalizeConfig(config)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function saveConfig(config, path = defaultConfigPath()) {
|
|
41
|
+
validateConfig(config)
|
|
42
|
+
const normalized = normalizeConfig(config)
|
|
43
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 })
|
|
44
|
+
await writeFile(path, `${JSON.stringify(normalized, null, 2)}\n`, {
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
mode: 0o600,
|
|
47
|
+
})
|
|
48
|
+
await chmod(path, 0o600)
|
|
49
|
+
return normalized
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function normalizeConfig(config) {
|
|
53
|
+
const configuredModel = config.model === 'gtp-sol'
|
|
54
|
+
? DEFAULT_CODEX_MODEL
|
|
55
|
+
: config.model
|
|
56
|
+
const configuredCommand = typeof config.codexCommand === 'string'
|
|
57
|
+
? config.codexCommand.trim()
|
|
58
|
+
: ''
|
|
59
|
+
return {
|
|
60
|
+
appUrl: config.appUrl.replace(/\/+$/, ''),
|
|
61
|
+
connectionKey: config.connectionKey.trim(),
|
|
62
|
+
codexCommand: configuredCommand && configuredCommand !== 'codex'
|
|
63
|
+
? configuredCommand
|
|
64
|
+
: null,
|
|
65
|
+
model: configuredModel || DEFAULT_CODEX_MODEL,
|
|
66
|
+
modelProvider: config.modelProvider || null,
|
|
67
|
+
reasoningEffort: (typeof config.reasoningEffort === 'string'
|
|
68
|
+
&& config.reasoningEffort.trim()) || DEFAULT_REASONING_EFFORT,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function validateConfig(config) {
|
|
73
|
+
if (!config || typeof config !== 'object') {
|
|
74
|
+
throw new Error('Sidecar configuration is missing.')
|
|
75
|
+
}
|
|
76
|
+
if (!/^https?:\/\//.test(config.appUrl || '')) {
|
|
77
|
+
throw new Error('appUrl must start with http:// or https://.')
|
|
78
|
+
}
|
|
79
|
+
const appUrl = new URL(config.appUrl)
|
|
80
|
+
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(appUrl.hostname)
|
|
81
|
+
if (appUrl.protocol !== 'https:' && !loopback) {
|
|
82
|
+
throw new Error('HTTPS is required outside the local machine.')
|
|
83
|
+
}
|
|
84
|
+
if (!/^sc_[0-9a-f-]{36}\.[A-Za-z0-9_-]{40,}$/.test(config.connectionKey || '')) {
|
|
85
|
+
throw new Error('The sidecar connection key is invalid.')
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { fieldContext, openField, sealField } from './vault.js'
|
|
2
|
+
|
|
3
|
+
export const MARKER = 'sciilo:sealed:v1:'
|
|
4
|
+
|
|
5
|
+
const TOKEN = /sciilo:sealed:v1:([0-9a-fA-F-]{36}):([a-z_]+):([A-Za-z0-9+/]+={0,2})/g
|
|
6
|
+
|
|
7
|
+
export function isSealed(value) {
|
|
8
|
+
return typeof value === 'string' && value.startsWith(MARKER)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function contextOf(documentId, field) {
|
|
12
|
+
return fieldContext({ documentId, field })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function sealValue(key, documentId, field, plaintext) {
|
|
16
|
+
const sealed = await sealField(key, String(plaintext), contextOf(documentId, field))
|
|
17
|
+
return `${MARKER}${documentId}:${field}:${sealed}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function openValue(key, token) {
|
|
21
|
+
const parts = TOKEN.exec(token)
|
|
22
|
+
TOKEN.lastIndex = 0
|
|
23
|
+
if (!parts) throw new Error('Not a sealed field.')
|
|
24
|
+
const [, documentId, field, payload] = parts
|
|
25
|
+
return openField(key, payload, contextOf(documentId, field))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Opens every sealed token embedded in a piece of text.
|
|
30
|
+
*
|
|
31
|
+
* <p>The replacement goes through a FUNCTION, never a string. Passing the
|
|
32
|
+
* plaintext as the replacement made `$&`, `` $` ``, `$'` and `$1` inside it
|
|
33
|
+
* expand as substitution patterns: a decrypted note containing `100 $&` used to
|
|
34
|
+
* paste the ciphertext token back into what the agent reads. The content is
|
|
35
|
+
* data, and this is the only way to say so.</p>
|
|
36
|
+
*
|
|
37
|
+
* <p>Tokens are opened once each and substituted in a single pass. Replacing
|
|
38
|
+
* them one at a time re-scanned the whole text per token, which on a board
|
|
39
|
+
* carrying several sealed fields is quadratic for no reason.</p>
|
|
40
|
+
*/
|
|
41
|
+
export async function openText(key, text) {
|
|
42
|
+
if (typeof text !== 'string' || !text.includes(MARKER)) return text
|
|
43
|
+
const tokens = [...new Set(text.match(TOKEN) || [])]
|
|
44
|
+
if (!tokens.length) return text
|
|
45
|
+
|
|
46
|
+
const clear = new Map()
|
|
47
|
+
await Promise.all(tokens.map(async token => {
|
|
48
|
+
try {
|
|
49
|
+
clear.set(token, await openValue(key, token))
|
|
50
|
+
} catch {
|
|
51
|
+
// Left as it stands: a token that refuses to open is better shown
|
|
52
|
+
// unreadable than replaced by nothing, which would read as empty.
|
|
53
|
+
}
|
|
54
|
+
}))
|
|
55
|
+
return text.replace(TOKEN, token => clear.get(token) ?? token)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function diagramKind(language, source) {
|
|
59
|
+
const lang = String(language || '').trim().toLowerCase()
|
|
60
|
+
const lower = String(source || '').trim().toLowerCase()
|
|
61
|
+
|
|
62
|
+
if (lang === 'plantuml') {
|
|
63
|
+
if (/\b(?:class|interface|enum|annotation|entity|struct)\b/.test(lower)) {
|
|
64
|
+
return 'Class diagram'
|
|
65
|
+
}
|
|
66
|
+
if (lower.includes('[*]') || /\bstate\s+[\w"]/.test(lower)) return 'State diagram'
|
|
67
|
+
return 'Sequence diagram'
|
|
68
|
+
}
|
|
69
|
+
if (lang !== 'mermaid') return 'Diagram'
|
|
70
|
+
|
|
71
|
+
if (lower.includes('%% sciilo:product-vision')) return 'Product Vision'
|
|
72
|
+
if (/^erdiagram\b/.test(lower)) return 'Data model'
|
|
73
|
+
if (/^classdiagram\b/.test(lower)) return 'Class diagram'
|
|
74
|
+
if (/^sequencediagram\b/.test(lower)) return 'Sequence diagram'
|
|
75
|
+
if (/^statediagram(?:-v2)?\b/.test(lower)) return 'State diagram'
|
|
76
|
+
if (/^mindmap\b/.test(lower)) return 'Mind map'
|
|
77
|
+
if (/^journey\b/.test(lower)) return 'User journey'
|
|
78
|
+
if (/^c4(?:context|container|component|dynamic|deployment)\b/.test(lower)) return 'C4 architecture'
|
|
79
|
+
if (/^(?:flowchart|graph)\b/.test(lower)) return 'Flowchart'
|
|
80
|
+
return 'Mermaid diagram'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** PlantUML announces itself; everything else here is Mermaid. */
|
|
84
|
+
export function guessLanguage(source) {
|
|
85
|
+
return /^\s*@startuml/i.test(String(source || '')) ? 'plantuml' : 'mermaid'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function excerptOf(source) {
|
|
89
|
+
const plain = String(source || '')
|
|
90
|
+
.replace(/^\s{0,3}#{1,6}\s+/gm, '')
|
|
91
|
+
.replace(/\[([^\]]+)]\([^)]+\)/g, '$1')
|
|
92
|
+
.replace(/[*_`>|]/g, '')
|
|
93
|
+
.replace(/\s+/g, ' ')
|
|
94
|
+
.trim()
|
|
95
|
+
return plain.length > 180 ? `${plain.slice(0, 177).trimEnd()}...` : plain
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const VISION_MARKER = '%% sciilo:product-vision'
|
|
99
|
+
|
|
100
|
+
const VISION_SECTIONS = [
|
|
101
|
+
['emotions', 'Desired momentum'],
|
|
102
|
+
['problem', 'Problem'],
|
|
103
|
+
['users', 'Users'],
|
|
104
|
+
['value', 'Value proposition'],
|
|
105
|
+
['capabilities', 'Main capabilities'],
|
|
106
|
+
['journeys', 'Key journeys'],
|
|
107
|
+
['unknowns', 'Unknowns to confirm'],
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
function visionEscaped(value) {
|
|
111
|
+
return String(value ?? '').replace(/\r/g, ' ').replace(/\n/g, ' ').replace(/"/g, '#quot;')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function visionBranch(lines, key, label, items) {
|
|
115
|
+
lines.push(` ${key}["${visionEscaped(label)}"]`)
|
|
116
|
+
items.forEach((item, index) => {
|
|
117
|
+
lines.push(` ${key}_${index + 1}["${visionEscaped(item)}"]`)
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function visionList(raw) {
|
|
122
|
+
if (!Array.isArray(raw)) return []
|
|
123
|
+
return raw.map(value => String(value ?? '').trim()).filter(Boolean).slice(0, 6)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function productVisionSource(args = {}) {
|
|
127
|
+
const lines = [VISION_MARKER, 'mindmap', ` product(("${visionEscaped(args.product)}"))`]
|
|
128
|
+
visionBranch(lines, 'vision', 'Vision', [String(args.vision ?? '')])
|
|
129
|
+
for (const [key, label] of VISION_SECTIONS) {
|
|
130
|
+
visionBranch(lines, key, label, visionList(args[key]))
|
|
131
|
+
}
|
|
132
|
+
return lines.join('\n')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const CONTENT_TOOLS = {
|
|
136
|
+
create_diagram: { fields: ['source'], creates: true, derive: deriveDiagram },
|
|
137
|
+
update_diagram: { fields: ['source'], idFrom: 'document_id', derive: deriveDiagram },
|
|
138
|
+
create_markdown_board: { fields: ['source', 'excerpt'], creates: true, derive: deriveMarkdown },
|
|
139
|
+
update_markdown_board: { fields: ['source', 'excerpt'], idFrom: 'document_id', derive: deriveMarkdown },
|
|
140
|
+
create_product_vision: {
|
|
141
|
+
fields: ['source'],
|
|
142
|
+
creates: true,
|
|
143
|
+
derive: deriveVision,
|
|
144
|
+
strip: ['product', 'vision', ...VISION_SECTIONS.map(([key]) => key)],
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
add_note: { fields: ['content'], idFrom: 'document_id' },
|
|
148
|
+
update_note: { fields: ['content'], idFrom: 'document_id' },
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function deriveDiagram(args) {
|
|
152
|
+
const lang = args.lang || guessLanguage(args.source)
|
|
153
|
+
return { kind: diagramKind(lang, args.source) }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function deriveMarkdown(args) {
|
|
157
|
+
return { excerpt: excerptOf(args.source) }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function deriveVision(args) {
|
|
161
|
+
return { source: productVisionSource(args), lang: 'mermaid' }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Seals the content-bearing arguments of a tool call.
|
|
166
|
+
*
|
|
167
|
+
* Returns the arguments unchanged when there is no key, when the tool carries
|
|
168
|
+
* no content, or when there is nothing to seal. Refusing to write would be a
|
|
169
|
+
* worse failure than writing in clear: the user would lose the work, and the
|
|
170
|
+
* database guard already reports anything readable that reaches storage.
|
|
171
|
+
*/
|
|
172
|
+
export async function sealArguments(key, tool, args) {
|
|
173
|
+
const spec = CONTENT_TOOLS[tool]
|
|
174
|
+
if (!key || !spec || !args || typeof args !== 'object') return args
|
|
175
|
+
|
|
176
|
+
const sealedArgs = { ...args, ...(spec.derive ? spec.derive(args) : {}) }
|
|
177
|
+
const documentId = spec.creates
|
|
178
|
+
? (sealedArgs.documentId = randomId())
|
|
179
|
+
: sealedArgs[spec.idFrom]
|
|
180
|
+
if (!documentId) return args
|
|
181
|
+
|
|
182
|
+
for (const field of spec.fields) {
|
|
183
|
+
const value = sealedArgs[field]
|
|
184
|
+
if (typeof value !== 'string' || !value || isSealed(value)) continue
|
|
185
|
+
sealedArgs[field] = await sealValue(key, documentId, field, value)
|
|
186
|
+
}
|
|
187
|
+
for (const field of spec.strip || []) {
|
|
188
|
+
delete sealedArgs[field]
|
|
189
|
+
}
|
|
190
|
+
return sealedArgs
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function randomId() {
|
|
194
|
+
return crypto.randomUUID()
|
|
195
|
+
}
|