n8n-nodes-lifespace 0.1.23 → 0.1.25

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 CHANGED
@@ -44,25 +44,26 @@ Restart n8n afterwards. In queue mode, install the package anywhere that may exe
44
44
 
45
45
  ## Quick start
46
46
 
47
- Create a **LifeSpace API** credential for Runtime Discovery and LifeSpace API calls.
47
+ LifeSpace has two explicit execution modes in n8n.
48
48
 
49
- It contains:
49
+ For **Service Principal(服务主体)automation**, create a **LifeSpace API** credential with the Core **API Base URL** and an opaque `lsp_pat_*` **Service API Token**. This remains the default for scheduled/system workflows where `Principal = Actor = Service`.
50
50
 
51
- - **API Base URL**, for example:
51
+ For a user-initiated **Delegated Agent(委托智能体)** Tool or Batch, create a separate **LifeSpace Delegated Agent API** credential containing:
52
52
 
53
- ```text
54
- https://api.example.com/api/v1
55
- ```
53
+ - **Core API Base URL**;
54
+ - **Identity API Base URL**;
55
+ - the server-only Application credential `lsa_*`;
56
+ - the registered LifeSpace `agt_*` Agent ID.
56
57
 
57
- - **Service API Token**, an opaque LifeSpace `lsp_pat_*` token.
58
+ Delegated mode does **not** require or fall back to a Service PAT. The adapter uses the Application credential only to mint a short-lived `User Principal → Agent Actor` execution token from LifeSpace Identity, then calls Core with that token plus explicit Delegation selectors.
58
59
 
59
- The API Base URL is the LifeSpace Core API root. Do not include a Space ID or Record Type path.
60
+ `Principal User ID`, `Delegation ID`, `Read Delegation ID` and per-item Batch Delegation IDs are execution/control context, never LLM Tool arguments. `Read Delegation ID` is intentionally separate: it covers delegated design-time Discovery and only the runtime calls that actually need relation-name lookup or optimistic-concurrency pre-reads, so those helper reads cannot accidentally consume a single-use Delegation reserved for the actual query/mutation/action. A pure runtime mutation with no helper read does not exercise the Read Delegation.
60
61
 
61
- The token is shown only when it is created or rotated. Store it in n8n immediately; do not put it in workflow fields, URLs, source files, or workflow exports.
62
+ Credentials are secrets. Store them only in n8n credentials; do not put `lsp_pat_*`, `lsa_*`, Agent JWTs or other secrets in workflow fields, URLs, source files or workflow exports.
62
63
 
63
64
  A **LifeSpace Trigger** additionally uses a **LifeSpace Webhook Signing** credential containing the endpoint-scoped HMAC signing secret. This is deliberately separate from the API credential: the Service API Token authenticates outbound n8n → LifeSpace calls, while the signing secret verifies inbound LifeSpace → n8n deliveries and rotates with its Webhook Endpoint. The Trigger still reuses the LifeSpace API credential for Space/Record Type discovery, so API context is not duplicated.
64
65
 
65
- Runtime Discovery determines which Spaces, Record Types, fields, queries, Actions and relation lookup capabilities the current API credential can use. The adapter starts from the compact current-principal inventory, then loads static semantic detail only for the selected Space/Record Type. Relation target lookup remains lazy and field-scoped. Execution authorization is still enforced by LifeSpace from the current principal, credential scope, Application × Model Access and current Space/Data Grant authority.
66
+ Runtime Discovery determines which Spaces, Record Types, fields, queries, Actions and relation lookup capabilities the current execution context can use. Service mode uses the Service PAT. Delegated Agent mode uses the short-lived Agent token; progressive semantic detail uses the configured Read Delegation selector. Relation target lookup remains lazy and field-scoped. Execution authorization is always enforced by LifeSpace from verified Application / Principal / Actor, credential scope, Application × Model Access, current Space/Data Grant authority and the explicitly selected Delegation when Principal and Actor differ.
66
67
 
67
68
  ## Generated Record UX
68
69
 
