Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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