@uipath/maestro-builder-sdk 6.68.5 → 6.68.7

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.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Which trigger a case In-arg (`.input()`) binds to — the rule `check` reports,
3
+ * `serialize` applies and `decompile` follows, kept in one place so they cannot
4
+ * drift (builder-sdk#1368).
5
+ *
6
+ * An In-arg's value arrives from a trigger: the caseplan carries a formal slot
7
+ * in `variables.inputs[]`, a same-name `variables.inputOutputs[]` companion
8
+ * keyed to the trigger node, and a bridge on that node. That is the only shape
9
+ * the Case Designer writes, and `uip maestro case validate --strict` refuses an
10
+ * In-arg with no companion (STRICT_INPUTOUTPUTS_COMPANION). So an In-arg always
11
+ * binds:
12
+ *
13
+ * - `.input(shape, { from: t })` binds to `t`;
14
+ * - with no `{ from }`, it binds to the PRIMARY trigger: the case's only
15
+ * trigger (the one `.trigger()` added, or the implicit manual trigger when
16
+ * none was added, whatever its kind); with several, the one trigger that is
17
+ * not a timer — the one a caller starts the case through;
18
+ * - with no `{ from }` and no single such trigger (several non-timer
19
+ * triggers, or only timers) there is no right guess, and the case is
20
+ * refused with CASE_IN_ARG_UNBOUND.
21
+ *
22
+ * This is the rule `uip maestro case sdd convert` applies to an `In` row with a
23
+ * blank `sourceTriggers` cell (cli `sdd-convert/variables.ts`,
24
+ * `boundTriggerFor`), and the one its `in_arg_trigger_bind` grader checks.
25
+ */
26
+ import type { BuiltTrigger, CaseVarDecl } from './case-sdk.js';
27
+ /**
28
+ * The index, among the triggers the case emits, of the one an In-arg without
29
+ * `{ from }` binds to.
30
+ *
31
+ * @param isTimer - For each emitted trigger, in order, whether it is a timer.
32
+ * An empty list stands for the implicit manual trigger.
33
+ * @returns The trigger's index, or `undefined` when no single trigger qualifies.
34
+ */
35
+ export declare function implicitInArgTrigger(isTimer: readonly boolean[]): number | undefined;
36
+ /**
37
+ * {@link implicitInArgTrigger} over a built case's declared triggers.
38
+ *
39
+ * @param triggers - The case's `.trigger()` list.
40
+ * @returns The index of the trigger a `{ from }`-less In-arg binds to, or `undefined`.
41
+ */
42
+ export declare function implicitInArgBuiltTrigger(triggers: readonly BuiltTrigger[]): number | undefined;
43
+ /**
44
+ * Whether an In-arg declared without `{ from }` has no trigger to bind to.
45
+ *
46
+ * @param vd - The declared variable.
47
+ * @param triggers - The case's `.trigger()` list.
48
+ * @returns `true` when the In-arg must name its trigger with `{ from }`.
49
+ */
50
+ export declare function isUnboundInArg(vd: CaseVarDecl, triggers: readonly BuiltTrigger[]): boolean;
51
+ /**
52
+ * The CASE_IN_ARG_UNBOUND message.
53
+ *
54
+ * @param name - The In-arg's name.
55
+ * @param triggers - The case's `.trigger()` list.
56
+ * @returns The message both `check` and `serialize` report.
57
+ */
58
+ export declare function unboundInArgMessage(name: string, triggers: readonly BuiltTrigger[]): string;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The index, among the triggers the case emits, of the one an In-arg without
3
+ * `{ from }` binds to.
4
+ *
5
+ * @param isTimer - For each emitted trigger, in order, whether it is a timer.
6
+ * An empty list stands for the implicit manual trigger.
7
+ * @returns The trigger's index, or `undefined` when no single trigger qualifies.
8
+ */
9
+ export function implicitInArgTrigger(isTimer) {
10
+ if (isTimer.length <= 1)
11
+ return 0;
12
+ const callable = isTimer.flatMap((timer, i) => (timer ? [] : [i]));
13
+ return callable.length === 1 ? callable[0] : undefined;
14
+ }
15
+ /**
16
+ * {@link implicitInArgTrigger} over a built case's declared triggers.
17
+ *
18
+ * @param triggers - The case's `.trigger()` list.
19
+ * @returns The index of the trigger a `{ from }`-less In-arg binds to, or `undefined`.
20
+ */
21
+ export function implicitInArgBuiltTrigger(triggers) {
22
+ return implicitInArgTrigger(triggers.map((t) => t.kind === 'timer'));
23
+ }
24
+ /**
25
+ * Whether an In-arg declared without `{ from }` has no trigger to bind to.
26
+ *
27
+ * @param vd - The declared variable.
28
+ * @param triggers - The case's `.trigger()` list.
29
+ * @returns `true` when the In-arg must name its trigger with `{ from }`.
30
+ */
31
+ export function isUnboundInArg(vd, triggers) {
32
+ return vd.direction === 'in' && !vd.sourceTrigger && implicitInArgBuiltTrigger(triggers) === undefined;
33
+ }
34
+ /**
35
+ * The CASE_IN_ARG_UNBOUND message.
36
+ *
37
+ * @param name - The In-arg's name.
38
+ * @param triggers - The case's `.trigger()` list.
39
+ * @returns The message both `check` and `serialize` report.
40
+ */
41
+ export function unboundInArgMessage(name, triggers) {
42
+ const callable = triggers.filter((t) => t.kind !== 'timer').length;
43
+ const why = callable === 0
44
+ ? `all ${triggers.length} of the case's triggers are timers`
45
+ : `${callable} of the case's ${triggers.length} triggers are not timers`;
46
+ return `In-arg "${name}" has no { from }, and ${why}, so there is no primary trigger to bind it to. `
47
+ + `Pass .input({ ${name}: ... }, { from: <trigger> }) to name the trigger whose start supplies it, or declare it with .var() if no trigger supplies it.`;
48
+ }
@@ -19,7 +19,7 @@ import { bindingLabelRefusal } from '../core/binding-messages.js';
19
19
  import { resolveEventWhere } from '../core/lookups.js';
20
20
  import { resolveCaseSubscription } from './subscription.js';
21
21
  import { dedupeConditionNames } from './rule-names.js';
22
- import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, EXTERNAL_TASK_EVENT, externalDescriptorRefusal, externalEventRefusal, externalServiceType, isExternalExecutionMode, } from './external.js';
22
+ import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, externalDescriptorRefusal, externalExecution, externalServiceType, } from './external.js';
23
23
  import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS, caseVersionMismatch } from './format-profile.js';
