Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tempo-monorepo",
"version": "4.1.0",
"version": "4.1.1",
"private": true,
"engines": {
"node": ">=20.0.0"
Expand Down Expand Up @@ -28,6 +28,10 @@
"version:sync": "tempo-cli version-sync",
"catalog:sync": "tempo-cli catalog-sync",
"providers:sync": "npm run build:library && tempo-cli sync-providers",
"plugins:check-versions": "./packages/plugins/.bin/check-versions.sh",
"plugins:check-diff": "./packages/plugins/.bin/check-branch-diff.sh",
"check:versions": "npm run plugins:check-versions",
"check:diff": "npm run plugins:check-diff",
"repl": "npm run repl --workspace=@magmacomputing/tempo",
"repl:plugins": "tsx --import ./packages/plugins/.bin/temporal-polyfill.mts ./packages/plugins/.bin/repl.mts",
"repl:dist": "npm run repl:dist --workspace=@magmacomputing/tempo",
Expand Down
2 changes: 1 addition & 1 deletion packages/library/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/library",
"version": "4.1.0",
"version": "4.1.1",
"description": "Shared utility library for Tempo",
"author": "Magma Computing Solutions",
"license": "MIT",
Expand Down
14 changes: 8 additions & 6 deletions packages/library/src/common/runtime/mapper.library.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { CONTEXT, getContext } from '#library/utility.library.js';
import { isNullish, isNumber } from '#library/assertion.library.js';
import { geoLocation } from '#browser/mapper.library.js';
import { serverGeoLocation } from '#server/mapper.library.js';
import { isNullish, isNumber, isString } from '#library/assertion.library.js';

export interface GeoLookupResult {
lat?: number;
Expand Down Expand Up @@ -98,8 +96,8 @@ export const coerceGeo = (input?: any): GeoConfig | undefined => {
if (isNumber(lng)) result.longitude = lng;
if (isNumber(elevation)) result.elevation = elevation;
if (sphere === 'north' || sphere === 'south') result.sphere = sphere;
if (typeof country === 'string') result.country = country;
if (typeof city === 'string') result.city = city;
if (isString(country)) result.country = country;
if (isString(city)) result.city = city;

return Object.keys(result).length > 0 ? result : undefined;
};
Expand Down Expand Up @@ -139,6 +137,7 @@ export const geoLookup = async (opts: Record<string, any> = {}): Promise<GeoLook

switch (type) {
case CONTEXT.Browser: {
const { geoLocation } = await import('#browser/mapper.library.js');
const res = await geoLocation(opts as any);
if (res.error)
return { error: res.error };
Expand All @@ -153,13 +152,16 @@ export const geoLookup = async (opts: Record<string, any> = {}): Promise<GeoLook
if (stashed && isNumber(stashed.latitude) && isNumber(stashed.longitude))
return { lat: stashed.latitude, lng: stashed.longitude, latitude: stashed.latitude, longitude: stashed.longitude };

const { serverGeoLocation } = await import('#server/mapper.library.js');
return serverGeoLocation(opts as any);
}

case CONTEXT.NodeJS:
case CONTEXT.Deno:
default:
default: {
const { serverGeoLocation } = await import('#server/mapper.library.js');
return serverGeoLocation(opts as any);
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/.bin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ It includes:
- **REPL Environment (`repl.mts`)**: Scripts to initialize an interactive Node.js REPL session with Tempo and Temporal pre-loaded, making it easy to experiment with plugins from the CLI.
- **Polyfill Setup (`temporal-polyfill.mts`)**: Initialization scripts to ensure the `@js-temporal/polyfill` is correctly loaded into the global scope during testing or REPL sessions, allowing plugins to work with native `Temporal` APIs before they are officially adopted by all runtimes.
- **Catalog Synchronization (`catalog-sync.mjs`)**: A developer utility that scans all local and external plugin `package.json` files and extracts their metadata into a centralized `catalog.json` file. Run via `npm run catalog:sync`.
- **Version Check (`check-versions.sh`)**: Compares published NPM versions against local workspace versions to determine which plugins need to be published or re-published. Run via `npm run check:versions` or `npm run plugins:check-versions`.
- **Branch Diff Check (`check-branch-diff.sh`)**: Compares plugin files on the current branch against `main` (or a specified branch) to verify if modified plugins had their semantic versions bumped. Run via `npm run check:diff` or `npm run plugins:check-diff`.
- **TypeScript Configuration (`tsconfig.json`)**: Specific compiler options for running these support scripts directly via tools like `tsx`.

These files are meant for local monorepo development and testing purposes only. They are not published or distributed with any NPM packages.
21 changes: 13 additions & 8 deletions packages/plugins/.bin/check-versions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@

set -e

# Resolve repository root path (3 levels up from packages/plugins/.bin)
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"

packages=(
"tempo:packages/tempo/package.json"
"tempo-plugin-ai:packages/plugins/ai/package.json"
"tempo-plugin-astro:packages/plugins/astro/package.json"
"tempo-plugin-batch:packages/plugins/batch/package.json"
"tempo-plugin-finance:packages/plugins/finance/package.json"
"tempo-plugin-snap:packages/plugins/snap/package.json"
"tempo-plugin-sync:packages/plugins/sync/package.json"
)

# Resolve repository root path (3 levels up from packages/plugins/.bin)
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
for plugin_dir in "${REPO_ROOT}/packages/plugins"/*; do
if [ -d "${plugin_dir}" ] && [ -f "${plugin_dir}/package.json" ]; then
is_private=$(node --input-type=module -e "import fs from 'fs'; console.log(JSON.parse(fs.readFileSync(process.argv[2], 'utf8')).private ? 'true' : 'false')" dummy "${plugin_dir}/package.json" 2>/dev/null || echo "false")
if [ "${is_private}" = "true" ]; then
continue
fi
plugin_name=$(basename "${plugin_dir}")
packages+=("tempo-plugin-${plugin_name}:packages/plugins/${plugin_name}/package.json")
fi
done

printf "%-38s | %-16s | %-16s | %-12s\n" "Package Name" "Published (NPM)" "Local Workspace" "Status"
printf "%-38s-+-%-16s-+-%-16s-+-%-12s\n" "--------------------------------------" "----------------" "----------------" "------------"
Expand Down
13 changes: 11 additions & 2 deletions packages/plugins/.setup/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"packageName": "@magmacomputing/tempo-plugin-ai",
"plan": "community",
"status": "active",
"version": "1.1.1"
"version": "1.2.1"
},
{
"id": "ticker",
Expand All @@ -69,7 +69,16 @@
"packageName": "@magmacomputing/tempo-plugin-ticker",
"plan": "community",
"status": "active",
"version": "2.3.0"
"version": "2.3.1"
},
{
"id": "geo",
"name": "Geo Plugin",
"description": "Tempo community plugin for IP geolocation lookup, browser hardware location services, and coordinate resolution.",
"packageName": "@magmacomputing/tempo-plugin-geo",
"plan": "community",
"status": "active",
"version": "1.0.0"
},
{
"id": "_std",
Expand Down
8 changes: 8 additions & 0 deletions packages/plugins/ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to the `@magmacomputing/tempo-plugin-ai` project will be doc
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.2.1] - 2026-09-07

### Security & Reliability
- **Self-Contained Network Transport (`fetch.ts`)**:
- Decoupled network request utilities from `@magmacomputing/tempo/library` into a self-contained local transport helper.
- Added chunk-by-chunk stream consumption via `res.body.getReader()` with proactive byte accounting and immediate reader cancellation (`await reader.cancel()`).
- Enforced upfront `Content-Length` checks against `maxBytes` and strictly bounded stream reads to prevent unbounded memory allocation and OS thread starvation.

## [1.2.0] - 2026-09-06

### Added
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/ai/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/tempo-plugin-ai",
"version": "1.2.0",
"version": "1.2.1",
"description": "Tempo community plugin for LLM-powered natural language parsing.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
104 changes: 104 additions & 0 deletions packages/plugins/ai/src/core/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
export class HttpError extends Error {
constructor(
public status: number,
public statusText: string,
public body: any = null
) {
super(`${status}: ${statusText}`);
this.name = 'HttpError';
}
}

export interface FetchRequestConfig {
timeout?: number;
maxBytes?: number;
prefix?: string;
rawText?: boolean;
}

/**
* Perform a bounded HTTP fetch request with timeout and error handling.
*/
export async function fetchRequest<T = any>(
url: string | URL,
init: RequestInit = {},
config: FetchRequestConfig = {}
): Promise<T> {
const timeout = config.timeout ?? 5000;
const signal = init.signal
? AbortSignal.any([init.signal, AbortSignal.timeout(timeout)])
: AbortSignal.timeout(timeout);

const res = await fetch(url, { ...init, signal });
if (!res.ok) {
let errorBody: any = null;
try {
const errorText = await res.text();
Comment thread
magmacomputing marked this conversation as resolved.
Outdated
try { errorBody = JSON.parse(errorText); } catch { errorBody = errorText; }
} catch { }
throw new HttpError(res.status, res.statusText, errorBody);
}

if (config.maxBytes) {
const contentLength = res.headers?.get?.('content-length');
if (contentLength) {
const parsed = parseInt(contentLength, 10);
if (!Number.isNaN(parsed) && parsed > config.maxBytes) {
try { await res.body?.cancel?.(); } catch { }
throw new HttpError(413, `Payload length exceeds limit (${config.maxBytes} bytes)`, null);
}
}
}

let text: string;
if (config.maxBytes && res.body && typeof res.body.getReader === 'function') {
const reader = res.body.getReader();
const decoder = new TextDecoder();
let totalBytes = 0;
const chunks: string[] = [];
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (value) {
totalBytes += value.byteLength;
if (totalBytes > config.maxBytes) {
await reader.cancel('maxBytes exceeded');
throw new HttpError(413, `Payload length exceeds limit (${config.maxBytes} bytes)`, null);
}
chunks.push(decoder.decode(value, { stream: true }));
}
}
chunks.push(decoder.decode());
text = chunks.join('');
} catch (err) {
try { await reader.cancel(); } catch { }
throw err;
} finally {
try { reader.releaseLock(); } catch { }
}
} else {
text = await res.text();
if (config.maxBytes && new TextEncoder().encode(text).byteLength > config.maxBytes) {
throw new HttpError(413, `Payload length exceeds limit (${config.maxBytes} bytes)`, null);
}
}

if (config.rawText)
return text as unknown as T;

const contentType = res.headers?.get?.('content-type') || '';
if (contentType.includes('application/json')) {
try {
return JSON.parse(text) as T;
} catch {
return text as unknown as T;
}
}

try {
return JSON.parse(text) as T;
} catch {
return text as unknown as T;
}
}
3 changes: 2 additions & 1 deletion packages/plugins/ai/src/core/manifest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { asText, evaluate, fetchRequest, isObject, isString, parseJSONC } from '@magmacomputing/tempo/library';
import { asText, evaluate, isObject, isString, parseJSONC } from '@magmacomputing/tempo/library';
import { fetchRequest } from './fetch.js';
import { DEFAULT_PROVIDERS } from './config.js';
import type { AiProvider } from '../types/index.js';

Expand Down
3 changes: 2 additions & 1 deletion packages/plugins/ai/src/core/models.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { TempoAiError } from './error.js';
import { isValidManifestUrl } from './manifest.js';
import { RE_SAFE_PROVIDER_ID } from './patterns.js';
import { asText, asNumber, fetchRequest, HttpError, isString, parseJSONC } from '@magmacomputing/tempo/library';
import { fetchRequest, HttpError } from './fetch.js';
import { asText, asNumber, isString, parseJSONC } from '@magmacomputing/tempo/library';

export interface ProviderModelInfo {
id: string;
Expand Down
4 changes: 2 additions & 2 deletions packages/plugins/ai/src/functions/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ function calculateGroundingMetrics(startTempo: Tempo, endTempo: Tempo, holidays?
const holidaySet = new Set<string>(holidays ?? []);
const matchedHolidays: string[] = [];

let curr = from.set({ start: 'day' });
const limit = to.set({ start: 'day' });
let curr = from.set({ day: 'start' });
const limit = to.set({ day: 'start' });
let businessDaysCount = 0;

while (curr.epoch.ms < limit.epoch.ms) {
Expand Down
5 changes: 5 additions & 0 deletions packages/plugins/ai/test/manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,13 @@ describe('Remote Provider Manifest & Dynamic Defaults', () => {

it('should reject streamed response without Content-Length when cumulative bytes exceed MAX_MANIFEST_BYTES', async () => {
let cancelled = false;
let chunksProduced = 0;
const stream = new ReadableStream({
pull(controller) {
if (chunksProduced++ > 20) {
controller.close();
return;
}
const chunk = new Uint8Array(256 * 1024);
controller.enqueue(chunk);
},
Expand Down
Loading
Loading