@objectstack/runtime 7.1.0 → 7.2.1

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/dist/index.cjs CHANGED
@@ -1284,10 +1284,11 @@ function collectBundleFunctions(bundle) {
1284
1284
  merge(bundle?.manifest?.functions);
1285
1285
  return out;
1286
1286
  }
1287
- var AppPlugin;
1287
+ var import_types, AppPlugin;
1288
1288
  var init_app_plugin = __esm({
1289
1289
  "src/app-plugin.ts"() {
1290
1290
  "use strict";
1291
+ import_types = require("@objectstack/types");
1291
1292
  init_seed_loader();
1292
1293
  init_quickjs_runner();
1293
1294
  init_body_runner();
@@ -1614,7 +1615,7 @@ var init_app_plugin = __esm({
1614
1615
  } catch (e) {
1615
1616
  ctx.logger.warn("[Seeder] Failed to register seed-datasets/seed-replayer service", { error: e?.message });
1616
1617
  }
1617
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
1618
+ const multiTenant = String((0, import_types.readEnvWithDeprecation)("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
1618
1619
  if (multiTenant) {
1619
1620
  ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
1620
1621
  } else {
@@ -2249,6 +2250,7 @@ __export(index_exports, {
2249
2250
  SandboxError: () => SandboxError,
2250
2251
  SeedLoaderService: () => SeedLoaderService,
2251
2252
  UnimplementedScriptRunner: () => UnimplementedScriptRunner,
2253
+ _resetEnvDeprecationWarnings: () => import_types6._resetEnvDeprecationWarnings,
2252
2254
  actionBodyRunnerFactory: () => actionBodyRunnerFactory,
2253
2255
  backfillPlatformSsoClients: () => backfillPlatformSsoClients,
2254
2256
  buildPlatformSsoRedirectUri: () => buildPlatformSsoRedirectUri,
@@ -2273,6 +2275,7 @@ __export(index_exports, {
2273
2275
  mergeRuntimeModule: () => mergeRuntimeModule,
2274
2276
  parseTraceparent: () => parseTraceparent,
2275
2277
  readArtifactSource: () => readArtifactSource,
2278
+ readEnvWithDeprecation: () => import_types6.readEnvWithDeprecation,
2276
2279
  resolveCloudUrl: () => resolveCloudUrl,
2277
2280
  resolveDefaultArtifactPath: () => resolveDefaultArtifactPath,
2278
2281
  resolveErrorReporter: () => resolveErrorReporter,
@@ -2335,6 +2338,7 @@ var import_node_path2 = require("path");
2335
2338
  var import_node_fs = require("fs");
2336
2339
  var import_node_os = require("os");
2337
2340
  var import_zod = require("zod");
2341
+ var import_types2 = require("@objectstack/types");
2338
2342
  init_load_artifact_bundle();
2339
2343
  function resolveObjectStackHome() {
2340
2344
  const raw = process.env.OS_HOME?.trim();
@@ -2385,7 +2389,7 @@ async function createStandaloneStack(config) {
2385
2389
  const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "proj_local";
2386
2390
  const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? (0, import_node_path2.resolve)(cwd, "dist/objectstack.json");
2387
2391
  const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : (0, import_node_path2.resolve)(cwd, artifactPathInput);
2388
- const dbUrl = cfg.databaseUrl ?? process.env.OS_DATABASE_URL?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${(0, import_node_path2.resolve)(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}`);
2392
+ const dbUrl = cfg.databaseUrl ?? (0, import_types2.readEnvWithDeprecation)("OS_DATABASE_URL", "DATABASE_URL")?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${(0, import_node_path2.resolve)(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}`);
2389
2393
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2390
2394
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
2391
2395
  let driverPlugin;
@@ -2548,6 +2552,7 @@ init_seed_loader();
2548
2552
 
2549
2553
  // src/http-dispatcher.ts
2550
2554
  var import_core2 = require("@objectstack/core");
2555
+ var import_types3 = require("@objectstack/types");
2551
2556
  var import_system = require("@objectstack/spec/system");
2552
2557
  var import_shared = require("@objectstack/spec/shared");
2553
2558
 
@@ -4038,7 +4043,7 @@ var _HttpDispatcher = class _HttpDispatcher {
4038
4043
  }
4039
4044
  }
4040
4045
  if (parts.length === 3 && parts[0] === "admin" && parts[1] === "platform-sso" && parts[2] === "backfill" && m === "POST") {
4041
- const baseSecret = (process.env.OS_AUTH_SECRET ?? process.env.AUTH_SECRET ?? "").trim();
4046
+ const baseSecret = ((0, import_types3.readEnvWithDeprecation)("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
4042
4047
  if (!baseSecret) {
4043
4048
  return { handled: true, response: this.error("OS_AUTH_SECRET not configured on this worker", 503) };
4044
4049
  }
@@ -4228,7 +4233,7 @@ var _HttpDispatcher = class _HttpDispatcher {
4228
4233
  });
4229
4234
  try {
4230
4235
  const { seedPlatformSsoClient: seedPlatformSsoClient2 } = await Promise.resolve().then(() => (init_platform_sso(), platform_sso_exports));
4231
- const baseSecret = (process.env.OS_AUTH_SECRET ?? process.env.AUTH_SECRET ?? "").trim();
4236
+ const baseSecret = ((0, import_types3.readEnvWithDeprecation)("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
4232
4237
  if (baseSecret) {
4233
4238
  await seedPlatformSsoClient2({
4234
4239
  ql,
@@ -7686,6 +7691,7 @@ async function createDriver(driverType, databaseUrl, authToken) {
7686
7691
  // src/cloud/artifact-kernel-factory.ts
7687
7692
  var import_node_crypto2 = require("crypto");
7688
7693
  var import_core3 = require("@objectstack/core");
7694
+ var import_types4 = require("@objectstack/types");
7689
7695
  init_driver_plugin();
7690
7696
  init_app_plugin();
7691
7697
 
@@ -7825,7 +7831,7 @@ var ArtifactKernelFactory = class {
7825
7831
  this.envRegistry = config.envRegistry;
7826
7832
  this.logger = config.logger ?? console;
7827
7833
  this.kernelConfig = config.kernelConfig;
7828
- this.authBaseSecret = (config.authBaseSecret ?? process.env.OS_AUTH_SECRET ?? process.env.AUTH_SECRET ?? "").trim();
7834
+ this.authBaseSecret = (config.authBaseSecret ?? (0, import_types4.readEnvWithDeprecation)("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
7829
7835
  }
7830
7836
  async create(environmentId) {
7831
7837
  let cached = this.envRegistry.peekById(environmentId);
@@ -7938,7 +7944,7 @@ var ArtifactKernelFactory = class {
7938
7944
  this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
7939
7945
  }
7940
7946
  try {
7941
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
7947
+ const multiTenant = String((0, import_types4.readEnvWithDeprecation)("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
7942
7948
  if (multiTenant) {
7943
7949
  try {
7944
7950
  const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
@@ -8475,6 +8481,36 @@ function resolveCloudUrl(explicit) {
8475
8481
  return picked.replace(/\/+$/, "");
8476
8482
  }
8477
8483
 
8484
+ // src/cloud/marketplace-public-url.ts
8485
+ function resolveMarketplacePublicBaseUrl(explicit) {
8486
+ const raw = (explicit ?? process.env.OS_MARKETPLACE_PUBLIC_BASE_URL ?? "").trim();
8487
+ const lower = raw.toLowerCase();
8488
+ if (!raw || lower === "off" || lower === "none" || lower === "disabled" || lower === "false") {
8489
+ return "";
8490
+ }
8491
+ return raw.replace(/\/+$/, "");
8492
+ }
8493
+ function publicMarketplaceKeyForApiPath(pathname) {
8494
+ const prefix = "/api/v1/marketplace/packages";
8495
+ if (pathname === prefix) return "packages.json";
8496
+ if (!pathname.startsWith(`${prefix}/`)) return null;
8497
+ const tail = pathname.slice(prefix.length + 1);
8498
+ if (!tail) return null;
8499
+ const parts = tail.split("/");
8500
+ if (parts.length === 1) {
8501
+ const id = decodeURIComponent(parts[0] ?? "");
8502
+ if (!id) return null;
8503
+ return `packages/${encodeURIComponent(id)}.json`;
8504
+ }
8505
+ if (parts.length === 4 && parts[1] === "versions" && parts[3] === "manifest") {
8506
+ const id = decodeURIComponent(parts[0] ?? "");
8507
+ const versionId = decodeURIComponent(parts[2] ?? "");
8508
+ if (!id || !versionId) return null;
8509
+ return `packages/${encodeURIComponent(id)}/versions/${encodeURIComponent(versionId)}/manifest.json`;
8510
+ }
8511
+ return null;
8512
+ }
8513
+
8478
8514
  // src/cloud/marketplace-proxy-plugin.ts
8479
8515
  var MARKETPLACE_PREFIX = "/api/v1/marketplace";
8480
8516
  var DEFAULT_LRU_MAX = 200;
@@ -8514,7 +8550,7 @@ var LruTtlCache = class {
8514
8550
  var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8515
8551
  constructor(config = {}) {
8516
8552
  this.name = "com.objectstack.runtime.marketplace-proxy";
8517
- this.version = "1.0.0";
8553
+ this.version = "1.1.0";
8518
8554
  this.init = async (_ctx) => {
8519
8555
  };
8520
8556
  this.start = async (ctx) => {
@@ -8532,7 +8568,11 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8532
8568
  }
8533
8569
  const rawApp = httpServer.getRawApp();
8534
8570
  const cloudUrl = this.cloudUrl;
8571
+ const publicBaseUrl = this.publicBaseUrl;
8535
8572
  const cache = this.cache;
8573
+ if (publicBaseUrl) {
8574
+ ctx.logger?.info?.(`[MarketplaceProxyPlugin] public R2 fast-path enabled \u2192 ${publicBaseUrl}`);
8575
+ }
8536
8576
  const handler = async (c, next) => {
8537
8577
  if (!cloudUrl) {
8538
8578
  return c.json({
@@ -8548,8 +8588,18 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8548
8588
  if (incomingUrl.pathname.startsWith(`${MARKETPLACE_PREFIX}/install-local`)) {
8549
8589
  return next();
8550
8590
  }
8551
- const target = `${cloudUrl}${incomingUrl.pathname}${incomingUrl.search}`;
8552
8591
  const method = String(c.req.method ?? "GET").toUpperCase();
8592
+ if (publicBaseUrl && (method === "GET" || method === "HEAD")) {
8593
+ const r2Resp = await tryPublicMarketplaceFetch(
8594
+ publicBaseUrl,
8595
+ incomingUrl,
8596
+ method,
8597
+ c.req.header("accept"),
8598
+ ctx.logger
8599
+ );
8600
+ if (r2Resp) return r2Resp;
8601
+ }
8602
+ const target = `${cloudUrl}${incomingUrl.pathname}${incomingUrl.search}`;
8553
8603
  if (method !== "GET" && method !== "HEAD") {
8554
8604
  return c.json({
8555
8605
  success: false,
@@ -8630,12 +8680,82 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8630
8680
  });
8631
8681
  };
8632
8682
  this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
8683
+ this.publicBaseUrl = resolveMarketplacePublicBaseUrl(config.publicMarketplaceBaseUrl);
8633
8684
  const envFlag = (process.env.OS_MARKETPLACE_CACHE ?? "").trim().toLowerCase();
8634
8685
  const envDisabled = ["off", "false", "0", "no", "disable", "disabled"].includes(envFlag);
8635
8686
  const disabled = config.cacheDisabled ?? envDisabled;
8636
8687
  this.cache = disabled ? null : new LruTtlCache(Math.max(8, config.cacheMaxEntries ?? DEFAULT_LRU_MAX));
8637
8688
  }
8638
8689
  };
8690
+ async function tryPublicMarketplaceFetch(publicBaseUrl, incomingUrl, method, acceptHeader, logger) {
8691
+ const key = publicMarketplaceKeyForApiPath(incomingUrl.pathname);
8692
+ if (!key) return null;
8693
+ const target = `${publicBaseUrl}/${key}`;
8694
+ let resp;
8695
+ try {
8696
+ resp = await fetch(target, {
8697
+ method: "GET",
8698
+ headers: {
8699
+ "Accept": acceptHeader || "application/json",
8700
+ "User-Agent": `objectos-marketplace-proxy/public-r2`
8701
+ }
8702
+ });
8703
+ } catch (err) {
8704
+ logger?.warn?.(`[MarketplaceProxyPlugin] public R2 fetch failed (${target}): ${err?.message ?? err}`);
8705
+ return null;
8706
+ }
8707
+ if (resp.status === 404) return null;
8708
+ if (!resp.ok) {
8709
+ logger?.warn?.(`[MarketplaceProxyPlugin] public R2 ${target} returned ${resp.status} \u2014 falling back to cloud`);
8710
+ return null;
8711
+ }
8712
+ const isList = key === "packages.json";
8713
+ const hasFilters = isList && (incomingUrl.searchParams.has("q") || incomingUrl.searchParams.has("category") || incomingUrl.searchParams.has("limit") || incomingUrl.searchParams.has("offset"));
8714
+ if (!hasFilters) {
8715
+ const headers2 = new Headers();
8716
+ const ct = resp.headers.get("content-type") ?? "application/json; charset=utf-8";
8717
+ headers2.set("content-type", ct);
8718
+ const cc = resp.headers.get("cache-control");
8719
+ if (cc) headers2.set("cache-control", cc);
8720
+ const etag = resp.headers.get("etag");
8721
+ if (etag) headers2.set("etag", etag);
8722
+ headers2.set("x-cache", "PUBLIC-R2");
8723
+ const body2 = method === "HEAD" ? null : resp.body;
8724
+ return new Response(body2, { status: 200, headers: headers2 });
8725
+ }
8726
+ let snapshot;
8727
+ try {
8728
+ snapshot = await resp.json();
8729
+ } catch (err) {
8730
+ logger?.warn?.(`[MarketplaceProxyPlugin] public R2 list snapshot parse failed: ${err?.message ?? err}`);
8731
+ return null;
8732
+ }
8733
+ const items = Array.isArray(snapshot?.data?.items) ? snapshot.data.items : [];
8734
+ const q = (incomingUrl.searchParams.get("q") ?? "").trim().toLowerCase();
8735
+ const category = (incomingUrl.searchParams.get("category") ?? "").trim();
8736
+ const limit = Math.min(Math.max(Number(incomingUrl.searchParams.get("limit") ?? 50), 1), 100);
8737
+ const offset = Math.max(Number(incomingUrl.searchParams.get("offset") ?? 0), 0);
8738
+ let filtered = items;
8739
+ if (q) {
8740
+ filtered = filtered.filter((r) => {
8741
+ const dn = String(r?.display_name ?? "").toLowerCase();
8742
+ const mid = String(r?.manifest_id ?? "").toLowerCase();
8743
+ return dn.includes(q) || mid.includes(q);
8744
+ });
8745
+ }
8746
+ if (category) {
8747
+ filtered = filtered.filter((r) => String(r?.category ?? "") === category);
8748
+ }
8749
+ const total = filtered.length;
8750
+ const page = filtered.slice(offset, offset + limit);
8751
+ const body = JSON.stringify({ success: true, data: { items: page, total, limit, offset } });
8752
+ const headers = new Headers({
8753
+ "content-type": "application/json; charset=utf-8",
8754
+ "cache-control": "public, max-age=30",
8755
+ "x-cache": "PUBLIC-R2-FILTERED"
8756
+ });
8757
+ return new Response(method === "HEAD" ? null : body, { status: 200, headers });
8758
+ }
8639
8759
  var PASSTHROUGH_HEADERS = ["content-type", "cache-control", "etag", "last-modified", "vary"];
8640
8760
  function collectHeaders(src) {
8641
8761
  const out = {};
@@ -9027,6 +9147,7 @@ async function createObjectOSStack(config) {
9027
9147
  // src/cloud/marketplace-install-local-plugin.ts
9028
9148
  var import_node_fs3 = require("fs");
9029
9149
  var import_node_path6 = require("path");
9150
+ var import_types5 = require("@objectstack/types");
9030
9151
  var ROUTE_BASE = "/api/v1/marketplace/install-local";
9031
9152
  var DEFAULT_DIR = ".objectstack/installed-packages";
9032
9153
  function safeFilename(manifestId) {
@@ -9119,22 +9240,55 @@ var MarketplaceInstallLocalPlugin = class {
9119
9240
  return c.json({ success: false, error: { code: "bad_request", message: "packageId is required." } }, 400);
9120
9241
  }
9121
9242
  let payload;
9122
- try {
9123
- const url = `${this.cloudUrl}/api/v1/marketplace/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest`;
9124
- const resp = await fetch(url, { headers: { "Accept": "application/json" } });
9125
- if (!resp.ok) {
9126
- const text = await resp.text().catch(() => "");
9243
+ const publicBase = resolveMarketplacePublicBaseUrl();
9244
+ const fetchAttempts = [];
9245
+ if (publicBase) {
9246
+ fetchAttempts.push({
9247
+ label: "public-r2",
9248
+ url: `${publicBase}/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest.json`
9249
+ });
9250
+ }
9251
+ fetchAttempts.push({
9252
+ label: "cloud",
9253
+ url: `${this.cloudUrl}/api/v1/marketplace/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest`
9254
+ });
9255
+ let lastErrStatus = 0;
9256
+ let lastErrText = "";
9257
+ for (const attempt of fetchAttempts) {
9258
+ try {
9259
+ const resp = await fetch(attempt.url, { headers: { "Accept": "application/json" } });
9260
+ if (!resp.ok) {
9261
+ lastErrStatus = resp.status;
9262
+ lastErrText = (await resp.text().catch(() => "")).slice(0, 200);
9263
+ if (attempt.label === "public-r2" && resp.status === 404) {
9264
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] public-r2 miss for ${packageId}@${versionId}, falling back to cloud`);
9265
+ continue;
9266
+ }
9267
+ if (attempt.label === "public-r2" && resp.status >= 500) {
9268
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 ${resp.status}, falling back to cloud`);
9269
+ continue;
9270
+ }
9271
+ break;
9272
+ }
9273
+ payload = await resp.json();
9274
+ lastErrStatus = 0;
9275
+ break;
9276
+ } catch (err) {
9277
+ if (attempt.label === "public-r2") {
9278
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 fetch error: ${err?.message ?? err}, falling back to cloud`);
9279
+ continue;
9280
+ }
9127
9281
  return c.json({
9128
9282
  success: false,
9129
- error: { code: "cloud_fetch_failed", message: `Cloud returned ${resp.status}: ${text.slice(0, 200)}` }
9130
- }, resp.status === 404 ? 404 : 502);
9283
+ error: { code: "cloud_fetch_failed", message: err?.message ?? String(err) }
9284
+ }, 502);
9131
9285
  }
9132
- payload = await resp.json();
9133
- } catch (err) {
9286
+ }
9287
+ if (!payload) {
9134
9288
  return c.json({
9135
9289
  success: false,
9136
- error: { code: "cloud_fetch_failed", message: err?.message ?? String(err) }
9137
- }, 502);
9290
+ error: { code: "cloud_fetch_failed", message: `Cloud returned ${lastErrStatus}: ${lastErrText}` }
9291
+ }, lastErrStatus === 404 ? 404 : 502);
9138
9292
  }
9139
9293
  const data = payload?.data ?? payload;
9140
9294
  const manifest = data?.manifest;
@@ -9516,7 +9670,7 @@ var MarketplaceInstallLocalPlugin = class {
9516
9670
  }
9517
9671
  }
9518
9672
  if (opts.seedNow && datasets.length > 0) {
9519
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
9673
+ const multiTenant = String((0, import_types5.readEnvWithDeprecation)("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
9520
9674
  try {
9521
9675
  const ql = ctx.getService("objectql");
9522
9676
  let metadata;
@@ -9662,6 +9816,7 @@ init_body_runner();
9662
9816
  // src/index.ts
9663
9817
  var import_rest = require("@objectstack/rest");
9664
9818
  __reExport(index_exports, require("@objectstack/core"), module.exports);
9819
+ var import_types6 = require("@objectstack/types");
9665
9820
  // Annotate the CommonJS export names for ESM import in node:
9666
9821
  0 && (module.exports = {
9667
9822
  AppPlugin,
@@ -9700,6 +9855,7 @@ __reExport(index_exports, require("@objectstack/core"), module.exports);
9700
9855
  SandboxError,
9701
9856
  SeedLoaderService,
9702
9857
  UnimplementedScriptRunner,
9858
+ _resetEnvDeprecationWarnings,
9703
9859
  actionBodyRunnerFactory,
9704
9860
  backfillPlatformSsoClients,
9705
9861
  buildPlatformSsoRedirectUri,
@@ -9724,6 +9880,7 @@ __reExport(index_exports, require("@objectstack/core"), module.exports);
9724
9880
  mergeRuntimeModule,
9725
9881
  parseTraceparent,
9726
9882
  readArtifactSource,
9883
+ readEnvWithDeprecation,
9727
9884
  resolveCloudUrl,
9728
9885
  resolveDefaultArtifactPath,
9729
9886
  resolveErrorReporter,