24
24
  import { isResourceLabelRefusal, resolveResourceRef, resourceIdentity } from './resource-identity.js';
25
25
  import { ownEntry } from '../core/own-entry.js';
@@ -31,6 +31,7 @@ import { readEventFilterLeaf, eventFilterTreeLeaf } from '../core/event-filters.
31
31
  import { responseJsonSchema } from '../core/connector-response-schema.js';
32
32
  import { ruleShapeProblems } from './rule-options.js';
33
33
  import { CompileRefusal } from '../core/compile-refusal.js';
34
+ import { implicitInArgBuiltTrigger, unboundInArgMessage } from './in-arg-binding.js';
34
35
  import { taskShapeProblems } from './task-shape.js';
35
36
  import { connectorPlaceholderData, isConnectorPlaceholder } from './connector-placeholder.js';
36
37
  /** The ids a serialize pass hashed from element ids, by the document it returned. */
@@ -258,7 +259,7 @@ export function serialize(built, opts = {}) {
258
259
  // outputs bound (collected during stage emission above). A declared var wins
259
260
  // on a name clash. An own-name output (`id == var`) writes no root entry: like
260
261
  // the Designer, the output row is itself the variable `=vars.<id>` reads.
261
- const variables = emitVariables(built.vars, ids, triggerNodeByRef);
262
+ const variables = emitVariables(built.vars, ids, triggerNodeByRef, triggerNodes, built.triggers);
262
263
  const inputOutputs = variables.inputOutputs;
263
264
  const declared = new Set(inputOutputs.map((e) => e.id));
264
265
  for (const [name, companion] of resolver.varCompanions)
@@ -960,7 +961,8 @@ function reseed(value, ids) {
960
961
  value[key] = next;
961
962
  }
962
963
  }