@@ -89,7 +90,7 @@ Current Calendar models expose the canonical `capabilityBindings.calendar.rangeF
89
90
 
90
91
  ## LifeSpace contract compatibility
91
92
 
92
- This package follows the current LifeSpace Core Kernel `0.36.0` contract family. It consumes Runtime/Discovery semantics only; Eventing configuration and webhook delivery semantics are owned independently by Integration/Eventing `0.1.0`.
93
+ This package follows the current LifeSpace Core Kernel `0.40.0` contract family. Integration/Eventing is independently versioned at `0.3.0`. Authority v2 requires an explicit Delegation selector whenever `Principal != Actor`, and Model Contracts expose the first-class atomic Generic Runtime Batch surface.
93
94
 
94
95
  The UX depends on these Kernel capabilities:
95
96
 
@@ -112,6 +113,7 @@ The UX depends on these Kernel capabilities:
112
113
  - `0.34.0`: explicit `eq/lt/lte/gt/gte` comparison transports, first-class `createdAt` / `updatedAt` envelope comparisons and Core-owned datetime local-date-window conversion become discoverable.
113
114
  - `0.35.0`: grouped `query.capabilityQueries` introduced capability-owned compatibility queries.
114
115
  - `0.36.0`: `query.canonical` unifies Search, typed Boolean Filter, multi-Sort and cursor Pagination behind `POST .../records/query`; Runtime Discovery also exposes canonical `instant`, `range<date>`, `range<instant>`, `temporal_range` field types and the Calendar `rangeField` binding. The human Workflow node consumes those current semantics while retaining historical Discovery spellings only for compatibility.
116
+ - `0.40.0`: Authority v2 separates verified Application / Principal / Actor from the explicit opaque Delegation selector; one Delegation has one Space/Model/Record scope; first-class `POST /spaces/{spaceId}/models/batch` atomically commits 1-20 create/update/delete operations and returns one `changeSetId`.
115
117
 
116
118
  The adapter prefers the `0.27+` progressive flow while configuring a node:
117
119
 
@@ -181,7 +183,15 @@ Credentials are intentionally static secure configuration and are not workflow-e
181
183
 
182
184
  The normal n8n-facing resource is **Record**. LifeSpace still owns **Model** semantics internally; the adapter uses **Record Type** for the workflow-facing selection.
183
185
 
184
- Supported operations:
186
+ Supported human Workflow operations include ordinary Record CRUD/Query/Action. **Create / Update / Delete expose an optional Batch Mode under Record Options, default Off.** When enabled, the node consumes the incoming n8n `items[]` directly, requires 1-20 items resolving to one Space and one Record Type, and sends exactly one atomic Core Batch request. The response is expanded back to one n8n output item per input item with preserved pairing and a shared `changeSetId`.
187
+
188
+ Update/Delete `Version` remains optional in both modes. In single mode, omission keeps the historical behavior of reading the current record immediately before mutation. In Batch Mode, omission is delegated to the LifeSpace Batch contract: Core resolves current versions set-wise during Batch preparation and still commits conditionally, so there is no N-read HTTP waterfall and no blind write.
189
+
190
+ **Advanced Mixed Batch** remains available for workflows that deliberately need mixed create/update/delete or cross-Record-Type operations in one atomic ChangeSet. It uses the explicit JSON operations surface and is not the primary Batch UX.
191
+
192
+ The native Agent Tool supports Query/Create/Update/Delete/Action plus **Create Records (Batch)** for 1-20 records of its pinned Record Type. Agent Batch Update/Delete are not exposed by this workflow-UX change; they can be added separately against the same upstream optional-version Batch semantics without reintroducing N pre-read HTTP calls.
193
+
194
+ Ordinary Record operations remain:
185
195
 
186
196
  - **Create**;
187
197
  - **Get**;
@@ -333,13 +343,16 @@ Webhook Endpoint / Event Subscription creation is intentionally not performed by
333
343
 
334
344
  ## What the package provides
335
345
 
336
- - **LifeSpace API** credential for API authentication and Runtime Discovery;
346
+ - **LifeSpace API** credential for Service Principal authentication and Runtime Discovery;
347
+ - **LifeSpace Delegated Agent API** credential for server-only Application authentication, short-lived Agent token minting, and delegated Core execution without a Service PAT;
337
348
  - **LifeSpace Webhook Signing** credential for endpoint-scoped inbound HMAC verification;
338
- - **LifeSpace** human workflow node with Discovery-driven Record operations plus advanced API Request;
339
- - **LifeSpace Agent Tool** native AiTool with Discovery-driven semantic schemas;
349
+ - **LifeSpace** human workflow node with Discovery-driven Record operations, first-class 1-20 atomic Batch create/update/delete, plus advanced API Request;
350
+ - **LifeSpace Agent Tool** native AiTool with Discovery-driven semantic schemas and bounded same-model Batch Create;
340
351
  - **LifeSpace Trigger** with signed multi-Record-Type Domain Event filtering;
341
352
  - shared thin adapter projection from LifeSpace Runtime Discovery into human n8n controls and model-facing Tool schemas.
342
353
 
354
+ The human Batch surface is the generic cross-model atomic transport. The Agent Tool deliberately exposes only same-model Batch Create in this release: Agent Update/Delete continue to resolve optimistic concurrency through a read before mutation, and the adapter does not turn multiple such reads into an N+1 pseudo-Batch. Cross-model or mutation-mixed Agent Batch should be added only when its full input contract can remain deterministic and bounded without reintroducing per-item Runtime round trips.
355
+
343
356
  LifeSpace remains authoritative for validation, authorization, defaults, Mutation Authority, Action semantics, query/time semantics, relation semantics and event contracts. Runtime Discovery and Relation Target Lookup are current capability/reference projections, not execution-authorization proofs.
344
357
 
345
358
  ## Architecture boundary
@@ -0,0 +1,13 @@
1
+ import type { IAuthenticateGeneric, ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class LifeSpaceAgentExecutionApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ icon: {
6
+ readonly light: "file:lifespace.svg";
7
+ readonly dark: "file:lifespace.dark.svg";
8
+ };
9
+ documentationUrl: string;
10
+ properties: INodeProperties[];
11
+ authenticate: IAuthenticateGeneric;
12
+ test: ICredentialTestRequest;
13
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LifeSpaceAgentExecutionApi = void 0;
4
+ class LifeSpaceAgentExecutionApi {
5
+ constructor() {
6
+ this.name = 'lifeSpaceAgentExecutionApi';
7
+ this.displayName = 'LifeSpace Agent Execution API';
8
+ this.icon = {
9
+ light: 'file:lifespace.svg',
10
+ dark: 'file:lifespace.dark.svg',
11
+ };
12
+ this.documentationUrl = 'https://github.com/huangshirui/LifeSpace/blob/main/docs/application-authentication.md';
13
+ this.properties = [
14
+ {
15
+ displayName: 'Core API Base URL',
16
+ name: 'coreBaseUrl',
17
+ type: 'string',
18
+ default: '',
19
+ placeholder: 'https://core.example.com/api/v1',
20
+ required: true,
21
+ description: 'LifeSpace Core API root used by Agent business requests and Delegation creation',
22
+ },
23
+ {
24
+ displayName: 'Identity API Base URL',
25
+ name: 'identityBaseUrl',
26
+ type: 'string',
27
+ default: '',
28
+ placeholder: 'https://identity.example.com',
29
+ required: true,
30
+ description: 'LifeSpace Identity origin. Do not include an /internal/v1 token route.',
31
+ },
32
+ {
33
+ displayName: 'Application Credential',
34
+ name: 'applicationSecret',
35
+ type: 'string',
36
+ typeOptions: { password: true },
37
+ default: '',
38
+ placeholder: 'lsa_...',
39
+ required: true,
40
+ description: 'Server-only trusted LifeSpace Application credential used to mint short-lived User and Agent execution tokens',
41
+ },
42
+ {
43
+ displayName: 'Agent ID',
44
+ name: 'agentId',
45
+ type: 'string',
46
+ default: '',
47
+ placeholder: 'agt_...',
48
+ required: true,
49
+ description: 'LifeSpace Agent identity bound to this registered Application',
50
+ },
51
+ ];
52
+ this.authenticate = {
53
+ type: 'generic',
54
+ properties: {
55
+ headers: {
56
+ Authorization: '=Bearer {{$credentials.applicationSecret}}',
57
+ },
58
+ },
59
+ };
60
+ this.test = {
61
+ request: {
62
+ baseURL: '={{$credentials.identityBaseUrl.endsWith("/") ? $credentials.identityBaseUrl.slice(0, -1) : $credentials.identityBaseUrl}}',
63
+ url: '/internal/v1/agents',
64
+ method: 'GET',
65
+ },
66
+ };
67
+ }
68
+ }
69
+ exports.LifeSpaceAgentExecutionApi = LifeSpaceAgentExecutionApi;
70
+ //# sourceMappingURL=LifeSpaceAgentExecutionApi.credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LifeSpaceAgentExecutionApi.credentials.js","sourceRoot":"","sources":["../../credentials/LifeSpaceAgentExecutionApi.credentials.ts"],"names":[],"mappings":";;;AAOA,MAAa,0BAA0B;IAAvC;QACE,SAAI,GAAG,4BAA4B,CAAC;QAEpC,gBAAW,GAAG,+BAA+B,CAAC;QAE9C,SAAI,GAAG;YACL,KAAK,EAAE,oBAAoB;YAC3B,IAAI,EAAE,yBAAyB;SACvB,CAAC;QAEX,qBAAgB,GAAG,uFAAuF,CAAC;QAE3G,eAAU,GAAsB;YAC9B;gBACE,WAAW,EAAE,mBAAmB;gBAChC,IAAI,EAAE,aAAa;gBACnB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;gBACX,WAAW,EAAE,iCAAiC;gBAC9C,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE,iFAAiF;aAC/F;YACD;gBACE,WAAW,EAAE,uBAAuB;gBACpC,IAAI,EAAE,iBAAiB;gBACvB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;gBACX,WAAW,EAAE,8BAA8B;gBAC3C,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE,wEAAwE;aACtF;YACD;gBACE,WAAW,EAAE,wBAAwB;gBACrC,IAAI,EAAE,mBAAmB;gBACzB,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;gBAC/B,OAAO,EAAE,EAAE;gBACX,WAAW,EAAE,SAAS;gBACtB,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE,+GAA+G;aAC7H;YACD;gBACE,WAAW,EAAE,UAAU;gBACvB,IAAI,EAAE,SAAS;gBACf,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;gBACX,WAAW,EAAE,SAAS;gBACtB,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE,+DAA+D;aAC7E;SACF,CAAC;QAEF,iBAAY,GAAyB;YACnC,IAAI,EAAE,SAAS;YACf,UAAU,EAAE;gBACV,OAAO,EAAE;oBACP,aAAa,EAAE,4CAA4C;iBAC5D;aACF;SACF,CAAC;QAEF,SAAI,GAA2B;YAC7B,OAAO,EAAE;gBACP,OAAO,EAAE,4HAA4H;gBACrI,GAAG,EAAE,qBAAqB;gBAC1B,MAAM,EAAE,KAAK;aACd;SACF,CAAC;IACJ,CAAC;CAAA;AApED,gEAoEC"}
@@ -4,6 +4,7 @@ exports.LifeSpace = void 0;
4
4
  const n8n_workflow_1 = require("n8n-workflow");
5
5
  const lifespaceDiscovery_1 = require("../lifespaceDiscovery");
6
6
  const lifeSpaceActionSnapshot_1 = require("../shared/lifeSpaceActionSnapshot");
7
+ const lifeSpaceExecutionAuthority_1 = require("../shared/lifeSpaceExecutionAuthority");
7
8
  const LEGACY_MODEL_ROUTE_TO_KEY = Object.freeze({
8
9
  tasks: 'task',
9
10
  wishes: 'wish',
@@ -86,6 +87,86 @@ function parseJsonObject(context, itemIndex, value, fieldName) {
86
87
  }
87
88
  return parsed;
88
89
  }
90
+ function batchMutationOperations(context, itemIndex, value, delegated) {
91
+ let parsed;
92
+ try {
93
+ parsed = typeof value === 'string' ? JSON.parse(value) : value;
94
+ }
95
+ catch (error) {
96
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), error, { itemIndex });
97
+ }
98
+ if (!Array.isArray(parsed) || parsed.length < 1 || parsed.length > 20) {
99
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Batch Operations must be a JSON array containing 1 to 20 create/update/delete operations', { itemIndex });
100
+ }
101
+ return parsed.map((raw, index) => {
102
+ var _a, _b, _c, _d;
103
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
104
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Batch operation ${index} must be a JSON object`, { itemIndex });
105
+ }
106
+ const input = raw;
107
+ const operation = String((_a = input.operation) !== null && _a !== void 0 ? _a : '');
108
+ const modelKey = String((_b = input.modelKey) !== null && _b !== void 0 ? _b : '').trim();
109
+ if (!['create', 'update', 'delete'].includes(operation)) {
110
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Batch operation ${index} must be create, update, or delete`, { itemIndex });
111
+ }
112
+ if (!/^[A-Za-z][A-Za-z0-9_]{0,99}$/u.test(modelKey)) {
113
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Batch operation ${index} has an invalid modelKey`, { itemIndex });
114
+ }
115
+ const delegationId = String((_c = input.delegationId) !== null && _c !== void 0 ? _c : '').trim();
116
+ if (delegated && !/^dlg_[A-Za-z0-9_-]+$/u.test(delegationId)) {
117
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Batch operation ${index} requires its own valid delegationId in Delegated Agent mode`, { itemIndex });
118
+ }
119
+ if (!delegated && delegationId) {
120
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Batch operation ${index} cannot use delegationId in Service Principal mode`, { itemIndex });
121
+ }
122
+ if (operation === 'create') {
123
+ const data = input.data;
124
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
125
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Batch create ${index} requires an object data field`, { itemIndex });
126
+ }
127
+ return {
128
+ operation: 'create',
129
+ modelKey,
130
+ data: data,
131
+ ...(delegationId ? { delegationId } : {}),
132
+ };
133
+ }
134
+ const recordId = String((_d = input.recordId) !== null && _d !== void 0 ? _d : '').trim();
135
+ const rawVersion = input.version;
136
+ let version;
137
+ if (rawVersion !== undefined && rawVersion !== null && rawVersion !== '') {
138
+ const parsedVersion = Number(rawVersion);
139
+ if (!Number.isInteger(parsedVersion) || parsedVersion < 1) {
140
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Batch operation ${index} version must be a positive integer when supplied`, { itemIndex });
141
+ }
142
+ version = parsedVersion;
143
+ }
144
+ if (!/^rec_[A-Za-z0-9_-]+$/u.test(recordId)) {
145
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Batch operation ${index} has an invalid recordId`, { itemIndex });
146
+ }
147
+ if (operation === 'delete') {
148
+ return {
149
+ operation: 'delete',
150
+ modelKey,
151
+ recordId,
152
+ ...(version === undefined ? {} : { version }),
153
+ ...(delegationId ? { delegationId } : {}),
154
+ };
155
+ }
156
+ const data = input.data;
157
+ if (!data || typeof data !== 'object' || Array.isArray(data) || Object.keys(data).length === 0) {
158
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Batch update ${index} requires non-empty object data`, { itemIndex });
159
+ }
160
+ return {
161
+ operation: 'update',
162
+ modelKey,
163
+ recordId,
164
+ ...(version === undefined ? {} : { version }),
165
+ data: data,
166
+ ...(delegationId ? { delegationId } : {}),
167
+ };
168
+ });
169
+ }
89
170
  function mappedValue(context, itemIndex, parameterName) {
90
171
  const value = context.getNodeParameter(`${parameterName}.value`, itemIndex, {});
91
172
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
@@ -464,13 +545,120 @@ async function currentRecord(context, itemIndex, baseUrl, recordPath) {
464
545
  async function currentRecordVersion(context, itemIndex, baseUrl, recordPath) {
465
546
  return recordVersion(context, itemIndex, await currentRecord(context, itemIndex, baseUrl, recordPath));
466
547
  }
467
- async function mutationVersion(context, itemIndex, baseUrl, recordPath) {
548
+ function configuredMutationVersion(context, itemIndex) {
468
549
  const options = context.getNodeParameter('mutationOptions', itemIndex, {});
469
550
  const configuredVersion = options.version;
551
+ if (configuredVersion === undefined || configuredVersion === null || configuredVersion === '') {
552
+ return undefined;
553
+ }
470
554
  if (typeof configuredVersion === 'number' && Number.isInteger(configuredVersion) && configuredVersion >= 1) {
471
555
  return configuredVersion;
472
556
  }
473
- return currentRecordVersion(context, itemIndex, baseUrl, recordPath);
557
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Version must be a positive integer when supplied', { itemIndex });
558
+ }
559
+ async function mutationVersion(context, itemIndex, baseUrl, recordPath) {
560
+ var _a;
561
+ return (_a = configuredMutationVersion(context, itemIndex)) !== null && _a !== void 0 ? _a : currentRecordVersion(context, itemIndex, baseUrl, recordPath);
562
+ }
563
+ function recordBatchMode(context, itemIndex) {
564
+ const options = context.getNodeParameter('recordOptions', itemIndex, {});
565
+ return options.batchMode === true;
566
+ }
567
+ function configuredRecordModelKey(context, itemIndex) {
568
+ var _a, _b;
569
+ const recordType = (0, lifespaceDiscovery_1.decodeRecordTypeSelector)(context.getNodeParameter('recordType', itemIndex, ''));
570
+ const legacyModelRoute = String((_a = context.getNodeParameter('modelRoute', itemIndex, '')) !== null && _a !== void 0 ? _a : '').trim();
571
+ const modelKey = (_b = recordType === null || recordType === void 0 ? void 0 : recordType.modelKey) !== null && _b !== void 0 ? _b : LEGACY_MODEL_ROUTE_TO_KEY[legacyModelRoute];
572
+ if (!modelKey) {
573
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'LifeSpace Record Type selector is invalid. Choose a Record Type from Discovery or pass a Trigger recordType value.', { itemIndex });
574
+ }
575
+ return modelKey;
576
+ }
577
+ async function executeNativeRecordBatch(context, items) {
578
+ var _a, _b, _c, _d, _e;
579
+ if (items.length < 1)
580
+ return [];
581
+ if (items.length > 20) {
582
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'LifeSpace Batch Mode accepts at most 20 input items and never auto-chunks an atomic Batch');
583
+ }
584
+ const operation = String(context.getNodeParameter('operation', 0, ''));
585
+ if (!['create', 'update', 'delete'].includes(operation)) {
586
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'LifeSpace Batch Mode is available only for Create, Update, or Delete');
587
+ }
588
+ let sourceSpaceId = '';
589
+ let sourceModelKey = '';
590
+ const operations = [];
591
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex += 1) {
592
+ const itemOperation = String(context.getNodeParameter('operation', itemIndex, operation));
593
+ if (itemOperation !== operation) {
594
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'All input items in LifeSpace Batch Mode must use the same operation', { itemIndex });
595
+ }
596
+ const rawSpaceId = String((_a = context.getNodeParameter('spaceId', itemIndex, '')) !== null && _a !== void 0 ? _a : '').trim();
597
+ if (!rawSpaceId) {
598
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'LifeSpace Batch Mode requires a Space ID', { itemIndex });
599
+ }
600
+ const modelKey = configuredRecordModelKey(context, itemIndex);
601
+ if (!sourceSpaceId)
602
+ sourceSpaceId = rawSpaceId;
603
+ if (!sourceModelKey)
604
+ sourceModelKey = modelKey;
605
+ if (rawSpaceId !== sourceSpaceId || modelKey !== sourceModelKey) {
606
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'All input items in LifeSpace Batch Mode must resolve to the same Space and Record Type', { itemIndex });
607
+ }
608
+ if (operation === 'create') {
609
+ operations.push({
610
+ operation: 'create',
611
+ modelKey,
612
+ data: mutationMappedValues(context, itemIndex),
613
+ });
614
+ continue;
615
+ }
616
+ const recordId = String((_b = context.getNodeParameter('recordId', itemIndex, '')) !== null && _b !== void 0 ? _b : '').trim();
617
+ if (!/^rec_[A-Za-z0-9_-]+$/u.test(recordId)) {
618
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Record ID must be a valid rec_* identifier in Batch Mode', { itemIndex });
619
+ }
620
+ const version = configuredMutationVersion(context, itemIndex);
621
+ if (operation === 'delete') {
622
+ operations.push({
623
+ operation: 'delete',
624
+ modelKey,
625
+ recordId,
626
+ ...(version === undefined ? {} : { version }),
627
+ });
628
+ continue;
629
+ }
630
+ operations.push({
631
+ operation: 'update',
632
+ modelKey,
633
+ recordId,
634
+ ...(version === undefined ? {} : { version }),
635
+ data: mutationMappedValues(context, itemIndex),
636
+ });
637
+ }
638
+ const baseUrl = (0, lifespaceDiscovery_1.normalizeBaseUrl)((await context.getCredentials('lifeSpaceApi', 0)).baseUrl);
639
+ const response = await context.helpers.httpRequestWithAuthentication.call(context, 'lifeSpaceApi', {
640
+ method: 'POST',
641
+ url: `${baseUrl}/spaces/${encodeURIComponent(sourceSpaceId)}/models/batch`,
642
+ body: { operations },
643
+ json: true,
644
+ });
645
+ const changeSetId = String((_d = (_c = response === null || response === void 0 ? void 0 : response.data) === null || _c === void 0 ? void 0 : _c.changeSetId) !== null && _d !== void 0 ? _d : '').trim();
646
+ const results = (_e = response === null || response === void 0 ? void 0 : response.data) === null || _e === void 0 ? void 0 : _e.items;
647
+ if (!/^cgs_[A-Za-z0-9_-]+$/u.test(changeSetId) || !Array.isArray(results) || results.length !== items.length) {
648
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'LifeSpace Batch response did not match the input item count or ChangeSet contract');
649
+ }
650
+ return results.map((result, itemIndex) => {
651
+ if (!result || typeof result !== 'object' || Array.isArray(result)) {
652
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `LifeSpace Batch result ${itemIndex} is invalid`, { itemIndex });
653
+ }
654
+ return {
655
+ json: {
656
+ ...result,
657
+ changeSetId,
658
+ },
659
+ pairedItem: { item: itemIndex },
660
+ };
661
+ });
474
662
  }
