Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import discover from './discover/handler.js';
import index from './index/handler.js';
import live from './live/handler.js';
import log from './log/handler.js';
import medialog from './medialog/handler.js';
import { adobeExchange, auth, discoveryKeys } from './auth/handler.js';
import { login, logout } from './login/handler.js';
import media from './media/handler.js';
Expand Down Expand Up @@ -84,6 +85,7 @@ export const router = new Router(nameSelector)
.add('/:org/sites/:site/index/*', index)
.add('/:org/sites/:site/live/*', live)
.add('/:org/sites/:site/log', log)
.add('/:org/sites/:site/medialog', medialog)
.add('/:org/sites/:site/login', login)
.add('/:org/sites/:site/media/*', media)
.add('/:org/sites/:site/preview/*', preview)
Expand Down
55 changes: 55 additions & 0 deletions src/medialog/add.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import { Response } from '@adobe/fetch';
import { errorResponse } from '../support/utils.js';
import { MediaLogBatch } from '../support/medialog.js';

/**
* Adds entries to a media log.
*
* @param {import('../support/AdminContext.js').AdminContext} context context
* @param {import('../support/RequestInfo.js').RequestInfo} info request info
* @returns {Promise<Response>} response
*/
export default async function add(context) {
const { authInfo, contentBusId, log } = context;
const { data: { entries } } = context;

if (!entries || !Array.isArray(entries)) {
return errorResponse(log, 400, 'Adding media logs requires an array in \'entries\'');
}
if (entries.length > 10) {
return errorResponse(log, 400, 'Array in \'entries\' should not contain more than 10 messages');
}

const user = authInfo.resolveEmail();
const batch = new MediaLogBatch(contentBusId);

entries.forEach((entry) => {
const notification = {
...entry,
timestamp: Date.now(),
};
if (user && !notification.user) {
notification.user = user;
}
batch.addNotification(notification);
});
await batch.send(context);

return new Response('', {
status: 201,
headers: {
'content-type': 'text/plain; charset=utf-8',
},
});
}
44 changes: 44 additions & 0 deletions src/medialog/handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import { Response } from '@adobe/fetch';
import add from './add.js';
import query from './query.js';

/**
* Allowed methods for that handler
*/
const ALLOWED_METHODS = ['GET', 'POST'];

/**
* Handles the /medialog route
*
* @param {import('../support/AdminContext').AdminContext} context context
* @param {import('../support/RequestInfo').RequestInfo} info request info
* @returns {Promise<Response>} response
*/
export default async function medialogHandler(context, info) {
const { authInfo } = context;

if (ALLOWED_METHODS.indexOf(info.method) < 0) {
return new Response('method not allowed', {
status: 405,
});
}

authInfo.assertPermissions('log:read');
if (info.method === 'GET') {
return query(context, info);
}

authInfo.assertPermissions('log:write');
return add(context, info);
}
91 changes: 91 additions & 0 deletions src/medialog/query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import { Response } from '@adobe/fetch';
import { MediaLog } from '@adobe/helix-admin-support';
import { errorResponse } from '../support/utils.js';
import {
decode, encode, getNextLinkUrl, parseIntWithCond, parseTimespan,
} from '../log/utils.js';

/**
* Total size of collected entries in log, when stringified.
*/
export const MAX_ENTRIES_SIZE = 3_000_000;

/**
* Queries a media log.
*
* @param {import('../support/AdminContext.js').AdminContext} context context
* @param {import('../support/RequestInfo.js').RequestInfo} info request info
* @returns {Promise<Response>} response
*/
export default async function query(context, info) {
const {
contentBusId, log, data: {
from: fromS, to: toS, since: sinceS, limit: limitS, nextToken,
},
} = context;

let from;
let to;

try {
([from, to] = parseTimespan(fromS, toS, sinceS));
} catch (e) {
return errorResponse(log, 400, e.message);
}

const limit = parseIntWithCond(limitS, (value) => {
if (value >= 1 && value <= 1000) {
return true;
}
log.warn(`'limit' should be between 1 and 1000: ' ${value}`);
return false;
}, 1000);

const mediaLogReader = MediaLog.createReader(contentBusId, log);

try {
await mediaLogReader.init();

const { entries, next } = await mediaLogReader.getEntries(
from,
to,
{ limit, maxSize: MAX_ENTRIES_SIZE },
decode(nextToken),
);
const result = {
from: new Date(from).toISOString(),
to: new Date(to).toISOString(),
entries,
};
if (next) {
result.nextToken = encode(next);
result.links = {
next: getNextLinkUrl(info, {
from: result.from,
to: result.to,
limit: limitS,
nextToken: result.nextToken,
}),
};
}
return new Response(JSON.stringify(result), {
status: 200,
headers: {
'content-type': 'application/json',
},
});
} finally {
mediaLogReader.close();
}
}
2 changes: 1 addition & 1 deletion src/support/AuditBatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function shouldAudit(context, info, res) {
const { attributes, log } = context;
const { route, method, org } = info;

if (route === 'log') {
if (['log', 'medialog'].includes(route)) {
return false;
}
if (!['POST', 'DELETE', 'PUT'].includes(method)) {
Expand Down
67 changes: 67 additions & 0 deletions src/support/medialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import { BatchedQueueClient } from '@adobe/helix-admin-support';
import { getPackedMessageQueue } from './utils.js';

/**
* Represents a batch of media log messages.
*/
export class MediaLogBatch {
constructor(contentBusId) {
this.contentBusId = contentBusId;
this.updates = [];
}

/**
* Add a notification to the batch.
*
* @param {object} notification media log notification
*/
addNotification(notification) {
this.updates.push(notification);
}

/**
* Send the batch as a package to the media log queue.
*
* @param {import('./AdminContext.js').AdminContext} context admin context
*/
async send(context) {
const { contentBusId, updates } = this;

/* c8 ignore next 3 - defensive check */
if (updates.length === 0) {
return;
}

const { log, runtime: { region, accountId } } = context;
const payload = {
MessageGroupId: contentBusId,
MessageDeduplicationId: crypto.randomUUID(),
MessageBody: JSON.stringify({ contentBusId, updates }),
};

const queueClient = new BatchedQueueClient({
log,
outQueue: getPackedMessageQueue(region, accountId, 'media-log', !!process.env.HLX_DEV_SERVER_HOST),
swapBucket: context.attributes.bucketMap.content,
});

try {
const messageIds = await queueClient.send([payload]);
log.info(`Sent media log batch: [${messageIds.map((messageId) => messageId.substring(0, 8)).join(',')}]`);
} finally {
queueClient.close();
}
}
}
Loading
Loading