@adobe/alloy-node 0.1.0 → 0.2.0-beta.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/README.md
CHANGED
|
@@ -100,6 +100,19 @@ separate wiring needed. If you build a custom instance with
|
|
|
100
100
|
`setConsent` will still exist as a method but will reject when called, the
|
|
101
101
|
same as calling an unregistered command in the browser bundle.
|
|
102
102
|
|
|
103
|
+
> [!WARNING]
|
|
104
|
+
> `defaultConsent: "pending"` is rejected by `configure()` — not supported
|
|
105
|
+
> in Node. A pending `sendEvent()`/`getIdentity()` on one `forRequest()`
|
|
106
|
+
> call can only ever be released by a `setConsent()` on that *exact same*
|
|
107
|
+
> call, never by a later request for the same visitor (each `forRequest()`
|
|
108
|
+
> is a fresh instance with its own private pending queue) — unlike a
|
|
109
|
+
> browser page, Node has no equivalent long-lived session to hold that
|
|
110
|
+
> state across separate HTTP requests. Use `"in"` or `"out"`, and gate
|
|
111
|
+
> calling `sendEvent()`/`setConsent()` on your own persisted consent
|
|
112
|
+
> decision instead. If you need the multi-request pending flow to work,
|
|
113
|
+
> let us know — we haven't built it because we don't yet have signal that
|
|
114
|
+
> anyone needs it, not because it's impossible.
|
|
115
|
+
|
|
103
116
|
## Context
|
|
104
117
|
|
|
105
118
|
A minimal, always-on Context component attaches `implementationDetails` to
|
|
@@ -124,6 +137,95 @@ local timezone) has no honest server-side source and isn't guessed at here —
|
|
|
124
137
|
merge real values directly via `sendEvent({ xdm: { ... } })` if you have
|
|
125
138
|
them.
|
|
126
139
|
|
|
140
|
+
## Authenticated requests (Edge Network Server API, v2)
|
|
141
|
+
|
|
142
|
+
Because Node runs in a trusted, client-controlled environment (unlike a
|
|
143
|
+
browser), it authenticates its own requests instead of sending them
|
|
144
|
+
unauthenticated — `configure()` **requires** OAuth Server-to-Server
|
|
145
|
+
credentials from the [Adobe Developer Console](https://developer.adobe.com/console):
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
await alloy.configure({
|
|
149
|
+
orgId: "...",
|
|
150
|
+
datastreamId: "...",
|
|
151
|
+
edgeCredentials: {
|
|
152
|
+
clientId: "...",
|
|
153
|
+
clientSecret: "...",
|
|
154
|
+
scopes: ["openid", "AdobeID", ...], // whatever your project grants
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`configure()` rejects if `edgeCredentials` is missing — unlike the browser
|
|
160
|
+
bundle, where the same config field is optional (and generally shouldn't be
|
|
161
|
+
used at all: shipping a client secret to a browser defeats the point of it
|
|
162
|
+
being a secret). `sendEvent()` then goes to the authenticated Edge Network
|
|
163
|
+
Server API (`server.adobedc.net`, `v2`) instead of the standard one, with a
|
|
164
|
+
real IMS access token attached.
|
|
165
|
+
|
|
166
|
+
The IMS token is fetched once and cached (~24h) for the life of the
|
|
167
|
+
process, not once per request — it's server-credential state, not visitor
|
|
168
|
+
state, shared automatically across every `forRequest()` call the same way
|
|
169
|
+
`network`/`runtime` already are.
|
|
170
|
+
|
|
171
|
+
### Running the real-network integration suite
|
|
172
|
+
|
|
173
|
+
`packages/node/test/integration/nodeConsumer.spec.js` makes requests to
|
|
174
|
+
authentication-required APIs, so it needs a real credential — it reads
|
|
175
|
+
`CLIENT_ID`/`CLIENT_SECRET`/`SCOPES`/`IMS_ORG_ID`/`DATASTREAM_ID` from
|
|
176
|
+
`process.env` and skips itself (doesn't fail) whenever any of them are
|
|
177
|
+
unset. `DATASTREAM_ID` must be a real datastream under that same
|
|
178
|
+
`IMS_ORG_ID`.
|
|
179
|
+
|
|
180
|
+
Because it needs credentials, this suite does not run with the basic
|
|
181
|
+
`pnpm run test`/`test:coverage` testing commands (which exclude it via
|
|
182
|
+
`--project=!node-integration`) into its own command:
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
# Locally, from a .env file at the repo root (gitignored):
|
|
186
|
+
node --env-file=.env ./node_modules/.bin/vitest run --project node-integration
|
|
187
|
+
# or, equivalently:
|
|
188
|
+
node --env-file=.env ./node_modules/.bin/pnpm run test:node-integration
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
In CI (`.github/workflows/quality-checks.yml`'s `tests` job), `pnpm run
|
|
192
|
+
test:node-integration` is its own step, with those same env vars populated
|
|
193
|
+
from repo secrets — `ALLOY_NODE_INTEGRATION_CLIENT_ID`, `_CLIENT_SECRET`,
|
|
194
|
+
`_SCOPES`, `_IMS_ORG_ID`, `_DATASTREAM_ID` — configured under this repo's
|
|
195
|
+
**Settings → Secrets and variables → Actions**. Until those secrets exist,
|
|
196
|
+
the suite just keeps skipping in CI, same as it does locally without a
|
|
197
|
+
`.env`. GitHub doesn't expose secrets to a fork's `pull_request` run either
|
|
198
|
+
way, so it'll always skip there regardless.
|
|
199
|
+
|
|
200
|
+
Verified against a real, credentialed project — two things worth knowing,
|
|
201
|
+
neither of which needs any code changes on your part, but do affect what
|
|
202
|
+
you pass to `sendEvent()`:
|
|
203
|
+
|
|
204
|
+
- **Only `sendEvent` uses v2.** `setConsent` and `getIdentity` still go
|
|
205
|
+
through the standard v1 endpoint even when `edgeCredentials` is
|
|
206
|
+
configured — the authenticated Server API returns a flat 404 for
|
|
207
|
+
`privacy/set-consent` and `identity/acquire`. This is handled
|
|
208
|
+
automatically; you don't need to do anything differently for those
|
|
209
|
+
calls.
|
|
210
|
+
- **You must supply a primary identity.** Since there's no browser cookie
|
|
211
|
+
jar for Edge Network to resolve identity from on a pure server-to-server
|
|
212
|
+
call, an authenticated `sendEvent()` needs an explicit identity in the
|
|
213
|
+
event itself, or it's rejected:
|
|
214
|
+
|
|
215
|
+
```js
|
|
216
|
+
await request.sendEvent({
|
|
217
|
+
xdm: {
|
|
218
|
+
eventType: "...",
|
|
219
|
+
identityMap: {
|
|
220
|
+
Email: [{ id: "visitor@example.com", primary: true }],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
(The `events` → `event` payload reshaping v2 requires under the hood is
|
|
227
|
+
handled internally — nothing to do there either.)
|
|
228
|
+
|
|
127
229
|
## Platform services
|
|
128
230
|
|
|
129
231
|
Both `createInstance(options)` and `forRequest(overrides)` accept a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/alloy-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.0",
|
|
4
4
|
"description": "Adobe Experience Platform Web SDK - Node.js entrypoint",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@adobe/alloy-core": "^1.
|
|
27
|
+
"@adobe/alloy-core": "^1.3.0-beta.0"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"typecheck": "tsc -p jsconfig.json"
|
package/src/alloy-core.d.ts
CHANGED
|
@@ -4,9 +4,19 @@ declare module "@adobe/alloy-core" {
|
|
|
4
4
|
options: Record<string, unknown>,
|
|
5
5
|
createPlatformServices?: () => unknown,
|
|
6
6
|
coreConfigValidators?: unknown,
|
|
7
|
+
getImsAccessToken?: { getAccessToken: () => Promise<string> },
|
|
7
8
|
): (commandName: string, options?: Record<string, unknown>) => Promise<any>;
|
|
8
9
|
|
|
9
10
|
export function createCoreConfigs(): unknown;
|
|
10
11
|
|
|
12
|
+
export function createGetImsAccessToken(options: {
|
|
13
|
+
edgeCredentials: {
|
|
14
|
+
clientId: string;
|
|
15
|
+
clientSecret: string;
|
|
16
|
+
scopes: string[];
|
|
17
|
+
imsHost: string;
|
|
18
|
+
};
|
|
19
|
+
}): { getAccessToken: () => Promise<string> };
|
|
20
|
+
|
|
11
21
|
export function consent(options: Record<string, unknown>): unknown;
|
|
12
22
|
}
|
|
@@ -15,7 +15,7 @@ governing permissions and limitations under the License.
|
|
|
15
15
|
import packageJson from "../../package.json" with { type: "json" };
|
|
16
16
|
import getHeader from "../services/getHeader.js";
|
|
17
17
|
|
|
18
|
-
const LIBRARY_NAME = "https://ns.adobe.com/experience/alloy";
|
|
18
|
+
const LIBRARY_NAME = "https://ns.adobe.com/experience/alloy-node";
|
|
19
19
|
const { version: LIBRARY_VERSION } = packageJson;
|
|
20
20
|
|
|
21
21
|
/**
|
package/src/createNodeAlloy.js
CHANGED
|
@@ -16,7 +16,9 @@ governing permissions and limitations under the License.
|
|
|
16
16
|
import {
|
|
17
17
|
createCustomInstance as createCoreCustomInstance,
|
|
18
18
|
createCoreConfigs,
|
|
19
|
+
createGetImsAccessToken,
|
|
19
20
|
} from "@adobe/alloy-core";
|
|
21
|
+
import { DEFAULT_IMS_HOST } from "@adobe/alloy-core/constants/domain.js";
|
|
20
22
|
import createNodePlatformServices from "./services/createNodePlatformServices.js";
|
|
21
23
|
import * as allRequiredComponents from "./components/requiredComponentCreators.js";
|
|
22
24
|
|
|
@@ -97,6 +99,10 @@ const createNodeAlloy = ({
|
|
|
97
99
|
|
|
98
100
|
/** @type {Record<string, unknown> | undefined} */
|
|
99
101
|
let capturedConfig;
|
|
102
|
+
// Built once, after configure() resolves, and shared across every
|
|
103
|
+
// forRequest() call — see createGetImsAccessToken's docs for why.
|
|
104
|
+
/** @type {ReturnType<typeof createGetImsAccessToken> | undefined} */
|
|
105
|
+
let sharedGetImsAccessToken;
|
|
100
106
|
|
|
101
107
|
/**
|
|
102
108
|
* A handle scoped to a single request, backed by its own fresh instance
|
|
@@ -142,6 +148,7 @@ const createNodeAlloy = ({
|
|
|
142
148
|
// request, so they must not be checked for uniqueness against every
|
|
143
149
|
// other request this process has ever handled.
|
|
144
150
|
createCoreConfigs(),
|
|
151
|
+
sharedGetImsAccessToken,
|
|
145
152
|
);
|
|
146
153
|
requestExecuteCommand("configure", capturedConfig);
|
|
147
154
|
return bindCommandMethods(requestExecuteCommand, COMMAND_NAMES);
|
|
@@ -151,12 +158,48 @@ const createNodeAlloy = ({
|
|
|
151
158
|
...bindCommandMethods(executeCommand, COMMAND_NAMES),
|
|
152
159
|
/** @param {Record<string, unknown>} options */
|
|
153
160
|
configure(options) {
|
|
161
|
+
if (options.defaultConsent === "pending") {
|
|
162
|
+
return Promise.reject(
|
|
163
|
+
new Error(
|
|
164
|
+
'@adobe/alloy-node does not support defaultConsent: "pending" — ' +
|
|
165
|
+
"Node has no cross-request session to hold a pending decision " +
|
|
166
|
+
'in (unlike a long-lived browser page). Use "in" or "out", and ' +
|
|
167
|
+
"gate calling sendEvent()/setConsent() on your own persisted " +
|
|
168
|
+
"consent decision instead.",
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Node runs in a trusted, client-controlled environment, so
|
|
174
|
+
// authenticating is required here, unlike the browser bundle's
|
|
175
|
+
// optional opt-in.
|
|
176
|
+
if (!options.edgeCredentials) {
|
|
177
|
+
return Promise.reject(
|
|
178
|
+
new Error(
|
|
179
|
+
"@adobe/alloy-node requires edgeCredentials. See the README for " +
|
|
180
|
+
"how to obtain OAuth Server-to-Server credentials from the " +
|
|
181
|
+
"Adobe Developer Console.",
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
154
186
|
// Only capture the config once configure() has actually succeeded —
|
|
155
187
|
// if it rejects (invalid config), capturedConfig must stay unset, so
|
|
156
188
|
// a subsequent forRequest() call still throws instead of silently
|
|
157
189
|
// reconfiguring every request with a config core already rejected.
|
|
158
190
|
return executeCommand("configure", options).then((result) => {
|
|
159
191
|
capturedConfig = options;
|
|
192
|
+
// core's config validation applies this same default, but only to
|
|
193
|
+
// its own internal copy — this instance's token accessor is built
|
|
194
|
+
// from the raw options, so an omitted imsHost would otherwise
|
|
195
|
+
// become the literal string "undefined" in the token URL.
|
|
196
|
+
const edgeCredentials = /** @type {any} */ (options).edgeCredentials;
|
|
197
|
+
sharedGetImsAccessToken = createGetImsAccessToken({
|
|
198
|
+
edgeCredentials: {
|
|
199
|
+
...edgeCredentials,
|
|
200
|
+
imsHost: edgeCredentials.imsHost || DEFAULT_IMS_HOST,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
160
203
|
return result;
|
|
161
204
|
});
|
|
162
205
|
},
|
|
@@ -26,12 +26,15 @@ const createNodeNetworkService = ({ headers: forwardedHeaders = {} } = {}) => {
|
|
|
26
26
|
/**
|
|
27
27
|
* @param {string} url
|
|
28
28
|
* @param {string} body
|
|
29
|
+
* @param {Record<string, string>} [perCallHeaders] e.g. IMS auth headers
|
|
30
|
+
* for an authenticated Server API request. Can't override `Content-Type`.
|
|
29
31
|
*/
|
|
30
|
-
const sendFetchRequest = (url, body) =>
|
|
32
|
+
const sendFetchRequest = (url, body, perCallHeaders) =>
|
|
31
33
|
fetch(url, {
|
|
32
34
|
method: "POST",
|
|
33
35
|
headers: {
|
|
34
36
|
...forwardedHeaders,
|
|
37
|
+
...perCallHeaders,
|
|
35
38
|
"Content-Type": "text/plain; charset=UTF-8",
|
|
36
39
|
},
|
|
37
40
|
body,
|
|
@@ -13,6 +13,7 @@ governing permissions and limitations under the License.
|
|
|
13
13
|
/** @import { NetworkService, StorageService, CookieService, RuntimeService, LegacyService, GlobalsService, PlatformServices } from "@adobe/alloy-core/services" */
|
|
14
14
|
/** @import { Logger } from "@adobe/alloy-core/core/types.js" */
|
|
15
15
|
|
|
16
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
16
17
|
import createNodeNetworkService from "./createNodeNetworkService.js";
|
|
17
18
|
import createNodeStorageService from "./createNodeStorageService.js";
|
|
18
19
|
import createNodeCookieService from "./createNodeCookieService.js";
|
|
@@ -49,7 +50,7 @@ import pickForwardableHeaders from "./pickForwardableHeaders.js";
|
|
|
49
50
|
* Node is proxying an event for, if any — forwarded to Edge Network via
|
|
50
51
|
* `pickForwardableHeaders` (unless `network` is overridden), and exposed
|
|
51
52
|
* as-is on the returned object for the Context component to read.
|
|
52
|
-
* @returns {PlatformServices & { request?: NodeRequestLike }}
|
|
53
|
+
* @returns {PlatformServices & { request?: NodeRequestLike, libraryVersionOverride: string }}
|
|
53
54
|
*/
|
|
54
55
|
const createNodePlatformServices = ({
|
|
55
56
|
network,
|
|
@@ -72,6 +73,9 @@ const createNodePlatformServices = ({
|
|
|
72
73
|
legacy: legacy || createNodeLegacyService(),
|
|
73
74
|
globals: globals || createNodeGlobalsService(),
|
|
74
75
|
request,
|
|
76
|
+
// Node ships unbundled source, so the build-time __VERSION__ placeholder
|
|
77
|
+
// never gets replaced — report the real package.json version instead.
|
|
78
|
+
libraryVersionOverride: packageJson.version,
|
|
75
79
|
});
|
|
76
80
|
|
|
77
81
|
export default createNodePlatformServices;
|