Skip to content
Open
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
13,767 changes: 13,767 additions & 0 deletions deno.json

Large diffs are not rendered by default.

10,323 changes: 10,323 additions & 0 deletions deno.lock

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,42 @@
"private": true,
"license": "MPL-2.0",
"type": "module",
"trex": {
"functions": {
"init": [],
"api": [
{
"source": "/logto",
"function": "/packages/core/src",
"imports": "/deno.json",
"env": "d2e-logto"
}
],
"env": {
"d2e-logto": {
"NODE_ENV": "${NODE_ENV:-production}",
"DB_URL": "${LOGTO_DB_URL}",
"ENDPOINT": "${LOGTO_ENDPOINT}"
}
},
"roles": {
"ALP_SYSTEM_ADMIN": [
"portal.logto.read",
"portal.logto.write"
]
},
"scopes": [
{
"path": "^/logto-f(.*)",
"scopes": [
"portal.logto.read",
"portal.logto.write"
],
"httpMethods": ["GET", "POST", "DELETE", "PUT", "PATCH"]
}
]
}
},
"scripts": {
"preinstall": "npx only-allow pnpm",
"pnpm:devPreinstall": "cd packages/connectors && node templates/sync-preset.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/connector/loader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'node:path';

import type { AllConnector, CreateConnector } from '@logto/connector-kit';
import connectorKitMeta from '@logto/connector-kit/package.json' assert { type: 'json' };
import connectorKitMeta from '@logto/connector-kit/package.json' with { type: 'json' };
import { satisfies } from 'semver';

import { consoleLog } from '../utils.js';
Expand Down
54 changes: 50 additions & 4 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,52 @@
import dotenv from 'dotenv';
import { findUp } from 'find-up';
import { appInsights } from '@logto/app-insights/node';
import { ConsoleLog } from '@logto/shared';
import { trySafe } from '@silverhand/essentials';
import chalk from 'chalk';
import Koa from 'koa';

dotenv.config({ path: await findUp('.env', {}) });
import initApp from './app/init.ts';
import { redisCache } from './caches/index.ts';
import { EnvSet } from './env-set/index.ts';
import { checkPreconditions } from './env-set/preconditions.ts';
import initI18n from './i18n/init.ts';
import SystemContext from './tenants/SystemContext.ts';
import { tenantPool } from './tenants/index.ts';
import { loadConnectorFactories } from './utils/connectors/index.ts';

await import('./main.js');
const consoleLog = new ConsoleLog(chalk.magenta('index'));

console.log('Logto worker initializing...');

if (await appInsights.setup('core')) {
consoleLog.info('Initialized ApplicationInsights');
}

try {
const app = new Koa({
proxy: EnvSet.values.trustProxyHeader,
});

const sharedAdminPool = await EnvSet.sharedPool;

await Promise.all([
initI18n(),
redisCache.connect(),
loadConnectorFactories(),
checkPreconditions(sharedAdminPool),
SystemContext.shared.loadProviderConfigs(sharedAdminPool),
]);

process.on('unhandledRejection', (error) => {
consoleLog.error(error);
void appInsights.trackException(error);
});

await initApp(app);

console.log('Logto worker initialized successfully');
} catch (error: unknown) {
console.error('Error while initializing Logto worker:');
console.error(error);

void Promise.all([trySafe(tenantPool.endAll()), trySafe(redisCache.disconnect())]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { generateStandardId } from '@logto/shared';
import { conditional } from '@silverhand/essentials';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
import { type PublicKeyCredentialRequestOptionsJSON } from 'node_modules/@simplewebauthn/server/esm/deps.js';
import { type PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/server/esm/deps.js';
import { z } from 'zod';

import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
Expand Down
42 changes: 10 additions & 32 deletions packages/core/src/utils/custom-jwt/local-vm.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { types } from 'node:util';
import { runInNewContext } from 'node:vm';

import { ResponseError } from '@withtyped/client';

/**
Expand All @@ -24,6 +21,9 @@ export class LocalVmError extends ResponseError {
}

/**
* Stub implementation for Deno environment.
* The node:vm module is not supported in Deno, so custom JWT scripts cannot run locally.
*
* This function is used to execute a named function in a customized code script in a local
* virtual machine with the given payload as input.
*
Expand All @@ -33,34 +33,13 @@ export class LocalVmError extends ResponseError {
* @returns The result of the function execution.
*/
export const runScriptFunctionInLocalVm = async (
script: string,
functionName: string,
payload: unknown
_script: string,
_functionName: string,
_payload: unknown
) => {
const globalContext = Object.freeze({
fetch: async (...args: Parameters<typeof fetch>) => fetch(...args),
});
const customFunction: unknown = runInNewContext(script + `;${functionName};`, globalContext);

if (typeof customFunction !== 'function') {
throw new TypeError(`The script does not have a function named \`${functionName}\``);
}

/**
* We can not use top-level await in `vm`, use the following implementation instead.
*
* Ref:
* 1. https://github.com/nodejs/node/issues/40898
* 2. https://github.com/n-riesco/ijavascript/issues/173#issuecomment-693924098
*/
const result: unknown = await runInNewContext(
'(async () => customFunction(payload))();',
Object.freeze({ customFunction, payload }),
// Limit the execution time to 3 seconds, throws error if the script takes too long to execute.
{ timeout: 3000 }
throw new Error(
'Custom JWT scripts (node:vm) are not supported in Deno environment. Please use Logto Cloud for this feature.'
);

return result;
};

/**
Expand All @@ -69,11 +48,10 @@ export const runScriptFunctionInLocalVm = async (
* @remarks
*
* Catch the error from vm module, and build the error body.
* Use `isNativeError` to check if the error is an instance of `Error`.
* If the error comes from `node:vm` module, then it will not be an instance of `Error` but can be captured by `isNativeError`.
* Use Error instance check for Deno compatibility.
*
*/
export const buildLocalVmErrorBody = (error: unknown) =>
types.isNativeError(error)
error instanceof Error
? { message: error.message, stack: error.stack }
: { message: String(error) };
33 changes: 33 additions & 0 deletions run-with-deno.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/bash

# Helper script to run Logto with Deno
# This script will continue to run and show errors as they occur

echo "🦕 Running Logto with Deno..."
echo "========================================="
echo ""
echo "Note: This is an experimental setup. You may see errors"
echo "related to missing dependencies or incompatibilities."
echo ""
echo "========================================="
echo ""

cd "$(dirname "$0")"

deno run \
--config deno.json \
--allow-all \
--unstable-node-globals \
--unstable-fs \
--unstable-sloppy-imports \
packages/core/src/index.ts

exit_code=$?

echo ""
echo "========================================="
echo "Exit code: $exit_code"
echo "========================================="

exit $exit_code

83 changes: 83 additions & 0 deletions scripts/gen-deno-imports.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')

const REPO_ROOT = process.cwd()
const PACKAGES_DIR = path.join(REPO_ROOT, 'packages')
const DENO_JSON = path.join(REPO_ROOT, 'deno.json')
const TREX_PREFIX = '/var/tmp/sb-compile-trex/logto/packages/'
const FILE_URL_PREFIX = 'file:///var/tmp/sb-compile-trex/logto/packages/'

function walk(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true })
const files = []
for (const e of entries) {
if (e.name.startsWith('.')) continue
const full = path.join(dir, e.name)
if (e.isDirectory()) files.push(...walk(full))
else files.push(full)
}
return files
}

function main() {
if (!fs.existsSync(PACKAGES_DIR)) {
console.error('packages/ not found; skipping deno import map generation')
process.exit(0)
}
const deno = fs.existsSync(DENO_JSON)
? JSON.parse(fs.readFileSync(DENO_JSON, 'utf-8'))
: { imports: {} }
deno.imports = deno.imports || {}

// Ensure broad prefix mappings exist so new files resolve without per-file entries
// Map Trex temp prefixes to local packages directory (import-map remaps during dynamic import)
deno.imports[TREX_PREFIX] = './packages/'
deno.imports[FILE_URL_PREFIX] = './packages/'

const files = walk(PACKAGES_DIR)
.filter((f) => /\.(ts|tsx|mts|cts|js|mjs|cjs)$/.test(f))
.filter((f) => !/\.(d|test|spec)\.(ts|tsx|mts|cts|js|mjs|cjs)$/.test(f))

for (const abs of files) {
const rel = path.relative(PACKAGES_DIR, abs).replace(/\\/g, '/')
const withoutExt = rel.replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/, '')
const spec = TREX_PREFIX + withoutExt
const target = './packages/' + rel
deno.imports[spec] = target

// For TypeScript files, also add .js mapping since TS compiles imports to .js
if (/\.(ts|tsx|mts|cts)$/.test(rel)) {
deno.imports[TREX_PREFIX + withoutExt + '.js'] = target
deno.imports[FILE_URL_PREFIX + withoutExt + '.js'] = target
}

// Add file:// URL specifier
deno.imports[FILE_URL_PREFIX + withoutExt] = target

// Add directory prefix mappings (with trailing slash) for the parent dir
const parentDir = withoutExt.includes('/') ? withoutExt.substring(0, withoutExt.lastIndexOf('/') + 1) : ''
if (parentDir) {
const trexDirPrefix = TREX_PREFIX + parentDir
const fileUrlDirPrefix = FILE_URL_PREFIX + parentDir
const dirTarget = './packages/' + parentDir
deno.imports[trexDirPrefix] = dirTarget
deno.imports[fileUrlDirPrefix] = dirTarget
}

// If this is an index file, also map the directory itself to the index target
if (withoutExt.endsWith('/index')) {
const dirWithoutIndex = withoutExt.slice(0, -('/index'.length))
const dirSpec = TREX_PREFIX + dirWithoutIndex
const fileUrlDirSpec = FILE_URL_PREFIX + dirWithoutIndex
deno.imports[dirSpec] = target
deno.imports[fileUrlDirSpec] = target
}
}

fs.writeFileSync(DENO_JSON, JSON.stringify(deno, null, 2) + '\n')
console.log(`Updated ${DENO_JSON} with ${files.length} file mappings for logto`)
}

main()

78 changes: 78 additions & 0 deletions scripts/gen-deno-imports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')

const REPO_ROOT = process.cwd()
const PACKAGES_DIR = path.join(REPO_ROOT, 'packages')
const DENO_JSON = path.join(REPO_ROOT, 'deno.json')
const TREX_PREFIX = '/var/tmp/sb-compile-trex/logto/packages/'
const FILE_URL_PREFIX = 'file:///var/tmp/sb-compile-trex/logto/packages/'

function walk(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true })
const files = []
for (const e of entries) {
if (e.name.startsWith('.')) continue
const full = path.join(dir, e.name)
if (e.isDirectory()) files.push(...walk(full))
else files.push(full)
}
return files
}

