feat: add user created timestamp on whoami (#3538)

This commit is contained in:
Daniel Salazar
2026-08-10 19:08:50 -07:00
committed by GitHub
parent 2a6d2fea32
commit 0c2d7dfa34
5 changed files with 35 additions and 11 deletions
+6 -1
View File
@@ -109,6 +109,9 @@ describe('whoami extension — handleWhoami', () => {
expect(body.oidc_only).toBe(false);
// is_user_token is present (true) for user actors.
expect(body.is_user_token).toBe(true);
// Account creation time, in unix seconds.
expect(typeof body.created_ts).toBe('number');
expect(body.created_ts).toBeGreaterThan(0);
// `directories` is only sent to user actors — confirm its present.
expect(body.directories).toBeDefined();
// taskbar_items is only sent to user actors.
@@ -132,7 +135,7 @@ describe('whoami extension — handleWhoami', () => {
expect(flags.payment_bypass).toBeUndefined();
});
it('strips desktop_bg_* and human_readable_age fields for app actors', async () => {
it('strips desktop_bg_*, created_ts and human_readable_age fields for app actors', async () => {
const user = await seedUser();
const { res, captured } = makeRes();
@@ -154,6 +157,8 @@ describe('whoami extension — handleWhoami', () => {
expect(body.desktop_bg_color).toBeUndefined();
expect(body.desktop_bg_fit).toBeUndefined();
expect(body.human_readable_age).toBeUndefined();
// Account age, in either form, is not exposed to apps.
expect(body.created_ts).toBeUndefined();
// Directories are user-only.
expect(body.directories).toBeUndefined();
});
+15 -10
View File
@@ -14,6 +14,15 @@ const timeago = (() => {
return new TimeAgo('en-US');
})();
// User timestamps come off the DB as SQL datetime strings; the wire format
// for all of them is unix seconds. Unparseable values are dropped rather
// than sent as NaN.
const toUnixSeconds = (value: unknown): number | undefined => {
if (!value) return undefined;
const ms = new Date(value as string | number | Date).getTime();
return Number.isNaN(ms) ? undefined : Math.round(ms / 1000);
};
// Allowlist of `config.feature_flags` keys safe to surface via /whoami.
// Anything not listed here stays server-side, so internal flags
// (payment_bypass, staff_only_*, etc.) cannot leak by accident. Add a
@@ -98,6 +107,7 @@ export const handleWhoami = async (
: undefined,
otp: !!user.otp_enabled,
feature_flags,
created_ts: toUnixSeconds(user.timestamp),
human_readable_age: user.timestamp
? timeago.format(new Date(user.timestamp as string))
: null,
@@ -141,14 +151,9 @@ export const handleWhoami = async (
}
// Last activity
if (user.last_activity_ts) {
try {
details.last_activity_ts = Math.round(
new Date(user.last_activity_ts as string).getTime() / 1000,
);
} catch {
/* ignore parse error */
}
const lastActivityTs = toUnixSeconds(user.last_activity_ts);
if (lastActivityTs !== undefined) {
details.last_activity_ts = lastActivityTs;
}
// Strip sensitive fields for app actors
@@ -164,6 +169,7 @@ export const handleWhoami = async (
delete details.desktop_bg_color;
delete details.desktop_bg_fit;
delete details.human_readable_age;
delete details.created_ts;
delete details.is_user_token;
delete details.metadata;
}
@@ -183,8 +189,7 @@ export const handleWhoami = async (
}
const subscription = details.subscription as
| { offering?: Record<string, unknown> }
| undefined;
{ offering?: Record<string, unknown> } | undefined;
if (subscription?.offering) {
delete subscription.offering.group;
delete subscription.offering.benefits;
+4
View File
@@ -27,6 +27,10 @@ A number value containing the user's free storage.
A string containing the current active app.
#### `created_ts` (Number)
A number value indicating when the user's account was created, in seconds since the Unix epoch. Only returned to user tokens; apps acting on a user's behalf do not receive it.
#### `is_temp` (Boolean)
A boolean value indicating whether the user's account is temporary.
@@ -5,6 +5,11 @@ export default suite('os', {
const user = await t.puter.os.user();
t.assert.ok(user && typeof user === 'object', 'user should be an object');
t.assert.equal(user.username, t.env.users.user.username);
t.assert.equal(
typeof user.created_ts,
'number',
'created_ts should be unix seconds',
);
},
'user accepts trailing success/error callbacks': async (t) => {
+5
View File
@@ -12,6 +12,11 @@ export interface User {
actual_free_storage?: number;
/** The current active app. */
app_name?: string;
/**
* When the account was created, in unix seconds. Only returned to user
* tokens apps acting on a user's behalf do not receive it.
*/
created_ts?: number;
feature_flags?: Record<string, unknown>;
hasDevAccountAccess?: boolean;
/** Whether the user's account is temporary. */