Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
4 changes: 3 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ on:
- '@magmacomputing/tempo-plugin-snap'
- '@magmacomputing/tempo-plugin-sync'
- '@magmacomputing/tempo-plugin-ticker'
- '@magmacomputing/tempo-plugin-geo'
- 'all'
release:
types: [published]
Expand Down Expand Up @@ -57,7 +58,7 @@ jobs:

# Input validation
case "$TARGET" in
@magmacomputing/tempo|@magmacomputing/tempo-fns|@magmacomputing/tempo-plugin-astro|@magmacomputing/tempo-plugin-celestial|@magmacomputing/tempo-plugin-ai|@magmacomputing/tempo-plugin-batch|@magmacomputing/tempo-plugin-finance|@magmacomputing/tempo-plugin-snap|@magmacomputing/tempo-plugin-sync|@magmacomputing/tempo-plugin-ticker|all)
@magmacomputing/tempo|@magmacomputing/tempo-fns|@magmacomputing/tempo-plugin-astro|@magmacomputing/tempo-plugin-celestial|@magmacomputing/tempo-plugin-ai|@magmacomputing/tempo-plugin-batch|@magmacomputing/tempo-plugin-finance|@magmacomputing/tempo-plugin-snap|@magmacomputing/tempo-plugin-sync|@magmacomputing/tempo-plugin-ticker|@magmacomputing/tempo-plugin-geo|all)
;;
*)
echo "❌ Error: Invalid target '$TARGET'"
Expand Down Expand Up @@ -130,6 +131,7 @@ jobs:
publish_pkg "@magmacomputing/tempo-plugin-snap"
publish_pkg "@magmacomputing/tempo-plugin-sync"
publish_pkg "@magmacomputing/tempo-plugin-ticker"
publish_pkg "@magmacomputing/tempo-plugin-geo"
else
publish_pkg "$TARGET"
fi
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
15 changes: 15 additions & 0 deletions packages/library/src/common/primitives/assertion.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ export const isSymbol = (obj: unknown): obj is symbol => isType<symbol>(obj, 'Sy
export const isSymbolFor = (obj: unknown): obj is symbol => isType<symbol>(obj, 'Symbol') && Symbol.keyFor(obj as symbol) !== undefined;
export const isPropertyKey = (obj: unknown): obj is PropertyKey => isType<PropertyKey>(obj, 'String', 'Number', 'Symbol');

/**
* Asserts if a property key is safe against prototype pollution and prototype hijacking.
* Returns false for '__proto__', 'constructor', and 'prototype'.
*
* @param key - The property key to check
* @returns True if the key is safe to assign or merge
* @example
* ```ts
* isSafeKey('name'); // true
* isSafeKey('__proto__'); // false
* ```
*/
export const isSafeKey = (key: PropertyKey): boolean =>
key !== '__proto__' && key !== 'constructor' && key !== 'prototype';

export const isNull = (obj: unknown): obj is null => isType<null>(obj, 'Null');
export const isNullish = (obj: unknown): obj is Nullish => isType<Nullish>(obj, 'Null', 'Undefined', 'Void', 'Empty');
export const isUndefined = (obj: unknown): obj is undefined => isType<undefined>(obj, 'Undefined', 'Void', 'Empty');
Expand Down
11 changes: 7 additions & 4 deletions packages/library/src/common/primitives/object.library.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ownKeys, ownEntries } from '#library/primitive.library.js';
import { isObject, isArray, isFunction, isDefined, isNullish, isMap, isSet } from '#library/assertion.library.js';
import { isObject, isArray, isFunction, isDefined, isNullish, isMap, isSet, isSafeKey } from '#library/assertion.library.js';
import { getType } from '#library/type.library.js';
import type { Extend, Property } from '#library/type.library.js';

