Skip to content

Commit 7e24505

Browse files
committed
first pass at fetching dag data, we'll see
1 parent 7d8b3bd commit 7e24505

10 files changed

Lines changed: 31192 additions & 2505 deletions

File tree

keeperapi/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
"scripts": {
1111
"start": "rollup -cw",
1212
"build": "node ./scripts/cleanDistFolder.js && rollup -c && cp src/proto.d.ts dist",
13-
"update-proto:es6": "pbjs -t static-module -w es6 -o src/proto.js ../../keeperapp-protobuf/APIRequest.proto ../../keeperapp-protobuf/AccountSummary.proto ../../keeperapp-protobuf/automator.proto ../../keeperapp-protobuf/breachwatch.proto ../../keeperapp-protobuf/client.proto ../../keeperapp-protobuf/externalservice.proto ../../keeperapp-protobuf/folder.proto ../../keeperapp-protobuf/push.proto ../../keeperapp-protobuf/record.proto ../../keeperapp-protobuf/servicelogger.proto ../../keeperapp-protobuf/ssocloud.proto ../../keeperapp-protobuf/token.proto ../../keeperapp-protobuf/upsell.proto ../../keeperapp-protobuf/SyncDown.proto ../../keeperapp-protobuf/BI.proto && pbts -o src/proto.d.ts src/proto.js",
14-
"update-proto:cjs": "pbjs -t json-module -w commonjs -o src/proto.js ../../keeperapp-protobuf/APIRequest.proto ../../keeperapp-protobuf/AccountSummary.proto ../../keeperapp-protobuf/automator.proto ../../keeperapp-protobuf/breachwatch.proto ../../keeperapp-protobuf/client.proto ../../keeperapp-protobuf/externalservice.proto ../../keeperapp-protobuf/folder.proto ../../keeperapp-protobuf/push.proto ../../keeperapp-protobuf/record.proto ../../keeperapp-protobuf/servicelogger.proto ../../keeperapp-protobuf/ssocloud.proto ../../keeperapp-protobuf/token.proto ../../keeperapp-protobuf/upsell.proto ../../keeperapp-protobuf/SyncDown.proto && pbjs -t static-module -w commonjs ../../keeperapp-protobuf/APIRequest.proto ../../keeperapp-protobuf/AccountSummary.proto ../../keeperapp-protobuf/automator.proto ../../keeperapp-protobuf/breachwatch.proto ../../keeperapp-protobuf/client.proto ../../keeperapp-protobuf/externalservice.proto ../../keeperapp-protobuf/folder.proto ../../keeperapp-protobuf/push.proto ../../keeperapp-protobuf/record.proto ../../keeperapp-protobuf/servicelogger.proto ../../keeperapp-protobuf/ssocloud.proto ../../keeperapp-protobuf/token.proto ../../keeperapp-protobuf/upsell.proto ../../keeperapp-protobuf/SyncDown.proto ../../keeperapp-protobuf/BI.proto | pbts -o src/proto.d.ts -",
13+
"update-proto:es6": "pbjs -t static-module -w es6 -o src/proto.js ../../keeperapp-protobuf/APIRequest.proto ../../keeperapp-protobuf/AccountSummary.proto ../../keeperapp-protobuf/automator.proto ../../keeperapp-protobuf/breachwatch.proto ../../keeperapp-protobuf/client.proto ../../keeperapp-protobuf/externalservice.proto ../../keeperapp-protobuf/folder.proto ../../keeperapp-protobuf/push.proto ../../keeperapp-protobuf/record.proto ../../keeperapp-protobuf/servicelogger.proto ../../keeperapp-protobuf/ssocloud.proto ../../keeperapp-protobuf/token.proto ../../keeperapp-protobuf/upsell.proto ../../keeperapp-protobuf/SyncDown.proto ../../keeperapp-protobuf/BI.proto ../../keeperapp-protobuf/router.proto && pbts -o src/proto.d.ts src/proto.js",
14+
"update-proto:cjs": "pbjs -t json-module -w commonjs -o src/proto.js ../../keeperapp-protobuf/APIRequest.proto ../../keeperapp-protobuf/AccountSummary.proto ../../keeperapp-protobuf/automator.proto ../../keeperapp-protobuf/breachwatch.proto ../../keeperapp-protobuf/client.proto ../../keeperapp-protobuf/externalservice.proto ../../keeperapp-protobuf/folder.proto ../../keeperapp-protobuf/push.proto ../../keeperapp-protobuf/record.proto ../../keeperapp-protobuf/servicelogger.proto ../../keeperapp-protobuf/ssocloud.proto ../../keeperapp-protobuf/token.proto ../../keeperapp-protobuf/upsell.proto ../../keeperapp-protobuf/SyncDown.proto ../../keeperapp-protobuf/router.proto && pbjs -t static-module -w commonjs ../../keeperapp-protobuf/APIRequest.proto ../../keeperapp-protobuf/AccountSummary.proto ../../keeperapp-protobuf/automator.proto ../../keeperapp-protobuf/breachwatch.proto ../../keeperapp-protobuf/client.proto ../../keeperapp-protobuf/externalservice.proto ../../keeperapp-protobuf/folder.proto ../../keeperapp-protobuf/push.proto ../../keeperapp-protobuf/record.proto ../../keeperapp-protobuf/servicelogger.proto ../../keeperapp-protobuf/ssocloud.proto ../../keeperapp-protobuf/token.proto ../../keeperapp-protobuf/upsell.proto ../../keeperapp-protobuf/SyncDown.proto ../../keeperapp-protobuf/BI.proto ../../keeperapp-protobuf/router.proto | pbts -o src/proto.d.ts -",
1515
"test": "jest",
1616
"types": "tsc --watch",
1717
"types:ci": "tsc",

keeperapi/src/auth.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,10 @@ export class Auth {
11211121
return this.endpoint.executeRest(message, this._sessionToken, options);
11221122
}
11231123

1124+
async executeRouterRest<TIn, TOut>(message: RestMessage<TIn, TOut>): Promise<TOut> {
1125+
return this.endpoint.executeRouterRest(message, this._sessionToken);
1126+
}
1127+
11241128
async executeRestCommand<Request, Response>(command: RestCommand<Request, Response>): Promise<Response> {
11251129
if (!command.baseRequest.username) {
11261130
command.baseRequest.username = this._username;

keeperapi/src/browser/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from "../platform";
1111
export * from "../proto";
1212
export * from "../cryptoWorker";
1313
export * from "../qrc";
14+
export * from "../pam";
1415
import {connectPlatform} from "../platform";
1516
import {browserPlatform} from "./platform";
1617

keeperapi/src/endpoint.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {KeeperError} from './configuration'
2-
import {Authentication, Push, SsoCloud} from './proto'
2+
import {Authentication, Push, Router, SsoCloud} from './proto'
33
import {platform} from './platform'
44
import {
55
formatTimeDiff,
66
generateTransmissionKey,
77
generateHpkeTransmissionKey,
8+
getKeeperRouterUrl,
89
getKeeperUrl,
910
isTwoFactorResultCode, log,
1011
normal64Bytes,
@@ -167,6 +168,36 @@ export class KeeperEndpoint {
167168
return this.executeRestInternal(message, sessionToken)
168169
}
169170

171+
/**
172+
* Executes a request against the PAM router endpoint.
173+
* Unlike the standard Keeper API, the router uses header-based auth (EC-only, no HPKE):
174+
* TransmissionKey: base64(eciesEncrypt(transmissionKey, serverPublicKey))
175+
* Authorization: KeeperUser base64(aesGcmEncrypt(sessionTokenBytes, transmissionKey))
176+
* The request body is the raw AES-GCM-encrypted protobuf (no ApiRequest wrapper).
177+
* The response is a Router.RouterResponse protobuf with an encrypted payload.
178+
*/
179+
async executeRouterRest<TIn, TOut>(message: RestMessage<TIn, TOut>, sessionToken: string): Promise<TOut> {
180+
const transmissionKey = await this.getTransmissionKey()
181+
const sessionTokenBytes = normal64Bytes(sessionToken)
182+
const encryptedSessionToken = await platform.aesGcmEncrypt(sessionTokenBytes, transmissionKey.key)
183+
const encryptedPayload = await platform.aesGcmEncrypt(message.toBytes(), transmissionKey.key)
184+
const headers = {
185+
'TransmissionKey': platform.bytesToBase64(transmissionKey.ecEncryptedKey),
186+
'Authorization': `KeeperUser ${platform.bytesToBase64(encryptedSessionToken)}`
187+
}
188+
const url = getKeeperRouterUrl(this.options.host, message.path)
189+
const response = await platform.post(url, encryptedPayload, headers)
190+
if (!response.data || response.data.length === 0) {
191+
throw new Error(`Empty response from router for ${message.path}`)
192+
}
193+
const routerResponse = Router.RouterResponse.decode(response.data)
194+
if (!routerResponse.encryptedPayload || routerResponse.encryptedPayload.length === 0) {
195+
throw new Error(`Router error for ${message.path}: [${routerResponse.responseCode}] ${routerResponse.errorMessage}`)
196+
}
197+
const decryptedPayload = await platform.aesGcmDecrypt(routerResponse.encryptedPayload, transmissionKey.key)
198+
return message.fromBytes(decryptedPayload)
199+
}
200+
170201
private async executeRestInternal<TIn, TOut>(message: RestInMessage<TIn> | RestOutMessage<TOut> | RestMessage<TIn, TOut> | RestActionMessage, sessionToken?: string, options?: ExecuteRestOptions): Promise<TOut | void> {
171202
this._transmissionKey = await this.getTransmissionKey()
172203
while (true) {

keeperapi/src/node/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from "../platform";
1111
export * from "../proto";
1212
export * from "../cryptoWorker";
1313
export * from "../qrc";
14+
export * from "../pam";
1415
import {connectPlatform} from "../platform";
1516
import {nodePlatform} from "./platform";
1617

keeperapi/src/pam.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {Auth} from './auth'
2+
import {GraphSync} from './proto'
3+
import {normal64Bytes, webSafe64FromBytes} from './utils'
4+
import {pamGetLeafsMessage, pamMultiSyncMessage, pamSyncMessage} from './restMessages'
5+
6+
export type PamDagSyncRequest = {
7+
streamId: string
8+
syncPoint?: number
9+
}
10+
11+
/**
12+
* Syncs the PAM link DAG for a single record UID.
13+
* Resolves the config root via get_leafs, then syncs that stream.
14+
* Use this for on-demand per-record syncing.
15+
*/
16+
export async function syncDagForPamRecord(
17+
auth: Auth,
18+
recordUid: string,
19+
origin?: Uint8Array
20+
): Promise<GraphSync.IGraphSyncMultiResult> {
21+
const recordUidBytes = normal64Bytes(recordUid)
22+
const leafsResult = await auth.executeRouterRest(pamGetLeafsMessage({ vertices: [recordUidBytes] }))
23+
const refs = leafsResult.refs ?? []
24+
if (refs.length === 0) {
25+
return { results: [] }
26+
}
27+
const queries: GraphSync.IGraphSyncQuery[] = refs
28+
.filter(ref => ref.value && ref.value.length > 0)
29+
.map(ref => ({
30+
streamId: ref.value!,
31+
origin,
32+
syncPoint: 0
33+
}))
34+
if (queries.length === 0) {
35+
return { results: [] }
36+
}
37+
return auth.executeRouterRest(pamMultiSyncMessage({ queries }))
38+
}
39+
40+
/**
41+
* Syncs the PAM link DAG for a known config root UID.
42+
* Use this when the config/network UID is already known.
43+
*/
44+
export async function syncPamLinkDagForConfigRoot(
45+
auth: Auth,
46+
configUid: string,
47+
syncPoint: number = 0,
48+
origin?: Uint8Array
49+
): Promise<GraphSync.IGraphSyncResult> {
50+
const streamId = normal64Bytes(configUid)
51+
return auth.executeRouterRest(pamSyncMessage({ streamId, origin, syncPoint }))
52+
}
53+
54+
/**
55+
* Bulk-loads the PAM link DAG for multiple record UIDs and/or config UIDs.
56+
* Resolves config roots for all record UIDs via get_leafs, unions with explicit
57+
* configUids, then multi-syncs all streams. Use this for startup bulk loading.
58+
*/
59+
export async function loadPamLinkDag(
60+
auth: Auth,
61+
recordUids: string[],
62+
configUids: string[] = [],
63+
origin?: Uint8Array
64+
): Promise<GraphSync.IGraphSyncMultiResult> {
65+
const uniqueConfigUidBytes = new Map<string, Uint8Array>()
66+
67+
if (recordUids.length > 0) {
68+
const vertices = recordUids.map(uid => normal64Bytes(uid))
69+
const leafsResult = await auth.executeRouterRest(pamGetLeafsMessage({ vertices }))
70+
for (const ref of leafsResult.refs ?? []) {
71+
if (ref.value && ref.value.length > 0) {
72+
const key = webSafe64FromBytes(ref.value)
73+
uniqueConfigUidBytes.set(key, ref.value)
74+
}
75+
}
76+
}
77+
78+
for (const uid of configUids) {
79+
const bytes = normal64Bytes(uid)
80+
uniqueConfigUidBytes.set(uid, bytes)
81+
}
82+
83+
if (uniqueConfigUidBytes.size === 0) {
84+
return { results: [] }
85+
}
86+
87+
const queries: GraphSync.IGraphSyncQuery[] = Array.from(uniqueConfigUidBytes.values()).map(streamId => ({
88+
streamId,
89+
origin,
90+
syncPoint: 0,
91+
maxCount: 500
92+
}))
93+
94+
return auth.executeRouterRest(pamMultiSyncMessage({ queries }))
95+
}
96+
97+
/**
98+
* Fetches the config root UIDs (stream IDs) for the given leaf (resource) UIDs.
99+
* Returns the raw refs from the router response.
100+
*/
101+
export async function getConfigRootsForRecordUids(
102+
auth: Auth,
103+
recordUids: string[]
104+
): Promise<GraphSync.IGraphSyncRef[]> {
105+
if (recordUids.length === 0) return []
106+
const vertices = recordUids.map(uid => normal64Bytes(uid))
107+
const result = await auth.executeRouterRest(pamGetLeafsMessage({ vertices }))
108+
return result.refs ?? []
109+
}

0 commit comments

Comments
 (0)