@enrichlayer/el-linear 1.28.0 → 1.29.0
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/commands/batch.js +2 -2
- package/dist/commands/issues.js +9 -6
- package/dist/config/registry-resolve.d.ts +23 -0
- package/dist/config/registry-resolve.js +59 -0
- package/dist/config/resolver.d.ts +11 -0
- package/dist/config/resolver.js +20 -0
- package/dist/utils/graphql-issues-service.js +18 -0
- package/package.json +1 -1
package/dist/commands/batch.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveAssignee, resolveLabels,
|
|
1
|
+
import { resolveAssignee, resolveLabels, resolveMemberWithRegistry, resolveTeam, } from "../config/resolver.js";
|
|
2
2
|
import { createIssuesService } from "../utils/issues-service-bootstrap.js";
|
|
3
3
|
import { logger } from "../utils/logger.js";
|
|
4
4
|
import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
|
|
@@ -45,7 +45,7 @@ async function resolveTargetIssues(options, rootOpts) {
|
|
|
45
45
|
? await resolveAssignee(filters.assignee, rootOpts)
|
|
46
46
|
: undefined,
|
|
47
47
|
delegateId: filters.delegate
|
|
48
|
-
?
|
|
48
|
+
? await resolveMemberWithRegistry(filters.delegate)
|
|
49
49
|
: undefined,
|
|
50
50
|
project: filters.project
|
|
51
51
|
? { kind: "id", id: filters.project }
|
package/dist/commands/issues.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { loadConfig } from "../config/config.js";
|
|
3
3
|
import { enrichProjectResolverError, enrichValidationErrors, } from "../config/error-enrichment.js";
|
|
4
4
|
import { enforceValidation, validateIssueCreation, } from "../config/issue-validation.js";
|
|
5
|
-
import { resolveAssignee, resolveLabels,
|
|
5
|
+
import { resolveAssignee, resolveLabels, resolveMemberWithRegistry, resolveTeam, } from "../config/resolver.js";
|
|
6
6
|
import { resolveDefaultStatus } from "../config/status-defaults.js";
|
|
7
7
|
import { enforceTerms } from "../config/term-enforcer.js";
|
|
8
8
|
import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, } from "../queries/issues.js";
|
|
@@ -210,7 +210,7 @@ async function handleListIssues(options, command) {
|
|
|
210
210
|
? await resolveAssignee(options.assignee, rootOpts)
|
|
211
211
|
: undefined,
|
|
212
212
|
delegateId: options.delegate
|
|
213
|
-
?
|
|
213
|
+
? await resolveMemberWithRegistry(options.delegate)
|
|
214
214
|
: undefined,
|
|
215
215
|
project: resolveProjectFlag(options.project),
|
|
216
216
|
labelNames: options.labels ? splitList(options.labels) : undefined,
|
|
@@ -256,7 +256,7 @@ async function handleSearchIssues(query, options, command) {
|
|
|
256
256
|
? await resolveAssignee(options.assignee, rootOpts)
|
|
257
257
|
: undefined,
|
|
258
258
|
delegateId: options.delegate
|
|
259
|
-
?
|
|
259
|
+
? await resolveMemberWithRegistry(options.delegate)
|
|
260
260
|
: undefined,
|
|
261
261
|
project: resolveProjectFlag(options.project),
|
|
262
262
|
status: explicitStatus,
|
|
@@ -384,7 +384,7 @@ async function resolveCreateInputs(title, options, rootOpts) {
|
|
|
384
384
|
? await resolveAssignee(effectiveAssignee, rootOpts)
|
|
385
385
|
: undefined;
|
|
386
386
|
const delegateId = options.delegate
|
|
387
|
-
?
|
|
387
|
+
? await resolveMemberWithRegistry(options.delegate)
|
|
388
388
|
: undefined;
|
|
389
389
|
let labelIds = [];
|
|
390
390
|
if (options.labels) {
|
|
@@ -407,7 +407,10 @@ async function resolveCreateInputs(title, options, rootOpts) {
|
|
|
407
407
|
});
|
|
408
408
|
let subscriberIds;
|
|
409
409
|
if (options.subscriber) {
|
|
410
|
-
|
|
410
|
+
// --subscriber resolves through the opt-in registry too (DEV-4880),
|
|
411
|
+
// matching --assignee / --delegate. Each entry falls back to config on a
|
|
412
|
+
// miss; resolveMemberWithRegistry is fail-open and dormant when unconfigured.
|
|
413
|
+
subscriberIds = await Promise.all(splitList(options.subscriber).map((s) => resolveMemberWithRegistry(s)));
|
|
411
414
|
}
|
|
412
415
|
const priority = effectivePriorityInput
|
|
413
416
|
? validatePriority(effectivePriorityInput)
|
|
@@ -843,7 +846,7 @@ async function handleUpdateIssue(issueId, options, command) {
|
|
|
843
846
|
const delegateId = options.clearDelegate
|
|
844
847
|
? null
|
|
845
848
|
: options.delegate
|
|
846
|
-
?
|
|
849
|
+
? await resolveMemberWithRegistry(options.delegate)
|
|
847
850
|
: undefined;
|
|
848
851
|
const updateArgs = buildUpdateArgs(issueId, options, assigneeId, delegateId);
|
|
849
852
|
const result = await withProjectResolverEnrichment(() => issuesService.updateIssue(updateArgs, options.labelBy || "adding"), {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional, opt-in resolution against the company-wide identity registry
|
|
3
|
+
* (DEV-4827 / DEV-4871). EL-only: this activates **solely** when
|
|
4
|
+
* `EL_IDENTITY_URL` is set. el-linear is MIT/open-source — a non-EL install
|
|
5
|
+
* leaves this dormant (no network, no config, nothing written), and the package
|
|
6
|
+
* never takes a dependency on EL-internal infrastructure. It mirrors the
|
|
7
|
+
* env-gated CF-Access pattern of the tools `@enrichlayer/el-identity` client but
|
|
8
|
+
* is kept self-contained so the OSS package carries no private dependency.
|
|
9
|
+
*
|
|
10
|
+
* The registry is an *enhancement* for EL users: callers try it first, then fall
|
|
11
|
+
* back to the bundled config (`resolveMember`). It must never throw or break a
|
|
12
|
+
* command because the registry is unreachable.
|
|
13
|
+
*/
|
|
14
|
+
/** True only when the registry is explicitly configured (opt-in). */
|
|
15
|
+
export declare function isRegistryConfigured(env?: NodeJS.ProcessEnv): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Resolve any identifier (alias, handle, email, name, Linear UUID) to a Linear
|
|
18
|
+
* UUID via the registry's `GET /api/people/resolve`. Returns `null` when the
|
|
19
|
+
* registry is not configured, when nothing matches, or on **any** failure
|
|
20
|
+
* (unreachable / timeout / CF-Access challenge / malformed response) — the
|
|
21
|
+
* caller falls back to the config-based resolver. Never throws.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveViaRegistry(identifier: string, env?: NodeJS.ProcessEnv, fetchImpl?: typeof fetch): Promise<string | null>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional, opt-in resolution against the company-wide identity registry
|
|
3
|
+
* (DEV-4827 / DEV-4871). EL-only: this activates **solely** when
|
|
4
|
+
* `EL_IDENTITY_URL` is set. el-linear is MIT/open-source — a non-EL install
|
|
5
|
+
* leaves this dormant (no network, no config, nothing written), and the package
|
|
6
|
+
* never takes a dependency on EL-internal infrastructure. It mirrors the
|
|
7
|
+
* env-gated CF-Access pattern of the tools `@enrichlayer/el-identity` client but
|
|
8
|
+
* is kept self-contained so the OSS package carries no private dependency.
|
|
9
|
+
*
|
|
10
|
+
* The registry is an *enhancement* for EL users: callers try it first, then fall
|
|
11
|
+
* back to the bundled config (`resolveMember`). It must never throw or break a
|
|
12
|
+
* command because the registry is unreachable.
|
|
13
|
+
*/
|
|
14
|
+
const URL_ENV = "EL_IDENTITY_URL";
|
|
15
|
+
const CF_ID_ENV = "EL_IDENTITY_CF_ACCESS_CLIENT_ID";
|
|
16
|
+
const CF_SECRET_ENV = "EL_IDENTITY_CF_ACCESS_CLIENT_SECRET";
|
|
17
|
+
const TIMEOUT_MS = 8000;
|
|
18
|
+
/** True only when the registry is explicitly configured (opt-in). */
|
|
19
|
+
export function isRegistryConfigured(env = process.env) {
|
|
20
|
+
return Boolean(env[URL_ENV]?.trim());
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve any identifier (alias, handle, email, name, Linear UUID) to a Linear
|
|
24
|
+
* UUID via the registry's `GET /api/people/resolve`. Returns `null` when the
|
|
25
|
+
* registry is not configured, when nothing matches, or on **any** failure
|
|
26
|
+
* (unreachable / timeout / CF-Access challenge / malformed response) — the
|
|
27
|
+
* caller falls back to the config-based resolver. Never throws.
|
|
28
|
+
*/
|
|
29
|
+
export async function resolveViaRegistry(identifier, env = process.env, fetchImpl = fetch) {
|
|
30
|
+
const base = env[URL_ENV]?.trim();
|
|
31
|
+
if (!base) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const headers = { Accept: "application/json" };
|
|
35
|
+
const cfId = env[CF_ID_ENV];
|
|
36
|
+
const cfSecret = env[CF_SECRET_ENV];
|
|
37
|
+
if (cfId && cfSecret) {
|
|
38
|
+
headers["CF-Access-Client-Id"] = cfId;
|
|
39
|
+
headers["CF-Access-Client-Secret"] = cfSecret;
|
|
40
|
+
}
|
|
41
|
+
const url = `${base.replace(/\/+$/, "")}/api/people/resolve?identifier=${encodeURIComponent(identifier)}`;
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetchImpl(url, {
|
|
44
|
+
headers,
|
|
45
|
+
// A 3xx is a Cloudflare Access SSO bounce, not a result — `redirect:
|
|
46
|
+
// "manual"` surfaces it as a non-ok status we treat as a miss.
|
|
47
|
+
redirect: "manual",
|
|
48
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const record = (await res.json());
|
|
54
|
+
return record?.linearId ?? null;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -13,6 +13,17 @@ export declare function resolveMember(input: string): string;
|
|
|
13
13
|
* Falls back to resolveMember for all other inputs.
|
|
14
14
|
*/
|
|
15
15
|
export declare function resolveAssignee(input: string, rootOpts: Record<string, unknown>): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a member identifier (alias / handle / name) to a UUID, consulting the
|
|
18
|
+
* opt-in identity registry first when configured, then the bundled config
|
|
19
|
+
* (`resolveMember`). The shared async resolution path behind `--assignee` and
|
|
20
|
+
* `--delegate` (DEV-4871 / DEV-4872).
|
|
21
|
+
*
|
|
22
|
+
* EL-only and env-gated on `EL_IDENTITY_URL`; fail-open — when unconfigured, on
|
|
23
|
+
* a miss, or on any failure it falls back to config, so a non-EL install and an
|
|
24
|
+
* unreachable registry both behave exactly as before. Never throws.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveMemberWithRegistry(input: string): Promise<string>;
|
|
16
27
|
/**
|
|
17
28
|
* Resolve a user's display name from their UUID via config fullNames map.
|
|
18
29
|
* Returns the full name if found, otherwise returns the original name.
|
package/dist/config/resolver.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createGraphQLService } from "../utils/graphql-service.js";
|
|
|
2
2
|
import { outputWarning } from "../utils/output.js";
|
|
3
3
|
import { isUuid, isUuidPrefix } from "../utils/uuid.js";
|
|
4
4
|
import { loadConfig } from "./config.js";
|
|
5
|
+
import { isRegistryConfigured, resolveViaRegistry, } from "./registry-resolve.js";
|
|
5
6
|
/**
|
|
6
7
|
* Resolve a team key/name/alias to its UUID.
|
|
7
8
|
* Case-insensitive: "fe" → FE UUID, "frontend" → FE UUID via alias.
|
|
@@ -98,6 +99,25 @@ export async function resolveAssignee(input, rootOpts) {
|
|
|
98
99
|
}
|
|
99
100
|
return result.viewer.id;
|
|
100
101
|
}
|
|
102
|
+
return resolveMemberWithRegistry(input);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a member identifier (alias / handle / name) to a UUID, consulting the
|
|
106
|
+
* opt-in identity registry first when configured, then the bundled config
|
|
107
|
+
* (`resolveMember`). The shared async resolution path behind `--assignee` and
|
|
108
|
+
* `--delegate` (DEV-4871 / DEV-4872).
|
|
109
|
+
*
|
|
110
|
+
* EL-only and env-gated on `EL_IDENTITY_URL`; fail-open — when unconfigured, on
|
|
111
|
+
* a miss, or on any failure it falls back to config, so a non-EL install and an
|
|
112
|
+
* unreachable registry both behave exactly as before. Never throws.
|
|
113
|
+
*/
|
|
114
|
+
export async function resolveMemberWithRegistry(input) {
|
|
115
|
+
if (isRegistryConfigured()) {
|
|
116
|
+
const viaRegistry = await resolveViaRegistry(input);
|
|
117
|
+
if (viaRegistry) {
|
|
118
|
+
return viaRegistry;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
101
121
|
return resolveMember(input);
|
|
102
122
|
}
|
|
103
123
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRegistryConfigured, resolveViaRegistry, } from "../config/registry-resolve.js";
|
|
1
2
|
import { resolveUserDisplayName } from "../config/resolver.js";
|
|
2
3
|
import { ARCHIVE_ISSUE_MUTATION, BATCH_GET_ISSUES_QUERY, BATCH_RESOLVE_FOR_CREATE_QUERY, BATCH_RESOLVE_FOR_SEARCH_QUERY, BATCH_RESOLVE_FOR_UPDATE_QUERY, buildResolveLabelsByNameQuery, CREATE_ISSUE_MUTATION, DELETE_ISSUE_MUTATION, FILTERED_SEARCH_ISSUES_QUERY, GET_ISSUE_BY_ID_QUERY, GET_ISSUE_BY_IDENTIFIER_QUERY, GET_ISSUE_CLAIM_CONTEXT_QUERY, GET_ISSUE_START_CONTEXT_QUERY, GET_ISSUE_TEAM_QUERY, GET_ISSUES_QUERY, SEARCH_ISSUES_QUERY, TEAM_STARTED_STATUSES_QUERY, UPDATE_ISSUE_MUTATION, } from "../queries/issues.js";
|
|
3
4
|
import { CREATE_LABEL_MUTATION } from "../queries/labels.js";
|
|
@@ -1048,6 +1049,15 @@ export class GraphQLIssuesService {
|
|
|
1048
1049
|
if (!assigneeId || isUuid(assigneeId)) {
|
|
1049
1050
|
return assigneeId;
|
|
1050
1051
|
}
|
|
1052
|
+
// Opt-in registry resolution (DEV-4872): try the identity registry first,
|
|
1053
|
+
// falling back to the Linear-API user lookup below on a miss. EL-only,
|
|
1054
|
+
// env-gated, fail-open — a non-EL install never reaches the network here.
|
|
1055
|
+
if (isRegistryConfigured()) {
|
|
1056
|
+
const viaRegistry = await resolveViaRegistry(assigneeId);
|
|
1057
|
+
if (viaRegistry) {
|
|
1058
|
+
return viaRegistry;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1051
1061
|
// A plain name (no `@`) resolves via the user lookup — same as
|
|
1052
1062
|
// `resolveDelegateId`. Without this, a non-config full name like
|
|
1053
1063
|
// "Yury Tsukerman" fell through unchanged and was sent to the
|
|
@@ -1068,6 +1078,14 @@ export class GraphQLIssuesService {
|
|
|
1068
1078
|
if (!delegateId || isUuid(delegateId)) {
|
|
1069
1079
|
return delegateId;
|
|
1070
1080
|
}
|
|
1081
|
+
// Opt-in registry resolution (DEV-4872): registry-first, Linear-API
|
|
1082
|
+
// fallback. EL-only, env-gated, fail-open.
|
|
1083
|
+
if (isRegistryConfigured()) {
|
|
1084
|
+
const viaRegistry = await resolveViaRegistry(delegateId);
|
|
1085
|
+
if (viaRegistry) {
|
|
1086
|
+
return viaRegistry;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1071
1089
|
if (!delegateId.includes("@")) {
|
|
1072
1090
|
return this.linearService.resolveUserId(delegateId);
|
|
1073
1091
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enrichlayer/el-linear",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
4
|
"description": "A pragmatic CLI for Linear.app — deterministic team/label/member resolution, structured issue validation, configurable term enforcement, and a GraphQL escape hatch.",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"types": "dist/main.d.ts",
|