@bardioc/app-sdk 0.4.8 → 0.4.10

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/README.md CHANGED
@@ -105,7 +105,7 @@ export default defineConfig({
105
105
  });
106
106
  ```
107
107
 
108
- Handles dev base path, port config, host session proxy routes, optional Cloudflare tunnel, platform fonts, and manifest validation on build.
108
+ Handles dev base path, port config, host session proxy routes, platform fonts, and manifest validation on build.
109
109
 
110
110
  ### Platform fonts
111
111
 
@@ -118,7 +118,7 @@ The Bardioc platform guarantees a stable font endpoint wherever an app runs —
118
118
  It provides `Nunito Sans Variable` (normal + italic) and `Geist Mono Variable` as variable fonts (latin subset), matching the `--font-nunito-sans` / `--font-geist-mono` theme variables that `@bardioc/ui` resolves by default.
119
119
 
120
120
  - **Embedded in the OS** the URL resolves same-origin against the host, which serves it with immutable caching — one download shared by the host and every app.
121
- - **Standalone dev / tunnel mode** the `bardiocApp()` plugin serves the same files from assets shipped inside this package (dev server only — they are never added to your bundle). This applies to Vite-based apps; the angular and nextjs templates run their own dev servers, where the link is a harmless 404 standalone and resolves normally embedded in the OS.
121
+ - **Standalone dev** the `bardiocApp()` plugin serves the same files from assets shipped inside this package (dev server only — they are never added to your bundle). This applies to Vite-based apps; the angular and nextjs templates run their own dev servers, where the link is a harmless 404 standalone and resolves normally embedded in the OS.
122
122
 
123
123
  New apps scaffolded with `create-bardioc-app` include the `<link>` already.
124
124
 
@@ -131,7 +131,16 @@ Standalone local development is host-backed only.
131
131
 
132
132
  The Vite plugin proxies `/rest/*` requests to `API_BASE_URL` with the `X-Dev-Secret` header, and `installDevBridge()` handles SDK messages when running in standalone dev.
133
133
 
134
- For public testing against a live URL, run `pnpm dev:live` and use the emitted tunnel URL as the app's live URL in the host App Store configuration.
134
+ Dev servers that cannot host the Vite plugin (angular, nextjs) run the same proxy as a standalone server:
135
+
136
+ ```javascript
137
+ import { startDevAuthProxy } from '@bardioc/app-sdk/dev-proxy';
138
+
139
+ // Reads DEV_SECRET / API_BASE_URL from the project's .env files; resolves to null when unset.
140
+ await startDevAuthProxy({ projectDir: process.cwd(), port: 3105 });
141
+ ```
142
+
143
+ Point the app at it with `installDevBridge({ apiBaseUrl: 'http://127.0.0.1:3105/rest' })`.
135
144
 
136
145
  ## React hooks
137
146
 
@@ -199,14 +208,34 @@ await sdk.transport.graph.connect(fromId, toId, 'ogit/relates');
199
208
  // Delete node
200
209
  await sdk.transport.graph.delete('node-id');
201
210
 
211
+ // Combined query: Elasticsearch picks the start vertices, Gremlin traverses from them
212
+ const speeches = await sdk.transport.graph.combined(
213
+ '+(ogit\\/_type: "ogit/Politics/AgendaItem")',
214
+ "inE().outV().has('ogit/_type', 'ogit/Politics/Discussion')",
215
+ { limit: 1000 }
216
+ );
217
+
202
218
  // Time series
203
219
  const values = await sdk.transport.graph.timeseries.get('ts-id', {
204
220
  from: '2026-01-01',
205
221
  to: '2026-01-31',
206
222
  });
223
+
224
+ // Several series in one request, averaged per timestamp
225
+ const merged = await sdk.transport.graph.timeseries.get('ts-id', {
226
+ from: '2026-01-01',
227
+ to: '2026-01-31',
228
+ withIds: ['ts-id-2', 'ts-id-3'],
229
+ aggregate: 'avg',
230
+ });
207
231
  ```
208
232
 
209
- Available methods: `get`, `getMany`, `getByXid`, `query`, `gremlin`, `combined`, `create`, `update`, `delete`, `connect`, `history`, `batch.delete`, `timeseries.get`, `timeseries.add`, `timeseries.query`, `content.set`, `content.get`.
233
+ Available methods: `get`, `getMany`, `getByXid`, `query`, `gremlin`, `combined`, `create`, `update`, `delete`, `connect`, `history`, `batch.delete`, `timeseries.get`, `timeseries.add`, `timeseries.query`, `timeseries.history`, `content.set`, `content.get`.
234
+
235
+ `combined` and multi-ID time series reads both return one flat result set: a combined query carries
236
+ no marker for which start vertex a row came from (label and `select()` in the traversal when you need
237
+ it), and time series entries carry only `timestamp` and `value` (read one ID per request when values
238
+ must stay attributable).
210
239
 
211
240
  All graph request option types accept optional `scopeId` when a request must target a specific scope.
212
241
 
@@ -0,0 +1,15 @@
1
+ export interface DevAuthEnv {
2
+ devSecret?: string;
3
+ apiBaseUrl?: string;
4
+ }
5
+ export interface ResolveDevAuthEnvOptions {
6
+ /** Directory holding the `.env` files (Vite's `envDir`, or the app root). */
7
+ envDir: string;
8
+ /** Vite mode, when the mode-specific files should be read too. */
9
+ mode?: string;
10
+ }
11
+ /**
12
+ * Resolves the standalone dev-auth contract (`DEV_SECRET` + `API_BASE_URL`) the same way
13
+ * Vite loads env: dotenv files first, `process.env` last so a shell value always wins.
14
+ */
15
+ export declare function resolveDevAuthEnv(options: ResolveDevAuthEnvOptions): DevAuthEnv;
@@ -0,0 +1,33 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { readDotEnvFile } from './dot-env.js';
4
+ /**
5
+ * Resolves the standalone dev-auth contract (`DEV_SECRET` + `API_BASE_URL`) the same way
6
+ * Vite loads env: dotenv files first, `process.env` last so a shell value always wins.
7
+ */
8
+ export function resolveDevAuthEnv(options) {
9
+ const mode = options.mode?.trim();
10
+ const env = {};
11
+ for (const fileName of [
12
+ '.env',
13
+ '.env.local',
14
+ mode ? `.env.${mode}` : null,
15
+ mode ? `.env.${mode}.local` : null,
16
+ ]) {
17
+ if (!fileName)
18
+ continue;
19
+ const filePath = resolve(options.envDir, fileName);
20
+ if (existsSync(filePath)) {
21
+ Object.assign(env, readDotEnvFile(filePath));
22
+ }
23
+ }
24
+ for (const [key, value] of Object.entries(process.env)) {
25
+ if (typeof value === 'string') {
26
+ env[key] = value;
27
+ }
28
+ }
29
+ return {
30
+ devSecret: env.DEV_SECRET?.trim(),
31
+ apiBaseUrl: env.API_BASE_URL?.trim().replace(/\/$/, ''),
32
+ };
33
+ }
@@ -1,4 +1,4 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http';
1
+ import { type IncomingMessage, type Server, type ServerResponse } from 'node:http';
2
2
  export interface DevAuthProxyOptions {
3
3
  /** Upstream BFF base URL (e.g. `https://your-bff-host`). */
4
4
  apiBaseUrl: string;
@@ -9,9 +9,30 @@ export interface DevAuthProxyOptions {
9
9
  }
10
10
  interface SendJsonOptions {
11
11
  allowOrigin?: string;
12
+ requestOrigin?: string;
12
13
  }
13
14
  export declare function readRequestBody(req: IncomingMessage): Promise<Buffer>;
14
- export declare function applyCors(res: ServerResponse, allowOrigin?: string): void;
15
+ export declare function applyCors(res: ServerResponse, allowOrigin?: string, requestOrigin?: string): void;
15
16
  export declare function sendJson(res: ServerResponse, status: number, body: unknown, options?: SendJsonOptions): void;
16
17
  export declare function createDevAuthProxyHandler(options: DevAuthProxyOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
18
+ export interface StartDevAuthProxyOptions {
19
+ /** Port the proxy listens on. Templates use the app port + 100. */
20
+ port: number;
21
+ /** Directory the `.env` files are read from. Defaults to `process.cwd()`. */
22
+ projectDir?: string;
23
+ /** Interface to bind. Defaults to `127.0.0.1`. */
24
+ host?: string;
25
+ /** CORS origin for the app dev server. Defaults to `*` (echoed back per request). */
26
+ allowOrigin?: string;
27
+ /** Explicit upstream, skips env resolution when both this and `devSecret` are given. */
28
+ apiBaseUrl?: string;
29
+ devSecret?: string;
30
+ /** Mode-specific `.env.<mode>` files to include. */
31
+ mode?: string;
32
+ }
33
+ /**
34
+ * Starts a standalone dev-auth proxy for dev servers that cannot use the Vite plugin
35
+ * (angular, nextjs). Resolves to `null` when `DEV_SECRET` / `API_BASE_URL` are missing.
36
+ */
37
+ export declare function startDevAuthProxy(options: StartDevAuthProxyOptions): Promise<Server | null>;
17
38
  export {};
package/dist/dev-proxy.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { createServer } from 'node:http';
2
+ import { resolveDevAuthEnv } from './dev-auth-env.js';
1
3
  export async function readRequestBody(req) {
2
4
  return new Promise((resolve, reject) => {
3
5
  const chunks = [];
@@ -6,16 +8,23 @@ export async function readRequestBody(req) {
6
8
  req.on('error', reject);
7
9
  });
8
10
  }
9
- export function applyCors(res, allowOrigin) {
11
+ export function applyCors(res, allowOrigin, requestOrigin) {
10
12
  if (!allowOrigin) {
11
13
  return;
12
14
  }
13
- res.setHeader('Access-Control-Allow-Origin', allowOrigin);
15
+ // The dev bridge fetches with `credentials: 'include'`, which a browser refuses to pair
16
+ // with a wildcard origin — echo the caller's origin instead and allow credentials.
17
+ const credentialed = allowOrigin === '*' && !!requestOrigin;
18
+ res.setHeader('Access-Control-Allow-Origin', credentialed ? requestOrigin : allowOrigin);
19
+ if (credentialed) {
20
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
21
+ res.setHeader('Vary', 'Origin');
22
+ }
14
23
  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
15
24
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Scope-ID, X-Requested-With');
16
25
  }
17
26
  export function sendJson(res, status, body, options = {}) {
18
- applyCors(res, options.allowOrigin);
27
+ applyCors(res, options.allowOrigin, options.requestOrigin);
19
28
  res.statusCode = status;
20
29
  res.setHeader('Content-Type', 'application/json');
21
30
  res.setHeader('Cache-Control', 'no-store');
@@ -46,14 +55,15 @@ export function createDevAuthProxyHandler(options) {
46
55
  const { devSecret, allowOrigin } = options;
47
56
  return async (req, res) => {
48
57
  const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
58
+ const requestOrigin = typeof req.headers.origin === 'string' ? req.headers.origin : undefined;
49
59
  if (req.method === 'OPTIONS') {
50
- applyCors(res, allowOrigin);
60
+ applyCors(res, allowOrigin, requestOrigin);
51
61
  res.statusCode = 204;
52
62
  res.end();
53
63
  return;
54
64
  }
55
65
  if (!url.pathname.startsWith('/rest/')) {
56
- sendJson(res, 404, { error: 'not_found' }, { allowOrigin });
66
+ sendJson(res, 404, { error: 'not_found' }, { allowOrigin, requestOrigin });
57
67
  return;
58
68
  }
59
69
  const upstreamPath = url.pathname.slice('/rest'.length) + url.search;
@@ -95,7 +105,7 @@ export function createDevAuthProxyHandler(options) {
95
105
  body = await readRequestBody(req);
96
106
  }
97
107
  catch {
98
- sendJson(res, 400, { error: 'invalid_body' }, { allowOrigin });
108
+ sendJson(res, 400, { error: 'invalid_body' }, { allowOrigin, requestOrigin });
99
109
  return;
100
110
  }
101
111
  try {
@@ -105,7 +115,7 @@ export function createDevAuthProxyHandler(options) {
105
115
  body: body.length > 0 ? new Uint8Array(body) : undefined,
106
116
  cache: 'no-store',
107
117
  });
108
- applyCors(res, allowOrigin);
118
+ applyCors(res, allowOrigin, requestOrigin);
109
119
  res.statusCode = upstreamRes.status;
110
120
  res.setHeader('Cache-Control', 'no-store');
111
121
  const upstreamContentType = upstreamRes.headers.get('content-type');
@@ -116,7 +126,45 @@ export function createDevAuthProxyHandler(options) {
116
126
  res.end(responseBody);
117
127
  }
118
128
  catch {
119
- sendJson(res, 502, { error: 'upstream_unavailable' }, { allowOrigin });
129
+ sendJson(res, 502, { error: 'upstream_unavailable' }, { allowOrigin, requestOrigin });
120
130
  }
121
131
  };
122
132
  }
133
+ /**
134
+ * Starts a standalone dev-auth proxy for dev servers that cannot use the Vite plugin
135
+ * (angular, nextjs). Resolves to `null` when `DEV_SECRET` / `API_BASE_URL` are missing.
136
+ */
137
+ export async function startDevAuthProxy(options) {
138
+ const { port, host = '127.0.0.1', allowOrigin = '*' } = options;
139
+ const env = options.apiBaseUrl && options.devSecret
140
+ ? { apiBaseUrl: options.apiBaseUrl, devSecret: options.devSecret }
141
+ : resolveDevAuthEnv({ envDir: options.projectDir ?? process.cwd(), mode: options.mode });
142
+ if (!env.apiBaseUrl || !env.devSecret) {
143
+ process.stderr.write('[bardioc-dev-auth] DEV_SECRET and API_BASE_URL are required for dev auth — proxy disabled\n');
144
+ return null;
145
+ }
146
+ const handler = createDevAuthProxyHandler({
147
+ apiBaseUrl: env.apiBaseUrl,
148
+ devSecret: env.devSecret,
149
+ allowOrigin,
150
+ });
151
+ const server = createServer((req, res) => {
152
+ void handler(req, res);
153
+ });
154
+ await new Promise((resolve, reject) => {
155
+ server.once('error', reject);
156
+ server.listen(port, host, () => {
157
+ server.off('error', reject);
158
+ resolve();
159
+ });
160
+ });
161
+ const address = server.address();
162
+ const boundPort = typeof address === 'object' && address ? address.port : port;
163
+ process.stdout.write(`[bardioc-dev-auth] proxying /rest/* → ${env.apiBaseUrl} on http://${host}:${boundPort}/rest\n`);
164
+ for (const signal of ['SIGINT', 'SIGTERM']) {
165
+ process.once(signal, () => {
166
+ server.close(() => process.exit(0));
167
+ });
168
+ }
169
+ return server;
170
+ }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  export { createHostBridge } from './host-bridge.js';
2
2
  export { DEFAULT_WINDOW_SIZE, MANIFEST_FILENAME, WINDOW_SIZES, validateManifest, } from './manifest.js';
3
3
  export type { AppManifest, AppManifestPermission, AppManifestWindowConfig, CustomWindowSize, WindowSizePreset, } from './manifest.js';
4
- export { useGraphEvents, useOsConfig } from './react.js';
5
4
  export { SDK_CONTRACT_MATRIX, getRequiredPermissionForSdkMessage } from './contract-matrix.js';
6
5
  export { ALLOWED_KERNEL_MESSAGES, SDK_MSG } from './protocol.js';
7
6
  export type { SdkMessageType } from './protocol.js';
8
7
  export { EntityNotFoundError, NetworkError, PermissionError, SdkError, TimeoutError, ValidationError, } from './errors.js';
9
- export type { Application, BatchResult, Configuration, CreateMemberInput, CreateUnitInput, DeleteOptions, EventSubscribeRequest, EventSubscribeResponse, EventUnsubscribeRequest, EventsApi, FileEntry, GetNodeOptions, GraphEdgeRaw, GraphEvent, GraphEventPushPayload, GraphEventType, GraphNodeRaw, GraphTransport, GremlinOptions, HistoryEntry, HistoryOptions, HostBridge, HostBridgeConfig, InstanceList, MutationOptions, NotifyLevel, OrgMember, OrgStructure, OrgUnit, OsConfig, OsRequestOptions, OsTransport, QueryOptions, SdkCommandDefinition, SdkCommandHandler, SdkCommandInvokePayload, SdkIdbDeletePayload, SdkIdbEntry, SdkIdbGetPayload, SdkIdbPutPayload, SdkIdbQueryPayload, SdkIdbStore, SdkInitResponse, SdkKernelProxyPayload, SdkMenuItem, SdkMenuRoot, SdkMessage, SdkNotifyPayload, SdkResponse, SdkSetActiveTabPayload, SdkSetCommandsPayload, SdkSetMenuPayload, SdkShortcutEventPayload, SdkStorage, SdkStorageDeletePayload, SdkStorageGetPayload, SdkStorageSetPayload, SdkTransport, SdkTransportPayload, TimeseriesOptions, TimeseriesQueryOptions, TimeseriesValue, TransportScopeOptions, UpdateMemberInput, UpdateUnitInput, UserProfile, } from './types.js';
8
+ export type { Application, BatchResult, CombinedQueryOptions, Configuration, CreateMemberInput, CreateUnitInput, DeleteOptions, EventSubscribeRequest, EventSubscribeResponse, EventUnsubscribeRequest, EventsApi, FileEntry, GetNodeOptions, GraphEdgeRaw, GraphEvent, GraphEventPushPayload, GraphEventType, GraphNodeRaw, GraphTransport, GremlinOptions, HistoryEntry, HistoryOptions, HostBridge, HostBridgeConfig, InstanceList, MutationOptions, NotifyLevel, OrgMember, OrgStructure, OrgUnit, OsConfig, OsRequestOptions, OsTransport, QueryOptions, SdkCommandDefinition, SdkCommandHandler, SdkCommandInvokePayload, SdkIdbDeletePayload, SdkIdbEntry, SdkIdbGetPayload, SdkIdbPutPayload, SdkIdbQueryPayload, SdkIdbStore, SdkInitResponse, SdkKernelProxyPayload, SdkMenuItem, SdkMenuRoot, SdkMessage, SdkNotifyPayload, SdkResponse, SdkSetActiveTabPayload, SdkSetCommandsPayload, SdkSetMenuPayload, SdkShortcutEventPayload, SdkStorage, SdkStorageDeletePayload, SdkStorageGetPayload, SdkStorageSetPayload, SdkTransport, SdkTransportPayload, TimeseriesAggregate, TimeseriesHistory, TimeseriesHistoryOptions, TimeseriesOptions, TimeseriesQueryOptions, TimeseriesValue, TransportScopeOptions, UpdateMemberInput, UpdateUnitInput, UserProfile, } from './types.js';
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  export { createHostBridge } from './host-bridge.js';
2
2
  export { DEFAULT_WINDOW_SIZE, MANIFEST_FILENAME, WINDOW_SIZES, validateManifest, } from './manifest.js';
3
- export { useGraphEvents, useOsConfig } from './react.js';
4
3
  export { SDK_CONTRACT_MATRIX, getRequiredPermissionForSdkMessage } from './contract-matrix.js';
5
4
  export { ALLOWED_KERNEL_MESSAGES, SDK_MSG } from './protocol.js';
6
5
  export { EntityNotFoundError, NetworkError, PermissionError, SdkError, TimeoutError, ValidationError, } from './errors.js';
@@ -1,4 +1,4 @@
1
- import type { BatchResult, DeleteOptions, GetNodeOptions, GraphEdgeRaw, GraphNodeRaw, GremlinOptions, HistoryEntry, HistoryOptions, MutationOptions, QueryOptions, SdkTransportPayload, TimeseriesOptions, TimeseriesQueryOptions, TimeseriesValue, TransportScopeOptions } from '../types.js';
1
+ import type { BatchResult, CombinedQueryOptions, DeleteOptions, GetNodeOptions, GraphEdgeRaw, GraphNodeRaw, GremlinOptions, HistoryEntry, HistoryOptions, MutationOptions, QueryOptions, SdkTransportPayload, TimeseriesHistory, TimeseriesHistoryOptions, TimeseriesOptions, TimeseriesQueryOptions, TimeseriesValue, TransportScopeOptions } from '../types.js';
2
2
  /** Graph transport class providing graph database operations */
3
3
  export declare class GraphTransport {
4
4
  private _request;
@@ -64,20 +64,24 @@ export declare class GraphTransport {
64
64
  */
65
65
  gremlin<T = unknown>(rootId: string, query: string, options?: GremlinOptions): Promise<T[]>;
66
66
  /**
67
- * Execute combined query (Lucene + Gremlin in single call)
68
- * Returns raw results (structure varies by query)
67
+ * Execute combined query: Elasticsearch selects the start vertices, the Gremlin traversal
68
+ * runs from their IDs. Returns raw results (structure varies by the traversal).
69
+ *
70
+ * The result is one flat set for all start vertices — it carries no marker for which start
71
+ * vertex a row came from. Label and select in the traversal when attribution is needed.
69
72
  *
70
73
  * @example
71
- * const results = await graph.combined(
72
- * 'ogit/_type:ogit/Person AND name:Test',
73
- * { limit: 50 }
74
+ * const speeches = await graph.combined(
75
+ * '+(ogit\\/_type: "ogit/Politics/AgendaItem")',
76
+ * "inE().outV().has('ogit/_type', 'ogit/Politics/Discussion')",
77
+ * { limit: 1000 }
74
78
  * );
75
79
  *
76
- * @throws {ValidationError} if query syntax is invalid
80
+ * @throws {ValidationError} if either query is invalid
77
81
  * @throws {NetworkError} on HTTP errors
78
82
  * @throws {TimeoutError} on request timeout
79
83
  */
80
- combined<T = unknown>(query: string, options?: QueryOptions): Promise<T[]>;
84
+ combined<T = unknown>(elasticsearchQuery: string, gremlinQuery: string, options?: CombinedQueryOptions): Promise<T[]>;
81
85
  /**
82
86
  * Create new graph node
83
87
  *
@@ -165,7 +169,11 @@ export declare class GraphTransport {
165
169
  */
166
170
  timeseries: {
167
171
  /**
168
- * Get time series values
172
+ * Get time series values.
173
+ *
174
+ * `withIds` pulls further time series into the same request and `aggregate` combines the
175
+ * values sharing a timestamp. Entries carry only timestamp and value, so a multi-ID request
176
+ * yields one merged series — read a single ID per request when values must stay attributable.
169
177
  *
170
178
  * @throws {EntityNotFoundError} if time series doesn't exist
171
179
  * @throws {NetworkError} on HTTP errors
@@ -189,6 +197,15 @@ export declare class GraphTransport {
189
197
  * @throws {TimeoutError} on request timeout
190
198
  */
191
199
  query: (options: TimeseriesQueryOptions) => Promise<TimeseriesValue[]>;
200
+ /**
201
+ * Get every value ever written for one timestamp of a time series.
202
+ * Keyed by timestamp, so a single-timestamp request returns a single-entry map.
203
+ *
204
+ * @throws {EntityNotFoundError} if time series doesn't exist
205
+ * @throws {NetworkError} on HTTP errors
206
+ * @throws {TimeoutError} on request timeout
207
+ */
208
+ history: (id: string, timestamp: number, options?: TimeseriesHistoryOptions) => Promise<TimeseriesHistory>;
192
209
  };
193
210
  /**
194
211
  * Content/blob operations
@@ -232,38 +232,40 @@ export class GraphTransport {
232
232
  }
233
233
  }
234
234
  /**
235
- * Execute combined query (Lucene + Gremlin in single call)
236
- * Returns raw results (structure varies by query)
235
+ * Execute combined query: Elasticsearch selects the start vertices, the Gremlin traversal
236
+ * runs from their IDs. Returns raw results (structure varies by the traversal).
237
+ *
238
+ * The result is one flat set for all start vertices — it carries no marker for which start
239
+ * vertex a row came from. Label and select in the traversal when attribution is needed.
237
240
  *
238
241
  * @example
239
- * const results = await graph.combined(
240
- * 'ogit/_type:ogit/Person AND name:Test',
241
- * { limit: 50 }
242
+ * const speeches = await graph.combined(
243
+ * '+(ogit\\/_type: "ogit/Politics/AgendaItem")',
244
+ * "inE().outV().has('ogit/_type', 'ogit/Politics/Discussion')",
245
+ * { limit: 1000 }
242
246
  * );
243
247
  *
244
- * @throws {ValidationError} if query syntax is invalid
248
+ * @throws {ValidationError} if either query is invalid
245
249
  * @throws {NetworkError} on HTTP errors
246
250
  * @throws {TimeoutError} on request timeout
247
251
  */
248
- async combined(query, options) {
252
+ async combined(elasticsearchQuery, gremlinQuery, options) {
249
253
  try {
250
- const qs = buildQueryString({
251
- query,
252
- limit: options?.limit,
253
- offset: options?.offset,
254
- fields: options?.fields,
255
- includeDeleted: options?.includeDeleted,
256
- listMeta: options?.listMeta,
257
- order: options?.order,
258
- });
259
- this.log('combined', { path: `${GRAPH_BASE}/query/combined${qs}`, query });
254
+ const path = `${GRAPH_BASE}/query/combined`;
255
+ this.log('combined', { path, elasticsearchQuery, gremlinQuery, limit: options?.limit });
260
256
  return await this._request(this.withScope({
261
- path: `${GRAPH_BASE}/query/combined${qs}`,
262
- method: 'GET',
257
+ path,
258
+ method: 'POST',
259
+ body: {
260
+ elasticsearch_query: elasticsearchQuery,
261
+ gremlin_query: gremlinQuery,
262
+ ...(options?.limit === undefined ? {} : { limit: options.limit }),
263
+ },
264
+ contentType: 'application/json',
263
265
  }, options));
264
266
  }
265
267
  catch (error) {
266
- throw parseError(error, 'graph.combined', { query, ...options });
268
+ throw parseError(error, 'graph.combined', { elasticsearchQuery, gremlinQuery, ...options });
267
269
  }
268
270
  }
269
271
  /**
@@ -450,7 +452,11 @@ export class GraphTransport {
450
452
  */
451
453
  timeseries = {
452
454
  /**
453
- * Get time series values
455
+ * Get time series values.
456
+ *
457
+ * `withIds` pulls further time series into the same request and `aggregate` combines the
458
+ * values sharing a timestamp. Entries carry only timestamp and value, so a multi-ID request
459
+ * yields one merged series — read a single ID per request when values must stay attributable.
454
460
  *
455
461
  * @throws {EntityNotFoundError} if time series doesn't exist
456
462
  * @throws {NetworkError} on HTTP errors
@@ -463,6 +469,9 @@ export class GraphTransport {
463
469
  to: options?.to,
464
470
  limit: options?.limit,
465
471
  order: options?.order,
472
+ with: options?.withIds?.join(','),
473
+ aggregate: options?.aggregate,
474
+ includeDeleted: options?.includeDeleted,
466
475
  });
467
476
  return await this._request(this.withScope({
468
477
  path: `${GRAPH_BASE}/${encodeURIComponent(id)}/values${qs}`,
@@ -520,6 +529,29 @@ export class GraphTransport {
520
529
  throw parseError(error, 'graph.timeseries.query', { ...options });
521
530
  }
522
531
  },
532
+ /**
533
+ * Get every value ever written for one timestamp of a time series.
534
+ * Keyed by timestamp, so a single-timestamp request returns a single-entry map.
535
+ *
536
+ * @throws {EntityNotFoundError} if time series doesn't exist
537
+ * @throws {NetworkError} on HTTP errors
538
+ * @throws {TimeoutError} on request timeout
539
+ */
540
+ history: async (id, timestamp, options) => {
541
+ try {
542
+ const qs = buildQueryString({
543
+ timestamp,
544
+ includeDeleted: options?.includeDeleted,
545
+ });
546
+ return await this._request(this.withScope({
547
+ path: `${GRAPH_BASE}/${encodeURIComponent(id)}/values/history${qs}`,
548
+ method: 'GET',
549
+ }, options));
550
+ }
551
+ catch (error) {
552
+ throw parseError(error, 'graph.timeseries.history', { id, timestamp, ...options });
553
+ }
554
+ },
523
555
  };
524
556
  /**
525
557
  * Content/blob operations
package/dist/types.d.ts CHANGED
@@ -231,6 +231,10 @@ export interface QueryOptions extends TransportScopeOptions {
231
231
  listMeta?: boolean;
232
232
  order?: string;
233
233
  }
234
+ export interface CombinedQueryOptions extends TransportScopeOptions {
235
+ /** Cap on the vertices the Elasticsearch stage feeds into the traversal. */
236
+ limit?: number;
237
+ }
234
238
  export interface GremlinOptions extends TransportScopeOptions {
235
239
  fields?: string;
236
240
  includeDeleted?: boolean;
@@ -259,16 +263,26 @@ export interface TimeseriesValue {
259
263
  timestamp: number;
260
264
  value: unknown;
261
265
  }
266
+ export type TimeseriesAggregate = 'avg' | 'min' | 'max' | 'sum' | 'none';
262
267
  export interface TimeseriesOptions extends TransportScopeOptions {
263
268
  from?: number | string;
264
269
  to?: number | string;
265
270
  limit?: number;
266
271
  order?: 'asc' | 'desc';
272
+ /** Further time series merged into the same request (sent as `with`). */
273
+ withIds?: string[];
274
+ /** How values sharing a timestamp are combined across the requested series. */
275
+ aggregate?: TimeseriesAggregate;
276
+ includeDeleted?: boolean;
267
277
  }
268
278
  export interface TimeseriesQueryOptions extends TimeseriesOptions {
269
279
  ids?: string[];
270
- aggregate?: string;
271
280
  }
281
+ export interface TimeseriesHistoryOptions extends TransportScopeOptions {
282
+ includeDeleted?: boolean;
283
+ }
284
+ /** Values ever written for a timestamp, keyed by that timestamp in ms. */
285
+ export type TimeseriesHistory = Record<string, unknown>;
272
286
  export interface BatchResult {
273
287
  succeeded: string[];
274
288
  failed: Array<{
package/dist/vite.js CHANGED
@@ -1,95 +1,17 @@
1
1
  /** Vite plugin for Bardioc iframe apps. Usage: `bardiocApp({ appName, port })` */
2
- import { execSync, spawn } from 'node:child_process';
3
2
  import { existsSync, readFileSync } from 'node:fs';
4
3
  import { resolve, sep } from 'node:path';
5
4
  import { fileURLToPath } from 'node:url';
5
+ import { resolveDevAuthEnv } from './dev-auth-env.js';
6
6
  import { createDevAuthProxyHandler } from './dev-proxy.js';
7
- import { readDotEnvFile } from './dot-env.js';
8
7
  import { MANIFEST_FILENAME, validateManifest } from './manifest.js';
9
- function hasCommand(cmd) {
10
- try {
11
- execSync(`which ${cmd}`, { stdio: 'ignore' });
12
- return true;
13
- }
14
- catch {
15
- return false;
16
- }
17
- }
18
- function startTunnel(port) {
19
- return new Promise((resolve, reject) => {
20
- if (!hasCommand('cloudflared')) {
21
- reject(new Error('cloudflared is not installed.\n' +
22
- ' macOS: brew install cloudflared\n' +
23
- ' Linux: apt install cloudflared\n' +
24
- ' Other: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/'));
25
- return;
26
- }
27
- const proc = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${port}`], {
28
- stdio: ['ignore', 'pipe', 'pipe'],
29
- });
30
- let found = false;
31
- const timeout = setTimeout(() => {
32
- if (!found) {
33
- proc.kill();
34
- reject(new Error('Tunnel timed out waiting for URL'));
35
- }
36
- }, 30_000);
37
- function onData(data) {
38
- const text = data.toString();
39
- const match = text.match(/(https:\/\/[a-z0-9-]+\.trycloudflare\.com)/);
40
- if (match?.[1] && !found) {
41
- found = true;
42
- clearTimeout(timeout);
43
- resolve({ url: match[1], process: proc });
44
- }
45
- }
46
- proc.stdout?.on('data', onData);
47
- proc.stderr?.on('data', onData);
48
- proc.on('close', code => {
49
- if (!found) {
50
- clearTimeout(timeout);
51
- reject(new Error(`cloudflared exited with code ${code} before providing a URL`));
52
- }
53
- });
54
- });
55
- }
56
8
  function devProxy(opts) {
57
9
  const { port } = opts;
58
- const wantTunnel = !!process.env.BARDIOC_TUNNEL;
59
- let tunnelProcess = null;
60
- let tunnelHost = null;
61
- let tunnelReady = null;
62
10
  return {
63
11
  name: 'bardioc-dev-proxy',
64
12
  apply: 'serve',
65
13
  configureServer(server) {
66
- if (wantTunnel) {
67
- tunnelReady = (async () => {
68
- try {
69
- const manifestPath = resolve(process.cwd(), 'public', MANIFEST_FILENAME);
70
- let manifestId = opts.appName;
71
- if (existsSync(manifestPath)) {
72
- try {
73
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
74
- manifestId = manifest.id || manifestId;
75
- }
76
- catch {
77
- // ignore
78
- }
79
- }
80
- console.log(`[bardioc] Starting tunnel for "${manifestId}" on port ${port}...`);
81
- const tunnel = await startTunnel(port);
82
- tunnelProcess = tunnel.process;
83
- tunnelHost = new URL(tunnel.url).host;
84
- console.log(`[bardioc] Tunnel ready: ${tunnel.url}`);
85
- }
86
- catch (err) {
87
- console.error(`[bardioc] Tunnel failed: ${err.message}`);
88
- process.exit(1);
89
- }
90
- })();
91
- }
92
- server.httpServer?.on('listening', async () => {
14
+ server.httpServer?.on('listening', () => {
93
15
  const manifestPath = resolve(process.cwd(), 'public', MANIFEST_FILENAME);
94
16
  if (!existsSync(manifestPath)) {
95
17
  console.warn(`[bardioc] public/${MANIFEST_FILENAME} not found`);
@@ -103,26 +25,10 @@ function devProxy(opts) {
103
25
  console.error(`[bardioc] Failed to read manifest: ${err}`);
104
26
  return;
105
27
  }
106
- const manifestId = opts.appName || manifest.id;
107
- if (wantTunnel) {
108
- await tunnelReady;
109
- if (!tunnelHost) {
110
- console.error(`[bardioc] Tunnel was requested but failed to start`);
111
- process.exit(1);
112
- }
113
- console.log(`[bardioc] Use this live URL in the host App Store app configuration:`);
114
- console.log(`[bardioc] App Name: ${manifestId}`);
115
- console.log(`[bardioc] Live URL: https://${tunnelHost}`);
116
- }
117
- else {
118
- console.log(`[bardioc] Dev server ready: http://localhost:${port}`);
119
- console.log(`[bardioc] For standalone dev auth, set DEV_SECRET and API_BASE_URL in .env.`);
120
- console.log(`[bardioc] App Name: ${manifestId}`);
121
- console.log(`[bardioc] URL: http://localhost:${port}`);
122
- }
123
- });
124
- server.httpServer?.on('close', () => {
125
- tunnelProcess?.kill();
28
+ console.log(`[bardioc] Dev server ready: http://localhost:${port}`);
29
+ console.log(`[bardioc] For standalone dev auth, set DEV_SECRET and API_BASE_URL in .env.`);
30
+ console.log(`[bardioc] App Name: ${opts.appName || manifest.id}`);
31
+ console.log(`[bardioc] URL: http://localhost:${port}`);
126
32
  });
127
33
  },
128
34
  };
@@ -217,33 +123,6 @@ function manifestCheck() {
217
123
  },
218
124
  };
219
125
  }
220
- function readRuntimeEnv(server) {
221
- const envDir = server.config.envDir ?? server.config.root;
222
- const mode = server.config.mode?.trim();
223
- const env = {};
224
- for (const fileName of [
225
- '.env',
226
- '.env.local',
227
- mode ? `.env.${mode}` : null,
228
- mode ? `.env.${mode}.local` : null,
229
- ]) {
230
- if (!fileName)
231
- continue;
232
- const filePath = resolve(envDir, fileName);
233
- if (existsSync(filePath)) {
234
- Object.assign(env, readDotEnvFile(filePath));
235
- }
236
- }
237
- for (const [key, value] of Object.entries(process.env)) {
238
- if (typeof value === 'string') {
239
- env[key] = value;
240
- }
241
- }
242
- return {
243
- DEV_SECRET: env.DEV_SECRET?.trim(),
244
- API_BASE_URL: env.API_BASE_URL?.trim().replace(/\/$/, ''),
245
- };
246
- }
247
126
  export function bardiocDevAuth(options) {
248
127
  let proxyOptions = options.proxy ?? null;
249
128
  return {
@@ -252,14 +131,17 @@ export function bardiocDevAuth(options) {
252
131
  enforce: 'pre',
253
132
  configureServer(server) {
254
133
  if (!proxyOptions) {
255
- const env = readRuntimeEnv(server);
256
- if (!env.DEV_SECRET || !env.API_BASE_URL) {
134
+ const env = resolveDevAuthEnv({
135
+ envDir: server.config.envDir ?? server.config.root,
136
+ mode: server.config.mode,
137
+ });
138
+ if (!env.devSecret || !env.apiBaseUrl) {
257
139
  server.config.logger.warn(`[bardioc-dev-auth] DEV_SECRET and API_BASE_URL are required for dev auth — disabled for appName="${options.appName}".`);
258
140
  return;
259
141
  }
260
142
  proxyOptions = {
261
- apiBaseUrl: env.API_BASE_URL,
262
- devSecret: env.DEV_SECRET,
143
+ apiBaseUrl: env.apiBaseUrl,
144
+ devSecret: env.devSecret,
263
145
  };
264
146
  }
265
147
  const handler = createDevAuthProxyHandler(proxyOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bardioc/app-sdk",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "SDK for building apps for the Bardioc OS",
5
5
  "license": "MIT",
6
6
  "private": false,