function main() {
if (!fs.existsSync(PACKAGES_DIR)) {
console.error('packages/ not found; skipping deno import map generation')
process.exit(0)
}
const deno = fs.existsSync(DENO_JSON)
? JSON.parse(fs.readFileSync(DENO_JSON, 'utf-8'))
: { imports: {} }
deno.imports = deno.imports || {}

// Ensure broad prefix mappings exist so new files resolve without per-file entries
// Map Trex temp prefixes to local packages directory (import-map remaps during dynamic import)
deno.imports[TREX_PREFIX] = './packages/'
deno.imports[FILE_URL_PREFIX] = './packages/'

const files = walk(PACKAGES_DIR)
.filter((f) => /\.(ts|tsx|mts|cts|js|mjs|cjs)$/.test(f))
.filter((f) => !/\.(d|test|spec)\.(ts|tsx|mts|cts|js|mjs|cjs)$/.test(f))

for (const abs of files) {
const rel = path.relative(PACKAGES_DIR, abs).replace(/\\/g, '/')
const withoutExt = rel.replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/, '')
const spec = TREX_PREFIX + withoutExt
const target = './packages/' + rel
deno.imports[spec] = target

// Add equivalent file:// URL specifier so URL-based imports resolve
const fileUrlSpec = FILE_URL_PREFIX + withoutExt
deno.imports[fileUrlSpec] = target

// Add directory prefix mappings (with trailing slash) for the parent dir
const parentDir = withoutExt.includes('/') ? withoutExt.substring(0, withoutExt.lastIndexOf('/') + 1) : ''
if (parentDir) {
const trexDirPrefix = TREX_PREFIX + parentDir
const fileUrlDirPrefix = FILE_URL_PREFIX + parentDir
const dirTarget = './packages/' + parentDir
deno.imports[trexDirPrefix] = dirTarget
deno.imports[fileUrlDirPrefix] = dirTarget
}

// If this is an index file, also map the directory itself to the index target
if (withoutExt.endsWith('/index')) {
const dirWithoutIndex = withoutExt.slice(0, -('/index'.length))
const dirSpec = TREX_PREFIX + dirWithoutIndex
deno.imports[dirSpec] = target
const dirFileUrlSpec = FILE_URL_PREFIX + dirWithoutIndex
deno.imports[dirFileUrlSpec] = target
}
}

fs.writeFileSync(DENO_JSON, JSON.stringify(deno, null, 2) + '\n')
console.log(`Updated ${DENO_JSON} with ${files.length} file mappings for logto`)
}

main()

Loading