Skip to content

Commit fd177c2

Browse files
author
PV
committed
Support authenticated promotion in AIM
1 parent 831acb4 commit fd177c2

6 files changed

Lines changed: 72 additions & 9 deletions

File tree

ui/aim-chat/src/App.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export default function App() {
5151
const [gateRecipes, setGateRecipes] = useState<GateRecipe[]>([])
5252
const [selectedRecipe, setSelectedRecipe] = useState('')
5353
const [gateServerUrl, setGateServerUrl] = useState('http://localhost:8080')
54+
const [gatePromotionToken, setGatePromotionToken] = useState('')
5455
const [gateArgs, setGateArgs] = useState<Record<string, string>>({})
5556
const [gatePromoteThreshold, setGatePromoteThreshold] = useState(2)
5657

@@ -796,6 +797,11 @@ export default function App() {
796797
if (!activeProfileId || !selectedConversationId) {
797798
return
798799
}
800+
if (!gatePromotionToken.trim()) {
801+
setError('Enter the gateway promotion token')
802+
addToast('Enter the gateway promotion token', 'error')
803+
return
804+
}
799805

800806
setIsWorking(true)
801807
try {
@@ -804,11 +810,13 @@ export default function App() {
804810
activeProfile?.name || '',
805811
selectedConversationId,
806812
gateServerUrl.trim(),
813+
gatePromotionToken,
807814
gatePromoteThreshold,
808815
)
809816
await refreshHistory(activeProfileId, selectedConversationId)
810817
setStatus(`API Gateway enabled: ${gatePromoteThreshold} approvals required`)
811818
addToast(`API Gateway enabled: ${gatePromoteThreshold} approvals required`, 'success')
819+
setGatePromotionToken('')
812820
setError('')
813821
} catch (err) {
814822
const msg = err instanceof Error ? err.message : 'Failed to enable API Gateway'
@@ -1160,6 +1168,8 @@ export default function App() {
11601168
activeRecipe={activeRecipe}
11611169
gateServerUrl={gateServerUrl}
11621170
setGateServerUrl={setGateServerUrl}
1171+
gatePromotionToken={gatePromotionToken}
1172+
setGatePromotionToken={setGatePromotionToken}
11631173
gateArgs={gateArgs}
11641174
gatePromoteThreshold={gatePromoteThreshold}
11651175
setGatePromoteThreshold={setGatePromoteThreshold}

ui/aim-chat/src/api.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,21 @@ describe('api', () => {
7676
createdAt: new Date().toISOString(),
7777
})
7878

79-
const response = await api.gatePromote('profile-1', 'Alice', 'conv-1', 'http://gateway.test', 2)
80-
81-
expect(bootstrapSpy).toHaveBeenCalledWith('profile-1', 'conv-1', 'http://gateway.test')
79+
const response = await api.gatePromote(
80+
'profile-1',
81+
'Alice',
82+
'conv-1',
83+
'http://gateway.test',
84+
'promotion-token',
85+
2,
86+
)
87+
88+
expect(bootstrapSpy).toHaveBeenCalledWith(
89+
'profile-1',
90+
'conv-1',
91+
'http://gateway.test',
92+
'promotion-token',
93+
)
8294
expect(promoteSpy).toHaveBeenCalledWith('profile-1', 'Alice', 'conv-1', 'gateway-kid', 2)
8395
expect(response.message.bodyType).toBe('gate.promote')
8496
})

ui/aim-chat/src/api.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,15 @@ export const api = {
127127
profileName: string,
128128
conversationId: string,
129129
gateServerUrl: string,
130+
promotionToken: string,
130131
threshold: number,
131132
): Promise<{ message: ChatMessage; warning?: string }> {
132-
const bootstrap = await qntm.bootstrapGatewayForConversation(profileId, conversationId, gateServerUrl)
133+
const bootstrap = await qntm.bootstrapGatewayForConversation(
134+
profileId,
135+
conversationId,
136+
gateServerUrl,
137+
promotionToken,
138+
)
133139
const message = await qntm.gatePromoteRequest(
134140
profileId,
135141
profileName,

ui/aim-chat/src/components/GatePanel.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export interface GatePanelProps {
1313
activeRecipe: GateRecipe | null
1414
gateServerUrl: string
1515
setGateServerUrl: (value: string) => void
16+
gatePromotionToken: string
17+
setGatePromotionToken: (value: string) => void
1618
gateArgs: Record<string, string>
1719
gatePromoteThreshold: number
1820
setGatePromoteThreshold: (value: number) => void
@@ -44,6 +46,8 @@ export function GatePanel({
4446
activeRecipe,
4547
gateServerUrl,
4648
setGateServerUrl,
49+
gatePromotionToken,
50+
setGatePromotionToken,
4751
gateArgs,
4852
gatePromoteThreshold,
4953
setGatePromoteThreshold,
@@ -144,6 +148,17 @@ export function GatePanel({
144148
value={gateServerUrl}
145149
onChange={(event) => setGateServerUrl(event.target.value)}
146150
/>
151+
<label className="label" htmlFor="gate-promotion-token">Promotion token <Tooltip text="Provided by the gateway operator. Used only for this bootstrap request and never stored." /></label>
152+
<input
153+
id="gate-promotion-token"
154+
className="input"
155+
type="password"
156+
autoComplete="off"
157+
required
158+
placeholder="Gateway operator token"
159+
value={gatePromotionToken}
160+
onChange={(event) => setGatePromotionToken(event.target.value)}
161+
/>
147162
<label className="label" htmlFor="gate-promote-threshold">Required approvals <Tooltip text="The number of participants who must approve before an API call executes." /></label>
148163
<input
149164
id="gate-promote-threshold"
@@ -156,7 +171,7 @@ export function GatePanel({
156171
<button
157172
className="button full"
158173
type="button"
159-
disabled={isWorking}
174+
disabled={isWorking || !gatePromotionToken.trim()}
160175
onClick={() => void onGatePromote()}
161176
>
162177
{isWorking ? <Spinner /> : 'Enable API Gateway'}

ui/aim-chat/src/qntm.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ describe('browser qntm adapter', () => {
619619
const aliceIdentity = identityFor(alice.id)
620620
const bobIdentity = identityFor(bob.id)
621621

622-
vi.stubGlobal('fetch', vi.fn((input: string | URL | Request, init?: RequestInit) => {
622+
const fetchMock = vi.fn((input: string | URL | Request, init?: RequestInit) => {
623623
const url = typeof input === 'string'
624624
? input
625625
: input instanceof URL
@@ -636,9 +636,15 @@ describe('browser qntm adapter', () => {
636636
}
637637

638638
return relay.handleFetch(input, init)
639-
}))
639+
})
640+
vi.stubGlobal('fetch', fetchMock)
640641

641-
const bootstrap = await bootstrapGatewayForConversation(alice.id, conversationId, 'http://gateway.test')
642+
const bootstrap = await bootstrapGatewayForConversation(
643+
alice.id,
644+
conversationId,
645+
'http://gateway.test',
646+
'promotion-token',
647+
)
642648
expect(bootstrap).toEqual({
643649
gatewayPublicKey: publicKeyToString(hexToBytes(bobIdentity.publicKey)),
644650
gatewayKid: publicKeyToString(hexToBytes(bobIdentity.keyId)),
@@ -647,6 +653,12 @@ describe('browser qntm adapter', () => {
647653
publicKey: publicKeyToString(hexToBytes(bobIdentity.publicKey)),
648654
keyId: publicKeyToString(hexToBytes(bobIdentity.keyId)),
649655
})
656+
expect(fetchMock).toHaveBeenCalledWith(
657+
'http://gateway.test/v1/promote',
658+
expect.objectContaining({
659+
headers: expect.objectContaining({ Authorization: 'Bearer promotion-token' }),
660+
}),
661+
)
650662

651663
const secretMessage = await gateSecretRequest(
652664
alice.id,

ui/aim-chat/src/qntm.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,7 @@ export async function bootstrapGatewayForConversation(
992992
profileId: string,
993993
conversationId: string,
994994
gateServerUrl: string,
995+
promotionToken: string,
995996
): Promise<GatewayBootstrap> {
996997
const convCrypto = getConvCrypto(profileId, conversationId)
997998
if (!convCrypto) throw new Error(`Conversation ${conversationId} not found`)
@@ -1000,10 +1001,17 @@ export async function bootstrapGatewayForConversation(
10001001
if (!baseUrl) {
10011002
throw new Error('Gateway server URL is required')
10021003
}
1004+
const token = promotionToken.trim()
1005+
if (!token) {
1006+
throw new Error('Gateway promotion token is required')
1007+
}
10031008

10041009
const response = await fetch(`${baseUrl}/v1/promote`, {
10051010
method: 'POST',
1006-
headers: { 'Content-Type': 'application/json' },
1011+
headers: {
1012+
'Content-Type': 'application/json',
1013+
Authorization: `Bearer ${token}`,
1014+
},
10071015
body: JSON.stringify({
10081016
conv_id: conversationId,
10091017
conv_aead_key: base64UrlEncode(convCrypto.keys.aeadKey),

0 commit comments

Comments
 (0)