475
663
  async function actionBodyWithConcurrency(context, itemIndex, baseUrl, recordPath, action, semanticInput) {
476
664
  const normalizedInput = normalizeActionInput(context, itemIndex, semanticInput, action.input.fields);
@@ -565,7 +753,7 @@ class LifeSpace {
565
753
  },
566
754
  group: ['transform'],
567
755
  version: 1,
568
- subtitle: '={{$parameter["resource"] === "modelRecord" ? $parameter["operation"] : "API Request"}}',
756
+ subtitle: '={{$parameter["resource"] === "modelRecord" ? $parameter["operation"] : ($parameter["resource"] === "batchMutation" ? "Batch Mutation" : "API Request")}}',
569
757
  description: 'Use LifeSpace records and APIs in n8n workflows',
570
758
  defaults: {
571
759
  name: 'LifeSpace',
@@ -577,6 +765,22 @@ class LifeSpace {
577
765
  {
578
766
  name: 'lifeSpaceApi',
579
767
  required: true,
768
+ displayOptions: {
769
+ hide: {
770
+ resource: ['batchMutation'],
771
+ batchAuthorityMode: ['delegatedAgent'],
772
+ },
773
+ },
774
+ },
775
+ {
776
+ name: 'lifeSpaceAgentExecutionApi',
777
+ required: true,
778
+ displayOptions: {
779
+ show: {
780
+ resource: ['batchMutation'],
781
+ batchAuthorityMode: ['delegatedAgent'],
782
+ },
783
+ },
580
784
  },
581
785
  ],
582
786
  properties: [
@@ -590,6 +794,10 @@ class LifeSpace {
590
794
  name: 'Record',
591
795
  value: 'modelRecord',
592
796
  },
797
+ {
798
+ name: 'Advanced Mixed Batch',
799
+ value: 'batchMutation',
800
+ },
593
801
  {
594
802
  name: 'API Request',
595
803
  value: 'apiRequest',
@@ -597,6 +805,50 @@ class LifeSpace {
597
805
  ],
598
806
  default: 'modelRecord',
599
807
  },
808
+ {
809
+ displayName: 'Batch Authority Mode',
810
+ name: 'batchAuthorityMode',
811
+ type: 'options',
812
+ noDataExpression: true,
813
+ displayOptions: { show: { resource: ['batchMutation'] } },
814
+ options: [
815
+ {
816
+ name: 'Service Principal',
817
+ value: 'service',
818
+ description: 'Use the existing LifeSpace Service API Token for the atomic Batch',
819
+ },
820
+ {
821
+ name: 'Delegated Agent',
822
+ value: 'delegatedAgent',
823
+ description: 'Mint one short-lived User Principal -> Agent Actor token; every Batch item supplies its explicit Delegation selector',
824
+ },
825
+ ],
826
+ default: 'service',
827
+ },
828
+ {
829
+ displayName: 'Principal User ID',
830
+ name: 'batchPrincipalUserId',
831
+ type: 'string',
832
+ default: '',
833
+ required: true,
834
+ placeholder: 'usr_...',
835
+ displayOptions: {
836
+ show: {
837
+ resource: ['batchMutation'],
838
+ batchAuthorityMode: ['delegatedAgent'],
839
+ },
840
+ },
841
+ description: 'Execution context for delegated Batch only. This value is not part of any AI Tool input.',
842
+ },
843
+ {
844
+ displayName: 'Batch Operations',
845
+ name: 'batchOperations',
846
+ type: 'json',
847
+ default: '[]',
848
+ required: true,
849
+ displayOptions: { show: { resource: ['batchMutation'] } },
850
+ description: 'Advanced JSON surface for one atomic mixed/cross-model Batch. Update/delete require recordId; version is optional when the current Core Batch contract supports server-resolved concurrency. Delegated Agent mode requires delegationId on every item.',
851
+ },
600
852
  {
601
853
  displayName: 'Operation',
602
854
  name: 'operation',
@@ -613,10 +865,26 @@ class LifeSpace {
613
865
  ],
614
866
  default: 'list',
615
867
  },
868
+ {
869
+ displayName: 'Options',
870
+ name: 'recordOptions',
871
+ type: 'collection',
872
+ placeholder: 'Add Option',
873
+ default: {},
874
+ noDataExpression: true,
875
+ displayOptions: { show: { resource: ['modelRecord'], operation: ['create', 'update', 'delete'] } },
876
+ options: [{
877
+ displayName: 'Batch Mode',
878
+ name: 'batchMode',
879
+ type: 'boolean',
880
+ default: false,
881
+ description: 'Whether to send all incoming n8n items as one atomic LifeSpace Batch. Limited to 20 items in one Space and Record Type; the node never auto-chunks because that would weaken atomic semantics.',
882
+ }],
883
+ },
616
884
  {
617
885
  displayName: 'Space Name or ID', name: 'spaceId', type: 'options',
618
886
  typeOptions: { loadOptionsMethod: 'getSpaces' }, options: [], default: '', required: true,
619
- displayOptions: { show: { resource: ['modelRecord'] } },
887
+ displayOptions: { show: { resource: ['modelRecord', 'batchMutation'] } },
620
888
  description: 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
621
889
  },
622
890
  {
@@ -821,7 +1089,7 @@ class LifeSpace {
821
1089
  {
822
1090
  displayName: 'Concurrency Options', name: 'mutationOptions', type: 'collection', placeholder: 'Add Option', default: {},
823
1091
  displayOptions: { show: { resource: ['modelRecord'], operation: ['update', 'delete'] } },
824
- options: [{ displayName: 'Version', name: 'version', type: 'number', typeOptions: { minValue: 1, numberPrecision: 0 }, default: 1, description: 'Optional known record version. If omitted, the node reads the current record version immediately before the mutation.' }],
1092
+ options: [{ displayName: 'Version', name: 'version', type: 'number', typeOptions: { minValue: 1, numberPrecision: 0 }, default: 1, description: 'Optional known record version. In single mode, omission makes the node read the current version immediately before mutation. In Batch Mode, omission is sent to Core for set-wise current-version resolution before atomic commit.' }],
825
1093
  },
826
1094
  {
827
1095
  displayName: 'Action Name or ID', name: 'actionKey', type: 'options', noDataExpression: true,
@@ -1087,30 +1355,67 @@ class LifeSpace {
1087
1355
  };
1088
1356
  }
1089
1357
  async execute() {
1090
- var _a, _b, _c;
1358
+ var _a, _b;
1091
1359
  const items = this.getInputData();
1360
+ if (items.length > 0) {
1361
+ const resource = this.getNodeParameter('resource', 0);
1362
+ const operation = this.getNodeParameter('operation', 0, '');
1363
+ if (resource === 'modelRecord'
1364
+ && ['create', 'update', 'delete'].includes(operation)
1365
+ && recordBatchMode(this, 0)) {
1366
+ try {
1367
+ return [await executeNativeRecordBatch(this, items)];
1368
+ }
1369
+ catch (error) {
1370
+ if (this.continueOnFail()) {
1371
+ return [items.map((_item, itemIndex) => ({
1372
+ json: { error: error instanceof Error ? error.message : String(error) },
1373
+ pairedItem: { item: itemIndex },
1374
+ }))];
1375
+ }
1376
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error);
1377
+ }
1378
+ }
1379
+ }
1092
1380
  const output = [];
1093
- const credentials = await this.getCredentials('lifeSpaceApi');
1094
- const baseUrl = (0, lifespaceDiscovery_1.normalizeBaseUrl)(credentials.baseUrl);
1095
1381
  for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
1096
1382
  try {
1097
1383
  const resource = this.getNodeParameter('resource', itemIndex);
1098
1384
  let response;
1099
- if (resource === 'modelRecord') {
1385
+ if (resource === 'batchMutation') {
1386
+ const rawSpaceId = String(this.getNodeParameter('spaceId', itemIndex)).trim();
1387
+ if (!rawSpaceId) {
1388
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'LifeSpace Batch Mutation requires a Space ID', { itemIndex });
1389
+ }
1390
+ const batchAuthorityMode = String((_a = this.getNodeParameter('batchAuthorityMode', itemIndex, 'service')) !== null && _a !== void 0 ? _a : 'service');
1391
+ const baseUrl = batchAuthorityMode === 'delegatedAgent'
1392
+ ? (0, lifeSpaceExecutionAuthority_1.delegatedAgentCoreBaseUrl)(this, await this.getCredentials('lifeSpaceAgentExecutionApi', itemIndex))
1393
+ : (0, lifespaceDiscovery_1.normalizeBaseUrl)((await this.getCredentials('lifeSpaceApi', itemIndex)).baseUrl);
1394
+ const operations = batchMutationOperations(this, itemIndex, this.getNodeParameter('batchOperations', itemIndex, '[]'), batchAuthorityMode === 'delegatedAgent');
1395
+ const authority = await (0, lifeSpaceExecutionAuthority_1.executionAuthority)(this, itemIndex, 'write', {
1396
+ modeParameter: 'batchAuthorityMode',
1397
+ principalParameter: 'batchPrincipalUserId',
1398
+ delegationParameter: 'batchDelegationId',
1399
+ requireDelegation: false,
1400
+ });
1401
+ response = await (0, lifeSpaceExecutionAuthority_1.lifeSpaceRequest)(this, authority, {
1402
+ method: 'POST',
1403
+ url: `${baseUrl}/spaces/${encodeURIComponent(rawSpaceId)}/models/batch`,
1404
+ body: { operations },
1405
+ json: true,
1406
+ });
1407
+ }
1408
+ else if (resource === 'modelRecord') {
1409
+ const baseUrl = (0, lifespaceDiscovery_1.normalizeBaseUrl)((await this.getCredentials('lifeSpaceApi', itemIndex)).baseUrl);
1100
1410
  const operation = this.getNodeParameter('operation', itemIndex);
1101
1411
  const rawSpaceId = String(this.getNodeParameter('spaceId', itemIndex));
1102
- const recordType = (0, lifespaceDiscovery_1.decodeRecordTypeSelector)(this.getNodeParameter('recordType', itemIndex, ''));
1103
- const legacyModelRoute = String((_a = this.getNodeParameter('modelRoute', itemIndex, '')) !== null && _a !== void 0 ? _a : '').trim();
1104
- const rawModelKey = (_b = recordType === null || recordType === void 0 ? void 0 : recordType.modelKey) !== null && _b !== void 0 ? _b : LEGACY_MODEL_ROUTE_TO_KEY[legacyModelRoute];
1105
- if (!rawModelKey) {
1106
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'LifeSpace Record Type selector is invalid. Choose a Record Type from Discovery or pass a Trigger recordType value.', { itemIndex });
1107
- }
1412
+ const rawModelKey = configuredRecordModelKey(this, itemIndex);
1108
1413
  const spaceId = encodeURIComponent(rawSpaceId);
1109
1414
  const modelKey = encodeURIComponent(rawModelKey);
1110
1415
  const collectionPath = `/spaces/${spaceId}/models/${modelKey}/records`;
1111
1416
  if (operation === 'list') {
1112
1417
  const returnAll = this.getNodeParameter('returnAll', itemIndex, false);
1113
- const canonical = String((_c = this.getNodeParameter('queryMode', itemIndex, '')) !== null && _c !== void 0 ? _c : '') === 'canonical';
1418
+ const canonical = String((_b = this.getNodeParameter('queryMode', itemIndex, '')) !== null && _b !== void 0 ? _b : '') === 'canonical';
1114
1419
  const queryPageRequest = (limit, cursor) => {
1115
1420
  if (canonical) {
1116
1421
  return {
@@ -1203,6 +1508,7 @@ class LifeSpace {
1203
1508
  }
1204
1509
  }
1205
1510
  else {
1511
+ const baseUrl = (0, lifespaceDiscovery_1.normalizeBaseUrl)((await this.getCredentials('lifeSpaceApi', itemIndex)).baseUrl);
1206
1512
  const method = this.getNodeParameter('method', itemIndex);
1207
1513
  const path = String(this.getNodeParameter('path', itemIndex));
1208
1514
  const options = {