@dbx-tools/databricks 0.1.9

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/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @dbx-tools/node-databricks
2
+
3
+ Generic Node-side Databricks workspace and cloud infrastructure helpers.
4
+
5
+ Import this package when backend code needs workspace URL/id discovery, cloud
6
+ provider/region lookup, DNS resolution, or public-IP discovery without requiring
7
+ an AppKit plugin runtime.
8
+
9
+ Key features:
10
+
11
+ - Workspace URL and numeric workspace id resolution from AppKit context,
12
+ Databricks SDK config, env, and config files.
13
+ - Cloud provider/region detection by resolving workspace hosts against public
14
+ AWS, Azure, and GCP IP feeds.
15
+ - In-process and on-disk caching for cloud IP range feeds.
16
+ - DNS A/AAAA lookup helpers for Databricks and adjacent service hosts.
17
+ - Memoized outbound public-IP discovery for setup and diagnostics.
18
+
19
+ ## Resolve Workspace Identity
20
+
21
+ ```ts
22
+ import { workspace } from "@dbx-tools/node-databricks";
23
+
24
+ const url = await workspace.getWorkspaceUrl();
25
+ const id = await workspace.getWorkspaceId();
26
+ ```
27
+
28
+ `workspace.getWorkspaceUrl()` checks the active AppKit execution context when
29
+ present, then a default Databricks SDK client, then environment/config. Use it in
30
+ libraries that should work inside an AppKit request and from a standalone
31
+ script.
32
+
33
+ ## Detect Cloud Provider And Region
34
+
35
+ ```ts
36
+ import { cloud } from "@dbx-tools/node-databricks";
37
+
38
+ const location = await cloud.resolveCloudLocation("https://adb-123.azuredatabricks.net");
39
+ ```
40
+
41
+ `cloud.resolveCloudLocation()` DNS-resolves the workspace host and matches its
42
+ IPs against AWS, Azure, and GCP public range feeds. Feeds are cached on disk and
43
+ in process for 24 hours. Use this when constructing region-specific service URLs
44
+ or routing workspace-adjacent traffic.
45
+
46
+ Cloud detection is best-effort. It is intended for endpoint construction and
47
+ developer diagnostics, not for security policy decisions.
48
+
49
+ ## Resolve Network Details
50
+
51
+ ```ts
52
+ import { net } from "@dbx-tools/node-databricks";
53
+
54
+ const ips = await net.resolveHostIps("https://example.cloud.databricks.com");
55
+ const publicIp = await net.getPublicIp();
56
+ ```
57
+
58
+ `net.resolveHostIps()` accepts the same URL-like values as
59
+ `@dbx-tools/shared-core` `net.urlBuilder()`. `net.getPublicIp()` is memoized for
60
+ short-lived reuse.
61
+
62
+ ## Modules
63
+
64
+ - `workspace` - workspace URL and numeric id resolution.
65
+ - `cloud` - provider/region detection from public cloud IP ranges.
66
+ - `net` - DNS A/AAAA resolution and outbound public-IP discovery.
67
+
68
+ Zerobus endpoint construction builds on these helpers in
69
+ [`@dbx-tools/node-databricks-zerobus`](../databricks-zerobus).
package/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ // GENERATED by projen watch - DO NOT EDIT.
2
+ // Regenerated from the exporting modules in ./src.
3
+ // Hand edits are overwritten on the next watch; this file is read-only.
4
+
5
+ export * as cloud from "./src/cloud";
6
+ export * as net from "./src/net";
7
+ export * as workspace from "./src/workspace";
8
+ export type { CloudLocation } from "./src/cloud";
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@dbx-tools/databricks",
3
+ "repository": {
4
+ "type": "git",
5
+ "url": "git+https://github.com/reggie-db/dbx-tools.git",
6
+ "directory": "workspaces/node/databricks"
7
+ },
8
+ "devDependencies": {
9
+ "@types/node": "^24.6.0",
10
+ "tsx": "^4.23.0",
11
+ "typescript": "^5.9.3"
12
+ },
13
+ "dependencies": {
14
+ "@databricks/sdk-experimental": "^0.17.0",
15
+ "@dbx-tools/appkit": "0.1.9",
16
+ "@dbx-tools/core": "0.1.9",
17
+ "@dbx-tools/shared-core": "0.1.9"
18
+ },
19
+ "main": "index.ts",
20
+ "license": "UNLICENSED",
21
+ "version": "0.1.9",
22
+ "types": "index.ts",
23
+ "type": "module",
24
+ "exports": {
25
+ ".": "./index.ts",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "dbxToolsConfig": {
29
+ "tags": [
30
+ "node"
31
+ ]
32
+ },
33
+ "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\".",
34
+ "scripts": {
35
+ "build": "projen build",
36
+ "compile": "projen compile",
37
+ "default": "projen default",
38
+ "package": "projen package",
39
+ "post-compile": "projen post-compile",
40
+ "pre-compile": "projen pre-compile",
41
+ "test": "projen test",
42
+ "watch": "projen watch",
43
+ "projen": "projen"
44
+ }
45
+ }
package/src/cloud.ts ADDED
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Best-effort geolocation of a host to the cloud provider and region it
3
+ * runs in. Resolves the host to its IP address(es) via DNS, then matches
4
+ * those addresses against the public IP-range feeds each hyperscaler
5
+ * publishes (AWS, Azure, GCP). Handy for placing a Databricks workspace
6
+ * URL, but works for any host.
7
+ *
8
+ * The range feeds are large and change slowly, so each provider's feed
9
+ * is cached for {@link RANGE_CACHE_TTL_MS} (24 hours) at two layers: an
10
+ * on-disk copy under the OS temp dir (survives process restarts, shared
11
+ * across processes) plus an in-process memoized parse. A provider whose
12
+ * feed fails to load is skipped rather than failing the whole lookup -
13
+ * the other providers still answer.
14
+ *
15
+ * Server-only: DNS resolution needs `node:dns` (via `./net.ts`), the
16
+ * disk cache needs `node:fs` / `node:os` / `node:path`, and the feeds
17
+ * are fetched with the global `fetch`.
18
+ */
19
+
20
+ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
21
+ import { tmpdir } from "node:os";
22
+ import { dirname, join } from "node:path";
23
+
24
+ import { error, functionModule, hash, http, log, net } from "@dbx-tools/shared-core";
25
+ import { project } from "@dbx-tools/core";
26
+ import { resolveHostIps } from "./net";
27
+
28
+ const logger = log.logger("cloud");
29
+
30
+ /** How long a fetched provider IP-range feed is reused before refetch. */
31
+ export const RANGE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
32
+
33
+ /** AWS publishes a single stable, unauthenticated ranges feed. */
34
+ const AWS_RANGES_URL = "https://ip-ranges.amazonaws.com/ip-ranges.json";
35
+ /** GCP publishes a single stable, unauthenticated ranges feed. */
36
+ const GCP_RANGES_URL = "https://www.gstatic.com/ipranges/cloud.json";
37
+ /**
38
+ * Azure has no stable feed URL - the JSON download link (with a
39
+ * rotating date stamp) lives on this human-facing download page and
40
+ * must be scraped out. See {@link fetchAzureRanges}.
41
+ */
42
+ const AZURE_DOWNLOAD_PAGE = "https://www.microsoft.com/en-us/download/details.aspx?id=56519";
43
+ /** Regex that plucks the current `ServiceTags_Public_<date>.json` link off the Azure page. */
44
+ const AZURE_JSON_LINK =
45
+ /https:\/\/download\.microsoft\.com\/download\/[^"']*ServiceTags_Public_\d+\.json/;
46
+
47
+ /** Cloud hyperscaler a host resolves into. */
48
+ export enum CloudProvider {
49
+ Aws = "aws",
50
+ Azure = "azure",
51
+ Gcp = "gcp",
52
+ }
53
+
54
+ /**
55
+ * Where a host lives: the {@link CloudProvider}, the provider-native
56
+ * `region` string (whatever the provider's feed calls it, e.g.
57
+ * `"us-east-1"` on AWS, `"eastus2"` on Azure, `"us-east1"` on GCP), the
58
+ * resolved `ip` that matched, and the `cidr` block it fell in.
59
+ */
60
+ export interface CloudLocation {
61
+ provider: CloudProvider;
62
+ region: string;
63
+ ip: string;
64
+ cidr: string;
65
+ }
66
+
67
+ /** A parsed CIDR block tagged with the region it belongs to. */
68
+ interface RegionCidr extends net.Cidr {
69
+ region: string;
70
+ }
71
+
72
+ /** One provider's parsed, region-tagged range table. */
73
+ interface ProviderRanges {
74
+ provider: CloudProvider;
75
+ ranges: RegionCidr[];
76
+ }
77
+
78
+ /**
79
+ * Resolve `input`'s host to a {@link CloudLocation}, or `null` when the
80
+ * host can't be resolved or none of its IPs match a known cloud range.
81
+ * `input` is any {@link net.UrlLike} - a URL, a bare host, or a `{ url }`
82
+ * wrapper.
83
+ *
84
+ * The provider range feeds are cached for {@link RANGE_CACHE_TTL_MS}, so
85
+ * only the first call in a 24-hour window pays the fetch cost; the DNS
86
+ * lookup happens on every call.
87
+ *
88
+ * @example
89
+ * await resolveCloudLocation("https://adb-1234567890.7.azuredatabricks.net");
90
+ * // { provider: CloudProvider.Azure,
91
+ * // region: "eastus2", ip: "...", cidr: "..." }
92
+ *
93
+ * await resolveCloudLocation("dbc-abc123.cloud.databricks.com");
94
+ * // { provider: CloudProvider.Aws,
95
+ * // region: "us-west-2", ip: "...", cidr: "..." }
96
+ */
97
+ export async function resolveCloudLocation(input: net.UrlLike): Promise<CloudLocation | null> {
98
+ const ips = await resolveHostIps(input);
99
+ if (ips.length === 0) {
100
+ logger.debug("no ips resolved", { input: String(input) });
101
+ return null;
102
+ }
103
+ const providers = await loadProviderRanges();
104
+ for (const ip of ips) {
105
+ const parsed = net.parseIp(ip);
106
+ if (!parsed) continue;
107
+ for (const { provider, ranges } of providers) {
108
+ const match = net.findContainingCidr(parsed, ranges);
109
+ if (match) {
110
+ return { provider, region: match.region, ip, cidr: match.cidr };
111
+ }
112
+ }
113
+ }
114
+ logger.debug("no cloud range matched", { ips });
115
+ return null;
116
+ }
117
+
118
+ /**
119
+ * Load every provider's region-tagged range table, each cached for
120
+ * {@link RANGE_CACHE_TTL_MS}. Providers are loaded in parallel and a
121
+ * feed that fails to fetch or parse is dropped (logged, not thrown) so
122
+ * a single flaky feed never sinks the whole lookup. Exposed for callers
123
+ * that want to match many IPs against one cached snapshot without a DNS
124
+ * step per address.
125
+ */
126
+ export async function loadProviderRanges(): Promise<ProviderRanges[]> {
127
+ const settled = await Promise.allSettled([loadAwsRanges(), loadAzureRanges(), loadGcpRanges()]);
128
+ const out: ProviderRanges[] = [];
129
+ for (const result of settled) {
130
+ if (result.status === "fulfilled") out.push(result.value);
131
+ else logger.warn("provider range load failed", { error: error.errorMessage(result.reason) });
132
+ }
133
+ return out;
134
+ }
135
+
136
+ // ────────────────────────────────────────────────────────────────
137
+ // Per-provider cached loaders
138
+ // ────────────────────────────────────────────────────────────────
139
+
140
+ const loadAwsRanges = functionModule.memoize(fetchAwsRanges, { ttlMs: RANGE_CACHE_TTL_MS });
141
+ const loadAzureRanges = functionModule.memoize(fetchAzureRanges, { ttlMs: RANGE_CACHE_TTL_MS });
142
+ const loadGcpRanges = functionModule.memoize(fetchGcpRanges, { ttlMs: RANGE_CACHE_TTL_MS });
143
+
144
+ // ────────────────────────────────────────────────────────────────
145
+ // Provider feed fetch + parse
146
+ // ────────────────────────────────────────────────────────────────
147
+
148
+ interface AwsFeed {
149
+ prefixes?: { ip_prefix?: string; region?: string }[];
150
+ ipv6_prefixes?: { ipv6_prefix?: string; region?: string }[];
151
+ }
152
+
153
+ /**
154
+ * AWS `ip-ranges.json`: a flat list of IPv4 (`prefixes`) and IPv6
155
+ * (`ipv6_prefixes`) blocks, each already tagged with a `region` such
156
+ * as `"us-east-1"` (or `"GLOBAL"` for edge ranges).
157
+ */
158
+ async function fetchAwsRanges(): Promise<ProviderRanges> {
159
+ const feed = await fetchJson<AwsFeed>(AWS_RANGES_URL);
160
+ const ranges: RegionCidr[] = [];
161
+ for (const entry of feed.prefixes ?? []) {
162
+ addRange(ranges, entry.ip_prefix, entry.region);
163
+ }
164
+ for (const entry of feed.ipv6_prefixes ?? []) {
165
+ addRange(ranges, entry.ipv6_prefix, entry.region);
166
+ }
167
+ logger.debug("loaded aws ranges", { count: ranges.length });
168
+ return { provider: CloudProvider.Aws, ranges };
169
+ }
170
+
171
+ interface GcpFeed {
172
+ prefixes?: { ipv4Prefix?: string; ipv6Prefix?: string; scope?: string }[];
173
+ }
174
+
175
+ /**
176
+ * GCP `cloud.json`: a list of blocks each carrying an `ipv4Prefix` or
177
+ * `ipv6Prefix` and a `scope` that is the region (`"us-east1"`) or
178
+ * `"global"` for non-regional ranges.
179
+ */
180
+ async function fetchGcpRanges(): Promise<ProviderRanges> {
181
+ const feed = await fetchJson<GcpFeed>(GCP_RANGES_URL);
182
+ const ranges: RegionCidr[] = [];
183
+ for (const entry of feed.prefixes ?? []) {
184
+ addRange(ranges, entry.ipv4Prefix ?? entry.ipv6Prefix, entry.scope);
185
+ }
186
+ logger.debug("loaded gcp ranges", { count: ranges.length });
187
+ return { provider: CloudProvider.Gcp, ranges };
188
+ }
189
+
190
+ interface AzureFeed {
191
+ values?: {
192
+ name?: string;
193
+ properties?: { region?: string; addressPrefixes?: string[] };
194
+ }[];
195
+ }
196
+
197
+ /**
198
+ * Azure service tags: Microsoft ships no stable feed URL, so scrape the
199
+ * current `ServiceTags_Public_<date>.json` link off the download page
200
+ * and fetch it. Only the regional `AzureCloud.<region>` aggregates
201
+ * (which have a non-empty `region`) are kept; the cloud-wide
202
+ * `AzureCloud` tag and per-service tags are ignored so an address maps
203
+ * cleanly to one region.
204
+ */
205
+ async function fetchAzureRanges(): Promise<ProviderRanges> {
206
+ const page = await fetchText(AZURE_DOWNLOAD_PAGE);
207
+ const jsonUrl = page.match(AZURE_JSON_LINK)?.[0];
208
+ if (!jsonUrl) {
209
+ throw new Error("could not find ServiceTags JSON link on Azure download page");
210
+ }
211
+ const feed = await fetchJson<AzureFeed>(jsonUrl);
212
+ const ranges: RegionCidr[] = [];
213
+ for (const value of feed.values ?? []) {
214
+ const region = value.properties?.region;
215
+ if (!region || !value.name?.startsWith("AzureCloud.")) continue;
216
+ for (const prefix of value.properties?.addressPrefixes ?? []) {
217
+ addRange(ranges, prefix, region);
218
+ }
219
+ }
220
+ logger.debug("loaded azure ranges", { count: ranges.length });
221
+ return { provider: CloudProvider.Azure, ranges };
222
+ }
223
+
224
+ // ────────────────────────────────────────────────────────────────
225
+ // Helpers
226
+ // ────────────────────────────────────────────────────────────────
227
+
228
+ /** Parse `cidr` and, when valid and `region` is set, push a tagged range. */
229
+ function addRange(ranges: RegionCidr[], cidr?: string, region?: string): void {
230
+ if (!cidr || !region) return;
231
+ const parsed = net.parseCidr(cidr);
232
+ if (parsed) ranges.push({ ...parsed, region });
233
+ }
234
+
235
+ async function fetchJson<T>(url: string): Promise<T> {
236
+ const text = await fetchText(url);
237
+ return JSON.parse(text) as T;
238
+ }
239
+
240
+ /**
241
+ * Fetch `url` as text with a 24h on-disk cache under the OS temp dir
242
+ * (keyed by an FNV hash of the URL). A fresh cache file is returned
243
+ * directly; a miss/expiry fetches, writes to a unique temp file, and
244
+ * atomically renames it into place so concurrent callers never observe
245
+ * a half-written cache entry.
246
+ */
247
+ async function fetchText(url: string): Promise<string> {
248
+ const cacheDir = join(tmpdir(), "dbx-tools", "shared", "cloud");
249
+ const cacheDirCreated = await mkdir(cacheDir, { recursive: true });
250
+ const cachePath = join(cacheDir, `fetch-${hash.fnvHash(url)}.txt`);
251
+ const createdAt = cacheDirCreated ? undefined : await getCreated(cachePath);
252
+ if (createdAt) {
253
+ const expiresAt = new Date(createdAt.getTime() + RANGE_CACHE_TTL_MS);
254
+ if (expiresAt > new Date()) {
255
+ logger.debug("cached fetch hit", { url, cachePath });
256
+ return await readFile(cachePath, "utf8");
257
+ }
258
+ }
259
+ let tempPath: string | null = join(cacheDir, `${hash.id()}.txt`);
260
+ try {
261
+ const response = await fetch(url);
262
+ if (!response.ok) {
263
+ throw await http.createFetchError(response, url);
264
+ }
265
+ const responseText = await response.text();
266
+ await mkdir(dirname(tempPath), { recursive: true });
267
+ await writeFile(tempPath, responseText);
268
+
269
+ await rename(tempPath, cachePath);
270
+ logger.debug("cached fetch load", { url, cachePath });
271
+ tempPath = null;
272
+ return responseText;
273
+ } finally {
274
+ if (tempPath) {
275
+ await unlink(tempPath);
276
+ }
277
+ }
278
+ }
279
+
280
+ /** Birth time of `path`, or `undefined` when it doesn't exist yet. */
281
+ async function getCreated(path: string): Promise<Date | undefined> {
282
+ return (await project.stat(path))?.birthtime;
283
+ }
package/src/net.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Server-side networking helpers that need Node builtins, layered over the
3
+ * browser-safe URL / IP toolkit in `@dbx-tools/shared-core`'s `net` module: DNS
4
+ * resolution ({@link resolveHostIps}, `node:dns`) and public-IP discovery
5
+ * ({@link getPublicIp}).
6
+ */
7
+
8
+ import { lookup } from "node:dns/promises";
9
+ import { functionModule, http, net } from "@dbx-tools/shared-core";
10
+
11
+ /**
12
+ * This process's outbound public IP, cached for 5 minutes. Asks Cloudflare's
13
+ * `cdn-cgi/trace` first and falls back to ipify. Useful for allowlisting or for
14
+ * reasoning about egress from a Databricks App.
15
+ */
16
+ export const getPublicIp = functionModule.memoize(
17
+ async () => {
18
+ const cloudflareResponse = await fetch("https://cloudflare.com/cdn-cgi/trace");
19
+ if (cloudflareResponse.ok) {
20
+ const trace = await cloudflareResponse.text();
21
+ const ipKey = "ip=";
22
+ const ip = trace
23
+ .split("\n")
24
+ .map((line) => line.trim())
25
+ .find((line) => line.startsWith(ipKey))
26
+ ?.slice(ipKey.length);
27
+ if (ip) return ip;
28
+ }
29
+ const ipifyResponse = await fetch("https://api.ipify.org?format=json");
30
+ if (!ipifyResponse.ok) {
31
+ throw await http.createFetchError(ipifyResponse);
32
+ }
33
+ const ipifyData = (await ipifyResponse.json()) as { ip?: string };
34
+ const ip = ipifyData?.ip;
35
+ if (ip) return ip;
36
+ throw new Error("Could not determine public IP");
37
+ },
38
+ { ttlMs: 1000 * 60 * 5 },
39
+ );
40
+
41
+ /**
42
+ * Resolve the host of `input` to its IP address(es) via the OS resolver
43
+ * (`dns.lookup` with `all: true`, so both A and AAAA records are returned when
44
+ * the host is dual-stacked). Accepts any `net.UrlLike` - a bare hostname, a full
45
+ * URL, or a `{ url }` wrapper - and returns the deduplicated list of literal
46
+ * addresses. Never throws: an unparseable input, an IP-literal host (returned
47
+ * as-is without a DNS round-trip), or a resolution failure all yield the
48
+ * appropriate list or `[]`.
49
+ *
50
+ * `dns.lookup` (not `resolve4` / `resolve6`) is used so `/etc/hosts` and the
51
+ * platform resolver order are honored, matching what an outbound connection to
52
+ * the host would actually use.
53
+ */
54
+ export async function resolveHostIps(input: net.UrlLike): Promise<string[]> {
55
+ const host = net.urlBuilder(input)?.hostname;
56
+ if (!host) return [];
57
+ // WHATWG `URL.hostname` brackets IPv6 literals; strip before parsing.
58
+ const literal = net.parseIp(host.replace(/^\[|\]$/g, ""));
59
+ if (literal) return [host.replace(/^\[|\]$/g, "")];
60
+ try {
61
+ const results = await lookup(host, { all: true });
62
+ return [...new Set(results.map((r) => r.address))];
63
+ } catch {
64
+ return [];
65
+ }
66
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Resolve the current Databricks workspace's URL and numeric id from the active
3
+ * execution context (AppKit, when initialized), a default `WorkspaceClient`, or
4
+ * the environment. Server-only.
5
+ */
6
+
7
+ import { functionModule, net } from "@dbx-tools/shared-core";
8
+ import { appkit } from "@dbx-tools/appkit";
9
+ import { type Config, WorkspaceClient } from "@databricks/sdk-experimental";
10
+
11
+ /** Databricks workspace ids are a 10-20 digit run embedded in the host. */
12
+ const WORKSPACE_ID_REGEX = /\d{10,20}/;
13
+
14
+ /**
15
+ * Lazily-constructed default `WorkspaceClient` (env / profile auth), memoized so
16
+ * construction happens at most once per process. Used only when there's no
17
+ * AppKit execution context to borrow a client from.
18
+ */
19
+ const getDefaultWorkspaceClient = functionModule.memoize(async () => new WorkspaceClient({}));
20
+
21
+ /**
22
+ * The active workspace `Config`: the AppKit execution-context client's config
23
+ * when AppKit is initialized, else the default client's. Returns `undefined`
24
+ * (never throws) when neither is available.
25
+ */
26
+ async function getWorkspaceConfig(): Promise<Config | undefined> {
27
+ let client = appkit.tryGetExecutionContext()?.client as WorkspaceClient | undefined;
28
+ if (!client) {
29
+ try {
30
+ client = await getDefaultWorkspaceClient();
31
+ } catch {
32
+ // no client available; fall back to the environment
33
+ }
34
+ }
35
+ return client?.config;
36
+ }
37
+
38
+ /**
39
+ * Resolve the current workspace host as a `net.UrlBuilder`: the workspace
40
+ * `Config` host first, then the `DATABRICKS_HOST` env var, else `undefined`.
41
+ */
42
+ export async function getWorkspaceUrl(): Promise<net.UrlBuilder | undefined> {
43
+ const config = await getWorkspaceConfig();
44
+ if (config) {
45
+ const configHost = net.urlBuilder(await config.getHost());
46
+ if (configHost) return configHost;
47
+ }
48
+ const databricksHost = net.urlBuilder(process.env.DATABRICKS_HOST);
49
+ if (databricksHost) return databricksHost;
50
+ return undefined;
51
+ }
52
+
53
+ /**
54
+ * Resolve the numeric workspace id: the workspace `Config`'s `workspaceId`
55
+ * first, else the 10-20 digit run of `workspaceHost` (defaulting to
56
+ * {@link getWorkspaceUrl}'s host). `undefined` when neither yields an id.
57
+ */
58
+ export async function getWorkspaceId(workspaceHost?: string): Promise<string | undefined> {
59
+ const workspaceId = (await getWorkspaceConfig())?.workspaceId;
60
+ if (workspaceId) return workspaceId;
61
+ workspaceHost = workspaceHost ?? (await getWorkspaceUrl())?.host;
62
+ if (workspaceHost) {
63
+ const match = workspaceHost.match(WORKSPACE_ID_REGEX)?.[0];
64
+ if (match) return match;
65
+ }
66
+ return undefined;
67
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,41 @@
1
+ // ~~ Generated by projen. To modify, edit .projenrc.js and run "pnpm exec projen".
2
+ {
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "lib",
6
+ "alwaysStrict": true,
7
+ "declaration": true,
8
+ "esModuleInterop": true,
9
+ "experimentalDecorators": true,
10
+ "inlineSourceMap": true,
11
+ "inlineSources": true,
12
+ "lib": [
13
+ "ES2022"
14
+ ],
15
+ "module": "ESNext",
16
+ "noEmitOnError": false,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "noImplicitAny": true,
19
+ "noImplicitReturns": true,
20
+ "noImplicitThis": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "resolveJsonModule": true,
24
+ "strict": true,
25
+ "strictNullChecks": true,
26
+ "strictPropertyInitialization": true,
27
+ "stripInternal": true,
28
+ "target": "ES2022",
29
+ "types": [
30
+ "node"
31
+ ],
32
+ "moduleResolution": "bundler",
33
+ "skipLibCheck": true
34
+ },
35
+ "include": [
36
+ "src/**/*.ts"
37
+ ],
38
+ "exclude": [
39
+ "node_modules"
40
+ ]
41
+ }