Expand Down Expand Up @@ -41,7 +41,10 @@ export const asObject = <T>(obj?: Record<PropertyKey, any>) => {
const temp: any = isArray(obj) ? [] : {};

ownKeys(obj)
.forEach(key => temp[key] = asObject(obj[key]));
.forEach(key => {
if (!isSafeKey(key)) return;
temp[key] = asObject(obj[key]);
});

return temp as T;
}
Expand Down Expand Up @@ -142,7 +145,7 @@ export const getMethods = (obj: any, all = false) => {
export function ifDefined<T extends Property<any>>(obj: T) {
return ownEntries(obj)
.reduce((acc, [key, val]) => {
if (isDefined<any>(val))
if (isSafeKey(key) && isDefined<any>(val))
acc[key] = val;
return acc as T;
}, {} as T)
Expand Down Expand Up @@ -221,7 +224,7 @@ export const deepMerge = <T extends Record<PropertyKey, any>>(...objects: Partia
if (!isObject(obj)) return prev;

Object.entries(obj).forEach(([key, value]) => {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') return;
if (!isSafeKey(key)) return;
const pVal = prev[key];
if (isObject(pVal) && isObject(value)) {
prev[key as keyof T] = deepMerge(pVal, value) as any;
Expand Down
27 changes: 20 additions & 7 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, isSafeKey } from '#library/assertion.library.js';

export interface GeoLookupResult {
lat?: number;
Expand Down Expand Up @@ -98,8 +96,15 @@ 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;

if (geoObj && typeof geoObj === 'object') {
for (const key of Object.keys(geoObj)) {
if (isSafeKey(key) && !['latitude', 'lat', 'longitude', 'lng', 'lon', 'long', 'elevation', 'sphere', 'country', 'city'].includes(key))
(result as any)[key] = geoObj[key];
Comment thread
magmacomputing marked this conversation as resolved.
}
}

return Object.keys(result).length > 0 ? result : undefined;
};
Expand Down Expand Up @@ -139,6 +144,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 +159,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 All @@ -175,10 +184,14 @@ export const resolveGeoCoordinates = async (
input?: CoordinateInput,
opts: Record<string, any> = {}
): Promise<{ lat: number; lng: number } | null> => {
const coerced = coerceGeo(input) ?? getStashedGeo();
const coerced = coerceGeo(input);
if (coerced && isNumber(coerced.latitude) && isNumber(coerced.longitude))
return { lat: coerced.latitude, lng: coerced.longitude };

const stashed = getStashedGeo();
if (stashed && isNumber(stashed.latitude) && isNumber(stashed.longitude))
return { lat: stashed.latitude, lng: stashed.longitude };

const lookup = await geoLookup(opts);
if (isNullish(lookup.error) && isNumber(lookup.lat) && isNumber(lookup.lng))
return { lat: lookup.lat, lng: lookup.lng };
Expand Down
8 changes: 6 additions & 2 deletions packages/library/src/common/runtime/request.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,13 @@ export const fetchRequest = <T>(url: string | URL, init = {} as RequestInit, con

let errorBody: any = null;
try {
const errorText = await res.text();
const errorText = isDefined(config.maxBytes)
? await readBoundedBody(res, config.maxBytes)
: (isFunction(res.text) ? await res.text() : '');
try { errorBody = JSON.parse(errorText); } catch { errorBody = errorText; }
} catch { }
} catch (err) {
if (err instanceof HttpError && err.status === 413) throw err;
}

throw new HttpError(res.status, res.statusText, errorBody); // fetch not successful
})
Expand Down
4 changes: 2 additions & 2 deletions packages/library/src/common/runtime/utility.library.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ownEntries } from '#library/primitive.library.js';
import { isDefined, isFunction, isPrimitive } from '#library/assertion.library.js';
import { isDefined, isFunction, isPrimitive, isSafeKey } from '#library/assertion.library.js';
import { sym } from '#library/symbol.library.js';
import type { Secure, ValueOf } from '#library/type.library.js';

Expand Down Expand Up @@ -150,7 +150,7 @@ export function deepFreeze<const T extends object>(obj: T, options?: { skip?: We
seen.add(obj);

ownEntries(obj as any).forEach(([key, val]) => {
if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype')
if (isSafeKey(key))
deepFreeze(val, { skip }, seen);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isNumber, isNumeric, isText, isArrayLike, isPlainObject, isEmpty, isFunction } from '#library/assertion.library.js';
import { isNumber, isNumeric, isText, isArrayLike, isPlainObject, isEmpty, isFunction, isSafeKey } from '#library/assertion.library.js';

describe('Assertion Library', () => {

Expand Down Expand Up @@ -172,4 +172,20 @@ describe('Assertion Library', () => {
expect(isFunction(undefined)).toBe(false);
});
});

describe('isSafeKey', () => {
it('should return true for valid object property keys', () => {
expect(isSafeKey('name')).toBe(true);
expect(isSafeKey('id')).toBe(true);
expect(isSafeKey('latitude')).toBe(true);
expect(isSafeKey(0)).toBe(true);
expect(isSafeKey(Symbol('custom'))).toBe(true);
});

it('should return false for prototype pollution and hijacking keys', () => {
expect(isSafeKey('__proto__')).toBe(false);
expect(isSafeKey('constructor')).toBe(false);
expect(isSafeKey('prototype')).toBe(false);
});
});
});
Loading
Loading