963
- function emitVariables(vars, ids, triggerNodeByRef) {
964
+ function emitVariables(vars, ids, triggerNodeByRef, triggerNodes, triggers) {
965
+ const implicit = implicitInArgBuiltTrigger(triggers);
964
966
  const inputs = [];
965
967
  const outputs = [];
966
968
  const inputOutputs = [];
@@ -1002,26 +1004,38 @@ function emitVariables(vars, ids, triggerNodeByRef) {
1002
1004
  id: slotId,
1003
1005
  name: vd.name,
1004
1006
  type: vd.type,
1005
- ...encodedDefault(vd),
1007
+ // A trigger-bound In-arg always carries a default, `""` when none was
1008
+ // authored. It is written here, ahead of a structured var's schema, so
1009
+ // `{ type }` and `{ type, default: "" }` emit the same bytes and a
1010
+ // decompile (which reads the `""` back) recompiles byte-identically.
1011
+ ...(vd.direction === 'in' ? { default: encodedDefault(vd).default ?? '' } : encodedDefault(vd)),
1012
+ ...jsonSchemaFields(vd),
1006
1013
  };
1007
- Object.assign(entry, jsonSchemaFields(vd));
1008
- if (vd.direction === 'in' && vd.sourceTrigger) {
1009
- // Trigger-bound In-arg: formal slot + companion + a bridge on the trigger
1010
- // node that copies the slot into the companion at fire (see the skill's
1011
- // variables/io-binding + global-vars § In argument). Its companion is keyed
1012
- // to the trigger node, so it does not also take a root companion.
1013
- const node = triggerNodeByRef.get(vd.sourceTrigger);
1014
- if (!node)
1015
- throw new CompileRefusal('CASE_IN_ARG_TRIGGER_NOT_ADDED', `In-arg "${vd.name}" is bound via { from } to a trigger that was not added with .trigger(...).`);
1016
- entry.default = vd.default === undefined ? '' : encodedDefault(vd).default;
1014
+ if (vd.direction === 'in') {
1015
+ // An In-arg is always trigger-bound: formal slot + companion + a bridge on
1016
+ // the trigger node that copies the slot into the companion at fire (see the
1017
+ // skill's variables/io-binding + global-vars § In argument). Its companion
1018
+ // is keyed to the trigger node, so it does not also take a root companion.
1019
+ // The Designer writes no other In-arg shape, and `uip maestro case validate
1020
+ // --strict` refuses an In-arg without its companion
1021
+ // (STRICT_INPUTOUTPUTS_COMPANION). Without `{ from }` the arg binds to the
1022
+ // primary trigger (in-arg-binding.ts); when there is none it is refused
1023
+ // (`check` reports the same code first).
1024
+ let node;
1025
+ if (vd.sourceTrigger) {
1026
+ node = triggerNodeByRef.get(vd.sourceTrigger);
1027
+ if (!node)
1028
+ throw new CompileRefusal('CASE_IN_ARG_TRIGGER_NOT_ADDED', `In-arg "${vd.name}" is bound via { from } to a trigger that was not added with .trigger(...).`);
1029
+ }
1030
+ else if (implicit !== undefined)
1031
+ node = triggerNodes[implicit];
1032
+ else
1033
+ throw new CompileRefusal('CASE_IN_ARG_UNBOUND', unboundInArgMessage(vd.name, triggers));
1017
1034
  entry.elementId = node.id;
1018
1035
  inputs.push(entry);
1019
1036
  inputOutputs.push({ id: vd.name, name: vd.name, type: vd.type, elementId: node.id, ...jsonSchemaFields(vd) });
1020
1037
  appendTriggerBridge(node, { name: vd.name, type: vd.type, source: `=vars.${slotId}`, var: vd.name });
1021
1038
  }
1022
- else if (vd.direction === 'in') {
1023
- inputs.push(entry); // bare declaration (unbound In-arg; readable only via a trigger)
1024
- }
1025
1039
  else if (vd.direction === 'out') {
1026
1040
  outputs.push(entry); // formal declaration
1027
1041
  inputOutputs.push(readableCompanion(vd)); // …made readable as `vars.<name>`
@@ -1814,24 +1828,26 @@ function externalTaskData(kind, spec, resolver, elementId, where) {
1814
1828
  const refused = externalDescriptorRefusal(kind, rc);
1815
1829
  if (refused)
1816
1830
  throw new CompileRefusal(EXTERNAL_TASK_DESCRIPTOR_CONTRACT, refused);
1817
- if (!isExternalExecutionMode(spec.mode))
1818
- throw new CompileRefusal('EXTERNAL_TASK_MODE', `${kind} task mode must be the connector object's executionType: "sync", "async" or "hybrid".`);
1819
- const eventRefused = externalEventRefusal(where, spec.mode, spec.event);
1820
- if (eventRefused)
1821
- throw new CompileRefusal(EXTERNAL_TASK_EVENT, eventRefused);
1831
+ // The authored mode and event, else the library's (#1369): the same call check makes.
1832
+ const execution = externalExecution(where, spec, rc.entry?.operation ?? {});
1833
+ if (!execution)
1834
+ throw new Error(`internal SDK error: ${where} was judged without its library operation.`);
1835
+ if (execution.code !== undefined)
1836
+ throw new CompileRefusal(execution.code, execution.message);
1837
+ const { mode, event } = execution;
1822
1838
  if (!spec.connector.connection || !spec.connector.folder)
1823
1839
  throw new CompileRefusal(spec.connector.connection ? 'EXTERNAL_TASK_FOLDER' : 'EXTERNAL_TASK_CONNECTION', `${kind} task requires non-empty connection and folder binding names.`);
1824
- const data = connectorTaskData(spec.connector, resolver, elementId, where, spec.mode, spec.event);
1840
+ const data = connectorTaskData(spec.connector, resolver, elementId, where, mode, event);
1825
1841
  const context = data.context;
1826
1842
  const metadataAt = context.findIndex((row) => row.name === 'metadata');
1827
1843
  // The Designer's order: the event pair (when the object answers through one),
1828
1844
  // then the execution type, just before `metadata`.
1829
- context.splice(metadataAt, 0, ...(spec.event ? [
1830
- io('operation', 'string', { value: spec.event.operation }),
1831
- io('eventMode', 'string', { value: spec.event.mode }),
1832
- ] : []), io('executionType', 'string', { value: spec.mode }));
1845
+ context.splice(metadataAt, 0, ...(event ? [
1846
+ io('operation', 'string', { value: event.operation }),
1847
+ io('eventMode', 'string', { value: event.mode }),
1848
+ ] : []), io('executionType', 'string', { value: mode }));
1833
1849
  context.push(io('activityConfigurationVersion', 'string', { value: 'v2' }));
1834
- return { ...data, serviceType: externalServiceType(kind, spec.mode) };
1850
+ return { ...data, serviceType: externalServiceType(kind, mode) };
1835
1851
  }
1836
1852
  /** A context/output name/value entry (the caseplan `InputOutput` shape). */
1837
1853
  function io(name, type, extra) {
@@ -151,6 +151,7 @@ export declare const REFUSALS: {
151
151
  readonly CASE_APP_DETAILS_TYPE: "lint";
152
152
  readonly CASE_EXPORT_NOT_BUILT: "lowering";
153
153
  readonly CASE_IN_ARG_TRIGGER_NOT_ADDED: "lowering";
154
+ readonly CASE_IN_ARG_UNBOUND: "lowering";
154
155
  readonly CASE_LAYOUT_NODE_MATCH: "lowering";
155
156
  readonly CASE_LAYOUT_UNKNOWN_FIELD: "lowering";
156
157
  readonly CASE_NO_DEFAULT_EXPORT: "lowering";
@@ -277,6 +278,7 @@ export declare const REFUSALS: {
277
278
  readonly EXTERNAL_TASK_EVENT: "lowering";
278
279
  readonly EXTERNAL_TASK_FOLDER: "lowering";
279
280
  readonly EXTERNAL_TASK_MODE: "lowering";
281
+ readonly EXTERNAL_TASK_MODE_MISMATCH: "lowering";
280
282
  readonly EXTRACT_SCHEMA_SHAPE: "lint";
281
283
  readonly FAKE_JOIN: "lint";
282
284
  readonly FILE_NOT_FOUND: "lowering";
@@ -199,6 +199,7 @@ export const REFUSALS = {
199
199
  CASE_APP_DETAILS_TYPE: 'lint',
200
200
  CASE_EXPORT_NOT_BUILT: 'lowering',
201
201
  CASE_IN_ARG_TRIGGER_NOT_ADDED: 'lowering',
202
+ CASE_IN_ARG_UNBOUND: 'lowering',
202
203
  CASE_LAYOUT_NODE_MATCH: 'lowering',
203
204
  CASE_LAYOUT_UNKNOWN_FIELD: 'lowering',
204
205
  CASE_NO_DEFAULT_EXPORT: 'lowering',
@@ -328,6 +329,7 @@ export const REFUSALS = {
328
329
  EXTERNAL_TASK_EVENT: 'lowering',
329
330
  EXTERNAL_TASK_FOLDER: 'lowering',
330
331
  EXTERNAL_TASK_MODE: 'lowering',
332
+ EXTERNAL_TASK_MODE_MISMATCH: 'lowering',
331
333
  EXTRACT_SCHEMA_SHAPE: 'lint',
332
334
  FAKE_JOIN: 'lint',
333
335
  FILE_NOT_FOUND: 'lowering',
@@ -1,5 +1,6 @@
1
1
  import type { LibraryEntry } from "../model/library.js";
2
2
  import { type BuildContext } from "./context.js";
3
+ import { type Dict } from "./py.js";
3
4
  /**
4
5
  * Registry `httpMethod` (e.g. `GETBYID`) → a guess at the `is resources`
5
6
  * operation name, for an entry whose canonical `operation.name` is empty.
@@ -18,6 +19,25 @@ export declare function discoverOperationName(ctx: BuildContext, entry: LibraryE
18
19
  * payload; `objectDisplayName` and `display.operationLabel` fill gaps only.
19
20
  */
20
21
  export declare function applyDescribe(entry: LibraryEntry, data: unknown): LibraryEntry;
22
+ /**
23
+ * Record an external agent's or automation's execution facts on its operation,
24
+ * from the describe payload (#1369, #1387).
25
+ *
26
+ * Integration Service keeps them on the object's metadata, not in the registry
27
+ * node: `executionType` (`sync`, `async` or `hybrid`) and, for an `async` or
28
+ * `hybrid` object, the event it answers with. `uip is resources describe`
29
+ * reports them as `ExecutionType`, `EventType` and `EventMode` (uip#5702,
30
+ * read with the same `externalExecutionOf` as `case spec`). They become
31
+ * `operation.executionType` and `operation.executionEvent` (`{ operation, mode }`),
32
+ * which Case and BPMN derive the task's `mode` and `event` from. The event is
33
+ * kept apart from `eventOperation` / `eventMode`, which mark a connector EVENT
34
+ * entry (a trigger) and which `identifyEvent` matches on.
35
+ *
36
+ * Only an `AgentExecution` / `ProcessExecution` operation gets them, and only
37
+ * what the payload states (an execution type outside the three is not stored): a describe from a CLI that does not report them
38
+ * leaves the entry as it was, so an older build stays byte-identical.
39
+ */
40
+ export declare function applyExternalExecution(operation: Dict, data: unknown): void;
21
41
  /**
22
42
  * `enrich_entry`: augment a canonical entry with `is resources describe` data.
23
43
  * On any failure the entry is returned as it was, so the pipeline degrades
@@ -78,6 +78,7 @@ export function applyDescribe(entry, data) {
78
78
  const opCurated = pick(opData, "Curated", "curated");
79
79
  if (pyTruthy(opCurated) && !pyTruthy(get(display, "operationLabel")))
80
80
  display.operationLabel = opCurated;
81
+ applyExternalExecution(operation, data);
81
82
  // Parameters (path / query) — raw shape, camelCase keys.
82
83
  const params = pyOr(pick(data, "Parameters", "parameters"), []);
83
84
  if (pyTruthy(params))
@@ -92,6 +93,45 @@ export function applyDescribe(entry, data) {
92
93
  e.outputSchema = { fields: normalizeFieldKeys(responseFields) };
93
94
  return entry;
94
95
  }
96
+ /** The operation subtypes Case's and BPMN's external agent / workflow tasks run. */
97
+ const EXTERNAL_SUBTYPES = new Set(["AgentExecution", "ProcessExecution"]);
98
+ /** The execution types Integration Service reports, as Case and BPMN read them. */
99
+ const EXECUTION_TYPES = new Set(["sync", "async", "hybrid"]);
100
+ /**
101
+ * Record an external agent's or automation's execution facts on its operation,
102
+ * from the describe payload (#1369, #1387).
103
+ *
104
+ * Integration Service keeps them on the object's metadata, not in the registry
105
+ * node: `executionType` (`sync`, `async` or `hybrid`) and, for an `async` or
106
+ * `hybrid` object, the event it answers with. `uip is resources describe`
107
+ * reports them as `ExecutionType`, `EventType` and `EventMode` (uip#5702,
108
+ * read with the same `externalExecutionOf` as `case spec`). They become
109
+ * `operation.executionType` and `operation.executionEvent` (`{ operation, mode }`),
110
+ * which Case and BPMN derive the task's `mode` and `event` from. The event is
111
+ * kept apart from `eventOperation` / `eventMode`, which mark a connector EVENT
112
+ * entry (a trigger) and which `identifyEvent` matches on.
113
+ *
114
+ * Only an `AgentExecution` / `ProcessExecution` operation gets them, and only
115
+ * what the payload states (an execution type outside the three is not stored): a describe from a CLI that does not report them
116
+ * leaves the entry as it was, so an older build stays byte-identical.
117
+ */
118
+ export function applyExternalExecution(operation, data) {
119
+ if (!EXTERNAL_SUBTYPES.has(String(get(operation, "subType") ?? "")))
120
+ return;
121
+ // Stored only as one of the three values Case and BPMN read, lowercased: a
122
+ // `Hybrid` kept verbatim would be ignored downstream and read as "no record".
123
+ const reported = pick(data, "ExecutionType", "executionType");
124
+ const executionType = typeof reported === "string" ? reported.trim().toLowerCase() : "";
125
+ if (!EXECUTION_TYPES.has(executionType))
126
+ return;
127
+ operation.executionType = executionType;
128
+ if (executionType === "sync")
129
+ return;
130
+ const eventType = pick(data, "EventType", "eventType");
131
+ const eventMode = pick(data, "EventMode", "eventMode");
132
+ if (typeof eventType === "string" && eventType && typeof eventMode === "string" && eventMode)
133
+ operation.executionEvent = { operation: eventType, mode: eventMode };
134
+ }
95
135
  /**
96
136
  * `enrich_entry`: augment a canonical entry with `is resources describe` data.
97
137
  * On any failure the entry is returned as it was, so the pipeline degrades
@@ -85,6 +85,16 @@ export interface EntryOperation {
85
85
  objectFromDiscovery?: boolean;
86
86
  eventOperation?: string;
87
87
  eventMode?: string;
88
+ /**
89
+ * An external agent's or automation's `executionType` (`sync`, `async`,
90
+ * `hybrid`), on an `AgentExecution` / `ProcessExecution` operation only.
91
+ */
92
+ executionType?: string;
93
+ /** The event an `async` / `hybrid` external object answers with. */
94
+ executionEvent?: {
95
+ operation: string;
96
+ mode: string;
97
+ };
88
98
  schemaAction?: SchemaAction | null;
89
99
  dispatchStyle?: string;
90
100
  paramGrammars?: unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.68.5",
3
+ "version": "6.68.7",
4
4
  "description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.uipath.com/maestro",
@@ -100,5 +100,5 @@
100
100
  "@types/node": "^22.7.0",
101
101
  "esbuild": "^0.28.1"
102
102
  },
103
- "gitref": "3f8f0766113501aaf0d1d7c36835ef3c8eb6c2aa"
103
+ "gitref": "436dbc08c3130fcde3197adec572df61116f4ede"
104
104
  }